diff --git a/frontend/.eslintrc.js b/frontend/.eslintrc.js new file mode 100644 index 00000000..37fb4284 --- /dev/null +++ b/frontend/.eslintrc.js @@ -0,0 +1,18 @@ +module.exports = { + parser: "@typescript-eslint/parser", + extends: ["next", "plugin:@typescript-eslint/recommended"], + settings: { + next: { + rootDir: "." + } + }, + plugins: ["@typescript-eslint", "unused-imports"], + rules: { + "react-hooks/exhaustive-deps": "off", + "semi": ["error", "always"], + "unused-imports/no-unused-imports": "error", + "@typescript-eslint/no-explicit-any": "off", + "@typescript-eslint/no-unused-vars": "off", + "unused-imports/no-unused-vars": "off" + } +}; \ No newline at end of file diff --git a/frontend/.eslintrc.json b/frontend/.eslintrc.json deleted file mode 100644 index 697dc5b9..00000000 --- a/frontend/.eslintrc.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "extends": "next", - "settings": { - "next": { - "rootDir": "." - } - }, - "rules": { - "react-hooks/exhaustive-deps": "off", - "semi": ["error", "always"] - } -} \ No newline at end of file diff --git a/frontend/Writerside/.idea/.gitignore b/frontend/Writerside/.idea/.gitignore deleted file mode 100644 index 26d33521..00000000 --- a/frontend/Writerside/.idea/.gitignore +++ /dev/null @@ -1,3 +0,0 @@ -# Default ignored files -/shelf/ -/workspace.xml diff --git a/frontend/Writerside/.idea/Writerside.iml b/frontend/Writerside/.idea/Writerside.iml deleted file mode 100644 index 61021940..00000000 --- a/frontend/Writerside/.idea/Writerside.iml +++ /dev/null @@ -1,8 +0,0 @@ - - - - - - - - \ No newline at end of file diff --git a/frontend/Writerside/.idea/modules.xml b/frontend/Writerside/.idea/modules.xml deleted file mode 100644 index b2bb62a0..00000000 --- a/frontend/Writerside/.idea/modules.xml +++ /dev/null @@ -1,8 +0,0 @@ - - - - - - - - \ No newline at end of file diff --git a/frontend/Writerside/.idea/vcs.xml b/frontend/Writerside/.idea/vcs.xml deleted file mode 100644 index b2bdec2d..00000000 --- a/frontend/Writerside/.idea/vcs.xml +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/frontend/Writerside/c.list b/frontend/Writerside/c.list deleted file mode 100644 index c4c77a29..00000000 --- a/frontend/Writerside/c.list +++ /dev/null @@ -1,6 +0,0 @@ - - - - - \ No newline at end of file diff --git a/frontend/Writerside/fad.tree b/frontend/Writerside/fad.tree deleted file mode 100644 index fc017cc4..00000000 --- a/frontend/Writerside/fad.tree +++ /dev/null @@ -1,11 +0,0 @@ - - - - - - - - \ No newline at end of file diff --git a/frontend/Writerside/images/completion_procedure.png b/frontend/Writerside/images/completion_procedure.png deleted file mode 100644 index 3535a3f3..00000000 Binary files a/frontend/Writerside/images/completion_procedure.png and /dev/null differ diff --git a/frontend/Writerside/images/completion_procedure_dark.png b/frontend/Writerside/images/completion_procedure_dark.png deleted file mode 100644 index a65beb0e..00000000 Binary files a/frontend/Writerside/images/completion_procedure_dark.png and /dev/null differ diff --git a/frontend/Writerside/images/convert_table_to_xml.png b/frontend/Writerside/images/convert_table_to_xml.png deleted file mode 100644 index 2518a64c..00000000 Binary files a/frontend/Writerside/images/convert_table_to_xml.png and /dev/null differ diff --git a/frontend/Writerside/images/convert_table_to_xml_dark.png b/frontend/Writerside/images/convert_table_to_xml_dark.png deleted file mode 100644 index 47161226..00000000 Binary files a/frontend/Writerside/images/convert_table_to_xml_dark.png and /dev/null differ diff --git a/frontend/Writerside/images/new_topic_options.png b/frontend/Writerside/images/new_topic_options.png deleted file mode 100644 index bc6abb62..00000000 Binary files a/frontend/Writerside/images/new_topic_options.png and /dev/null differ diff --git a/frontend/Writerside/images/new_topic_options_dark.png b/frontend/Writerside/images/new_topic_options_dark.png deleted file mode 100644 index bf3e48d1..00000000 Binary files a/frontend/Writerside/images/new_topic_options_dark.png and /dev/null differ diff --git a/frontend/Writerside/topics/Database-Setup-and-CTFSWeb-Integration.md b/frontend/Writerside/topics/Database-Setup-and-CTFSWeb-Integration.md deleted file mode 100644 index 687a3abd..00000000 --- a/frontend/Writerside/topics/Database-Setup-and-CTFSWeb-Integration.md +++ /dev/null @@ -1,1649 +0,0 @@ -# CTFSWeb Data Migration - -Moving data from CTFSWeb's database schema to the ForestGEO App's schema is a layered, multi-step process. This guide -will outline the basic process developed to achieve this using a series of examples, SQL files, and Bash scripts. Please -ensure your development environment is compatible with Unix systems before you proceed. - ---- - -## Before You Begin - -Before getting started, please ensure you have the following starting components: -- A SQL flat file of an existing site - - This should be a very large (think 100s of MBs) SQL file that contains a direct SQL dump of a full site's census history. - -- An empty data source to migrate the old schema into. -- An empty data source to migrate the data into. -- Time! - - A word to the wise, this takes a ridiculous amount of time, so make sure you have a couple hours to spare. I'd - - recommend making sure your machine is plugged in and has a steady internet connection. - ---- - -For the purposes of this guide, we're going to name the old schema `ctfsweb` and the new schema `forestgeo`. - -### Why do you need to create a data source for the old data? -Migration from schema to schema is easier to work with than working from a flat file. Having the old data source directly -viewable will let you confirm whether the migration has worked by reviewing foreign key connections directly. -Additionally, the flat file will often contain other nonessential statements that can potentially disrupt the -execution of the flat file. - -### Preparing Data Sources -Begin by ensuring that your data sources are correctly formatted (have all required tables).
-1. For our `ctfsweb` example schema, you can use this SQL script -```SQL --- reset_ctfsweb_tables.sql -DROP TABLE IF EXISTS `Census`; -CREATE TABLE IF NOT EXISTS `Census` ( - `CensusID` int(10) unsigned NOT NULL AUTO_INCREMENT, - `PlotID` int(10) unsigned NOT NULL, - `PlotCensusNumber` char(16) DEFAULT NULL, - `StartDate` date DEFAULT NULL, - `EndDate` date DEFAULT NULL, - `Description` varchar(128) DEFAULT NULL, - PRIMARY KEY (`CensusID`), - KEY `Ref610` (`PlotID`) -) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=latin1; - - - -DROP TABLE IF EXISTS `CensusQuadrat`; -CREATE TABLE IF NOT EXISTS `CensusQuadrat` ( - `CensusID` int(10) unsigned NOT NULL, - `QuadratID` int(10) unsigned NOT NULL, - `CensusQuadratID` int(10) unsigned NOT NULL AUTO_INCREMENT, - PRIMARY KEY (`CensusQuadratID`), - KEY `Ref534` (`CensusID`), - KEY `QuadratID` (`QuadratID`) -) ENGINE=InnoDB AUTO_INCREMENT=641 DEFAULT CHARSET=latin1; - - - -DROP TABLE IF EXISTS `Coordinates`; -CREATE TABLE IF NOT EXISTS `Coordinates` ( - `CoorID` int(10) unsigned NOT NULL AUTO_INCREMENT, - `FeatureID` int(10) unsigned DEFAULT NULL, - `PlotID` int(10) unsigned DEFAULT NULL, - `QuadratID` int(10) unsigned DEFAULT NULL, - `GX` decimal(16,5) DEFAULT NULL, - `GY` decimal(16,5) DEFAULT NULL, - `GZ` decimal(16,5) DEFAULT NULL, - `PX` decimal(16,5) DEFAULT NULL, - `PY` decimal(16,5) DEFAULT NULL, - `PZ` decimal(16,5) DEFAULT NULL, - `QX` decimal(16,5) DEFAULT NULL, - `QY` decimal(16,5) DEFAULT NULL, - `QZ` decimal(16,5) DEFAULT NULL, - `CoordinateNo` int(10) unsigned DEFAULT NULL, - PRIMARY KEY (`CoorID`), - KEY `FeatureID` (`FeatureID`), - KEY `PlotID` (`PlotID`), - KEY `QuadratID` (`QuadratID`) -) ENGINE=InnoDB AUTO_INCREMENT=642 DEFAULT CHARSET=latin1; - - - -DROP TABLE IF EXISTS `Country`; -CREATE TABLE IF NOT EXISTS `Country` ( - `CountryID` smallint(5) unsigned NOT NULL AUTO_INCREMENT, - `CountryName` varchar(64) DEFAULT NULL, - PRIMARY KEY (`CountryID`) -) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=latin1; - - - -DROP TABLE IF EXISTS `CurrentObsolete`; -CREATE TABLE IF NOT EXISTS `CurrentObsolete` ( - `SpeciesID` int(10) unsigned NOT NULL, - `ObsoleteSpeciesID` int(10) unsigned NOT NULL, - `ChangeDate` datetime NOT NULL, - `ChangeCodeID` int(10) unsigned NOT NULL, - `ChangeNote` varchar(128) DEFAULT NULL, - PRIMARY KEY (`SpeciesID`,`ObsoleteSpeciesID`,`ChangeDate`), - KEY `Ref32191` (`ChangeCodeID`), - KEY `Ref92192` (`SpeciesID`), - KEY `Ref92212` (`ObsoleteSpeciesID`) -) ENGINE=InnoDB DEFAULT CHARSET=latin1; - - - -DROP TABLE IF EXISTS `DBH`; -CREATE TABLE IF NOT EXISTS `DBH` ( - `CensusID` int(10) unsigned NOT NULL, - `StemID` int(10) unsigned NOT NULL, - `DBH` float DEFAULT NULL, - `HOM` decimal(10,2) DEFAULT NULL, - `PrimaryStem` varchar(20) DEFAULT NULL, - `ExactDate` date DEFAULT NULL, - `DBHID` int(10) unsigned NOT NULL AUTO_INCREMENT, - `Comments` varchar(128) DEFAULT NULL, - PRIMARY KEY (`DBHID`), - KEY `Ref549` (`CensusID`), - KEY `Ref1951` (`StemID`) -) ENGINE=InnoDB AUTO_INCREMENT=278618 DEFAULT CHARSET=latin1; - - - -DROP TABLE IF EXISTS `DBHAttributes`; -CREATE TABLE IF NOT EXISTS `DBHAttributes` ( - `TSMID` int(10) unsigned NOT NULL, - `DBHID` int(10) unsigned DEFAULT NULL, - `DBHAttID` int(10) unsigned NOT NULL AUTO_INCREMENT, - PRIMARY KEY (`DBHAttID`), - KEY `Ref2053` (`TSMID`), - KEY `DBHID` (`DBHID`) -) ENGINE=InnoDB AUTO_INCREMENT=262884 DEFAULT CHARSET=latin1; - - - -DROP TABLE IF EXISTS `DataCollection`; -CREATE TABLE IF NOT EXISTS `DataCollection` ( - `CensusID` int(10) unsigned NOT NULL, - `StartDate` date DEFAULT NULL, - `EndDate` date DEFAULT NULL, - `DataCollectionID` int(10) unsigned NOT NULL AUTO_INCREMENT, - `PersonnelRoleID` int(10) unsigned NOT NULL, - `QuadratID` int(10) unsigned NOT NULL, - PRIMARY KEY (`DataCollectionID`), - KEY `Ref1743` (`CensusID`), - KEY `QuadratID` (`QuadratID`), - KEY `PersonnelRoleID` (`PersonnelRoleID`) -) ENGINE=InnoDB DEFAULT CHARSET=latin1; - - - -DROP TABLE IF EXISTS `Family`; -CREATE TABLE IF NOT EXISTS `Family` ( - `FamilyID` int(10) unsigned NOT NULL AUTO_INCREMENT, - `Family` char(32) DEFAULT NULL, - `ReferenceID` smallint(5) unsigned DEFAULT NULL, - PRIMARY KEY (`FamilyID`), - KEY `Ref84175` (`ReferenceID`) -) ENGINE=InnoDB AUTO_INCREMENT=550 DEFAULT CHARSET=latin1; - - - -DROP TABLE IF EXISTS `FeatureTypes`; -CREATE TABLE IF NOT EXISTS `FeatureTypes` ( - `FeatureTypeID` int(10) unsigned NOT NULL AUTO_INCREMENT, - `Type` varchar(32) NOT NULL, - PRIMARY KEY (`FeatureTypeID`) -) ENGINE=InnoDB DEFAULT CHARSET=latin1; - - - -DROP TABLE IF EXISTS `Features`; -CREATE TABLE IF NOT EXISTS `Features` ( - `FeatureID` int(10) unsigned NOT NULL AUTO_INCREMENT, - `FeatureTypeID` int(10) unsigned NOT NULL, - `Name` varchar(32) NOT NULL, - `ShortDescrip` varchar(32) DEFAULT NULL, - `LongDescrip` varchar(128) DEFAULT NULL, - PRIMARY KEY (`FeatureID`), - KEY `FeatureTypeID` (`FeatureTypeID`) -) ENGINE=InnoDB DEFAULT CHARSET=latin1; - - - -DROP TABLE IF EXISTS `Genus`; -CREATE TABLE IF NOT EXISTS `Genus` ( - `GenusID` int(10) unsigned NOT NULL AUTO_INCREMENT, - `Genus` char(32) DEFAULT NULL, - `ReferenceID` smallint(5) unsigned DEFAULT NULL, - `Authority` char(32) DEFAULT NULL, - `FamilyID` int(10) unsigned NOT NULL, - PRIMARY KEY (`GenusID`), - KEY `Ref2868` (`FamilyID`), - KEY `Ref84176` (`ReferenceID`) -) ENGINE=InnoDB AUTO_INCREMENT=21253 DEFAULT CHARSET=latin1; - - - -DROP TABLE IF EXISTS `Log`; -CREATE TABLE IF NOT EXISTS `Log` ( - `LogID` bigint(20) unsigned NOT NULL AUTO_INCREMENT, - `PersonnelID` smallint(5) unsigned DEFAULT NULL, - `ChangedTable` varchar(32) NOT NULL, - `PrimaryKey` varchar(32) NOT NULL, - `ChangedColumn` varchar(32) NOT NULL, - `ChangeDate` date DEFAULT NULL, - `ChangeTime` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, - `Description` varchar(256) DEFAULT NULL, - `Action` enum('I','D','U') NOT NULL, - `Old` varchar(512) NOT NULL, - `New` varchar(512) NOT NULL, - PRIMARY KEY (`LogID`), - KEY `PersonnelID` (`PersonnelID`) -) ENGINE=InnoDB AUTO_INCREMENT=4253 DEFAULT CHARSET=latin1; - - - -DROP TABLE IF EXISTS `LogLthValue`; -CREATE TABLE IF NOT EXISTS `LogLthValue` ( - `TreeID` int(10) unsigned NOT NULL, - `TreeHistoryID` int(10) unsigned NOT NULL, - `QuadratID` int(10) unsigned DEFAULT NULL, - `PlotID` int(10) unsigned DEFAULT NULL, - `Tag` char(10) DEFAULT NULL, - `X` float DEFAULT NULL, - `Y` float DEFAULT NULL, - `SubSpeciesID` int(10) unsigned DEFAULT NULL, - PRIMARY KEY (`TreeID`,`TreeHistoryID`) USING BTREE -) ENGINE=InnoDB DEFAULT CHARSET=latin1; - - - -DROP TABLE IF EXISTS `Measurement`; -CREATE TABLE IF NOT EXISTS `Measurement` ( - `MeasureID` int(10) unsigned NOT NULL AUTO_INCREMENT, - `CensusID` int(10) unsigned NOT NULL, - `TreeID` int(10) unsigned NOT NULL, - `StemID` int(10) unsigned NOT NULL, - `MeasurementTypeID` int(10) unsigned NOT NULL, - `Measure` varchar(256) NOT NULL, - `ExactDate` date NOT NULL, - `Comments` varchar(128) DEFAULT NULL, - PRIMARY KEY (`MeasureID`), - KEY `CensusID` (`CensusID`), - KEY `TreeID` (`TreeID`), - KEY `StemID` (`StemID`), - KEY `MeasurementTypeID` (`MeasurementTypeID`) -) ENGINE=InnoDB DEFAULT CHARSET=latin1; - - - -DROP TABLE IF EXISTS `MeasurementAttributes`; -CREATE TABLE IF NOT EXISTS `MeasurementAttributes` ( - `MAttID` int(10) unsigned NOT NULL AUTO_INCREMENT, - `MeasureID` int(10) unsigned NOT NULL, - `TSMID` int(10) unsigned NOT NULL, - PRIMARY KEY (`MAttID`), - KEY `MeasureID` (`MeasureID`), - KEY `TSMID` (`TSMID`) -) ENGINE=InnoDB DEFAULT CHARSET=latin1; - - - -DROP TABLE IF EXISTS `MeasurementType`; -CREATE TABLE IF NOT EXISTS `MeasurementType` ( - `MeasurementTypeID` int(10) unsigned NOT NULL AUTO_INCREMENT, - `UOM` varchar(32) NOT NULL, - `Type` varchar(256) DEFAULT NULL, - PRIMARY KEY (`MeasurementTypeID`) -) ENGINE=InnoDB DEFAULT CHARSET=latin1; - - - -DROP TABLE IF EXISTS `Personnel`; -CREATE TABLE IF NOT EXISTS `Personnel` ( - `PersonnelID` smallint(5) unsigned NOT NULL AUTO_INCREMENT, - `FirstName` varchar(32) DEFAULT NULL, - `LastName` varchar(32) NOT NULL, - PRIMARY KEY (`PersonnelID`) -) ENGINE=InnoDB AUTO_INCREMENT=55 DEFAULT CHARSET=latin1; - - - -DROP TABLE IF EXISTS `PersonnelRole`; -CREATE TABLE IF NOT EXISTS `PersonnelRole` ( - `PersonnelRoleID` int(10) unsigned NOT NULL AUTO_INCREMENT, - `PersonnelID` smallint(5) unsigned NOT NULL, - `RoleID` smallint(5) unsigned NOT NULL, - PRIMARY KEY (`PersonnelRoleID`), - KEY `RoleID` (`RoleID`), - KEY `PersonnelID` (`PersonnelID`) -) ENGINE=InnoDB AUTO_INCREMENT=49 DEFAULT CHARSET=latin1; - - - -DROP TABLE IF EXISTS `Quadrat`; -CREATE TABLE IF NOT EXISTS `Quadrat` ( - `PlotID` int(10) unsigned NOT NULL, - `QuadratName` char(8) DEFAULT NULL, - `Area` float unsigned DEFAULT NULL, - `IsStandardShape` enum('Y','N') NOT NULL, - `QuadratID` int(10) unsigned NOT NULL AUTO_INCREMENT, - PRIMARY KEY (`QuadratID`), - KEY `Ref69` (`PlotID`), - KEY `indQuadName` (`QuadratName`,`PlotID`) -) ENGINE=InnoDB AUTO_INCREMENT=641 DEFAULT CHARSET=latin1; - - - -DROP TABLE IF EXISTS `Reference`; -CREATE TABLE IF NOT EXISTS `Reference` ( - `ReferenceID` smallint(5) unsigned NOT NULL AUTO_INCREMENT, - `PublicationTitle` varchar(64) DEFAULT NULL, - `FullReference` varchar(256) DEFAULT NULL, - `DateofPublication` date DEFAULT NULL, - PRIMARY KEY (`ReferenceID`) -) ENGINE=InnoDB DEFAULT CHARSET=latin1; - - - -DROP TABLE IF EXISTS `RemeasAttribs`; -CREATE TABLE IF NOT EXISTS `RemeasAttribs` ( - `TSMID` int(10) unsigned NOT NULL, - `RemeasureID` int(10) unsigned NOT NULL, - `RmAttID` int(10) unsigned NOT NULL AUTO_INCREMENT, - PRIMARY KEY (`RmAttID`), - KEY `Ref2073` (`TSMID`), - KEY `RemeasureID` (`RemeasureID`) -) ENGINE=InnoDB DEFAULT CHARSET=latin1; - - - -DROP TABLE IF EXISTS `Remeasurement`; -CREATE TABLE IF NOT EXISTS `Remeasurement` ( - `CensusID` int(10) unsigned NOT NULL, - `StemID` int(10) unsigned NOT NULL, - `DBH` float DEFAULT NULL, - `HOM` float DEFAULT NULL, - `ExactDate` date DEFAULT NULL, - `RemeasureID` int(10) unsigned NOT NULL AUTO_INCREMENT, - PRIMARY KEY (`RemeasureID`), - KEY `Ref1957` (`StemID`), - KEY `Ref5106` (`CensusID`) -) ENGINE=InnoDB DEFAULT CHARSET=latin1; - - - -DROP TABLE IF EXISTS `ReviewChange`; -CREATE TABLE IF NOT EXISTS `ReviewChange` ( - `RID` int(4) unsigned NOT NULL, - `TreeID` int(10) unsigned NOT NULL, - `QuadratID` int(10) unsigned NOT NULL, - `PlotID` int(10) unsigned NOT NULL, - `FmSpeciesID` int(10) unsigned NOT NULL, - `ToSpeciesID` int(10) unsigned NOT NULL, - `ChangeCodeID` int(10) unsigned NOT NULL, - `Tag` char(10) DEFAULT NULL, - PRIMARY KEY (`RID`) -) ENGINE=InnoDB DEFAULT CHARSET=latin1; - - - -DROP TABLE IF EXISTS `ReviewNewSpecies`; -CREATE TABLE IF NOT EXISTS `ReviewNewSpecies` ( - `SpeciesID` int(10) unsigned NOT NULL, - `genusID` int(10) unsigned NOT NULL, - `ReferenceID` smallint(5) unsigned DEFAULT NULL, - `FullSpeciesName` char(128) DEFAULT NULL, - `Authority` varchar(128) DEFAULT NULL, - `IDLevel` char(8) DEFAULT NULL, - `FieldFamily` char(32) DEFAULT NULL, - `Description` varchar(128) DEFAULT NULL, - `PublicationTitle` varchar(128) DEFAULT NULL, - `FullReference` varchar(256) DEFAULT NULL, - `DateOfPublication` date DEFAULT NULL, - PRIMARY KEY (`SpeciesID`) -) ENGINE=InnoDB DEFAULT CHARSET=latin1; - - - -DROP TABLE IF EXISTS `RoleReference`; -CREATE TABLE IF NOT EXISTS `RoleReference` ( - `RoleID` smallint(5) unsigned NOT NULL AUTO_INCREMENT, - `Description` varchar(128) DEFAULT NULL, - PRIMARY KEY (`RoleID`) -) ENGINE=InnoDB AUTO_INCREMENT=11 DEFAULT CHARSET=latin1; - - - -DROP TABLE IF EXISTS `Site`; -CREATE TABLE IF NOT EXISTS `Site` ( - `PlotID` int(10) unsigned NOT NULL AUTO_INCREMENT, - `PlotName` char(64) DEFAULT NULL, - `LocationName` varchar(128) DEFAULT NULL, - `CountryID` smallint(5) unsigned NOT NULL, - `ShapeOfSite` char(32) DEFAULT NULL, - `DescriptionOfSite` varchar(128) DEFAULT NULL, - `Area` float unsigned NOT NULL, - `QDimX` float unsigned NOT NULL, - `QDimY` float unsigned NOT NULL, - `GUOM` varchar(32) NOT NULL, - `GZUOM` varchar(32) NOT NULL, - `PUOM` varchar(32) NOT NULL, - `QUOM` varchar(32) NOT NULL, - `GCoorCollected` varchar(32) DEFAULT NULL, - `PCoorCollected` varchar(32) DEFAULT NULL, - `QCoorCollected` varchar(32) DEFAULT NULL, - `IsStandardSize` enum('Y','N') DEFAULT NULL, - PRIMARY KEY (`PlotID`), - KEY `Ref87173` (`CountryID`) -) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=latin1; - - - -DROP TABLE IF EXISTS `Species`; -CREATE TABLE IF NOT EXISTS `Species` ( - `SpeciesID` int(10) unsigned NOT NULL AUTO_INCREMENT, - `CurrentTaxonFlag` smallint(6) DEFAULT NULL, - `ObsoleteTaxonFlag` smallint(6) DEFAULT NULL, - `GenusID` int(10) unsigned NOT NULL, - `ReferenceID` smallint(5) unsigned DEFAULT NULL, - `SpeciesName` char(64) DEFAULT NULL, - `Mnemonic` char(10) DEFAULT NULL, - `Authority` varchar(128) DEFAULT NULL, - `IDLEVEL` enum('subspecies','species','superspecies','genus','family','multiple','none','variety') DEFAULT NULL, - `FieldFamily` char(32) DEFAULT NULL, - `Description` varchar(128) DEFAULT NULL, - `Lifeform` enum('Emergent Tree','Tree','Midcanopy Tree','Understory Tree','Shrub','Herb','Liana') DEFAULT NULL, - `LocalName` varchar(128) DEFAULT NULL, - PRIMARY KEY (`SpeciesID`), - KEY `Ref26208` (`GenusID`), - KEY `indMnemonic` (`Mnemonic`), - KEY `Ref84209` (`ReferenceID`) -) ENGINE=InnoDB AUTO_INCREMENT=75 DEFAULT CHARSET=latin1; - - - -DROP TABLE IF EXISTS `SpeciesInventory`; -CREATE TABLE IF NOT EXISTS `SpeciesInventory` ( - `SpeciesInvID` int(10) unsigned NOT NULL AUTO_INCREMENT, - `CensusID` int(10) unsigned NOT NULL, - `PlotID` int(10) unsigned NOT NULL, - `SpeciesID` int(10) unsigned NOT NULL, - `SubSpeciesID` int(10) unsigned DEFAULT NULL, - PRIMARY KEY (`SpeciesInvID`), - KEY `Ref92198` (`SpeciesID`), - KEY `Ref93199` (`SubSpeciesID`), - KEY `Ref5222` (`CensusID`), - KEY `Ref642` (`PlotID`) -) ENGINE=InnoDB DEFAULT CHARSET=latin1; - - - -DROP TABLE IF EXISTS `Specimen`; -CREATE TABLE IF NOT EXISTS `Specimen` ( - `SpecimenID` int(10) unsigned NOT NULL AUTO_INCREMENT, - `TreeID` int(10) unsigned DEFAULT NULL, - `Collector` char(64) DEFAULT NULL, - `SpecimenNumber` int(10) unsigned DEFAULT NULL, - `SpeciesID` int(10) unsigned NOT NULL, - `SubSpeciesID` int(10) unsigned DEFAULT NULL, - `Herbarium` char(32) DEFAULT NULL, - `Voucher` smallint(5) unsigned DEFAULT NULL, - `CollectionDate` date DEFAULT NULL, - `DeterminedBy` char(64) DEFAULT NULL, - `Description` varchar(128) DEFAULT NULL, - PRIMARY KEY (`SpecimenID`), - KEY `Ref93194` (`SubSpeciesID`), - KEY `Ref92196` (`SpeciesID`), - KEY `Ref1171` (`TreeID`) -) ENGINE=InnoDB DEFAULT CHARSET=latin1; - - - -DROP TABLE IF EXISTS `SqlLog`; -CREATE TABLE IF NOT EXISTS `SqlLog` ( - `SqlID` int(4) NOT NULL DEFAULT '0', - `ToTableName` varchar(23) DEFAULT NULL, - `SqlStmt` varchar(16384) DEFAULT NULL, - PRIMARY KEY (`SqlID`) -) ENGINE=InnoDB DEFAULT CHARSET=latin1; - - - -DROP TABLE IF EXISTS `Stem`; -CREATE TABLE IF NOT EXISTS `Stem` ( - `StemID` int(10) unsigned NOT NULL AUTO_INCREMENT, - `TreeID` int(10) unsigned NOT NULL, - `StemTag` varchar(32) DEFAULT NULL, - `StemDescription` varchar(128) DEFAULT NULL, - `QuadratID` int(10) unsigned NOT NULL, - `StemNumber` int(10) unsigned DEFAULT NULL, - `Moved` enum('Y','N') NOT NULL DEFAULT 'N', - `GX` decimal(16,5) DEFAULT NULL, - `GY` decimal(16,5) DEFAULT NULL, - `GZ` decimal(16,5) DEFAULT NULL, - `PX` decimal(16,5) DEFAULT NULL, - `PY` decimal(16,5) DEFAULT NULL, - `PZ` decimal(16,5) DEFAULT NULL, - `QX` decimal(16,5) DEFAULT NULL, - `QY` decimal(16,5) DEFAULT NULL, - `QZ` decimal(16,5) DEFAULT NULL, - PRIMARY KEY (`StemID`), - KEY `Ref150` (`TreeID`) -) ENGINE=InnoDB AUTO_INCREMENT=72556 DEFAULT CHARSET=latin1; - - - -DROP TABLE IF EXISTS `SubSpecies`; -CREATE TABLE IF NOT EXISTS `SubSpecies` ( - `SubSpeciesID` int(10) unsigned NOT NULL AUTO_INCREMENT, - `SpeciesID` int(10) unsigned NOT NULL, - `CurrentTaxonFlag` smallint(6) DEFAULT NULL, - `ObsoleteTaxonFlag` smallint(6) DEFAULT NULL, - `SubSpeciesName` char(64) DEFAULT NULL, - `Mnemonic` char(10) DEFAULT NULL, - `Authority` varchar(128) DEFAULT NULL, - `InfraSpecificLevel` char(32) DEFAULT NULL, - PRIMARY KEY (`SubSpeciesID`), - KEY `Ref92193` (`SpeciesID`) -) ENGINE=InnoDB DEFAULT CHARSET=latin1; - - - -DROP TABLE IF EXISTS `TAX2temp`; -CREATE TABLE IF NOT EXISTS `TAX2temp` ( - `SpeciesID` int(11) NOT NULL, - `ObsoleteSpeciesID` int(11) NOT NULL, - `ObsoleteGenusName` char(32) DEFAULT NULL, - `ObsoleteSpeciesName` char(64) DEFAULT NULL, - `ObsoleteGenSpeName` char(128) DEFAULT NULL, - `Description` char(128) DEFAULT NULL, - `ChangeDate` date NOT NULL, - `Family` char(32) DEFAULT NULL, - `Genus` char(32) DEFAULT NULL, - `SpeciesName` char(64) DEFAULT NULL, - `Authority` char(128) DEFAULT NULL, - `IDLevel` char(8) DEFAULT NULL, - PRIMARY KEY (`SpeciesID`,`ObsoleteSpeciesID`,`ChangeDate`) -) ENGINE=InnoDB DEFAULT CHARSET=latin1; - - - -DROP TABLE IF EXISTS `TAX3temp`; -CREATE TABLE IF NOT EXISTS `TAX3temp` ( - `PlotSpeciesID` int(11) NOT NULL AUTO_INCREMENT, - `PlotID` int(11) NOT NULL, - `SpeciesID` int(11) NOT NULL, - `SubSpeciesID` int(11) DEFAULT NULL, - PRIMARY KEY (`PlotSpeciesID`), - KEY `TAX3Plot` (`PlotID`,`SpeciesID`,`SubSpeciesID`) -) ENGINE=InnoDB AUTO_INCREMENT=71 DEFAULT CHARSET=latin1; - - - -DROP TABLE IF EXISTS `TSMAttributes`; -CREATE TABLE IF NOT EXISTS `TSMAttributes` ( - `TSMID` int(10) unsigned NOT NULL AUTO_INCREMENT, - `TSMCode` char(10) NOT NULL, - `Description` varchar(128) NOT NULL, - `Status` enum('alive','alive-not measured','dead','missing','broken below','stem dead') DEFAULT NULL, - PRIMARY KEY (`TSMID`) -) ENGINE=InnoDB AUTO_INCREMENT=25 DEFAULT CHARSET=latin1; - - - -DROP TABLE IF EXISTS `TempQuadrat`; -CREATE TABLE IF NOT EXISTS `TempQuadrat` ( - `QuadratID` int(10) unsigned NOT NULL AUTO_INCREMENT, - `QuadratName` char(8) DEFAULT NULL, - `StartX` float DEFAULT NULL, - `StartY` float DEFAULT NULL, - `DimX` float DEFAULT NULL, - `DimY` float DEFAULT NULL, - `CensusID` int(10) unsigned NOT NULL, - `PlotCensusNumber` int(10) unsigned NOT NULL, - `PlotID` int(10) unsigned DEFAULT NULL, - PRIMARY KEY (`QuadratID`) -) ENGINE=InnoDB DEFAULT CHARSET=latin1; - - - -DROP TABLE IF EXISTS `TempQuadratDates`; -CREATE TABLE IF NOT EXISTS `TempQuadratDates` ( - `TempQuadID` int(10) unsigned NOT NULL AUTO_INCREMENT, - `QuadratName` varchar(12) DEFAULT NULL, - `PrevDate` date DEFAULT NULL, - PRIMARY KEY (`TempQuadID`) -) ENGINE=InnoDB DEFAULT CHARSET=latin1; - - - -DROP TABLE IF EXISTS `TempSpecies`; -CREATE TABLE IF NOT EXISTS `TempSpecies` ( - `TempID` int(10) unsigned NOT NULL AUTO_INCREMENT, - `Mnemonic` varchar(10) DEFAULT NULL, - `Genus` varchar(32) DEFAULT NULL, - `SpeciesName` varchar(64) DEFAULT NULL, - `FieldFamily` varchar(32) DEFAULT NULL, - `Authority` varchar(128) DEFAULT NULL, - `IDLevel` varchar(8) DEFAULT NULL, - `GenusID` int(10) DEFAULT NULL, - `SpeciesID` int(10) DEFAULT NULL, - `Family` char(32) DEFAULT NULL, - `Errors` varchar(256) DEFAULT NULL, - PRIMARY KEY (`TempID`), - KEY `indexGenus` (`Genus`) -) ENGINE=InnoDB DEFAULT CHARSET=latin1; - - - -DROP TABLE IF EXISTS `TempSpeciesError`; -CREATE TABLE IF NOT EXISTS `TempSpeciesError` ( - `TempID` int(10) unsigned NOT NULL DEFAULT '0', - `Mnemonic` varchar(10) DEFAULT NULL, - `Genus` varchar(32) DEFAULT NULL, - `SpeciesName` varchar(64) DEFAULT NULL, - `FieldFamily` varchar(32) DEFAULT NULL, - `Authority` varchar(128) DEFAULT NULL, - `IDLevel` varchar(8) DEFAULT NULL, - `GenusID` int(10) DEFAULT NULL, - `SpeciesID` int(10) DEFAULT NULL, - `Family` char(32) DEFAULT NULL, - `Errors` varchar(256) DEFAULT NULL -) ENGINE=InnoDB DEFAULT CHARSET=latin1; - - - -DROP TABLE IF EXISTS `Tree`; -CREATE TABLE IF NOT EXISTS `Tree` ( - `TreeID` int(10) unsigned NOT NULL AUTO_INCREMENT, - `Tag` char(10) DEFAULT NULL, - `SpeciesID` int(10) unsigned NOT NULL, - `SubSpeciesID` int(10) unsigned DEFAULT NULL, - PRIMARY KEY (`TreeID`), - KEY `Ref92217` (`SpeciesID`), - KEY `Ref93219` (`SubSpeciesID`), - KEY `indTreeTag` (`Tag`) -) ENGINE=InnoDB AUTO_INCREMENT=49311 DEFAULT CHARSET=latin1; - - - -DROP TABLE IF EXISTS `TreeAttributes`; -CREATE TABLE IF NOT EXISTS `TreeAttributes` ( - `CensusID` int(10) unsigned NOT NULL, - `TreeID` int(10) unsigned NOT NULL, - `TSMID` int(10) unsigned NOT NULL, - `TAttID` int(10) unsigned NOT NULL AUTO_INCREMENT, - PRIMARY KEY (`TAttID`), - KEY `Ref163` (`TreeID`), - KEY `Ref2064` (`TSMID`), - KEY `Ref5107` (`CensusID`) -) ENGINE=InnoDB DEFAULT CHARSET=latin1; - - - -DROP TABLE IF EXISTS `TreeTaxChange`; -CREATE TABLE IF NOT EXISTS `TreeTaxChange` ( - `ChangeCodeID` int(10) unsigned NOT NULL, - `Description` varchar(128) DEFAULT NULL, - PRIMARY KEY (`ChangeCodeID`) -) ENGINE=InnoDB DEFAULT CHARSET=latin1; - - - -DROP TABLE IF EXISTS `ViewFullTable`; -CREATE TABLE IF NOT EXISTS `ViewFullTable` ( - `DBHID` int(11) NOT NULL, - `PlotName` varchar(35) DEFAULT NULL, - `PlotID` int(11) DEFAULT NULL, - `Family` char(32) DEFAULT NULL, - `Genus` char(32) DEFAULT NULL, - `SpeciesName` char(64) DEFAULT NULL, - `Mnemonic` char(10) DEFAULT NULL, - `Subspecies` char(64) DEFAULT NULL, - `SpeciesID` int(11) DEFAULT NULL, - `SubspeciesID` int(11) DEFAULT NULL, - `QuadratName` varchar(12) DEFAULT NULL, - `QuadratID` int(11) DEFAULT NULL, - `PX` decimal(16,5) DEFAULT NULL, - `PY` decimal(16,5) DEFAULT NULL, - `QX` decimal(16,5) DEFAULT NULL, - `QY` decimal(16,5) DEFAULT NULL, - `TreeID` int(11) DEFAULT NULL, - `Tag` char(10) DEFAULT NULL, - `StemID` int(11) DEFAULT NULL, - `StemNumber` int(11) DEFAULT NULL, - `StemTag` varchar(32) DEFAULT NULL, - `PrimaryStem` char(20) DEFAULT NULL, - `CensusID` int(11) DEFAULT NULL, - `PlotCensusNumber` int(11) DEFAULT NULL, - `DBH` float DEFAULT NULL, - `HOM` decimal(10,2) DEFAULT NULL, - `ExactDate` date DEFAULT NULL, - `Date` int(11) DEFAULT NULL, - `ListOfTSM` varchar(256) DEFAULT NULL, - `HighHOM` tinyint(1) DEFAULT NULL, - `LargeStem` tinyint(1) DEFAULT NULL, - `Status` enum('alive','dead','stem dead','broken below','omitted','missing') DEFAULT 'alive', - PRIMARY KEY (`DBHID`), - KEY `SpeciesID` (`SpeciesID`), - KEY `SubspeciesID` (`SubspeciesID`), - KEY `QuadratID` (`QuadratID`), - KEY `TreeID` (`TreeID`), - KEY `StemID` (`StemID`), - KEY `Tag` (`Tag`), - KEY `CensusID` (`CensusID`), - KEY `Genus` (`Genus`,`SpeciesName`), - KEY `Mnemonic` (`Mnemonic`), - KEY `CensusID_2` (`CensusID`), - KEY `PlotCensusNumber` (`PlotCensusNumber`), - KEY `StemTag` (`StemTag`), - KEY `DBH` (`DBH`), - KEY `Date` (`Date`), - KEY `Date_2` (`Date`), - KEY `ListOfTSM` (`ListOfTSM`), - KEY `Status` (`Status`), - KEY `HighHOM` (`HighHOM`) -) ENGINE=MyISAM DEFAULT CHARSET=latin1; - - - -DROP TABLE IF EXISTS `ViewTaxonomy`; -CREATE TABLE IF NOT EXISTS `ViewTaxonomy` ( - `ViewID` int(11) NOT NULL AUTO_INCREMENT, - `SpeciesID` int(11) DEFAULT NULL, - `SubspeciesID` int(11) DEFAULT NULL, - `Family` char(32) DEFAULT NULL, - `Mnemonic` char(10) DEFAULT NULL, - `Genus` char(32) DEFAULT NULL, - `SpeciesName` char(64) DEFAULT NULL, - `Rank` char(20) DEFAULT NULL, - `Subspecies` char(64) DEFAULT NULL, - `Authority` char(128) DEFAULT NULL, - `IDLevel` char(12) DEFAULT NULL, - `subspMnemonic` char(10) DEFAULT NULL, - `subspAuthority` varchar(120) DEFAULT NULL, - `FieldFamily` char(32) DEFAULT NULL, - `Lifeform` char(20) DEFAULT NULL, - `Description` text, - `wsg` decimal(10,6) DEFAULT NULL, - `wsglevel` enum('local','species','genus','family','none') DEFAULT NULL, - `ListOfOldNames` varchar(255) DEFAULT NULL, - `Specimens` varchar(255) DEFAULT NULL, - `Reference` varchar(255) DEFAULT NULL, - PRIMARY KEY (`ViewID`), - KEY `SpeciesID` (`SpeciesID`), - KEY `SubspeciesID` (`SubspeciesID`), - KEY `IDLevel` (`IDLevel`) -) ENGINE=MyISAM AUTO_INCREMENT=76 DEFAULT CHARSET=latin1; - - - -DROP TABLE IF EXISTS `stagePersonnel`; -CREATE TABLE IF NOT EXISTS `stagePersonnel` ( - `PersonnelRoleID` int(10) unsigned NOT NULL AUTO_INCREMENT, - `FirstName` char(30) DEFAULT NULL, - `LastName` char(32) DEFAULT NULL, - `RoleID` int(11) DEFAULT NULL, - `PersonnelID` int(11) DEFAULT NULL, - `Role` char(25) DEFAULT NULL, - PRIMARY KEY (`PersonnelRoleID`) -) ENGINE=InnoDB AUTO_INCREMENT=19 DEFAULT CHARSET=latin1; -``` -{collapsible="true" collapsed-title="ctfsweb generation"} - -2. For our `forestgeo` example schema, you can use this SQL script: -```SQL -set foreign_key_checks = 0; -drop table if exists attributes; -create table attributes -( - Code varchar(10) not null - primary key, - Description text null, - Status enum ('alive', 'alive-not measured', 'dead', 'stem dead', 'broken below', 'omitted', 'missing') default 'alive' null -); - -drop table if exists personnel; -create table personnel -( - PersonnelID int auto_increment - primary key, - FirstName varchar(50) null, - LastName varchar(50) null, - Role varchar(150) null comment 'semicolon-separated, like attributes in coremeasurements' -); - -drop table if exists plots; -create table plots -( - PlotID int auto_increment - primary key, - PlotName text null, - LocationName text null, - CountryName text null, - DimensionX int null, - DimensionY int null, - Area decimal(10, 6) null, - GlobalX decimal(10, 6) null, - GlobalY decimal(10, 6) null, - GlobalZ decimal(10, 6) null, - Unit enum ('km', 'hm', 'dam', 'm', 'dm', 'cm', 'mm') default 'm' null, - PlotShape text null, - PlotDescription text null -); - -drop table if exists census; -create table census -( - CensusID int auto_increment - primary key, - PlotID int null, - StartDate date null, - EndDate date null, - Description text null, - PlotCensusNumber int null, - constraint Census_Plots_PlotID_fk - foreign key (PlotID) references plots (PlotID) - on update cascade -); - -drop table if exists quadrats; -create table quadrats -( - QuadratID int auto_increment - primary key, - PlotID int null, - CensusID int null, - QuadratName text null, - StartX decimal(10, 6) null, - StartY decimal(10, 6) null, - DimensionX int null, - DimensionY int null, - Area decimal(10, 6) null, - QuadratShape text null, - Unit enum ('km', 'hm', 'dam', 'm', 'dm', 'cm', 'mm') default 'm' null, - constraint Quadrats_Plots_FK - foreign key (PlotID) references plots (PlotID) - on update cascade, - constraint quadrats_census_CensusID_fk - foreign key (CensusID) references census (CensusID) - on update cascade -); - -drop table if exists quadratpersonnel; -create table quadratpersonnel -( - QuadratPersonnelID int auto_increment - primary key, - QuadratID int not null, - PersonnelID int not null, - AssignedDate date null, - constraint fk_QuadratPersonnel_Personnel - foreign key (PersonnelID) references personnel (PersonnelID) - on update cascade, - constraint fk_QuadratPersonnel_Quadrats - foreign key (QuadratID) references quadrats (QuadratID) - on update cascade -); - -drop table if exists reference; -create table reference -( - ReferenceID int auto_increment - primary key, - PublicationTitle varchar(64) null, - FullReference text null, - DateOfPublication date null, - Citation varchar(50) null -); - -drop table if exists family; -create table family -( - FamilyID int auto_increment - primary key, - Family varchar(32) null, - ReferenceID int null, - constraint Family - unique (Family), - constraint Family_Reference_ReferenceID_fk - foreign key (ReferenceID) references reference (ReferenceID) - on update cascade -); -drop table if exists genus; -create table genus -( - GenusID int auto_increment - primary key, - FamilyID int null, - Genus varchar(32) null, - ReferenceID int null, - GenusAuthority varchar(32) null, - constraint Genus - unique (Genus), - constraint Genus_Family_FamilyID_fk - foreign key (FamilyID) references family (FamilyID) - on update cascade, - constraint Genus_Reference_ReferenceID_fk - foreign key (ReferenceID) references reference (ReferenceID) - on update cascade -); -drop table if exists species; -create table species -( - SpeciesID int auto_increment - primary key, - GenusID int null, - SpeciesCode varchar(25) null, - CurrentTaxonFlag bit null, - ObsoleteTaxonFlag bit null, - SpeciesName varchar(64) null, - SubspeciesName varchar(255) null, - IDLevel varchar(20) null, - SpeciesAuthority varchar(128) null, - SubspeciesAuthority varchar(255) null, - FieldFamily varchar(32) null, - Description text null, - ReferenceID int null, - constraint SpeciesCode - unique (SpeciesCode), - constraint Species_SpeciesCode - unique (SpeciesCode), - constraint Species_Genus_GenusID_fk - foreign key (GenusID) references genus (GenusID) - on update cascade, - constraint Species_Reference_ReferenceID_fk - foreign key (ReferenceID) references reference (ReferenceID) - on update cascade -); -drop table if exists specieslimits; -create table specieslimits -( - SpeciesLimitID int auto_increment - primary key, - SpeciesCode varchar(25) null, - LimitType enum ('DBH', 'HOM') null, - UpperBound decimal(10, 6) null, - LowerBound decimal(10, 6) null, - Unit enum ('km', 'hm', 'dam', 'm', 'dm', 'cm', 'mm') default 'm' null, - constraint specieslimits_ibfk_1 - foreign key (SpeciesCode) references species (SpeciesCode) - on update cascade -); -drop table if exists subquadrats; -create table subquadrats -( - SubquadratID int auto_increment - primary key, - SubquadratName varchar(25) null, - QuadratID int null, - DimensionX int default 5 null, - DimensionY int default 5 null, - X int null comment 'corner x index value (standardized)', - Y int null comment 'corner y index value (standardized)', - Unit enum ('km', 'hm', 'dam', 'm', 'dm', 'cm', 'mm') default 'm' null, - Ordering int null comment 'SQindex should tell you in which order the subquads are surveyed. This will be useful later.', - constraint SQName - unique (SubquadratName), - constraint subquadrats_ibfk_1 - foreign key (QuadratID) references quadrats (QuadratID) - on update cascade -); - -create index QuadratID - on subquadrats (QuadratID); - -drop table if exists trees; -create table trees -( - TreeID int auto_increment - primary key, - TreeTag varchar(10) null, - SpeciesID int null, - constraint TreeTag - unique (TreeTag), - constraint Trees_Species_SpeciesID_fk - foreign key (SpeciesID) references species (SpeciesID) - on update cascade -); -drop table if exists stems; -create table stems -( - StemID int auto_increment - primary key, - TreeID int null, - QuadratID int null, - SubquadratID int null, - StemNumber int null, - StemTag varchar(10) null, - LocalX decimal(10, 6) null, - LocalY decimal(10, 6) null, - Unit enum ('km', 'hm', 'dam', 'm', 'dm', 'cm', 'mm') default 'm' null, - Moved bit null, - StemDescription text null, - constraint StemTag - unique (StemTag), - constraint FK_Stems_Trees - foreign key (TreeID) references trees (TreeID) - on update cascade, - constraint stems_quadrats_QuadratID_fk - foreign key (QuadratID) references quadrats (QuadratID) - on update cascade, - constraint stems_subquadrats_SQID_fk - foreign key (SubquadratID) references subquadrats (SubquadratID) - on update cascade -); -drop table if exists coremeasurements; -create table coremeasurements -( - CoreMeasurementID int auto_increment - primary key, - CensusID int null, - PlotID int null, - QuadratID int null, - SubQuadratID int null, - TreeID int null, - StemID int null, - PersonnelID int null, - IsValidated bit default b'0' null, - MeasurementDate date null, - MeasuredDBH decimal(10, 6) null, - DBHUnit enum ('km', 'hm', 'dam', 'm', 'dm', 'cm', 'mm') default 'm' null, - MeasuredHOM decimal(10, 6) null, - HOMUnit enum ('km', 'hm', 'dam', 'm', 'dm', 'cm', 'mm') default 'm' null, - Description text null, - UserDefinedFields text null, - constraint CoreMeasurements_Census_CensusID_fk - foreign key (CensusID) references census (CensusID) - on update cascade, - constraint CoreMeasurements_Personnel_PersonnelID_fk - foreign key (PersonnelID) references personnel (PersonnelID) - on update cascade, - constraint FK_CoreMeasurements_Stems - foreign key (StemID) references stems (StemID) - on update cascade, - constraint FK_CoreMeasurements_Trees - foreign key (TreeID) references trees (TreeID) - on update cascade, - constraint coremeasurements_plots_PlotID_fk - foreign key (PlotID) references plots (PlotID) - on update cascade, - constraint coremeasurements_quadrats_QuadratID_fk - foreign key (QuadratID) references quadrats (QuadratID) - on update cascade, - constraint coremeasurements_subquadrats_SQID_fk - foreign key (SubQuadratID) references subquadrats (SubquadratID) - on update cascade -); -drop table if exists cmattributes; -create table cmattributes -( - CMAID int auto_increment - primary key, - CoreMeasurementID int null, - Code varchar(10) null, - constraint CMAttributes_Attributes_Code_fk - foreign key (Code) references attributes (Code) - on update cascade, - constraint CMAttributes_CoreMeasurements_CoreMeasurementID_fk - foreign key (CoreMeasurementID) references coremeasurements (CoreMeasurementID) - on update cascade -); - -create index idx_censusid - on coremeasurements (CensusID); - -create index idx_quadratid - on coremeasurements (SubQuadratID); - -create index idx_stemid - on coremeasurements (StemID); - -create index idx_treeid - on coremeasurements (TreeID); - -create index idx_stemid - on stems (StemID); -drop table if exists validationchangelog; -create table validationchangelog -( - ValidationRunID int auto_increment - primary key, - ProcedureName varchar(255) not null, - RunDateTime datetime default CURRENT_TIMESTAMP not null, - TargetRowID int null, - ValidationOutcome enum ('Passed', 'Failed') null, - ErrorMessage text null, - ValidationCriteria text null, - MeasuredValue varchar(255) null, - ExpectedValueRange varchar(255) null, - AdditionalDetails text null -); -drop table if exists validationerrors; -create table validationerrors -( - ValidationErrorID int auto_increment - primary key, - ValidationErrorDescription text null -); -drop table if exists cmverrors; -create table cmverrors -( - CMVErrorID int auto_increment - primary key, - CoreMeasurementID int null, - ValidationErrorID int null, - constraint CMVErrors_CoreMeasurements_CoreMeasurementID_fk - foreign key (CoreMeasurementID) references coremeasurements (CoreMeasurementID) - on update cascade, - constraint cmverrors_validationerrors_ValidationErrorID_fk - foreign key (ValidationErrorID) references validationerrors (ValidationErrorID) - on update cascade -); - -set foreign_key_checks = 1; -``` -{collapsible="true" collapsed-title="forestgeo generation"} - -Make sure you run these while pointing to the correct databases! While we're using example values, you should use -appropriately named schemas for both of these cases (i.e., `ctfsweb_scbi` and `forestgeo_scbi`, or `ctfsweb_bci` and -`forestgeo_bci`). -### Importing Flat File to `ctfsweb` Schema -Next, we need to move all of the data from the site flat file we have on hand.
-Start by creating and running the following Bash script: -```Bash -#!/bin/bash - -# List of tables to extract INSERT INTO statements -tables=("Census" "CensusQuadrat" "Coordinates" "Country" "CurrentObsolete" "DBH" "DBHAttributes" "DataCollection" "Family" "FeatureTypes" "Features" "Genus" "Log" "LogLthValue" "Measurement" "MeasurementAttributes" "MeasurementType" "Personnel" "PersonnelRole" "Quadrat" "Reference" "RemeasAttribs" "Remeasurement" "ReviewChange" "ReviewNewSpecies" "RoleReference" "Site" "Species" "SpeciesInventory" "Specimen" "SqlLog" "Stem" "SubSpecies" "TAX2temp" "TAX3temp" "TSMAttributes" "TempQuadrat" "TempQuadratDates" "TempSpecies" "TempSpeciesError" "Tree" "TreeAttributes" "TreeTaxChange" "ViewFullTable" "ViewTaxonomy" "stagePersonnel") - -# Path to the source SQL file -source_file="crc 2.sql" - -# Output directory for the individual insert files -output_dir="insert_statements" -mkdir -p "$output_dir" - -for table in "${tables[@]}"; do - perl -0777 -ne " - if (/INSERT\s+INTO\s+\`${table}\`\s+VALUES\s+\(.*?\);.*?(?=\/\*\!40000 ALTER TABLE)/s) { - print \"SET FOREIGN_KEY_CHECKS = 0;\\n\$&\\nSET FOREIGN_KEY_CHECKS = 1;\\n\" - } - " "$source_file" > "$output_dir/${table}_insert.sql" -done -``` -{collapsible="true" collapsed-title="break down flat file"} -> Make sure you place this script in the same directory as your flat file, and change the `source_file` name to your -> flat file's name! -{style="warning"} - -After you run this script, you should see a new directory called `insert_statements` in your directory, which should -contain a collection of insertion SQL scripts.
-> Verify that those scripts are correctly populated by referencing the flat file before continuing. -{style="note"} - -Now that you have the `insert_statements` directory, run the following Bash script to load them all into a single -runnable SQL file that's been cleaned to only contain the core insertion statements and also toggles -foreign_key_checks before and after execution to ensure a clean insertion: -```Bash -#!/bin/bash - -# Output directory for the individual insert files -output_dir="insert_statements" - -# Combined file -combined_file="all_inserts.sql" - -echo "SET FOREIGN_KEY_CHECKS = 0;" > "$combined_file" -for file in "$output_dir"/*.sql; do - cat "$file" >> "$combined_file" -done -echo "SET FOREIGN_KEY_CHECKS = 1;" >> "$combined_file" -``` -{collapsible="true" collapsed-title="insertion statement assembly"} -> Make sure your `output_dir` reference correctly points to the `insert_statements` directory and isn't inside it.
-{style="warning"} - -Once this completes, you should end up with a single script called `all_inserts.sql` that contains all of the -insertion statements originally incorporated into your flat file. Now that you have this, access your MySQL server -and navigate to the schema you are using to hold the CTFSWeb-formatted data. - -Run the `all_inserts.sql` script, and verify that the tables are correctly populated by checking the flat file and -then the tables. - -### Migrating `ctfsweb` to `forestgeo` - -You're almost done! The only part left is to actually migrate the data in the `ctfsweb` schema to the `forestgeo` -schema. Use this script: -```SQL -SET foreign_key_checks = 0; - --- Create temporary mapping tables -CREATE TABLE IF NOT EXISTS temp_census_mapping ( - old_CensusID INT, - new_CensusID INT -); - -CREATE TABLE IF NOT EXISTS temp_plot_mapping ( - old_PlotID INT, - new_PlotID INT -); - -CREATE TABLE IF NOT EXISTS temp_personnel_mapping ( - old_PersonnelID INT, - new_PersonnelID INT -); - -CREATE TABLE IF NOT EXISTS temp_quadrat_mapping ( - old_QuadratID INT, - new_QuadratID INT -); - -CREATE TABLE IF NOT EXISTS temp_genus_mapping ( - old_GenusID INT, - new_GenusID INT -); - -CREATE TABLE IF NOT EXISTS temp_species_mapping ( - old_SpeciesID INT, - new_SpeciesID INT -); - -CREATE TABLE IF NOT EXISTS temp_stem_mapping ( - old_StemID INT, - new_StemID INT -); - --- Insert into plots and populate mapping table with GROUP BY to avoid duplicates -INSERT INTO forestgeo_scbi.plots (PlotName, LocationName, CountryName, Area, GlobalX, GlobalY, GlobalZ, Unit, PlotDescription) -SELECT LEFT(s.PlotName, 65535), LEFT(s.LocationName, 65535), c.CountryName, s.Area, - MIN(co.GX), MIN(co.GY), MIN(co.GZ), IF(s.PUOM IN ('km', 'hm', 'dam', 'm', 'dm', 'cm', 'mm'), s.PUOM, 'm'), LEFT(s.DescriptionOfSite, 65535) -FROM ctfsweb_scbi.Site s -LEFT JOIN ctfsweb_scbi.Country c ON s.CountryID = c.CountryID -LEFT JOIN ctfsweb_scbi.Coordinates co ON s.PlotID = co.PlotID -WHERE EXISTS (SELECT 1 FROM ctfsweb_scbi.Site s) -GROUP BY s.PlotID, s.PlotName, s.LocationName, c.CountryName, s.Area, s.PUOM, s.DescriptionOfSite; - --- Properly capture the last insert ID for each row inserted -INSERT INTO temp_plot_mapping (old_PlotID, new_PlotID) -SELECT s.PlotID, ( - SELECT PlotID - FROM forestgeo_scbi.plots - WHERE PlotName = LEFT(s.PlotName, 65535) - AND LocationName = LEFT(s.LocationName, 65535) - AND CountryName = c.CountryName - AND Area = s.Area - AND GlobalX = MIN(co.GX) - AND GlobalY = MIN(co.GY) - AND GlobalZ = MIN(co.GZ) - AND Unit = IF(s.PUOM IN ('km', 'hm', 'dam', 'm', 'dm', 'cm', 'mm'), s.PUOM, 'm') - AND PlotDescription = LEFT(s.DescriptionOfSite, 65535) - LIMIT 1 -) -FROM ctfsweb_scbi.Site s -LEFT JOIN ctfsweb_scbi.Country c ON s.CountryID = c.CountryID -LEFT JOIN ctfsweb_scbi.Coordinates co ON s.PlotID = co.PlotID -WHERE EXISTS (SELECT 1 FROM ctfsweb_scbi.Site s) -GROUP BY s.PlotID, s.PlotName, s.LocationName, c.CountryName, s.Area, s.PUOM, s.DescriptionOfSite; - --- Insert into personnel and populate mapping table -INSERT INTO forestgeo_scbi.personnel (FirstName, LastName, Role) -SELECT p.FirstName, p.LastName, GROUP_CONCAT(rr.Description SEPARATOR ', ') -FROM ctfsweb_scbi.Personnel p -JOIN ctfsweb_scbi.PersonnelRole pr ON p.PersonnelID = pr.PersonnelID -JOIN ctfsweb_scbi.RoleReference rr ON pr.RoleID = rr.RoleID -GROUP BY p.PersonnelID, p.FirstName, p.LastName; - -INSERT INTO temp_personnel_mapping (old_PersonnelID, new_PersonnelID) -SELECT p.PersonnelID, ( - SELECT PersonnelID - FROM forestgeo_scbi.personnel - WHERE FirstName = p.FirstName - AND LastName = p.LastName - LIMIT 1 -) -FROM ctfsweb_scbi.Personnel p -GROUP BY p.PersonnelID, p.FirstName, p.LastName; - --- Insert into census and populate mapping table -INSERT INTO forestgeo_scbi.census (PlotID, StartDate, EndDate, Description, PlotCensusNumber) -SELECT PlotID, StartDate, EndDate, LEFT(Description, 65535), PlotCensusNumber -FROM ctfsweb_scbi.Census -WHERE EXISTS (SELECT 1 FROM ctfsweb_scbi.Census); - -INSERT INTO temp_census_mapping (old_CensusID, new_CensusID) -SELECT CensusID, ( - SELECT CensusID - FROM forestgeo_scbi.census - WHERE PlotID = c.PlotID - AND StartDate = c.StartDate - AND EndDate = c.EndDate - AND Description = LEFT(c.Description, 65535) - AND PlotCensusNumber = c.PlotCensusNumber - LIMIT 1 -) -FROM ctfsweb_scbi.Census c -WHERE EXISTS (SELECT 1 FROM ctfsweb_scbi.Census); - --- Insert into quadrats and populate mapping table -INSERT INTO forestgeo_scbi.quadrats (PlotID, CensusID, QuadratName, StartX, StartY, DimensionX, DimensionY, Area, QuadratShape, Unit) -SELECT q.PlotID, cq.CensusID, LEFT(q.QuadratName, 65535), MIN(co.PX), MIN(co.PY), s.QDimX, s.QDimY, q.Area, - CASE WHEN q.IsStandardShape = 'Y' THEN 'standard' ELSE 'not standard' END, - IFNULL(s.QUOM, 'm') AS Unit -FROM ctfsweb_scbi.Quadrat q -LEFT JOIN ctfsweb_scbi.CensusQuadrat cq ON q.QuadratID = cq.QuadratID -LEFT JOIN ctfsweb_scbi.Coordinates co ON q.QuadratID = co.QuadratID -LEFT JOIN ctfsweb_scbi.Site s ON q.PlotID = s.PlotID -WHERE EXISTS (SELECT 1 FROM ctfsweb_scbi.Quadrat q) -GROUP BY q.QuadratID, q.PlotID, cq.CensusID, q.QuadratName, s.QDimX, s.QDimY, q.Area, q.IsStandardShape, s.QUOM; - -INSERT INTO temp_quadrat_mapping (old_QuadratID, new_QuadratID) -SELECT q.QuadratID, ( - SELECT QuadratID - FROM forestgeo_scbi.quadrats - WHERE PlotID = q.PlotID - AND CensusID = cq.CensusID - AND QuadratName = LEFT(q.QuadratName, 65535) - AND StartX = MIN(co.PX) - AND StartY = MIN(co.PY) - AND DimensionX = s.QDimX - AND DimensionY = s.QDimY - AND Area = q.Area - AND QuadratShape = CASE WHEN q.IsStandardShape = 'Y' THEN 'standard' ELSE 'not standard' END - AND Unit = IFNULL(s.QUOM, 'm') - LIMIT 1 -) -FROM ctfsweb_scbi.Quadrat q -LEFT JOIN ctfsweb_scbi.CensusQuadrat cq ON q.QuadratID = cq.QuadratID -LEFT JOIN ctfsweb_scbi.Coordinates co ON q.QuadratID = co.QuadratID -LEFT JOIN ctfsweb_scbi.Site s ON q.PlotID = s.PlotID -WHERE EXISTS (SELECT 1 FROM ctfsweb_scbi.Quadrat q) -GROUP BY q.QuadratID, q.PlotID, cq.CensusID, q.QuadratName, s.QDimX, s.QDimY, q.Area, q.IsStandardShape, s.QUOM; - --- Insert into genus and populate mapping table -INSERT INTO forestgeo_scbi.genus (FamilyID, Genus, ReferenceID) -SELECT FamilyID, Genus, ReferenceID -FROM ctfsweb_scbi.Genus -WHERE EXISTS (SELECT 1 FROM ctfsweb_scbi.Genus); - -INSERT INTO temp_genus_mapping (old_GenusID, new_GenusID) -SELECT GenusID, ( - SELECT GenusID - FROM forestgeo_scbi.genus - WHERE FamilyID = g.FamilyID - AND Genus = g.Genus - AND ReferenceID = g.ReferenceID - LIMIT 1 -) -FROM ctfsweb_scbi.Genus g -WHERE EXISTS (SELECT 1 FROM ctfsweb_scbi.Genus); - --- Insert into species and populate mapping table -INSERT INTO forestgeo_scbi.species (GenusID, SpeciesCode, CurrentTaxonFlag, ObsoleteTaxonFlag, SpeciesName, SubspeciesName, IDLevel, SpeciesAuthority, SubspeciesAuthority, FieldFamily, Description, ReferenceID) -SELECT sp.GenusID, sp.Mnemonic, sp.CurrentTaxonFlag, sp.ObsoleteTaxonFlag, sp.Mnemonic, MIN(subs.SubSpeciesName), sp.IDLevel, sp.Authority, MIN(subs.Authority), sp.FieldFamily, LEFT(sp.Description, 65535), sp.ReferenceID -FROM ctfsweb_scbi.Species sp -LEFT JOIN ctfsweb_scbi.SubSpecies subs ON sp.SpeciesID = subs.SpeciesID -LEFT JOIN ctfsweb_scbi.Reference ref ON sp.ReferenceID = ref.ReferenceID -WHERE EXISTS (SELECT 1 FROM ctfsweb_scbi.Species) -GROUP BY sp.SpeciesID, sp.GenusID, sp.Mnemonic, sp.CurrentTaxonFlag, sp.ObsoleteTaxonFlag, sp.IDLevel, sp.Authority, sp.FieldFamily, sp.Description, sp.ReferenceID; - -INSERT INTO temp_species_mapping (old_SpeciesID, new_SpeciesID) -SELECT sp.SpeciesID, ( - SELECT SpeciesID - FROM forestgeo_scbi.species - WHERE GenusID = sp.GenusID - AND SpeciesCode = sp.Mnemonic - AND CurrentTaxonFlag = sp.CurrentTaxonFlag - AND ObsoleteTaxonFlag = sp.ObsoleteTaxonFlag - AND SpeciesName = sp.Mnemonic - AND SubspeciesName = MIN(subs.SubSpeciesName) - AND IDLevel = sp.IDLevel - AND SpeciesAuthority = sp.Authority - AND SubspeciesAuthority = MIN(subs.Authority) - AND FieldFamily = sp.FieldFamily - AND Description = LEFT(sp.Description, 65535) - AND ReferenceID = sp.ReferenceID - LIMIT 1 -) -FROM ctfsweb_scbi.Species sp -LEFT JOIN ctfsweb_scbi.SubSpecies subs ON sp.SpeciesID = subs.SpeciesID -LEFT JOIN ctfsweb_scbi.Reference ref ON sp.ReferenceID = ref.ReferenceID -WHERE EXISTS (SELECT 1 FROM ctfsweb_scbi.Species) -GROUP BY sp.SpeciesID, sp.GenusID, sp.Mnemonic, sp.CurrentTaxonFlag, sp.ObsoleteTaxonFlag, sp.IDLevel, sp.Authority, sp.FieldFamily, sp.Description, sp.ReferenceID; - --- Insert into stems and populate mapping table -INSERT INTO forestgeo_scbi.stems (TreeID, QuadratID, SubquadratID, StemNumber, StemTag, LocalX, LocalY, Unit, Moved, StemDescription) -SELECT s.TreeID, s.QuadratID, NULL, s.StemNumber, s.StemTag, MIN(co.QX), MIN(co.QY), - IFNULL(si.QUOM, 'm') AS Unit, s.Moved, LEFT(s.StemDescription, 65535) -FROM ctfsweb_scbi.Stem s -LEFT JOIN ctfsweb_scbi.Coordinates co ON s.QuadratID = co.QuadratID -LEFT JOIN ctfsweb_scbi.Quadrat q ON q.QuadratID = s.QuadratID -LEFT JOIN ctfsweb_scbi.Site si ON q.PlotID = si.PlotID -WHERE EXISTS (SELECT 1 FROM ctfsweb_scbi.Stem) -GROUP BY s.StemID, s.TreeID, s.QuadratID, s.StemNumber, s.StemTag, s.Moved, s.StemDescription, si.QUOM; - -INSERT INTO temp_stem_mapping (old_StemID, new_StemID) -SELECT s.StemID, ( - SELECT StemID - FROM forestgeo_scbi.stems - WHERE TreeID = s.TreeID - AND QuadratID = s.QuadratID - AND SubquadratID IS NULL - AND StemNumber = s.StemNumber - AND StemTag = s.StemTag - AND LocalX = MIN(co.QX) - AND LocalY = MIN(co.QY) - AND Unit = IFNULL(si.QUOM, 'm') - AND Moved = s.Moved - AND StemDescription = LEFT(s.StemDescription, 65535) - LIMIT 1 -) -FROM ctfsweb_scbi.Stem s -LEFT JOIN ctfsweb_scbi.Coordinates co ON s.QuadratID = co.QuadratID -LEFT JOIN ctfsweb_scbi.Quadrat q ON q.QuadratID = s.QuadratID -LEFT JOIN ctfsweb_scbi.Site si ON q.PlotID = si.PlotID -WHERE EXISTS (SELECT 1 FROM ctfsweb_scbi.Stem) -GROUP BY s.StemID, s.TreeID, s.QuadratID, s.StemNumber, s.StemTag, s.Moved, s.StemDescription, si.QUOM; - --- Insert into coremeasurements and populate mapping table -INSERT INTO forestgeo_scbi.coremeasurements (CensusID, PlotID, QuadratID, SubQuadratID, TreeID, StemID, PersonnelID, IsValidated, MeasurementDate, MeasuredDBH, DBHUnit, MeasuredHOM, HOMUnit, Description, UserDefinedFields) -SELECT dbh.CensusID, s.PlotID, q.QuadratID, NULL, t.TreeID, dbh.StemID, NULL, NULL, - dbh.ExactDate, dbh.DBH, 'm', CONVERT(dbh.HOM, DECIMAL(10,6)), 'm', LEFT(dbh.Comments, 65535), NULL -FROM ctfsweb_scbi.DBH dbh -JOIN ctfsweb_scbi.Census c ON dbh.CensusID = c.CensusID -JOIN ctfsweb_scbi.Site s ON c.PlotID = s.PlotID -JOIN ctfsweb_scbi.Quadrat q ON s.PlotID = q.PlotID -JOIN ctfsweb_scbi.Tree t ON dbh.StemID = t.TreeID -WHERE EXISTS (SELECT 1 FROM ctfsweb_scbi.DBH) -GROUP BY dbh.DBHID, dbh.CensusID, s.PlotID, q.QuadratID, t.TreeID, dbh.StemID, dbh.ExactDate, dbh.DBH, dbh.HOM, dbh.Comments; - -INSERT INTO temp_stem_mapping (old_StemID, new_StemID) -SELECT dbh.StemID, ( - SELECT StemID - FROM forestgeo_scbi.coremeasurements cm - WHERE CensusID = dbh.CensusID - AND PlotID = s.PlotID - AND QuadratID = q.QuadratID - AND TreeID = t.TreeID - AND StemID = dbh.StemID - AND MeasurementDate = dbh.ExactDate - AND MeasuredDBH = dbh.DBH - AND MeasuredHOM = CONVERT(dbh.HOM, DECIMAL(10,6)) - LIMIT 1 -) -FROM ctfsweb_scbi.DBH dbh -JOIN ctfsweb_scbi.Census c ON dbh.CensusID = c.CensusID -JOIN ctfsweb_scbi.Site s ON c.PlotID = s.PlotID -JOIN ctfsweb_scbi.Quadrat q ON s.PlotID = q.PlotID -JOIN ctfsweb_scbi.Tree t ON dbh.StemID = t.TreeID -WHERE EXISTS (SELECT 1 FROM ctfsweb_scbi.DBH) -GROUP BY dbh.DBHID, dbh.CensusID, s.PlotID, q.QuadratID, t.TreeID, dbh.StemID, dbh.ExactDate, dbh.DBH, dbh.HOM, dbh.Comments; - --- Insert into quadratpersonnel -INSERT INTO forestgeo_scbi.quadratpersonnel (QuadratID, PersonnelID, AssignedDate) -SELECT dc.QuadratID, dc.PersonnelRoleID, dc.StartDate -FROM ctfsweb_scbi.DataCollection dc -WHERE EXISTS (SELECT 1 FROM ctfsweb_scbi.DataCollection) -GROUP BY dc.DataCollectionID, dc.QuadratID, dc.PersonnelRoleID, dc.StartDate; - --- Insert into attributes -INSERT INTO forestgeo_scbi.attributes (Code, Description, Status) -SELECT TSMCode, LEFT(Description, 65535), - CASE - WHEN Status IN ('alive', 'alive-not measured', 'dead', 'stem dead', 'broken below', 'omitted', 'missing') THEN Status - ELSE NULL - END -FROM ctfsweb_scbi.TSMAttributes -WHERE EXISTS (SELECT 1 FROM ctfsweb_scbi.TSMAttributes) -GROUP BY TSMCode, Description, Status; - --- Insert into cmattributes -INSERT INTO forestgeo_scbi.cmattributes (CoreMeasurementID, Code) -SELECT cm.CoreMeasurementID, a.Code -FROM ctfsweb_scbi.DBHAttributes dbha -JOIN ctfsweb_scbi.TSMAttributes tsm ON dbha.TSMID = tsm.TSMID -JOIN forestgeo_scbi.coremeasurements cm ON dbha.DBHID = cm.StemID -JOIN forestgeo_scbi.attributes a ON tsm.TSMCode = a.Code -WHERE EXISTS (SELECT 1 FROM ctfsweb_scbi.DBHAttributes) -GROUP BY dbha.DBHAttID, cm.CoreMeasurementID, a.Code; - --- Update foreign keys in related tables -UPDATE forestgeo_scbi.coremeasurements cm -JOIN temp_census_mapping tcm ON cm.CensusID = tcm.old_CensusID -SET cm.CensusID = tcm.new_CensusID; - -UPDATE forestgeo_scbi.coremeasurements cm -JOIN temp_plot_mapping tpm ON cm.PlotID = tpm.old_PlotID -SET cm.PlotID = tpm.new_PlotID; - -UPDATE forestgeo_scbi.coremeasurements cm -JOIN temp_quadrat_mapping tqm ON cm.QuadratID = tqm.old_QuadratID -SET cm.QuadratID = tqm.new_QuadratID; - -UPDATE forestgeo_scbi.coremeasurements cm -JOIN temp_stem_mapping tsm ON cm.StemID = tsm.old_StemID -SET cm.StemID = tsm.new_StemID; - -UPDATE forestgeo_scbi.coremeasurements cm -JOIN temp_species_mapping tsm ON cm.TreeID = tsm.old_SpeciesID -SET cm.TreeID = tsm.new_SpeciesID; - -UPDATE forestgeo_scbi.cmattributes ca -JOIN temp_species_mapping tsm ON ca.CoreMeasurementID = tsm.old_SpeciesID -SET ca.CoreMeasurementID = tsm.new_SpeciesID; - -UPDATE forestgeo_scbi.quadratpersonnel qp -JOIN temp_quadrat_mapping tqm ON qp.QuadratID = tqm.old_QuadratID -SET qp.QuadratID = tqm.new_QuadratID; - -UPDATE forestgeo_scbi.quadratpersonnel qp -JOIN temp_personnel_mapping tpm ON qp.PersonnelID = tpm.old_PersonnelID -SET qp.PersonnelID = tpm.new_PersonnelID; - --- Drop temporary mapping tables -DROP TABLE IF EXISTS temp_census_mapping; -DROP TABLE IF EXISTS temp_plot_mapping; -DROP TABLE IF EXISTS temp_personnel_mapping; -DROP TABLE IF EXISTS temp_quadrat_mapping; -DROP TABLE IF EXISTS temp_genus_mapping; -DROP TABLE IF EXISTS temp_species_mapping; -DROP TABLE IF EXISTS temp_stem_mapping; - -SET foreign_key_checks = 1; -``` -{collapsible="true" collapsed-title="migration script"} -> Make sure you change the name of the targeted and targeting schema in this script to your schema names! They are -> currently set to `ctfsweb_scbi` and `forestgeo_scbi`, respectively, and must be changed, otherwise the script will -> fail. -{style="warning"} - -### As the Migration Continues -This will take a good amount of time (average for me was between 1-3 hours). As the script runs, you may find it -beneficial to have some way to monitor the progress of the script's execution, so the following script may come in -hand. -> Make sure you run it in a new command-line/terminal instance! - -> Make sure you replace the DB_NAME variable with your schema's name! -```Bash -#!/bin/bash - -DB_NAME="forestgeo_scbi" - -# List of tables to monitor -TABLES=( - "plots" - "personnel" - "census" - "quadrats" - "genus" - "species" - "stems" - "coremeasurements" - "quadratpersonnel" - "attributes" - "cmattributes" -) - -# Interval in seconds -INTERVAL=20 -# Timeout for MySQL queries in seconds -QUERY_TIMEOUT=10 - -cleanup() { - process_ids=$(mysql --defaults-file=~/.my.cnf -D $DB_NAME -se "SHOW PROCESSLIST;" | awk '/SELECT COUNT/ {print $1}') - for pid in $process_ids; do - mysql --defaults-file=~/.my.cnf -D $DB_NAME -se "KILL $pid;" - echo "-----------------------------" - echo "Killed process ID $pid" - echo "-----------------------------" - done -} - -while true; do - echo "-----------------------------" - echo "$(date): Row counts in forestgeo_scbi schema" - echo "-----------------------------" - - for TABLE in "${TABLES[@]}"; do - timeout $QUERY_TIMEOUT mysql --defaults-file=~/.my.cnf -D $DB_NAME -se "SELECT COUNT(*) FROM $TABLE;" > /tmp/${TABLE}_count & - QUERY_PID=$! - wait $QUERY_PID - - if [ $? -eq 124 ]; then - echo "$TABLE: query timed out" - else - ROW_COUNT=$(cat /tmp/${TABLE}_count) - echo "$TABLE: $ROW_COUNT rows" - fi - - cleanup - done - - echo "-----------------------------" - echo "$(date): SHOW PROCESSLIST output" - echo "-----------------------------" - mysql --defaults-file=~/.my.cnf -D $DB_NAME -e "SHOW PROCESSLIST;" - echo "-----------------------------" - - sleep $INTERVAL -done - - -``` -{collapsible="true" collapsed-title="migration monitoring script"} - -Additionally, the following script may prove useful (it did for me) to remove accumulating `SELECT COUNT(*)` queries -in your processlist (I saw them starting to pile up as the script continued. The script as it is should -automatically remove them, so this is just a backup in case it becomes necessary). Like the migration script, run -this from another terminal instance: -```Bash -#!/bin/bash - -# Database credentials -DB_NAME="forestgeo_scbi" - -# Find all process IDs of the SELECT COUNT(*) queries -process_ids=$(mysql --defaults-file=~/.my.cnf -D $DB_NAME -se "SHOW PROCESSLIST;" | awk '/SELECT COUNT/ {print $1}') - -# Kill each process -for pid in $process_ids; do - mysql --defaults-file=~/.my.cnf -D $DB_NAME -se "KILL $pid;" - echo "Killed process ID $pid" -done -``` -{collapsible="true" collapsed-title="backup monitoring cleanup script"} - -#### Prerequisites: -1. Make sure you create the file `~/.my.cnf` in the `~` directory and add your connection credentials to it (example - here): -```Bash -[client] -user= -password= -host=forestgeo-mysqldataserver.mysql.database.azure.com -``` -> Make sure you replace the <> portions with your actual credentials! I've left the host endpoint as it is because -> that shouldn't change unless absolutely necessary, but make sure you check that as well. - -### After Migration Completes -Make sure you check all of the now-populated tables to ensure that the system has not messed up populating them. -> One of the issues I ran into was even though the flat file only specified one plot, the plots table after the -> migration contained several hundred duplicates of that single plot. -{style="note"} - diff --git a/frontend/Writerside/topics/starter-topic.md b/frontend/Writerside/topics/starter-topic.md deleted file mode 100644 index e81df639..00000000 --- a/frontend/Writerside/topics/starter-topic.md +++ /dev/null @@ -1,79 +0,0 @@ -# About ForestGEO Application Documentation - - - -## Add new topics -You can create empty topics, or choose a template for different types of content that contains some boilerplate structure to help you get started: - -![Create new topic options](new_topic_options.png){ width=290 }{border-effect=line} - -## Write content -%product% supports two types of markup: Markdown and XML. -When you create a new help article, you can choose between two topic types, but this doesn't mean you have to stick to a single format. -You can author content in Markdown and extend it with semantic attributes or inject entire XML elements. - -## Inject XML -For example, this is how you inject a procedure: - - - -

Start typing and select a procedure type from the completion suggestions:

- completion suggestions for procedure -
- -

Press Tab or Enter to insert the markup.

-
-
- -## Add interactive elements - -### Tabs -To add switchable content, you can make use of tabs (inject them by starting to type `tab` on a new line): - - - - ![Alt Text](new_topic_options.png){ width=450 } - - - - ]]> - - - -### Collapsible blocks -Apart from injecting entire XML elements, you can use attributes to configure the behavior of certain elements. -For example, you can collapse a chapter that contains non-essential information: - -#### Supplementary info {collapsible="true"} -Content under a collapsible header will be collapsed by default, -but you can modify the behavior by adding the following attribute: -`default-state="expanded"` - -### Convert selection to XML -If you need to extend an element with more functions, you can convert selected content from Markdown to semantic markup. -For example, if you want to merge cells in a table, it's much easier to convert it to XML than do this in Markdown. -Position the caret anywhere in the table and press Alt+Enter: - -Convert table to XML - -## Feedback and support -Please report any issues, usability improvements, or feature requests to our -YouTrack project -(you will need to register). - -You are welcome to join our -public Slack workspace. -Before you do, please read our [Code of conduct](https://plugins.jetbrains.com/plugin/20158-writerside/docs/writerside-code-of-conduct.html). -We assume that you’ve read and acknowledged it before joining. - -You can also always email us at [writerside@jetbrains.com](mailto:writerside@jetbrains.com). - - - - Markup reference - Reorder topics in the TOC - Build and publish - Configure Search - - \ No newline at end of file diff --git a/frontend/Writerside/v.list b/frontend/Writerside/v.list deleted file mode 100644 index 2d12cb39..00000000 --- a/frontend/Writerside/v.list +++ /dev/null @@ -1,5 +0,0 @@ - - - - - diff --git a/frontend/Writerside/writerside.cfg b/frontend/Writerside/writerside.cfg deleted file mode 100644 index 54b3b6e3..00000000 --- a/frontend/Writerside/writerside.cfg +++ /dev/null @@ -1,8 +0,0 @@ - - - - - - - - diff --git a/frontend/__tests__/api/cmid.test.tsx b/frontend/__tests__/api/cmid.test.tsx index 40398348..30c82bbc 100644 --- a/frontend/__tests__/api/cmid.test.tsx +++ b/frontend/__tests__/api/cmid.test.tsx @@ -1,8 +1,8 @@ -import { describe, it, expect, vi } from 'vitest'; -import { GET } from '@/app/api/details/cmid/route'; -import { getConn, runQuery } from '@/components/processors/processormacros'; -import { createMocks } from 'node-mocks-http'; -import { NextRequest } from 'next/server'; +import {describe, it, expect, vi} from 'vitest'; +import {GET} from '@/app/api/details/cmid/route'; +import {getConn, runQuery} from '@/components/processors/processormacros'; +import {createMocks} from 'node-mocks-http'; +import {NextRequest} from 'next/server'; vi.mock('@/components/processors/processormacros', () => ({ getConn: vi.fn(), @@ -31,7 +31,7 @@ describe('GET /api/details/cmid', () => { (getConn as jest.Mock).mockResolvedValue(conn); (runQuery as jest.Mock).mockResolvedValue(mockData); - const { req, res } = createMocks({ + const {req, res} = createMocks({ method: 'GET', url: 'http://localhost/api/details/cmid?cmid=1&schema=test_schema', }); @@ -55,7 +55,7 @@ describe('GET /api/details/cmid', () => { it('should return 500 if there is a database error', async () => { (getConn as jest.Mock).mockRejectedValue(new Error('Database error')); - const { req, res } = createMocks({ + const {req, res} = createMocks({ method: 'GET', url: 'http://localhost/api/details/cmid?cmid=1&schema=test_schema', }); @@ -66,7 +66,7 @@ describe('GET /api/details/cmid', () => { }); it('should return 400 if schema is not provided', async () => { - const { req, res } = createMocks({ + const {req, res} = createMocks({ method: 'GET', url: 'http://localhost/api/details/cmid?cmid=1', }); diff --git a/frontend/__tests__/api/cmprevalidation.test.tsx b/frontend/__tests__/api/cmprevalidation.test.tsx index f974468f..cf23f13c 100644 --- a/frontend/__tests__/api/cmprevalidation.test.tsx +++ b/frontend/__tests__/api/cmprevalidation.test.tsx @@ -1,9 +1,9 @@ -import { describe, it, expect, vi } from 'vitest'; -import { GET } from '@/app/api/cmprevalidation/[dataType]/[[...slugs]]/route'; -import { createMocks } from 'node-mocks-http'; -import { getConn, runQuery } from '@/components/processors/processormacros'; -import { HTTPResponses } from '@/config/macros'; -import { NextRequest } from 'next/server'; +import {describe, it, expect, vi} from 'vitest'; +import {GET} from '@/app/api/cmprevalidation/[dataType]/[[...slugs]]/route'; +import {createMocks} from 'node-mocks-http'; +import {getConn, runQuery} from '@/components/processors/processormacros'; +import {HTTPResponses} from '@/config/macros'; +import {NextRequest} from 'next/server'; vi.mock('@/components/processors/processormacros', () => ({ getConn: vi.fn(), @@ -20,14 +20,14 @@ describe('GET /api/cmprevalidation/[dataType]/[[...slugs]]', () => { (getConn as jest.Mock).mockResolvedValue(conn); (runQuery as jest.Mock).mockResolvedValue([]); - const { req } = createMocks({ + const {req} = createMocks({ method: 'GET', url: 'http://localhost/api/cmprevalidation/attributes/schema/1/1', }); const mockReq = new NextRequest(req.url); - const response = await GET(mockReq, { params: { dataType: 'attributes', slugs: ['schema', '1', '1'] } }); + const response = await GET(mockReq, {params: {dataType: 'attributes', slugs: ['schema', '1', '1']}}); expect(response.status).toBe(HTTPResponses.PRECONDITION_VALIDATION_FAILURE); }); @@ -41,14 +41,14 @@ describe('GET /api/cmprevalidation/[dataType]/[[...slugs]]', () => { (getConn as jest.Mock).mockResolvedValue(conn); (runQuery as jest.Mock).mockResolvedValue([[1]]); - const { req } = createMocks({ + const {req} = createMocks({ method: 'GET', url: 'http://localhost/api/cmprevalidation/attributes/schema/1/1', }); const mockReq = new NextRequest(req.url); - const response = await GET(mockReq, { params: { dataType: 'attributes', slugs: ['schema', '1', '1'] } }); + const response = await GET(mockReq, {params: {dataType: 'attributes', slugs: ['schema', '1', '1']}}); expect(response.status).toBe(HTTPResponses.OK); }); @@ -56,20 +56,20 @@ describe('GET /api/cmprevalidation/[dataType]/[[...slugs]]', () => { it('should return 412 if there is a database error', async () => { (getConn as jest.Mock).mockRejectedValue(new Error('Database error')); - const { req } = createMocks({ + const {req} = createMocks({ method: 'GET', url: 'http://localhost/api/cmprevalidation/attributes/schema/1/1', }); const mockReq = new NextRequest(req.url); - const response = await GET(mockReq, { params: { dataType: 'attributes', slugs: ['schema', '1', '1'] } }); + const response = await GET(mockReq, {params: {dataType: 'attributes', slugs: ['schema', '1', '1']}}); expect(response.status).toBe(HTTPResponses.PRECONDITION_VALIDATION_FAILURE); }); it('should return 400 if slugs are missing', async () => { - const { req } = createMocks({ + const {req} = createMocks({ method: 'GET', url: 'http://localhost/api/cmprevalidation/attributes', }); @@ -77,14 +77,14 @@ describe('GET /api/cmprevalidation/[dataType]/[[...slugs]]', () => { const mockReq = new NextRequest(req.url); try { - await GET(mockReq, { params: { dataType: 'attributes', slugs: [] } }); + await GET(mockReq, {params: {dataType: 'attributes', slugs: []}}); } catch (e) { expect((e as Error).message).toBe('incorrect slugs provided'); } }); it('should return 400 if slugs are incorrect', async () => { - const { req } = createMocks({ + const {req} = createMocks({ method: 'GET', url: 'http://localhost/api/cmprevalidation/attributes/schema', }); @@ -92,7 +92,7 @@ describe('GET /api/cmprevalidation/[dataType]/[[...slugs]]', () => { const mockReq = new NextRequest(req.url); try { - await GET(mockReq, { params: { dataType: 'attributes', slugs: ['schema'] } }); + await GET(mockReq, {params: {dataType: 'attributes', slugs: ['schema']}}); } catch (e) { expect((e as Error).message).toBe('incorrect slugs provided'); } diff --git a/frontend/__tests__/api/fetchall.test.tsx b/frontend/__tests__/api/fetchall.test.tsx index 5ea7ff9f..91743645 100644 --- a/frontend/__tests__/api/fetchall.test.tsx +++ b/frontend/__tests__/api/fetchall.test.tsx @@ -1,9 +1,9 @@ -import { describe, it, expect, vi, beforeEach } from 'vitest'; -import { GET } from '@/app/api/fetchall/[[...slugs]]/route'; -import { getConn, runQuery } from '@/components/processors/processormacros'; -import MapperFactory, { IDataMapper } from '@/config/datamapper'; -import { createMocks } from 'node-mocks-http'; -import { NextRequest } from 'next/server'; +import {describe, it, expect, vi, beforeEach} from 'vitest'; +import {GET} from '@/app/api/fetchall/[[...slugs]]/route'; +import {getConn, runQuery} from '@/components/processors/processormacros'; +import MapperFactory, {IDataMapper} from '@/config/datamapper'; +import {createMocks} from 'node-mocks-http'; +import {NextRequest} from 'next/server'; // Mocking getConn and runQuery functions vi.mock('@/components/processors/processormacros', () => ({ @@ -24,50 +24,50 @@ describe('GET /api/fetchall/[[...slugs]]', () => { }); it('should return 500 if schema is not provided', async () => { - const { req } = createMocks({ + const {req} = createMocks({ method: 'GET', url: 'http://localhost/api/fetchall/plots' }); const mockReq = new NextRequest(req.url); - await expect(GET(mockReq, { params: { slugs: ['plots'] } })).rejects.toThrow('Schema selection was not provided to API endpoint'); + await expect(GET(mockReq, {params: {slugs: ['plots']}})).rejects.toThrow('Schema selection was not provided to API endpoint'); }); it('should return 500 if fetchType is not provided', async () => { - const { req } = createMocks({ + const {req} = createMocks({ method: 'GET', url: 'http://localhost/api/fetchall?schema=test_schema' }); const mockReq = new NextRequest(req.url); - await expect(GET(mockReq, { params: { slugs: [] } })).rejects.toThrow('fetchType was not correctly provided'); + await expect(GET(mockReq, {params: {slugs: []}})).rejects.toThrow('fetchType was not correctly provided'); }); it('should return 200 and data if query is successful', async () => { - const mockConn = { release: vi.fn() }; + const mockConn = {release: vi.fn()}; (getConn as ReturnType).mockResolvedValue(mockConn); - const mockResults = [{ PlotID: 1, PlotName: 'Plot 1' }]; + const mockResults = [{PlotID: 1, PlotName: 'Plot 1'}]; (runQuery as ReturnType).mockResolvedValue(mockResults); const mockMapper: IDataMapper = { - mapData: vi.fn().mockReturnValue([{ plotID: 1, plotName: 'Plot 1' }]), + mapData: vi.fn().mockReturnValue([{plotID: 1, plotName: 'Plot 1'}]), demapData: vi.fn() }; (MapperFactory.getMapper as ReturnType).mockReturnValue(mockMapper); - const { req } = createMocks({ + const {req} = createMocks({ method: 'GET', url: 'http://localhost/api/fetchall/plots?schema=test_schema' }); const mockReq = new NextRequest(req.url); - const response = await GET(mockReq, { params: { slugs: ['plots'] } }); + const response = await GET(mockReq, {params: {slugs: ['plots']}}); expect(response.status).toBe(200); const data = await response.json(); - expect(data).toEqual([{ plotID: 1, plotName: 'Plot 1' }]); + expect(data).toEqual([{plotID: 1, plotName: 'Plot 1'}]); expect(getConn).toHaveBeenCalled(); expect(runQuery).toHaveBeenCalledWith(mockConn, expect.stringContaining('SELECT')); expect(mockMapper.mapData).toHaveBeenCalledWith(mockResults); @@ -75,18 +75,18 @@ describe('GET /api/fetchall/[[...slugs]]', () => { }); it('should return 500 if there is a database error', async () => { - const mockConn = { release: vi.fn() }; + const mockConn = {release: vi.fn()}; (getConn as ReturnType).mockResolvedValue(mockConn); (runQuery as ReturnType).mockRejectedValue(new Error('Database error')); - const { req } = createMocks({ + const {req} = createMocks({ method: 'GET', url: 'http://localhost/api/fetchall/plots?schema=test_schema' }); const mockReq = new NextRequest(req.url); - await expect(GET(mockReq, { params: { slugs: ['plots'] } })).rejects.toThrow('Call failed'); + await expect(GET(mockReq, {params: {slugs: ['plots']}})).rejects.toThrow('Call failed'); expect(getConn).toHaveBeenCalled(); expect(runQuery).toHaveBeenCalledWith(mockConn, expect.stringContaining('SELECT')); expect(mockConn.release).toHaveBeenCalled(); diff --git a/frontend/__tests__/api/filehandlers/deletefile.test.tsx b/frontend/__tests__/api/filehandlers/deletefile.test.tsx index 1bb7a42a..f8ad99f2 100644 --- a/frontend/__tests__/api/filehandlers/deletefile.test.tsx +++ b/frontend/__tests__/api/filehandlers/deletefile.test.tsx @@ -1,8 +1,8 @@ -import { describe, it, expect, vi, beforeEach } from 'vitest'; -import { DELETE } from '@/app/api/filehandlers/deletefile/route'; -import { getContainerClient } from '@/config/macros/azurestorage'; -import { createMocks } from 'node-mocks-http'; -import { NextRequest } from 'next/server'; +import {describe, it, expect, vi, beforeEach} from 'vitest'; +import {DELETE} from '@/app/api/filehandlers/deletefile/route'; +import {getContainerClient} from '@/config/macros/azurestorage'; +import {createMocks} from 'node-mocks-http'; +import {NextRequest} from 'next/server'; vi.mock('@/config/macros/azurestorage', () => ({ getContainerClient: vi.fn() @@ -14,7 +14,7 @@ describe('DELETE /api/filehandlers/deletefile', () => { }); it('should return 400 if container name or filename is missing', async () => { - const { req } = createMocks({ + const {req} = createMocks({ method: 'DELETE', url: 'http://localhost/api/filehandlers/deletefile' }); @@ -30,7 +30,7 @@ describe('DELETE /api/filehandlers/deletefile', () => { it('should return 400 if container client creation fails', async () => { (getContainerClient as ReturnType).mockResolvedValue(null); - const { req } = createMocks({ + const {req} = createMocks({ method: 'DELETE', url: 'http://localhost/api/filehandlers/deletefile?container=testContainer&filename=testFile' }); @@ -53,7 +53,7 @@ describe('DELETE /api/filehandlers/deletefile', () => { (getContainerClient as ReturnType).mockResolvedValue(mockContainerClient); - const { req } = createMocks({ + const {req} = createMocks({ method: 'DELETE', url: 'http://localhost/api/filehandlers/deletefile?container=testContainer&filename=testFile' }); @@ -70,7 +70,7 @@ describe('DELETE /api/filehandlers/deletefile', () => { it('should return 500 if there is an error', async () => { (getContainerClient as ReturnType).mockRejectedValue(new Error('Test error')); - const { req } = createMocks({ + const {req} = createMocks({ method: 'DELETE', url: 'http://localhost/api/filehandlers/deletefile?container=testContainer&filename=testFile' }); diff --git a/frontend/__tests__/api/filehandlers/downloadallfiles.test.tsx b/frontend/__tests__/api/filehandlers/downloadallfiles.test.tsx index 93d09db0..30f07d12 100644 --- a/frontend/__tests__/api/filehandlers/downloadallfiles.test.tsx +++ b/frontend/__tests__/api/filehandlers/downloadallfiles.test.tsx @@ -1,8 +1,8 @@ -import { describe, it, expect, vi, beforeEach } from 'vitest'; -import { GET } from '@/app/api/filehandlers/downloadallfiles/route'; -import { getContainerClient } from '@/config/macros/azurestorage'; -import { createMocks } from 'node-mocks-http'; -import { NextRequest } from 'next/server'; +import {describe, it, expect, vi, beforeEach} from 'vitest'; +import {GET} from '@/app/api/filehandlers/downloadallfiles/route'; +import {getContainerClient} from '@/config/macros/azurestorage'; +import {createMocks} from 'node-mocks-http'; +import {NextRequest} from 'next/server'; vi.mock('@/config/macros/azurestorage', () => ({ getContainerClient: vi.fn() @@ -14,7 +14,7 @@ describe('GET /api/filehandlers/downloadallfiles', () => { }); it('should return 400 if plot or census is not provided', async () => { - const { req } = createMocks({ + const {req} = createMocks({ method: 'GET', url: 'http://localhost/api/filehandlers/downloadallfiles' }); @@ -30,7 +30,7 @@ describe('GET /api/filehandlers/downloadallfiles', () => { it('should return 400 if container client creation fails', async () => { (getContainerClient as ReturnType).mockResolvedValue(null); - const { req } = createMocks({ + const {req} = createMocks({ method: 'GET', url: 'http://localhost/api/filehandlers/downloadallfiles?plot=testPlot&census=testCensus' }); @@ -51,7 +51,7 @@ describe('GET /api/filehandlers/downloadallfiles', () => { metadata: { user: 'testUser', FormType: 'testFormType', - FileErrorState: JSON.stringify([{ stemtag: 'testStemtag', tag: 'testTag', validationErrorID: 1 }]) + FileErrorState: JSON.stringify([{stemtag: 'testStemtag', tag: 'testTag', validationErrorID: 1}]) }, properties: { lastModified: new Date() @@ -62,7 +62,7 @@ describe('GET /api/filehandlers/downloadallfiles', () => { (getContainerClient as ReturnType).mockResolvedValue(mockContainerClient); - const { req } = createMocks({ + const {req} = createMocks({ method: 'GET', url: 'http://localhost/api/filehandlers/downloadallfiles?plot=testPlot&census=testCensus' }); @@ -79,7 +79,7 @@ describe('GET /api/filehandlers/downloadallfiles', () => { name: 'testBlob', user: 'testUser', formType: 'testFormType', - fileErrors: [{ stemtag: 'testStemtag', tag: 'testTag', validationErrorID: 1 }], + fileErrors: [{stemtag: 'testStemtag', tag: 'testTag', validationErrorID: 1}], date: expect.any(String) // Date will be serialized to a string }); }); @@ -93,7 +93,7 @@ describe('GET /api/filehandlers/downloadallfiles', () => { (getContainerClient as ReturnType).mockResolvedValue(mockContainerClient); - const { req } = createMocks({ + const {req} = createMocks({ method: 'GET', url: 'http://localhost/api/filehandlers/downloadallfiles?plot=testPlot&census=testCensus' }); diff --git a/frontend/__tests__/api/filehandlers/downloadfile.test.tsx b/frontend/__tests__/api/filehandlers/downloadfile.test.tsx index b8966e53..d4b726b2 100644 --- a/frontend/__tests__/api/filehandlers/downloadfile.test.tsx +++ b/frontend/__tests__/api/filehandlers/downloadfile.test.tsx @@ -1,11 +1,10 @@ -import { describe, it, expect, vi, beforeEach } from 'vitest'; -import { GET } from '@/app/api/filehandlers/downloadfile/route'; -import { getContainerClient } from '@/config/macros/azurestorage'; -import { createMocks } from 'node-mocks-http'; -import { NextRequest } from 'next/server'; +import {describe, it, expect, vi, beforeEach} from 'vitest'; +import {GET} from '@/app/api/filehandlers/downloadfile/route'; +import {getContainerClient} from '@/config/macros/azurestorage'; +import {createMocks} from 'node-mocks-http'; +import {NextRequest} from 'next/server'; import { BlobServiceClient, - BlobSASPermissions, generateBlobSASQueryParameters, StorageSharedKeyCredential } from '@azure/storage-blob'; @@ -31,7 +30,7 @@ describe('GET /api/filehandlers/downloadfile', () => { }); it('should return 400 if container name, filename, or storage connection string is missing', async () => { - const { req } = createMocks({ + const {req} = createMocks({ method: 'GET', url: 'http://localhost/api/filehandlers/downloadfile' }); @@ -48,7 +47,7 @@ describe('GET /api/filehandlers/downloadfile', () => { process.env.AZURE_STORAGE_CONNECTION_STRING = 'test-connection-string'; (getContainerClient as ReturnType).mockResolvedValue(null); - const { req } = createMocks({ + const {req} = createMocks({ method: 'GET', url: 'http://localhost/api/filehandlers/downloadfile?container=testContainer&filename=testFile' }); @@ -80,7 +79,7 @@ describe('GET /api/filehandlers/downloadfile', () => { toString: () => 'sastoken' }); - const { req } = createMocks({ + const {req} = createMocks({ method: 'GET', url: 'http://localhost/api/filehandlers/downloadfile?container=testContainer&filename=testFile' }); @@ -98,7 +97,7 @@ describe('GET /api/filehandlers/downloadfile', () => { (getContainerClient as ReturnType).mockRejectedValue(new Error('Test error')); - const { req } = createMocks({ + const {req} = createMocks({ method: 'GET', url: 'http://localhost/api/filehandlers/downloadfile?container=testContainer&filename=testFile' }); diff --git a/frontend/__tests__/api/filehandlers/storageload.test.tsx b/frontend/__tests__/api/filehandlers/storageload.test.tsx index 7c70e92f..6f48b27c 100644 --- a/frontend/__tests__/api/filehandlers/storageload.test.tsx +++ b/frontend/__tests__/api/filehandlers/storageload.test.tsx @@ -1,9 +1,8 @@ -import { describe, it, expect, vi, beforeEach } from 'vitest'; -import { POST } from '@/app/api/filehandlers/storageload/route'; -import { HTTPResponses } from '@/config/macros'; -import { getContainerClient, uploadValidFileAsBuffer } from '@/config/macros/azurestorage'; -import { createMocks } from 'node-mocks-http'; -import { NextRequest } from 'next/server'; +import {describe, it, expect, vi, beforeEach} from 'vitest'; +import {POST} from '@/app/api/filehandlers/storageload/route'; +import {getContainerClient, uploadValidFileAsBuffer} from '@/config/macros/azurestorage'; +import {createMocks} from 'node-mocks-http'; +import {NextRequest} from 'next/server'; vi.mock('@/config/macros/azurestorage', () => ({ getContainerClient: vi.fn(), @@ -16,7 +15,7 @@ describe('POST /api/filehandlers/storageload', () => { }); const createMockRequest = (url: string, formData: FormData) => { - const { req } = createMocks({ + const {req} = createMocks({ method: 'POST', url: url, headers: { @@ -24,9 +23,9 @@ describe('POST /api/filehandlers/storageload', () => { } }); - if (formData.get('file') === null){ + if (formData.get('file') === null) { console.log('createMockRequest: received empty formData: ', formData); - return new NextRequest(req.url!, { method: 'POST' }); + return new NextRequest(req.url!, {method: 'POST'}); } req.formData = async () => formData; @@ -37,7 +36,7 @@ describe('POST /api/filehandlers/storageload', () => { const body = `------WebKitFormBoundary7MA4YWxkTrZu0gW\r\nContent-Disposition: form-data; name="file"; filename="testfile.txt"\r\nContent-Type: text/plain\r\n\r\ntest content\r\n------WebKitFormBoundary7MA4YWxkTrZu0gW--`; - return new NextRequest(req.url!, { method: 'POST', headers, body }); + return new NextRequest(req.url!, {method: 'POST', headers, body}); }; it('should return 500 if container client creation fails', async () => { @@ -72,7 +71,7 @@ describe('POST /api/filehandlers/storageload', () => { }); it('should return 200 if file upload is successful', async () => { - const mockUploadResponse = { requestId: '12345', _response: { status: 200 } }; + const mockUploadResponse = {requestId: '12345', _response: {status: 200}}; (getContainerClient as ReturnType).mockResolvedValue({}); (uploadValidFileAsBuffer as ReturnType).mockResolvedValue(mockUploadResponse); diff --git a/frontend/__tests__/loginpage.test.tsx b/frontend/__tests__/loginpage.test.tsx index eaea1f7c..347b29fa 100644 --- a/frontend/__tests__/loginpage.test.tsx +++ b/frontend/__tests__/loginpage.test.tsx @@ -1,10 +1,10 @@ // loginPage.test.tsx -import { render, screen } from '@testing-library/react'; -import { describe, it, vi, beforeEach, Mock, expect } from 'vitest'; +import {render, screen} from '@testing-library/react'; +import {describe, it, vi, beforeEach, Mock, expect} from 'vitest'; import LoginPage from '@/app/(login)/login/page'; -import { useSession } from 'next-auth/react'; -import { redirect } from 'next/navigation'; +import {useSession} from 'next-auth/react'; +import {redirect} from 'next/navigation'; import '@testing-library/jest-dom/vitest'; // Mock the useSession hook and next/navigation functions @@ -29,9 +29,9 @@ describe('LoginPage Component', () => { it('renders the unauthenticated sidebar if the user is unauthenticated', () => { // Mock unauthenticated status - (useSession as Mock).mockReturnValue({ data: null, status: 'unauthenticated' }); + (useSession as Mock).mockReturnValue({data: null, status: 'unauthenticated'}); - render(); + render(); // Assert that the sidebar is present and visible expect(screen.getByTestId('unauthenticated-sidebar')).toBeInTheDocument(); @@ -39,9 +39,9 @@ describe('LoginPage Component', () => { it('redirects to dashboard if the user is authenticated', () => { // Mock authenticated status - (useSession as Mock).mockReturnValue({ data: { user: {} }, status: 'authenticated' }); + (useSession as Mock).mockReturnValue({data: {user: {}}, status: 'authenticated'}); - render(); + render(); // Assert that redirect was called to navigate to the dashboard expect(redirect).toHaveBeenCalledWith('/dashboard'); diff --git a/frontend/app/(hub)/dashboard/page.tsx b/frontend/app/(hub)/dashboard/page.tsx index 6dfc5530..2dd68e0d 100644 --- a/frontend/app/(hub)/dashboard/page.tsx +++ b/frontend/app/(hub)/dashboard/page.tsx @@ -86,8 +86,9 @@ export default function DashboardPage() { Following the same format as the Site, clicking on the Select Plot link will open a dialog box to allow you to select a plot -
- If you have administrator access, you will be able to click on Add New Plot or Edit/Delete Plot (within the selection menu) to make +
+ If you have administrator access, you will be able to click on Add New Plot or Edit/Delete Plot (within + the selection menu) to make changes to or add plots that are missing or incorrect. @@ -116,9 +117,11 @@ export default function DashboardPage() { chronological descending order.
Unlike the Site and Plot selections, Census selections are not restricted by access, so you should be able to select any available censuses. -
- If you have admin access, you will be able to close, edit, or delete censuses while the menu is open. Currently, - There is not (yet) functionality to reopen prior censuses, but if you need to open a new census, please use the Add New Census button! +
+ If you have admin access, you will be able to close, edit, or delete censuses while the menu is open. + Currently, + There is not (yet) functionality to reopen prior censuses, but if you need to open a new census, please + use the Add New Census button!
diff --git a/frontend/app/(hub)/fixeddatainput/alltaxonomies/page.tsx b/frontend/app/(hub)/fixeddatainput/alltaxonomies/page.tsx index 091add1c..3f5d260b 100644 --- a/frontend/app/(hub)/fixeddatainput/alltaxonomies/page.tsx +++ b/frontend/app/(hub)/fixeddatainput/alltaxonomies/page.tsx @@ -1,5 +1,5 @@ -import AllTaxonomiesViewDataGrid from "@/components/datagrids/applications/alltaxonomyviewdatagrid"; +import AllTaxonomiesViewDataGrid from "@/components/datagrids/applications/alltaxonomiesviewdatagrid"; export default function AllTaxonomiesPage() { - return ; + return ; } diff --git a/frontend/app/(hub)/fixeddatainput/attributes/page.tsx b/frontend/app/(hub)/fixeddatainput/attributes/page.tsx index 64dfd11f..05b71016 100644 --- a/frontend/app/(hub)/fixeddatainput/attributes/page.tsx +++ b/frontend/app/(hub)/fixeddatainput/attributes/page.tsx @@ -1,5 +1,5 @@ import AttributesDataGrid from "@/components/datagrids/applications/attributesdatagrid"; export default function AttributesPage() { - return ; + return ; } \ No newline at end of file diff --git a/frontend/app/(hub)/fixeddatainput/census/page.tsx b/frontend/app/(hub)/fixeddatainput/census/page.tsx index 4ec0fd5e..b602d83e 100644 --- a/frontend/app/(hub)/fixeddatainput/census/page.tsx +++ b/frontend/app/(hub)/fixeddatainput/census/page.tsx @@ -1,5 +1,5 @@ import CensusDataGrid from "@/components/datagrids/applications/censusdatagrid"; export default function CensusPage() { - return ; + return ; } \ No newline at end of file diff --git a/frontend/app/(hub)/fixeddatainput/personnel/page.tsx b/frontend/app/(hub)/fixeddatainput/personnel/page.tsx index 07bcbe67..fe23a45f 100644 --- a/frontend/app/(hub)/fixeddatainput/personnel/page.tsx +++ b/frontend/app/(hub)/fixeddatainput/personnel/page.tsx @@ -1,5 +1,5 @@ import PersonnelDataGrid from "@/components/datagrids/applications/personneldatagrid"; export default function PersonnelPage() { - return ; + return ; } \ No newline at end of file diff --git a/frontend/app/(hub)/fixeddatainput/quadratpersonnel/page.tsx b/frontend/app/(hub)/fixeddatainput/quadratpersonnel/page.tsx index 94a254e0..15bbbaf2 100644 --- a/frontend/app/(hub)/fixeddatainput/quadratpersonnel/page.tsx +++ b/frontend/app/(hub)/fixeddatainput/quadratpersonnel/page.tsx @@ -1,5 +1,5 @@ import QuadratPersonnelDataGrid from "@/components/datagrids/applications/quadratpersonneldatagrid"; export default function QuadratPersonnelPage() { - return ; + return ; } \ No newline at end of file diff --git a/frontend/app/(hub)/fixeddatainput/quadrats/page.tsx b/frontend/app/(hub)/fixeddatainput/quadrats/page.tsx index c64bc8c5..1e5be33f 100644 --- a/frontend/app/(hub)/fixeddatainput/quadrats/page.tsx +++ b/frontend/app/(hub)/fixeddatainput/quadrats/page.tsx @@ -1,5 +1,5 @@ import QuadratsDataGrid from "@/components/datagrids/applications/quadratsdatagrid"; export default function QuadratsPage() { - return ; + return ; } \ No newline at end of file diff --git a/frontend/app/(hub)/fixeddatainput/species/page.tsx b/frontend/app/(hub)/fixeddatainput/species/page.tsx index 57393322..12bc9f8f 100644 --- a/frontend/app/(hub)/fixeddatainput/species/page.tsx +++ b/frontend/app/(hub)/fixeddatainput/species/page.tsx @@ -1,5 +1,5 @@ import SpeciesDataGrid from "@/components/datagrids/applications/speciesdatagrid"; export default function SpeciesPage() { - return ; + return ; } \ No newline at end of file diff --git a/frontend/app/(hub)/fixeddatainput/stemdimensions/page.tsx b/frontend/app/(hub)/fixeddatainput/stemdimensions/page.tsx index 433e8e6d..86e457ff 100644 --- a/frontend/app/(hub)/fixeddatainput/stemdimensions/page.tsx +++ b/frontend/app/(hub)/fixeddatainput/stemdimensions/page.tsx @@ -1,141 +1,7 @@ "use client"; -import {GridRowModes, GridRowModesModel, GridRowsProp} from "@mui/x-data-grid"; -import {AlertProps} from "@mui/material"; -import React, {useState} from "react"; -import {StemDimensionsGridColumns} from '@/config/sqlrdsdefinitions/views/stemdimensionsviewrds'; -import {usePlotContext} from "@/app/contexts/userselectionprovider"; -import {randomId} from "@mui/x-data-grid-generator"; -import DataGridCommons from "@/components/datagrids/datagridcommons"; -import {useSession} from "next-auth/react"; -import {Box, Typography} from "@mui/joy"; -import UploadParentModal from "@/components/uploadsystemhelpers/uploadparentmodal"; -export default function StemTreeDetailsPage() { - const initialRows: GridRowsProp = [ - { - id: 0, - stemID: 0, - stemTag: '', - treeID: 0, - treeTag: '', - familyName: undefined, - genusName: undefined, - speciesName: undefined, - subSpeciesName: undefined, - quadratName: undefined, - plotName: undefined, - locationName: undefined, - countryName: undefined, - quadratDimensionX: undefined, - quadratDimensionY: undefined, - stemQuadX: undefined, - stemQuadY: undefined, - stemDescription: undefined, - }, - ]; - const [rows, setRows] = React.useState(initialRows); - const [rowCount, setRowCount] = useState(0); // total number of rows - const [rowModesModel, setRowModesModel] = React.useState({}); - const [snackbar, setSnackbar] = React.useState | null>(null); - const [refresh, setRefresh] = useState(false); - const [paginationModel, setPaginationModel] = useState({ - page: 0, - pageSize: 10, - }); - const [isNewRowAdded, setIsNewRowAdded] = useState(false); - const [shouldAddRowAfterFetch, setShouldAddRowAfterFetch] = useState(false); - let currentPlot = usePlotContext(); - const {data: session} = useSession(); - // Function to fetch paginated data - const addNewRowToGrid = () => { - const id = randomId(); - // New row object - const nextStemID = (rows.length > 0 - ? rows.reduce((max, row) => Math.max(row.stemID, max), 0) - : 0) + 1; +import StemDimensionsViewDataGrid from "@/components/datagrids/applications/stemdimensionsviewdatagrid"; - const newRow = { - id: id, - stemID: nextStemID, - stemTag: '', - treeID: 0, - treeTag: '', - familyName: undefined, - genusName: undefined, - speciesName: undefined, - subSpeciesName: undefined, - quadratName: undefined, - plotName: undefined, - locationName: undefined, - countryName: undefined, - quadratDimensionX: undefined, - quadratDimensionY: undefined, - stemQuadX: undefined, - stemQuadY: undefined, - stemDescription: undefined, - isNew: true - }; - // Add the new row to the state - setRows(oldRows => [...oldRows, newRow]); - // Set editing mode for the new row - setRowModesModel(oldModel => ({ - ...oldModel, - [id]: {mode: GridRowModes.Edit, fieldToFocus: 'stemTag'}, - })); - }; - return ( - <> - - - - {session?.user.isAdmin && ( - - Note: ADMINISTRATOR VIEW - - )} - - Note: This is a locked view and will not allow modification. - - - Please use this view as a way to confirm changes made to measurements. - - - - - - - - ); -} \ No newline at end of file +export default function StemDimensionsPage() { + return ; +} \ No newline at end of file diff --git a/frontend/app/(hub)/fixeddatainput/stemtaxonomies/page.tsx b/frontend/app/(hub)/fixeddatainput/stemtaxonomies/page.tsx index b4cbc403..b8fc7b70 100644 --- a/frontend/app/(hub)/fixeddatainput/stemtaxonomies/page.tsx +++ b/frontend/app/(hub)/fixeddatainput/stemtaxonomies/page.tsx @@ -1,5 +1,7 @@ "use client"; +import StemTaxonomiesViewDataGrid from "@/components/datagrids/applications/stemtaxonomiesviewdatagrid"; + export default function StemTaxonomiesPage() { - return <>; + return ; } \ No newline at end of file diff --git a/frontend/app/(hub)/fixeddatainput/subquadrats/page.tsx b/frontend/app/(hub)/fixeddatainput/subquadrats/page.tsx index 43a6c805..e7b50e0b 100644 --- a/frontend/app/(hub)/fixeddatainput/subquadrats/page.tsx +++ b/frontend/app/(hub)/fixeddatainput/subquadrats/page.tsx @@ -1,5 +1,5 @@ import SubquadratsDataGrid from "@/components/client/sqdatagrid"; export default function SubquadratsPage() { - return ; + return ; } \ No newline at end of file diff --git a/frontend/app/(hub)/layout.tsx b/frontend/app/(hub)/layout.tsx index bb8e10e5..347048ad 100644 --- a/frontend/app/(hub)/layout.tsx +++ b/frontend/app/(hub)/layout.tsx @@ -9,9 +9,6 @@ import Divider from "@mui/joy/Divider"; import { useLoading } from "@/app/contexts/loadingprovider"; import { getAllSchemas } from "@/components/processors/processorhelperfunctions"; import { - useOrgCensusDispatch, - usePlotDispatch, - useQuadratDispatch, useSiteContext, usePlotContext, useOrgCensusContext, @@ -28,8 +25,8 @@ import { useSubquadratListContext, useSubquadratListDispatch, } from "@/app/contexts/listselectionprovider"; -import { createAndUpdateCensusList } from "@/config/sqlrdsdefinitions/orgcensusrds"; -import { siteConfig } from "@/config/macros/siteconfigs"; +import {createAndUpdateCensusList} from "@/config/sqlrdsdefinitions/orgcensusrds"; +import {siteConfig} from "@/config/macros/siteconfigs"; const Sidebar = dynamic(() => import('@/components/sidebar'), { ssr: false }); const Header = dynamic(() => import('@/components/header'), { ssr: false }); @@ -37,7 +34,8 @@ const Header = dynamic(() => import('@/components/header'), { ssr: false }); function renderSwitch(endpoint: string) { switch (endpoint) { case '/dashboard': - return

Dashboard - ForestGEO Application User Guide

; + return

Dashboard - ForestGEO Application User + Guide

; case '/measurementshub/summary': return

Measurements Summary

; case '/measurementshub/validationhistory': @@ -90,7 +88,7 @@ export default function HubLayout({ children }: { children: React.ReactNode }) { const [manualReset, setManualReset] = useState(false); const [isSidebarVisible, setSidebarVisible] = useState(!!session); - let pathname = usePathname(); + const pathname = usePathname(); const coreDataLoaded = siteListLoaded && plotListLoaded && censusListLoaded && (quadratListLoaded || subquadratListLoaded); @@ -99,18 +97,14 @@ export default function HubLayout({ children }: { children: React.ReactNode }) { if (censusListContext !== undefined && censusListContext.length > 0) return { success: true }; setLoading(true, 'Loading raw census data'); - let response = await fetch(`/api/fetchall/census/${currentPlot.plotID}?schema=${currentSite?.schemaName || ''}`); + const response = await fetch(`/api/fetchall/census/${currentPlot.plotID}?schema=${currentSite?.schemaName || ''}`); const censusRDSLoad = await response.json(); setLoading(false); setLoading(true, 'Converting raw census data...'); const censusList = await createAndUpdateCensusList(censusRDSLoad); - if (!censusList) return { success: false, message: 'Failed to convert census data' }; - setLoading(false); - - setLoading(true, "Dispatching cleaned census list to context"); if (censusListDispatch) { - censusListDispatch({ censusList: censusList }); + censusListDispatch({ censusList }); } setLoading(false); setCensusListLoaded(true); @@ -118,11 +112,12 @@ export default function HubLayout({ children }: { children: React.ReactNode }) { }, [censusListContext, censusListDispatch, currentPlot, currentSite, setLoading]); const loadPlotsData = useCallback(async () => { + if (!currentSite) return { success: false, message: 'Site must be selected to load plot data' }; if (plotListContext !== undefined && plotListContext.length > 0) return { success: true }; setLoading(true, "Loading plot list information..."); - let plotsResponse = await fetch(`/api/fetchall/plots?schema=${currentSite?.schemaName || ''}`); - let plotsData = await plotsResponse.json(); + const plotsResponse = await fetch(`/api/fetchall/plots?schema=${currentSite?.schemaName || ''}`); + const plotsData = await plotsResponse.json(); if (!plotsData) return { success: false, message: 'Failed to load plots data' }; setLoading(false); @@ -136,12 +131,15 @@ export default function HubLayout({ children }: { children: React.ReactNode }) { }, [plotListContext, plotListDispatch, currentSite, setLoading]); const loadQuadratsData = useCallback(async () => { - if (!currentPlot || !currentCensus) return { success: false, message: 'Plot and Census must be selected to load quadrat data' }; + if (!currentPlot || !currentCensus) return { + success: false, + message: 'Plot and Census must be selected to load quadrat data' + }; if (quadratListContext !== undefined && quadratListContext.length > 0) return { success: true }; setLoading(true, "Loading quadrat list information..."); - let quadratsResponse = await fetch(`/api/fetchall/quadrats/${currentPlot.plotID}/${currentCensus.plotCensusNumber}?schema=${currentSite?.schemaName || ''}`); - let quadratsData = await quadratsResponse.json(); + const quadratsResponse = await fetch(`/api/fetchall/quadrats/${currentPlot.plotID}/${currentCensus.plotCensusNumber}?schema=${currentSite?.schemaName || ''}`); + const quadratsData = await quadratsResponse.json(); if (!quadratsData) return { success: false, message: 'Failed to load quadrats data' }; setLoading(false); @@ -155,12 +153,15 @@ export default function HubLayout({ children }: { children: React.ReactNode }) { }, [quadratListContext, quadratListDispatch, currentPlot, currentCensus, currentSite, setLoading]); const loadSubquadratsData = useCallback(async () => { - if (!currentPlot || !currentCensus || !currentQuadrat) return { success: false, message: 'Plot, Census, and Quadrat must be selected to load subquadrat data' }; + if (!currentPlot || !currentCensus || !currentQuadrat) return { + success: false, + message: 'Plot, Census, and Quadrat must be selected to load subquadrat data' + }; if (subquadratListContext !== undefined && subquadratListContext.length > 0) return { success: true }; setLoading(true, "Loading subquadrat list information..."); - let subquadratResponse = await fetch(`/api/fetchall/subquadrats/${currentPlot.plotID}/${currentCensus.plotCensusNumber}/${currentQuadrat.quadratID}?schema=${currentSite?.schemaName || ''}`); - let subquadratData = await subquadratResponse.json(); + const subquadratResponse = await fetch(`/api/fetchall/subquadrats/${currentPlot.plotID}/${currentCensus.plotCensusNumber}/${currentQuadrat.quadratID}?schema=${currentSite?.schemaName || ''}`); + const subquadratData = await subquadratResponse.json(); if (!subquadratData) return { success: false, message: 'Failed to load subquadrats data' }; setLoading(false); @@ -177,16 +178,16 @@ export default function HubLayout({ children }: { children: React.ReactNode }) { setLoading(true, 'Loading Sites...'); try { if (session && !siteListLoaded) { - let sites = session?.user?.allsites ?? []; + const sites = session?.user?.allsites ?? []; if (sites.length === 0) { throw new Error("Session sites undefined"); } else { - siteListDispatch ? await siteListDispatch({ siteList: sites }) : undefined; + siteListDispatch ? await siteListDispatch({siteList: sites}) : undefined; } } } catch (e: any) { const allsites = await getAllSchemas(); - siteListDispatch ? await siteListDispatch({ siteList: allsites }) : undefined; + siteListDispatch ? await siteListDispatch({siteList: allsites}) : undefined; } setLoading(false); }, [session, siteListLoaded, siteListDispatch, setLoading]); @@ -232,9 +233,9 @@ export default function HubLayout({ children }: { children: React.ReactNode }) { if (siteListLoaded && currentSite && plotListLoaded && censusListLoaded && !quadratListLoaded) { loadQuadratsData().catch(console.error); } - if (siteListLoaded && currentSite && plotListLoaded && censusListLoaded && quadratListLoaded && !subquadratListLoaded) { - loadSubquadratsData().catch(console.error); - } + // if (siteListLoaded && currentSite && plotListLoaded && censusListLoaded && quadratListLoaded && !subquadratListLoaded) { + // loadSubquadratsData().catch(console.error); + // } }, [siteListLoaded, currentSite, plotListLoaded, censusListLoaded, quadratListLoaded, subquadratListLoaded, loadCensusData, loadPlotsData, loadQuadratsData, loadSubquadratsData]); useEffect(() => { diff --git a/frontend/app/(hub)/measurementshub/summary/page.tsx b/frontend/app/(hub)/measurementshub/summary/page.tsx index d5b7247b..02073889 100644 --- a/frontend/app/(hub)/measurementshub/summary/page.tsx +++ b/frontend/app/(hub)/measurementshub/summary/page.tsx @@ -2,11 +2,9 @@ import React, { useEffect, useState } from "react"; import { GridRowModes, GridRowModesModel, GridRowsProp } from "@mui/x-data-grid"; import { Alert, AlertProps, LinearProgress, Tooltip, TooltipProps, styled, tooltipClasses } from "@mui/material"; -import DataGridCommons from "@/components/datagrids/datagridcommons"; -import { MeasurementsSummaryGridColumns } from '@/config/sqlrdsdefinitions/views/measurementssummaryviewrds'; +import { gridColumnsArrayMSVRDS, initialMeasurementsSummaryViewRDSRow } from '@/config/sqlrdsdefinitions/views/measurementssummaryviewrds'; import { Box, - IconButton, ListItemContent, ListItem, List, @@ -19,14 +17,12 @@ import { DialogActions, Snackbar, Stack, - Switch, } from "@mui/joy"; import Select, { SelectOption } from "@mui/joy/Select"; import { useSession } from "next-auth/react"; import { useOrgCensusContext, usePlotContext, - useQuadratContext, useQuadratDispatch, useSiteContext } from "@/app/contexts/userselectionprovider"; @@ -58,11 +54,10 @@ export default function SummaryPage() { const { data: session } = useSession(); const [quadrat, setQuadrat] = useState(); const [quadratList, setQuadratList] = useState([]); - let currentPlot = usePlotContext(); - let currentCensus = useOrgCensusContext(); - let currentSite = useSiteContext(); - let quadratListContext = useQuadratListContext(); - let currentQuadrat = useQuadratContext(); + const currentPlot = usePlotContext(); + const currentCensus = useOrgCensusContext(); + const currentSite = useSiteContext(); + const quadratListContext = useQuadratListContext(); const quadratDispatch = useQuadratDispatch(); const { validity, recheckValidityIfNeeded } = useDataValidityContext(); const [progressDialogOpen, setProgressDialogOpen] = useState(false); @@ -78,41 +73,7 @@ export default function SummaryPage() { } }, [currentPlot, quadratListContext]); - const initialRows: GridRowsProp = [ - { - id: 0, - coreMeasurementID: 0, - plotID: null, - plotName: null, - censusID: null, - censusStartDate: null, - censusEndDate: null, - quadratID: null, - quadratName: '', - subquadratID: null, - subquadratName: '', - stemID: null, - stemTag: '', - speciesID: null, - speciesCode: '', - treeID: null, - treeTag: '', - stemLocalX: 0, - stemLocalY: 0, - stemUnits: '', - personnelID: 0, - personnelName: '', - measurementDate: null, - measuredDBH: 0, - dbhUnits: '', - measuredHOM: 0, - homUnits: '', - description: '', - attributes: [], - } - ]; - - const [rows, setRows] = React.useState(initialRows); + const [rows, setRows] = React.useState([initialMeasurementsSummaryViewRDSRow] as GridRowsProp); const [rowCount, setRowCount] = useState(0); // total number of rows const [rowModesModel, setRowModesModel] = React.useState({}); const [snackbar, setSnackbar] = React.useState | null>(null); @@ -127,7 +88,7 @@ export default function SummaryPage() { useEffect(() => { const verifyPreconditions = async () => { - setIsUploadAllowed(!Object.values(validity).includes(false)); + setIsUploadAllowed(!Object.entries(validity).filter(item => item[0] !== 'subquadrats').map(item => item[1]).includes(false)); }; if (progressDialogOpen) { @@ -139,6 +100,7 @@ export default function SummaryPage() { const id = randomId(); // Define new row structure based on MeasurementsSummaryRDS type const newRow = { + ...initialMeasurementsSummaryViewRDSRow, id: id, coreMeasurementID: 0, plotID: currentPlot?.plotID, @@ -146,28 +108,6 @@ export default function SummaryPage() { censusID: currentCensus?.dateRanges[0].censusID, censusStartDate: currentCensus?.dateRanges[0]?.startDate, censusEndDate: currentCensus?.dateRanges[0]?.endDate, - quadratID: null, - quadratName: null, - subquadratID: null, - subquadratName: null, - speciesID: 0, - speciesCode: '', - treeID: 0, - treeTag: '', - stemID: 0, - stemTag: '', - stemLocalX: 0, - stemLocalY: 0, - stemUnits: '', - personnelID: 0, - personnelName: '', - measurementDate: undefined, - measuredDBH: 0, - dbhUnits: '', - measuredHOM: 0, - homUnits: '', - description: '', - attributes: [], isNew: true, }; setRows(oldRows => [...oldRows, newRow]); @@ -191,7 +131,7 @@ export default function SummaryPage() { } }; - const checklistItems: (keyof UnifiedValidityFlags)[] = ['attributes', 'species', 'personnel', 'quadrats', 'subquadrats']; + const checklistItems: (keyof UnifiedValidityFlags)[] = ['attributes', 'species', 'personnel', 'quadrats']; const ProgressDialog = () => ( { const updateUseSubquadrats = async () => { - let updatedPlot = { + const updatedPlot = { ...currentPlot, usesSubquadrats: useSubquadrats, }; @@ -370,20 +310,24 @@ export default function SummaryPage() { {currentPlot?.usesSubquadrats ? ( - - Note: This plot has been set to accept subquadrats.
+ + Note: This plot has been set to accept + subquadrats.
Please ensure you select a quadrat before proceeding.
) : ( - Note: This plot does not accept subquadrats.
- Please ensure that you use quadrat names when submitting new measurements instead of subquadrat names
+ Note: This plot does not accept + subquadrats.
+ Please ensure that you use quadrat names when submitting new measurements instead of subquadrat + names
)} {session?.user.isAdmin ? ( - Note: ADMINISTRATOR VIEW + {/* Note: ADMINISTRATOR VIEW - Please use the toggle to change this setting if it is incorrect + Please use the toggle to change this + setting if it is incorrect setUseSubquadrats(event.target.checked)} @@ -398,17 +342,20 @@ export default function SummaryPage() { }, }} /> - + */} - ) : ( - If this setting is inaccurate, please contact an administrator. + If this setting is inaccurate, please contact + an administrator. )}
- +
@@ -421,8 +368,7 @@ export default function SummaryPage() { formType={"measurements"} /> 3 || params.slugs.length < 3)) throw new Error("incorrect slugs provided"); @@ -18,53 +18,53 @@ export async function GET(_request: NextRequest, { params }: { params: { dataTyp case 'attributes': case 'personnel': case 'species': - let baseQuery = `SELECT 1 FROM ${schema}.${params.dataType} LIMIT 1`; // Check if the table has any row + const baseQuery = `SELECT 1 FROM ${schema}.${params.dataType} LIMIT 1`; // Check if the table has any row const baseResults = await runQuery(connection, baseQuery); if (connection) connection.release(); - if (baseResults.length === 0) return new NextResponse(null, { status: HTTPResponses.PRECONDITION_VALIDATION_FAILURE }); + if (baseResults.length === 0) return new NextResponse(null, {status: HTTPResponses.PRECONDITION_VALIDATION_FAILURE}); break; case 'quadrats': - let query = `SELECT 1 FROM ${schema}.${params.dataType} WHERE PlotID = ${plotID} AND CensusID IN (SELECT CensusID from ${schema}.census WHERE PlotCensusNumber = ${plotCensusNumber})`; // Check if the table has any row + const query = `SELECT 1 FROM ${schema}.${params.dataType} WHERE PlotID = ${plotID} AND CensusID IN (SELECT CensusID from ${schema}.census WHERE PlotCensusNumber = ${plotCensusNumber})`; // Check if the table has any row const results = await runQuery(connection, query); if (connection) connection.release(); - if (results.length === 0) return new NextResponse(null, { status: HTTPResponses.PRECONDITION_VALIDATION_FAILURE }); + if (results.length === 0) return new NextResponse(null, {status: HTTPResponses.PRECONDITION_VALIDATION_FAILURE}); break; case 'subquadrats': - let subquadratsQuery = `SELECT 1 + const subquadratsQuery = `SELECT 1 FROM ${schema}.${params.dataType} s JOIN ${schema}.quadrats q ON s.QuadratID = q.QuadratID WHERE q.PlotID = ${plotID} AND q.CensusID IN (SELECT CensusID from ${schema}.census WHERE PlotCensusNumber = ${plotCensusNumber}) LIMIT 1`; const subquadratsResults = await runQuery(connection, subquadratsQuery); if (connection) connection.release(); - if (subquadratsResults.length === 0) return new NextResponse(null, { status: HTTPResponses.PRECONDITION_VALIDATION_FAILURE }); + if (subquadratsResults.length === 0) return new NextResponse(null, {status: HTTPResponses.PRECONDITION_VALIDATION_FAILURE}); break; case 'quadratpersonnel': // Validation for quadrats table - let quadratsQuery = `SELECT 1 + const quadratsQuery = `SELECT 1 FROM ${schema}.quadrats WHERE PlotID = ${plotID} AND CensusID IN (SELECT CensusID from ${schema}.census WHERE PlotCensusNumber = ${plotCensusNumber}) LIMIT 1`; const quadratsResults = await runQuery(connection, quadratsQuery); if (connection) connection.release(); - if (quadratsResults.length === 0) return new NextResponse(null, { status: HTTPResponses.PRECONDITION_VALIDATION_FAILURE }); + if (quadratsResults.length === 0) return new NextResponse(null, {status: HTTPResponses.PRECONDITION_VALIDATION_FAILURE}); // Validation for personnel table - let personnelQuery = `SELECT 1 FROM ${schema}.personnel LIMIT 1`; + const personnelQuery = `SELECT 1 FROM ${schema}.personnel LIMIT 1`; const personnelResults = await runQuery(connection, personnelQuery); if (connection) connection.release(); - if (personnelResults.length === 0) return new NextResponse(null, { status: HTTPResponses.PRECONDITION_VALIDATION_FAILURE }); + if (personnelResults.length === 0) return new NextResponse(null, {status: HTTPResponses.PRECONDITION_VALIDATION_FAILURE}); break; default: - return new NextResponse(null, { status: HTTPResponses.PRECONDITION_VALIDATION_FAILURE }); + return new NextResponse(null, {status: HTTPResponses.PRECONDITION_VALIDATION_FAILURE}); } // If all conditions are satisfied connection.release(); - return new NextResponse(null, { status: 200 }); + return new NextResponse(null, {status: 200}); } catch (e: any) { console.error(e); - return new NextResponse(null, { status: HTTPResponses.PRECONDITION_VALIDATION_FAILURE }); + return new NextResponse(null, {status: HTTPResponses.PRECONDITION_VALIDATION_FAILURE}); } finally { if (connection) connection.release(); } diff --git a/frontend/app/api/details/cmid/route.ts b/frontend/app/api/details/cmid/route.ts index d3959095..6535569b 100644 --- a/frontend/app/api/details/cmid/route.ts +++ b/frontend/app/api/details/cmid/route.ts @@ -9,7 +9,7 @@ export async function GET(request: NextRequest) { let conn: PoolConnection | null = null; try { conn = await getConn(); - let query = ` + const query = ` SELECT cm.CoreMeasurementID, p.PlotName, diff --git a/frontend/app/api/fetchall/[[...slugs]]/route.ts b/frontend/app/api/fetchall/[[...slugs]]/route.ts index ae798544..f87640e1 100644 --- a/frontend/app/api/fetchall/[[...slugs]]/route.ts +++ b/frontend/app/api/fetchall/[[...slugs]]/route.ts @@ -1,8 +1,8 @@ -import { getConn, runQuery } from "@/components/processors/processormacros"; -import MapperFactory, { IDataMapper } from "@/config/datamapper"; -import { HTTPResponses } from "@/config/macros"; -import { PoolConnection } from "mysql2/promise"; -import { NextRequest, NextResponse } from "next/server"; +import {getConn, runQuery} from "@/components/processors/processormacros"; +import MapperFactory, {IDataMapper} from "@/config/datamapper"; +import {HTTPResponses} from "@/config/macros"; +import {PoolConnection} from "mysql2/promise"; +import {NextRequest, NextResponse} from "next/server"; const buildQuery = (schema: string, fetchType: string, plotID?: string, plotCensusNumber?: string, quadratID?: string): string => { if (fetchType === 'plots') { @@ -39,7 +39,7 @@ const buildQuery = (schema: string, fetchType: string, plotID?: string, plotCens }; -export async function GET(request: NextRequest, { params }: { params: { slugs?: string[] } }) { +export async function GET(request: NextRequest, {params}: { params: { slugs?: string[] } }) { const schema = request.nextUrl.searchParams.get('schema'); if (!schema || schema === 'undefined') { throw new Error("Schema selection was not provided to API endpoint"); @@ -48,7 +48,7 @@ export async function GET(request: NextRequest, { params }: { params: { slugs?: const [fetchType, plotID, censusID, quadratID] = params.slugs ?? []; if (!fetchType) { throw new Error("fetchType was not correctly provided"); - } + } console.log('fetchall --> slugs provided: fetchType: ', fetchType, 'plotID: ', plotID, 'censusID: ', censusID, 'quadratID: ', quadratID); const query = buildQuery(schema, fetchType, plotID, censusID, quadratID); @@ -58,12 +58,12 @@ export async function GET(request: NextRequest, { params }: { params: { slugs?: conn = await getConn(); const results = await runQuery(conn, query); if (!results) { - return new NextResponse(null, { status: 500 }); + return new NextResponse(null, {status: 500}); } const mapper: IDataMapper = MapperFactory.getMapper(fetchType); const rows = mapper.mapData(results); - return new NextResponse(JSON.stringify(rows), { status: HTTPResponses.OK }); + return new NextResponse(JSON.stringify(rows), {status: HTTPResponses.OK}); } catch (error) { console.error('Error:', error); throw new Error("Call failed"); diff --git a/frontend/app/api/filehandlers/deletefile/route.ts b/frontend/app/api/filehandlers/deletefile/route.ts index 9c959e6e..6e04f8a4 100644 --- a/frontend/app/api/filehandlers/deletefile/route.ts +++ b/frontend/app/api/filehandlers/deletefile/route.ts @@ -10,7 +10,7 @@ export async function DELETE(request: NextRequest) { } try { - const containerClient = await getContainerClient(containerName); // Adjust as needed + const containerClient = await getContainerClient(containerName.toLowerCase()); // Adjust as needed if (!containerClient) return new NextResponse('Container name and filename are required', {status: 400}); const blobClient = containerClient.getBlobClient(filename); diff --git a/frontend/app/api/filehandlers/downloadfile/route.ts b/frontend/app/api/filehandlers/downloadfile/route.ts index 4b7048dc..c14b8fc5 100644 --- a/frontend/app/api/filehandlers/downloadfile/route.ts +++ b/frontend/app/api/filehandlers/downloadfile/route.ts @@ -1,5 +1,5 @@ -import { NextRequest, NextResponse } from "next/server"; -import { getContainerClient } from "@/config/macros/azurestorage"; +import {NextRequest, NextResponse} from "next/server"; +import {getContainerClient} from "@/config/macros/azurestorage"; import { BlobSASPermissions, BlobServiceClient, @@ -13,13 +13,13 @@ export async function GET(request: NextRequest) { const storageAccountConnectionString = process.env.AZURE_STORAGE_CONNECTION_STRING; if (!containerName || !filename || !storageAccountConnectionString) { - return new NextResponse('Container name, filename, and storage connection string are required', { status: 400 }); + return new NextResponse('Container name, filename, and storage connection string are required', {status: 400}); } try { - const containerClient = await getContainerClient(containerName); + const containerClient = await getContainerClient(containerName.toLowerCase()); if (!containerClient) { - return new NextResponse('Failed to get container client', { status: 400 }); + return new NextResponse('Failed to get container client', {status: 400}); } const blobServiceClient = BlobServiceClient.fromConnectionString(storageAccountConnectionString); @@ -39,7 +39,7 @@ export async function GET(request: NextRequest) { } const url = `${blobClient.url}?${sasToken}`; - return new NextResponse(JSON.stringify({ url }), { + return new NextResponse(JSON.stringify({url}), { status: 200, headers: { 'Content-Type': 'application/json' @@ -47,6 +47,6 @@ export async function GET(request: NextRequest) { }); } catch (error) { console.error('Download file error:', error); - return new NextResponse((error as Error).message, { status: 500 }); + return new NextResponse((error as Error).message, {status: 500}); } } diff --git a/frontend/app/api/filehandlers/storageload/route.ts b/frontend/app/api/filehandlers/storageload/route.ts index a4418290..36bcaa1a 100644 --- a/frontend/app/api/filehandlers/storageload/route.ts +++ b/frontend/app/api/filehandlers/storageload/route.ts @@ -1,6 +1,6 @@ -import { NextRequest, NextResponse } from 'next/server'; -import { HTTPResponses } from '@/config/macros'; -import { getContainerClient, uploadValidFileAsBuffer } from '@/config/macros/azurestorage'; +import {NextRequest, NextResponse} from 'next/server'; +import {HTTPResponses} from '@/config/macros'; +import {getContainerClient, uploadValidFileAsBuffer} from '@/config/macros/azurestorage'; export async function POST(request: NextRequest) { let formData: FormData; @@ -8,30 +8,30 @@ export async function POST(request: NextRequest) { formData = await request.formData(); if (formData === null || formData === undefined || formData.entries().next().done) throw new Error(); } catch (error) { - return new NextResponse('File is required', { status: 400 }); - } + return new NextResponse('File is required', {status: 400}); + } console.log("formData: ", formData); - const fileName = request.nextUrl.searchParams.get('fileName')?.trim(); + const fileName = request.nextUrl.searchParams.get('fileName')?.trim() ; const plot = request.nextUrl.searchParams.get("plot")?.trim(); const census = request.nextUrl.searchParams.get("census")?.trim(); const user = request.nextUrl.searchParams.get("user"); const formType = request.nextUrl.searchParams.get('formType'); - const file = formData.get('file') as File | null; + const file = formData.get(fileName ?? 'file') as File | null; const fileRowErrors = formData.get('fileRowErrors') ? JSON.parse(formData.get('fileRowErrors')) : []; - if ((file === null || file === undefined) || - (fileName === undefined || fileName === null) || - (plot === undefined || plot === null) || - (census === undefined || census === null) || - (user === undefined || user === null) || + if ((file === null || file === undefined) || + (fileName === undefined || fileName === null) || + (plot === undefined || plot === null) || + (census === undefined || census === null) || + (user === undefined || user === null) || (formType === undefined || formType === null)) { - return new NextResponse('Missing required parameters', { status: 400 }); + return new NextResponse('Missing required parameters', {status: 400}); } let containerClient; try { - containerClient = await getContainerClient(`${plot}-${census}`.toLowerCase()); + containerClient = await getContainerClient(`${plot.toLowerCase()}-${census.toLowerCase()}`); } catch (error: any) { console.error("Error getting container client:", error.message); return new NextResponse( @@ -39,7 +39,7 @@ export async function POST(request: NextRequest) { responseMessage: "Error getting container client.", error: error.message, }), - { status: HTTPResponses.INTERNAL_SERVER_ERROR } + {status: HTTPResponses.INTERNAL_SERVER_ERROR} ); } @@ -49,7 +49,7 @@ export async function POST(request: NextRequest) { JSON.stringify({ responseMessage: "Container client is undefined", }), - { status: HTTPResponses.INTERNAL_SERVER_ERROR } + {status: HTTPResponses.INTERNAL_SERVER_ERROR} ); } @@ -66,9 +66,9 @@ export async function POST(request: NextRequest) { responseMessage: "File Processing error", error: error.message ? error.message : 'Unknown error', }), - { status: HTTPResponses.INTERNAL_SERVER_ERROR } + {status: HTTPResponses.INTERNAL_SERVER_ERROR} ); } - return new NextResponse(JSON.stringify({ message: "Insert to Azure Storage successful" }), { status: 200 }); + return new NextResponse(JSON.stringify({message: "Insert to Azure Storage successful"}), {status: 200}); } diff --git a/frontend/app/api/fixeddata/[dataType]/[[...slugs]]/route.ts b/frontend/app/api/fixeddata/[dataType]/[[...slugs]]/route.ts index 16b06a3b..41e3fe7e 100644 --- a/frontend/app/api/fixeddata/[dataType]/[[...slugs]]/route.ts +++ b/frontend/app/api/fixeddata/[dataType]/[[...slugs]]/route.ts @@ -1,8 +1,8 @@ -import { getConn, runQuery } from "@/components/processors/processormacros"; +import {getConn, runQuery} from "@/components/processors/processormacros"; import MapperFactory from "@/config/datamapper"; -import { handleError } from "@/utils/errorhandler"; -import { PoolConnection, format } from "mysql2/promise"; -import { NextRequest, NextResponse } from "next/server"; +import {handleError} from "@/utils/errorhandler"; +import {PoolConnection, format} from "mysql2/promise"; +import {NextRequest, NextResponse} from "next/server"; import { generateInsertOperations, generateUpdateOperations, @@ -10,9 +10,12 @@ import { AllTaxonomiesViewQueryConfig, StemTaxonomiesViewQueryConfig, } from '@/components/processors/processorhelperfunctions'; +import { HTTPResponses } from "@/config/macros"; // slugs SHOULD CONTAIN AT MINIMUM: schema, page, pageSize, plotID, plotCensusNumber, (optional) quadratID -export async function GET(request: NextRequest, { params }: { params: { dataType: string, slugs?: string[] } }) { +export async function GET(request: NextRequest, {params}: { + params: { dataType: string, slugs?: string[] } +}): Promise> { if (!params.slugs || params.slugs.length < 5) throw new Error("slugs not received."); const [schema, pageParam, pageSizeParam, plotIDParam, plotCensusNumberParam, quadratIDParam] = params.slugs; if ((!schema || schema === 'undefined') || (!pageParam || pageParam === 'undefined') || (!pageSizeParam || pageSizeParam === 'undefined')) throw new Error("core slugs schema/page/pageSize not correctly received"); @@ -24,6 +27,10 @@ export async function GET(request: NextRequest, { params }: { params: { dataType const plotCensusNumber = parseInt(plotCensusNumberParam); const quadratID = quadratIDParam ? parseInt(quadratIDParam) : undefined; let conn: PoolConnection | null = null; + let updatedMeasurementsExist = false; + let censusIDs; + let mostRecentCensusID: any; + let pastCensusIDs: string | any[]; try { conn = await getConn(); @@ -44,7 +51,6 @@ export async function GET(request: NextRequest, { params }: { params: { dataType LIMIT ?, ?`; queryParams.push(page * pageSize, pageSize); break; - case 'quadrats': paginatedQuery = ` SELECT SQL_CALC_FOUND_ROWS q.* @@ -60,7 +66,6 @@ export async function GET(request: NextRequest, { params }: { params: { dataType LIMIT ?, ?`; queryParams.push(plotID, plotCensusNumber, page * pageSize, pageSize); break; - case 'subquadrats': if (!quadratID || quadratID === 0) { throw new Error("QuadratID must be provided as part of slug fetch query, referenced fixeddata slug route"); @@ -80,7 +85,6 @@ export async function GET(request: NextRequest, { params }: { params: { dataType LIMIT ?, ?`; queryParams.push(quadratID, plotID, plotCensusNumber, page * pageSize, pageSize); break; - case 'census': paginatedQuery = ` SELECT SQL_CALC_FOUND_ROWS * @@ -89,13 +93,23 @@ export async function GET(request: NextRequest, { params }: { params: { dataType LIMIT ?, ?`; queryParams.push(plotID, page * pageSize, pageSize); break; - case 'coremeasurements': case 'measurementssummaryview': case 'stemdimensionsview': - paginatedQuery = ` + // Retrieve multiple past CensusID for the given PlotCensusNumber + const censusQuery = ` + SELECT CensusID + FROM ${schema}.census + WHERE PlotID = ? + AND PlotCensusNumber = ? + ORDER BY StartDate DESC + LIMIT 30 + `; + const censusResults = await runQuery(conn, format(censusQuery, [plotID, plotCensusNumber])); + if (censusResults.length < 2) { + paginatedQuery = ` SELECT SQL_CALC_FOUND_ROWS * - FROM ${schema}.${params.dataType} cm + FROM ${schema}.${params.dataType} WHERE PlotID = ? AND CensusID IN ( SELECT c.CensusID @@ -104,13 +118,24 @@ export async function GET(request: NextRequest, { params }: { params: { dataType AND c.PlotCensusNumber = ? ) LIMIT ?, ?`; - // ${quadratID ? `AND cm.QuadratID = ?` : ''} - queryParams.push(plotID, plotCensusNumber); - // if (quadratID) { - // queryParams.push(quadratID); - // } - queryParams.push(page * pageSize, pageSize); - break; + queryParams.push(plotID, plotCensusNumber, page * pageSize, pageSize); + break; + } else { + updatedMeasurementsExist = true; + censusIDs = censusResults.map((c: any) => c.CensusID); + mostRecentCensusID = censusIDs[0]; + pastCensusIDs = censusIDs.slice(1); + // Query to fetch paginated measurements from measurementssummaryview + paginatedQuery = ` + SELECT SQL_CALC_FOUND_ROWS * + FROM ${schema}.measurementssummaryview + WHERE PlotID = ? + AND CensusID IN (${censusIDs.map(() => '?').join(', ')}) + LIMIT ?, ? + `; + queryParams.push(plotID, ...censusIDs, page * pageSize, pageSize); + break; + } default: throw new Error(`Unknown dataType: ${params.dataType}`); } @@ -121,12 +146,41 @@ export async function GET(request: NextRequest, { params }: { params: { dataType } const paginatedResults = await runQuery(conn, format(paginatedQuery, queryParams)); + const totalRowsQuery = "SELECT FOUND_ROWS() as totalRows"; const totalRowsResult = await runQuery(conn, totalRowsQuery); const totalRows = totalRowsResult[0].totalRows; - const mapper = MapperFactory.getMapper(params.dataType); - const rows = mapper.mapData(paginatedResults); - return new NextResponse(JSON.stringify({ output: rows, totalCount: totalRows }), { status: 200 }); + + if (updatedMeasurementsExist) { + // Separate deprecated and non-deprecated rows + const deprecated = paginatedResults.filter((row: any) => pastCensusIDs.includes(row.CensusID)); + + // Ensure deprecated measurements are duplicates + const uniqueKeys = ['PlotID', 'QuadratID', 'TreeID', 'StemID']; // Define unique keys that should match + const outputKeys = paginatedResults.map((row: any) => + uniqueKeys.map((key) => row[key]).join('|') + ); + const filteredDeprecated = deprecated.filter((row: any) => + outputKeys.includes(uniqueKeys.map((key) => row[key]).join('|')) + ); + // Map data using the appropriate mapper + const mapper = MapperFactory.getMapper(params.dataType); + const deprecatedRows = mapper.mapData(filteredDeprecated); + const rows = mapper.mapData(paginatedResults); + return new NextResponse(JSON.stringify({ + output: rows, + deprecated: deprecatedRows, + totalCount: totalRows + }), {status: 200}); + } else { + const mapper = MapperFactory.getMapper(params.dataType); + const rows = mapper.mapData(paginatedResults); + return new NextResponse(JSON.stringify({ + output: rows, + deprecated: undefined, + totalCount: totalRows + }), {status: 200}); + } } catch (error: any) { if (conn) await conn.rollback(); throw new Error(error); @@ -136,19 +190,19 @@ export async function GET(request: NextRequest, { params }: { params: { dataType } // required dynamic parameters: dataType (fixed),[ schema, gridID value] -> slugs -export async function POST(request: NextRequest, { params }: { params: { dataType: string, slugs?: string[] } }) { +export async function POST(request: NextRequest, {params}: { params: { dataType: string, slugs?: string[] } }) { if (!params.slugs) throw new Error("slugs not provided"); const [schema, gridID] = params.slugs; if (!schema || !gridID) throw new Error("no schema or gridID provided"); let conn: PoolConnection | null = null; - const { newRow } = await request.json(); + const {newRow} = await request.json(); try { conn = await getConn(); await conn.beginTransaction(); if (Object.keys(newRow).includes('isNew')) delete newRow.isNew; const mapper = MapperFactory.getMapper(params.dataType); const newRowData = mapper.demapData([newRow])[0]; - let demappedGridID = gridID.charAt(0).toUpperCase() + gridID.substring(1); + const demappedGridID = gridID.charAt(0).toUpperCase() + gridID.substring(1); if (params.dataType.includes('view')) { let queryConfig; @@ -179,7 +233,7 @@ export async function POST(request: NextRequest, { params }: { params: { dataTyp await runQuery(conn, insertQuery); } await conn.commit(); - return NextResponse.json({ message: "Insert successful" }, { status: 200 }); + return NextResponse.json({message: "Insert successful"}, {status: 200}); } catch (error: any) { return handleError(error, conn, newRow); } finally { @@ -188,20 +242,20 @@ export async function POST(request: NextRequest, { params }: { params: { dataTyp } // slugs: schema, gridID -export async function PATCH(request: NextRequest, { params }: { params: { dataType: string, slugs?: string[] } }) { +export async function PATCH(request: NextRequest, {params}: { params: { dataType: string, slugs?: string[] } }) { if (!params.slugs) throw new Error("slugs not provided"); const [schema, gridID] = params.slugs; if (!schema || !gridID) throw new Error("no schema or gridID provided"); let conn: PoolConnection | null = null; - let demappedGridID = gridID.charAt(0).toUpperCase() + gridID.substring(1); - const { newRow, oldRow } = await request.json(); + const demappedGridID = gridID.charAt(0).toUpperCase() + gridID.substring(1); + const {newRow, oldRow} = await request.json(); try { conn = await getConn(); await conn.beginTransaction(); if (!['alltaxonomiesview', 'stemdimensionsview', 'stemtaxonomiesview', 'measurementssummaryview'].includes(params.dataType)) { const mapper = MapperFactory.getMapper(params.dataType); const newRowData = mapper.demapData([newRow])[0]; - const { [demappedGridID]: gridIDKey, ...remainingProperties } = newRowData; + const {[demappedGridID]: gridIDKey, ...remainingProperties} = newRowData; const updateQuery = format(`UPDATE ?? SET ? WHERE ?? = ?`, [`${schema}.${params.dataType}`, remainingProperties, demappedGridID, gridIDKey]); await runQuery(conn, updateQuery); await conn.commit(); @@ -226,7 +280,7 @@ export async function PATCH(request: NextRequest, { params }: { params: { dataTy } await conn.commit(); } - return NextResponse.json({ message: "Update successful" }, { status: 200 }); + return NextResponse.json({message: "Update successful"}, {status: 200}); } catch (error: any) { return handleError(error, conn, newRow); } finally { @@ -249,7 +303,7 @@ export async function DELETE(request: NextRequest, { params }: { params: { dataT const [schema, gridID] = params.slugs; if (!schema || !gridID) throw new Error("no schema or gridID provided"); let conn: PoolConnection | null = null; - let demappedGridID = gridID.charAt(0).toUpperCase() + gridID.substring(1); + const demappedGridID = gridID.charAt(0).toUpperCase() + gridID.substring(1); const { newRow } = await request.json(); try { conn = await getConn(); @@ -279,8 +333,13 @@ export async function DELETE(request: NextRequest, { params }: { params: { dataT await conn.commit(); return NextResponse.json({ message: "Delete successful" }, { status: 200 }); } catch (error: any) { + if (error.code === 'ER_ROW_IS_REFERENCED_2') { + const referencingTableMatch = error.message.match(/CONSTRAINT `(.*?)` FOREIGN KEY \(`(.*?)`\) REFERENCES `(.*?)`/); + const referencingTable = referencingTableMatch ? referencingTableMatch[3] : 'unknown'; + return NextResponse.json({ message: "Foreign key conflict detected", referencingTable }, { status: HTTPResponses.FOREIGN_KEY_CONFLICT }); + } return handleError(error, conn, newRow); } finally { if (conn) conn.release(); } -} +} \ No newline at end of file diff --git a/frontend/app/api/formsearch/attributes/route.ts b/frontend/app/api/formsearch/attributes/route.ts index 7f446f3b..87121c98 100644 --- a/frontend/app/api/formsearch/attributes/route.ts +++ b/frontend/app/api/formsearch/attributes/route.ts @@ -1,5 +1,4 @@ import {NextRequest, NextResponse} from "next/server"; -import {PoolConnection} from "mysql2/promise"; import {getConn, runQuery} from "@/components/processors/processormacros"; import {FORMSEARCH_LIMIT} from "@/config/macros/azurestorage"; @@ -7,8 +6,7 @@ export async function GET(request: NextRequest): Promise> const schema = request.nextUrl.searchParams.get('schema'); if ((!schema || schema === 'undefined')) throw new Error('no schema provided!'); const partialCode = request.nextUrl.searchParams.get('searchfor')!; - let conn: PoolConnection | null; - conn = await getConn(); + const conn = await getConn(); try { const query = partialCode === '' ? `SELECT DISTINCT Code FROM ${schema}.attributes ORDER BY Code LIMIT ${FORMSEARCH_LIMIT}` : diff --git a/frontend/app/api/formsearch/personnel/route.ts b/frontend/app/api/formsearch/personnel/route.ts index 6a0ede41..5b2ef6ee 100644 --- a/frontend/app/api/formsearch/personnel/route.ts +++ b/frontend/app/api/formsearch/personnel/route.ts @@ -1,5 +1,4 @@ import {NextRequest, NextResponse} from "next/server"; -import {PoolConnection} from "mysql2/promise"; import {getConn, runQuery} from "@/components/processors/processormacros"; import {FORMSEARCH_LIMIT} from "@/config/macros/azurestorage"; @@ -7,8 +6,7 @@ export async function GET(request: NextRequest): Promise> const schema = request.nextUrl.searchParams.get('schema'); if ((!schema || schema === 'undefined')) throw new Error('no schema provided!'); const partialLastName = request.nextUrl.searchParams.get('searchfor')!; - let conn: PoolConnection | null; - conn = await getConn(); + const conn = await getConn(); try { const query = partialLastName === '' ? `SELECT FirstName, LastName diff --git a/frontend/app/api/formsearch/personnelblock/route.ts b/frontend/app/api/formsearch/personnelblock/route.ts index 38f4313c..3e5df2be 100644 --- a/frontend/app/api/formsearch/personnelblock/route.ts +++ b/frontend/app/api/formsearch/personnelblock/route.ts @@ -8,8 +8,7 @@ export async function GET(request: NextRequest): Promise> const schema = request.nextUrl.searchParams.get('schema'); if ((!schema || schema === 'undefined')) throw new Error('no schema provided!'); const partialQuadratName = request.nextUrl.searchParams.get('searchfor')!; - let conn: PoolConnection | null; - conn = await getConn(); + const conn = await getConn(); try { const query = partialQuadratName === '' ? `SELECT QuadratName diff --git a/frontend/app/api/formsearch/species/route.ts b/frontend/app/api/formsearch/species/route.ts index 00580eb5..46f9b883 100644 --- a/frontend/app/api/formsearch/species/route.ts +++ b/frontend/app/api/formsearch/species/route.ts @@ -1,5 +1,4 @@ import {NextRequest, NextResponse} from "next/server"; -import {PoolConnection} from "mysql2/promise"; import {getConn, runQuery} from "@/components/processors/processormacros"; import {FORMSEARCH_LIMIT} from "@/config/macros/azurestorage"; @@ -7,8 +6,7 @@ export async function GET(request: NextRequest): Promise> const schema = request.nextUrl.searchParams.get('schema'); if ((!schema || schema === 'undefined')) throw new Error('no schema provided!'); const partialSpeciesCode = request.nextUrl.searchParams.get('searchfor')!; - let conn: PoolConnection | null; - conn = await getConn(); + const conn = await getConn(); try { const query = partialSpeciesCode === '' ? `SELECT SpeciesCode FROM ${schema}.species diff --git a/frontend/app/api/formsearch/stems/route.ts b/frontend/app/api/formsearch/stems/route.ts index 8bfd3fd1..013c7507 100644 --- a/frontend/app/api/formsearch/stems/route.ts +++ b/frontend/app/api/formsearch/stems/route.ts @@ -1,5 +1,4 @@ import {NextRequest, NextResponse} from "next/server"; -import {PoolConnection} from "mysql2/promise"; import {getConn, runQuery} from "@/components/processors/processormacros"; import {FORMSEARCH_LIMIT} from "@/config/macros/azurestorage"; @@ -7,8 +6,7 @@ export async function GET(request: NextRequest): Promise> const schema = request.nextUrl.searchParams.get('schema'); if ((!schema || schema === 'undefined')) throw new Error('no schema provided!'); const partialStemTag = request.nextUrl.searchParams.get('searchfor')!; - let conn: PoolConnection | null; - conn = await getConn(); + const conn = await getConn(); try { const query = partialStemTag === '' ? `SELECT StemTag diff --git a/frontend/app/api/formsearch/trees/route.ts b/frontend/app/api/formsearch/trees/route.ts index 0d246241..318f8fea 100644 --- a/frontend/app/api/formsearch/trees/route.ts +++ b/frontend/app/api/formsearch/trees/route.ts @@ -1,5 +1,4 @@ import {NextRequest, NextResponse} from "next/server"; -import {PoolConnection} from "mysql2/promise"; import {getConn, runQuery} from "@/components/processors/processormacros"; import {FORMSEARCH_LIMIT} from "@/config/macros/azurestorage"; @@ -7,8 +6,7 @@ export async function GET(request: NextRequest): Promise> const schema = request.nextUrl.searchParams.get('schema'); if ((!schema || schema === 'undefined')) throw new Error('no schema provided!'); const partialTreeTag = request.nextUrl.searchParams.get('searchfor')!; - let conn: PoolConnection | null; - conn = await getConn(); + const conn = await getConn(); try { const query = partialTreeTag === '' ? `SELECT TreeTag diff --git a/frontend/app/api/formvalidation/[dataType]/[[...slugs]]/route.ts b/frontend/app/api/formvalidation/[dataType]/[[...slugs]]/route.ts index 5984ed78..e914102d 100644 --- a/frontend/app/api/formvalidation/[dataType]/[[...slugs]]/route.ts +++ b/frontend/app/api/formvalidation/[dataType]/[[...slugs]]/route.ts @@ -1,28 +1,28 @@ -import { getConn, runQuery } from "@/components/processors/processormacros"; -import { PoolConnection, format } from "mysql2/promise"; -import { NextRequest, NextResponse } from "next/server"; +import {getConn, runQuery} from "@/components/processors/processormacros"; +import {PoolConnection, format} from "mysql2/promise"; +import {NextRequest, NextResponse} from "next/server"; // dataType // slugs: schema, columnName, value ONLY // needs to match dynamic format established by other slug routes! // refit to match entire rows, using dataType convention to determine what columns need testing? -export async function GET(request: NextRequest, { params }: { params: { dataType: string, slugs?: string[] } }) { +export async function GET(request: NextRequest, {params}: { params: { dataType: string, slugs?: string[] } }) { // simple dynamic validation to confirm table input values: if (!params.slugs || params.slugs.length !== 3) throw new Error("slugs missing -- formvalidation"); if (!params.dataType || params.dataType === 'undefined') throw new Error("no schema provided"); const [schema, columnName, value] = params.slugs; - if (!schema || !columnName || !value) return new NextResponse(null, { status: 404 }); + if (!schema || !columnName || !value) return new NextResponse(null, {status: 404}); let conn: PoolConnection | null = null; try { conn = await getConn(); - let query = `SELECT 1 FROM ?? WHERE ?? = ? LIMIT 1`; - let formatted = format(query, [`${schema}.${params.dataType}`, columnName, value]); - let results = await runQuery(conn, formatted); - if (results.length === 0) return new NextResponse(null, { status: 404 }); - return new NextResponse(null, { status: 200 }); + const query = `SELECT 1 FROM ?? WHERE ?? = ? LIMIT 1`; + const formatted = format(query, [`${schema}.${params.dataType}`, columnName, value]); + const results = await runQuery(conn, formatted); + if (results.length === 0) return new NextResponse(null, {status: 404}); + return new NextResponse(null, {status: 200}); } catch (error: any) { console.error(error); throw error; diff --git a/frontend/app/api/sqlload/route.ts b/frontend/app/api/sqlload/route.ts index 413377d4..7d04cf06 100644 --- a/frontend/app/api/sqlload/route.ts +++ b/frontend/app/api/sqlload/route.ts @@ -18,23 +18,23 @@ export async function POST(request: NextRequest) { if (!fileName) throw new Error('no file name provided!'); fileName = fileName.trim(); // plot ID - let plotIDParam = request.nextUrl.searchParams.get("plot"); + const plotIDParam = request.nextUrl.searchParams.get("plot"); if (!plotIDParam) throw new Error('no plot id provided!'); - let plotID = parseInt(plotIDParam.trim()); + const plotID = parseInt(plotIDParam.trim()); // census ID - let censusIDParam = request.nextUrl.searchParams.get("census"); + const censusIDParam = request.nextUrl.searchParams.get("census"); if (!censusIDParam) throw new Error('no census id provided!'); - let censusID = parseInt(censusIDParam.trim()); + const censusID = parseInt(censusIDParam.trim()); // quadrat ID - let quadratIDParam = request.nextUrl.searchParams.get("quadrat"); - if (!quadratIDParam) throw new Error("no quadrat ID provided"); - let quadratID = parseInt(quadratIDParam.trim()); + const quadratIDParam = request.nextUrl.searchParams.get("quadrat"); + if (!quadratIDParam) console.error("no quadrat ID provided"); + const quadratID = quadratIDParam ? parseInt(quadratIDParam.trim()) : undefined; // form type let formType = request.nextUrl.searchParams.get("formType"); if (!formType) throw new Error('no formType provided!'); formType = formType.trim(); -// full name - let fullName = request.nextUrl.searchParams.get("user") ?? undefined; + // full name + const fullName = request.nextUrl.searchParams.get("user") ?? undefined; // if (!fullName) throw new Error('no full name provided!'); // fullName = fullName.trim(); // unit of measurement --> use has been incorporated into form @@ -51,7 +51,7 @@ export async function POST(request: NextRequest) { let connection: PoolConnection | null = null; // Use PoolConnection type try { - let i = 0; + const i = 0; connection = await getConn(); } catch (error) { if (error instanceof Error) { @@ -61,7 +61,7 @@ export async function POST(request: NextRequest) { responseMessage: `Failure in connecting to SQL with ${error.message}`, error: error.message, }), - {status: HTTPResponses.SQL_CONNECTION_FAILURE} + { status: HTTPResponses.SQL_CONNECTION_FAILURE } ); } else { console.error("Unknown error in connecting to SQL:", error); @@ -69,7 +69,7 @@ export async function POST(request: NextRequest) { JSON.stringify({ responseMessage: `Unknown SQL connection error with error: ${error}`, }), - {status: HTTPResponses.SQL_CONNECTION_FAILURE} + { status: HTTPResponses.SQL_CONNECTION_FAILURE } ); } } @@ -80,17 +80,17 @@ export async function POST(request: NextRequest) { JSON.stringify({ responseMessage: "Container client or SQL connection is undefined", }), - {status: HTTPResponses.SERVICE_UNAVAILABLE} + { status: HTTPResponses.SERVICE_UNAVAILABLE } ); } - let idToRows: { coreMeasurementID: number; fileRow: FileRow }[] = []; + const idToRows: { coreMeasurementID: number; fileRow: FileRow }[] = []; for (const rowId in fileRowSet) { console.log(`rowID: ${rowId}`); const row = fileRowSet[rowId]; console.log('row for row ID: ', row); try { - let props: InsertUpdateProcessingProps = { + const props: InsertUpdateProcessingProps = { schema, connection, formType, @@ -99,13 +99,10 @@ export async function POST(request: NextRequest) { censusID, quadratID, fullName, - // dbhUnit: dbhUnit, - // homUnit: homUnit, - // coordUnit: coordUnit, }; const coreMeasurementID = await insertOrUpdate(props); if (formType === 'measurements' && coreMeasurementID) { - idToRows.push({coreMeasurementID: coreMeasurementID, fileRow: row}); + idToRows.push({ coreMeasurementID: coreMeasurementID, fileRow: row }); } else if (formType === 'measurements' && coreMeasurementID === undefined) { throw new Error("CoreMeasurement insertion failure at row: " + row); } @@ -117,7 +114,7 @@ export async function POST(request: NextRequest) { responseMessage: `Error processing row in file ${fileName}`, error: error.message, }), - {status: HTTPResponses.SERVICE_UNAVAILABLE} + { status: HTTPResponses.SERVICE_UNAVAILABLE } ); } else { console.error("Unknown error processing row:", error); @@ -125,12 +122,12 @@ export async function POST(request: NextRequest) { JSON.stringify({ responseMessage: `Unknown processing error at row, in file ${fileName}`, }), - {status: HTTPResponses.SERVICE_UNAVAILABLE} + { status: HTTPResponses.SERVICE_UNAVAILABLE } ); } } finally { if (connection) connection.release(); } } - return new NextResponse(JSON.stringify({message: "Insert to SQL successful", idToRows: idToRows}), {status: 200}); + return new NextResponse(JSON.stringify({ message: "Insert to SQL successful", idToRows: idToRows }), { status: 200 }); } \ No newline at end of file diff --git a/frontend/app/api/validations/validationerrordisplay/route.ts b/frontend/app/api/validations/validationerrordisplay/route.ts index 1ec8f286..b68a0244 100644 --- a/frontend/app/api/validations/validationerrordisplay/route.ts +++ b/frontend/app/api/validations/validationerrordisplay/route.ts @@ -1,7 +1,7 @@ -import { NextRequest, NextResponse } from "next/server"; -import { getConn, runQuery } from "@/components/processors/processormacros"; -import { PoolConnection } from "mysql2/promise"; -import { CMError } from "@/config/macros/uploadsystemmacros"; +import {NextRequest, NextResponse} from "next/server"; +import {getConn, runQuery} from "@/components/processors/processormacros"; +import {PoolConnection} from "mysql2/promise"; +import {CMError} from "@/config/macros/uploadsystemmacros"; import MapperFactory from "@/config/datamapper"; export async function GET(request: NextRequest) { @@ -55,7 +55,7 @@ export async function GET(request: NextRequest) { } }); } catch (error: any) { - return new NextResponse(JSON.stringify({ error: error.message }), { status: 500 }); + return new NextResponse(JSON.stringify({error: error.message}), {status: 500}); } finally { if (conn) conn.release(); } diff --git a/frontend/app/api/validations/validationlist/route.ts b/frontend/app/api/validations/validationlist/route.ts index 456a6e6a..413112d7 100644 --- a/frontend/app/api/validations/validationlist/route.ts +++ b/frontend/app/api/validations/validationlist/route.ts @@ -20,6 +20,7 @@ export async function GET(): Promise> { const validationMessages: ValidationMessages = results.reduce((acc, {ProcedureName, Description}) => { acc[ProcedureName] = Description; + console.log('validation created: ', acc); return acc; }, {} as ValidationMessages); diff --git a/frontend/app/contexts/datavalidityprovider.tsx b/frontend/app/contexts/datavalidityprovider.tsx index 8e22a7ec..d9d37765 100644 --- a/frontend/app/contexts/datavalidityprovider.tsx +++ b/frontend/app/contexts/datavalidityprovider.tsx @@ -1,15 +1,16 @@ "use client"; -import React, { createContext, useCallback, useContext, useEffect, useState } from "react"; -import { useOrgCensusContext, usePlotContext, useSiteContext } from "./userselectionprovider"; -import { UnifiedValidityFlags } from "@/config/macros"; -import { useLoading } from "./loadingprovider"; +import React, {createContext, useCallback, useContext, useEffect, useState} from "react"; +import {useOrgCensusContext, usePlotContext, useSiteContext} from "./userselectionprovider"; +import {UnifiedValidityFlags} from "@/config/macros"; +import {useLoading} from "./loadingprovider"; +import { useOrgCensusListDispatch, usePlotListDispatch, useQuadratListDispatch } from "./listselectionprovider"; +import { createAndUpdateCensusList } from "@/config/sqlrdsdefinitions/orgcensusrds"; const initialValidityState: UnifiedValidityFlags = { attributes: false, personnel: false, species: false, quadrats: false, - subquadrats: false, quadratpersonnel: false, }; @@ -20,9 +21,12 @@ const DataValidityContext = createContext<{ recheckValidityIfNeeded: () => Promise; }>({ validity: initialValidityState, - setValidity: () => { }, - triggerRefresh: () => { }, - recheckValidityIfNeeded: async () => { }, + setValidity: () => { + }, + triggerRefresh: () => { + }, + recheckValidityIfNeeded: async () => { + }, }); const debounce = (func: (...args: any[]) => void, wait: number) => { @@ -33,7 +37,7 @@ const debounce = (func: (...args: any[]) => void, wait: number) => { }; }; -export const DataValidityProvider = ({ children }: { children: React.ReactNode }) => { +export const DataValidityProvider = ({children}: { children: React.ReactNode }) => { const [validity, setValidityState] = useState(initialValidityState); const [refreshNeeded, setRefreshNeeded] = useState(false); @@ -44,23 +48,23 @@ export const DataValidityProvider = ({ children }: { children: React.ReactNode } const currentCensus = useOrgCensusContext(); const setValidity = useCallback((type: keyof UnifiedValidityFlags, value: boolean) => { - setValidityState(prev => ({ ...prev, [type]: value })); + setValidityState(prev => ({...prev, [type]: value})); }, []); const checkDataValidity = useCallback(async (type?: keyof UnifiedValidityFlags) => { if (!currentSite || !currentPlot || !currentCensus) return; setLoading(true, 'Pre-validation in progress...'); - let url = `/api/cmprevalidation/${type}/${currentSite.schemaName}/${currentPlot.id}/${currentCensus.plotCensusNumber}`; + const url = `/api/cmprevalidation/${type}/${currentSite.schemaName}/${currentPlot.id}/${currentCensus.plotCensusNumber}`; let response; try { - response = await fetch(url, { method: 'GET' }); + response = await fetch(url, {method: 'GET'}); } catch (error) { console.error(error); - response = { ok: false }; + response = {ok: false}; } setValidity(type as keyof UnifiedValidityFlags, response.ok); setLoading(false); - }, [currentSite, currentPlot, currentCensus, setValidity]); + }, [currentSite, currentPlot, currentCensus, setValidity, validity]); const recheckValidityIfNeeded = useCallback(async () => { if ((Object.values(validity).some(flag => !flag)) || refreshNeeded) { @@ -99,7 +103,7 @@ export const DataValidityProvider = ({ children }: { children: React.ReactNode } }, [setValidity]); return ( - + {children} ); diff --git a/frontend/app/contexts/listselectionprovider.tsx b/frontend/app/contexts/listselectionprovider.tsx index 4f668995..07a74721 100644 --- a/frontend/app/contexts/listselectionprovider.tsx +++ b/frontend/app/contexts/listselectionprovider.tsx @@ -1,17 +1,17 @@ // ListSelectionProvider.tsx "use client"; -import React, { createContext, Dispatch, useContext, useReducer } from 'react'; +import React, {createContext, Dispatch, useContext, useReducer} from 'react'; import { createEnhancedDispatch, EnhancedDispatch, genericLoadReducer, LoadAction } from "@/config/macros/contextreducers"; -import { QuadratRDS } from "@/config/sqlrdsdefinitions/tables/quadratrds"; -import { PlotRDS } from "@/config/sqlrdsdefinitions/tables/plotrds"; -import { SubquadratRDS } from '@/config/sqlrdsdefinitions/tables/subquadratrds'; -import { SitesRDS } from '@/config/sqlrdsdefinitions/tables/sitesrds'; -import { OrgCensus } from '@/config/sqlrdsdefinitions/orgcensusrds'; +import {QuadratRDS} from "@/config/sqlrdsdefinitions/tables/quadratrds"; +import {PlotRDS} from "@/config/sqlrdsdefinitions/tables/plotrds"; +import {SubquadratRDS} from '@/config/sqlrdsdefinitions/tables/subquadratrds'; +import {SitesRDS} from '@/config/sqlrdsdefinitions/tables/sitesrds'; +import {OrgCensus} from '@/config/sqlrdsdefinitions/orgcensusrds'; // contexts export const PlotListContext = createContext([]); @@ -28,7 +28,7 @@ export const SubquadratListDispatchContext = createContext | undefined>(undefined); export const FirstLoadDispatchContext = createContext | undefined>(undefined); -export function ListSelectionProvider({ children }: Readonly<{ children: React.ReactNode }>) { +export function ListSelectionProvider({children}: Readonly<{ children: React.ReactNode }>) { const [plotList, plotListDispatch] = useReducer>>(genericLoadReducer, []); const [orgCensusList, orgCensusListDispatch] = @@ -131,6 +131,7 @@ export function useSiteListDispatch() { export function usePlotListContext() { return useContext(PlotListContext); } + export function usePlotListDispatch() { return useContext(PlotListDispatchContext); } diff --git a/frontend/app/contexts/lockanimationcontext.tsx b/frontend/app/contexts/lockanimationcontext.tsx new file mode 100644 index 00000000..f047e320 --- /dev/null +++ b/frontend/app/contexts/lockanimationcontext.tsx @@ -0,0 +1,34 @@ +"use client"; +import React, {createContext, useContext, useState, ReactNode} from 'react'; + +interface LockAnimationContextProps { + isPulsing: boolean; + triggerPulse: () => void; +} + +const LockAnimationContext = createContext(undefined); + +export const LockAnimationProvider: React.FC<{ children: ReactNode }> = ({children}) => { + const [isPulsing, setIsPulsing] = useState(false); + + const triggerPulse = () => { + setIsPulsing(true); + setTimeout(() => { + setIsPulsing(false); + }, 3000); + }; + + return ( + + {children} + + ); +}; + +export const useLockAnimation = () => { + const context = useContext(LockAnimationContext); + if (context === undefined) { + throw new Error('useLockAnimation must be used within a LockAnimationProvider'); + } + return context; +}; diff --git a/frontend/app/contexts/userselectionprovider.tsx b/frontend/app/contexts/userselectionprovider.tsx index 82839b3f..587a61d2 100644 --- a/frontend/app/contexts/userselectionprovider.tsx +++ b/frontend/app/contexts/userselectionprovider.tsx @@ -1,15 +1,15 @@ // userselectionprovider.tsx "use client"; -import React, { createContext, useContext, useReducer } from "react"; +import React, {createContext, useContext, useReducer} from "react"; import { createEnhancedDispatch, EnhancedDispatch, genericLoadContextReducer, LoadAction } from "@/config/macros/contextreducers"; -import { Site } from "@/config/sqlrdsdefinitions/tables/sitesrds"; -import { Quadrat } from "@/config/sqlrdsdefinitions/tables/quadratrds"; -import { Plot } from "@/config/sqlrdsdefinitions/tables/plotrds"; +import {Site} from "@/config/sqlrdsdefinitions/tables/sitesrds"; +import {Quadrat} from "@/config/sqlrdsdefinitions/tables/quadratrds"; +import {Plot} from "@/config/sqlrdsdefinitions/tables/plotrds"; import { useOrgCensusListContext, usePlotListContext, @@ -17,8 +17,8 @@ import { useSiteListContext, useSubquadratListContext } from "@/app/contexts/listselectionprovider"; -import { OrgCensus } from "@/config/sqlrdsdefinitions/orgcensusrds"; -import { Subquadrat } from "@/config/sqlrdsdefinitions/tables/subquadratrds"; +import {OrgCensus} from "@/config/sqlrdsdefinitions/orgcensusrds"; +import {Subquadrat} from "@/config/sqlrdsdefinitions/tables/subquadratrds"; export const PlotContext = createContext(undefined); export const OrgCensusContext = createContext(undefined); @@ -31,7 +31,7 @@ export const QuadratDispatchContext = createContext | export const SubquadratDispatchContext = createContext | undefined>(undefined); export const SiteDispatchContext = createContext | undefined>(undefined); -export default function UserSelectionProvider({ children }: Readonly<{ children: React.ReactNode }>) { +export default function UserSelectionProvider({children}: Readonly<{ children: React.ReactNode }>) { const plotListContext = usePlotListContext(); const orgCensusListContext = useOrgCensusListContext(); const quadratListContext = useQuadratListContext(); diff --git a/frontend/app/layout.tsx b/frontend/app/layout.tsx index 48db180f..de13158f 100644 --- a/frontend/app/layout.tsx +++ b/frontend/app/layout.tsx @@ -1,35 +1,38 @@ import "@/styles/globals.css"; -import { Providers } from "./providers"; +import {Providers} from "./providers"; import React from "react"; -import { ListSelectionProvider } from "@/app/contexts/listselectionprovider"; -import { Box } from "@mui/joy"; +import {ListSelectionProvider} from "@/app/contexts/listselectionprovider"; +import {Box} from "@mui/joy"; import UserSelectionProvider from "@/app/contexts/userselectionprovider"; -import { LoadingProvider } from "@/app/contexts/loadingprovider"; -import { GlobalLoadingIndicator } from "@/styles/globalloadingindicator"; -import { DataValidityProvider } from "@/app/contexts/datavalidityprovider"; +import {LoadingProvider} from "@/app/contexts/loadingprovider"; +import {GlobalLoadingIndicator} from "@/styles/globalloadingindicator"; +import {DataValidityProvider} from "@/app/contexts/datavalidityprovider"; +import {LockAnimationProvider} from "./contexts/lockanimationcontext"; -export default function RootLayout({ children, }: Readonly<{ children: React.ReactNode; }>) { +export default function RootLayout({children,}: Readonly<{ children: React.ReactNode; }>) { return ( - - ForestGEO Data Entry - - - - - - - - - - {children} - - - - - - - + + ForestGEO Data Entry + + + + + + + + + + + {children} + + + + + + + + ); } diff --git a/frontend/app/providers.tsx b/frontend/app/providers.tsx index b84b562a..21460888 100644 --- a/frontend/app/providers.tsx +++ b/frontend/app/providers.tsx @@ -1,15 +1,15 @@ "use client"; import * as React from "react"; -import { SessionProvider } from "next-auth/react"; +import {SessionProvider} from "next-auth/react"; import ThemeRegistry from "@/components/themeregistry/themeregistry"; -import { LocalizationProvider } from "@mui/x-date-pickers"; -import { AdapterMoment } from '@mui/x-date-pickers/AdapterMoment'; +import {LocalizationProvider} from "@mui/x-date-pickers"; +import {AdapterMoment} from '@mui/x-date-pickers/AdapterMoment'; export interface ProvidersProps { children: React.ReactNode; } -export function Providers({ children }: Readonly) { +export function Providers({children}: Readonly) { return ( diff --git a/frontend/components/client/sqdatagrid.tsx b/frontend/components/client/sqdatagrid.tsx index 1f2e75fd..9e07256b 100644 --- a/frontend/components/client/sqdatagrid.tsx +++ b/frontend/components/client/sqdatagrid.tsx @@ -1,18 +1,23 @@ "use client"; -import { useOrgCensusContext, usePlotContext, useQuadratContext, useSiteContext } from "@/app/contexts/userselectionprovider"; -import { SubquadratGridColumns } from "@/config/sqlrdsdefinitions/tables/subquadratrds"; -import { AlertProps } from "@mui/material"; -import { Box } from "@mui/system"; -import { GridRowsProp, GridRowModesModel, GridRowModes, GridColDef } from "@mui/x-data-grid"; -import { randomId } from "@mui/x-data-grid-generator"; -import { useSession } from "next-auth/react"; -import React, { useEffect, useState } from "react"; +import { + useOrgCensusContext, + usePlotContext, + useQuadratContext, + useSiteContext +} from "@/app/contexts/userselectionprovider"; +import {SubquadratGridColumns} from "@/config/sqlrdsdefinitions/tables/subquadratrds"; +import {AlertProps} from "@mui/material"; +import {Box} from "@mui/system"; +import {GridRowsProp, GridRowModesModel, GridRowModes, GridColDef} from "@mui/x-data-grid"; +import {randomId} from "@mui/x-data-grid-generator"; +import {useSession} from "next-auth/react"; +import React, {useEffect, useState} from "react"; import DataGridCommons from "../datagrids/datagridcommons"; -import { Typography } from "@mui/joy"; +import {Typography} from "@mui/joy"; export default function SubquadratsDataGrid() { - let currentQuadrat = useQuadratContext(); + const currentQuadrat = useQuadratContext(); const initialRows: GridRowsProp = [ { id: 0, @@ -38,7 +43,7 @@ export default function SubquadratsDataGrid() { }); const [isNewRowAdded, setIsNewRowAdded] = useState(false); const [shouldAddRowAfterFetch, setShouldAddRowAfterFetch] = useState(false); - const { data: session } = useSession(); + const {data: session} = useSession(); const currentPlot = usePlotContext(); const currentCensus = useOrgCensusContext(); const currentSite = useSiteContext(); @@ -67,7 +72,7 @@ export default function SubquadratsDataGrid() { } return col; }); - + // Function to fetch paginated data const addNewRowToGrid = () => { const id = randomId(); @@ -75,10 +80,10 @@ export default function SubquadratsDataGrid() { const nextSubQuadratID = (rows.length > 0 ? rows.reduce((max, row) => Math.max(row.subquadratID, max), 0) : 0) + 1; - const nextSQIndex = (rows.length > 0 - ? rows.reduce((max, row) => Math.max(row.sqIndex, max), 0) - : 0) + 1; - + const nextSQIndex = (rows.length > 0 + ? rows.reduce((max, row) => Math.max(row.sqIndex, max), 0) + : 0) + 1; + const newRow = { id: id, subquadratID: nextSubQuadratID, @@ -94,12 +99,12 @@ export default function SubquadratsDataGrid() { // Set editing mode for the new row setRowModesModel(oldModel => ({ ...oldModel, - [id]: { mode: GridRowModes.Edit, fieldToFocus: 'subquadratName' }, + [id]: {mode: GridRowModes.Edit, fieldToFocus: 'subquadratName'}, })); }; return ( <> - + - + {session?.user.isAdmin && ( - + Note: ADMINISTRATOR VIEW )} - + Note: This is a locked view and will not allow modification. - + Please use this view as a way to confirm changes made to measurements. diff --git a/frontend/components/datagrids/applications/alltaxonomiesviewdatagrid.tsx b/frontend/components/datagrids/applications/alltaxonomiesviewdatagrid.tsx new file mode 100644 index 00000000..a199ffa1 --- /dev/null +++ b/frontend/components/datagrids/applications/alltaxonomiesviewdatagrid.tsx @@ -0,0 +1,103 @@ +// alltaxonomiesview datagrid +"use client"; +import {GridRowsProp} from "@mui/x-data-grid"; +import {AlertProps} from "@mui/material"; +import React, {useState} from "react"; +import {randomId} from "@mui/x-data-grid-generator"; +import DataGridCommons from "@/components/datagrids/datagridcommons"; +import {Box, Button, Typography} from "@mui/joy"; +import {useSession} from "next-auth/react"; +import UploadParentModal from "@/components/uploadsystemhelpers/uploadparentmodal"; +import {AllTaxonomiesViewGridColumns, initialAllTaxonomiesViewRDSRow} from "@/config/sqlrdsdefinitions/views/alltaxonomiesviewrds"; +import { useOrgCensusContext } from "@/app/contexts/userselectionprovider"; + +export default function AllTaxonomiesViewDataGrid() { + const [rows, setRows] = useState([initialAllTaxonomiesViewRDSRow] as GridRowsProp); + const [rowCount, setRowCount] = useState(0); + const [rowModesModel, setRowModesModel] = useState({}); + const [snackbar, setSnackbar] = React.useState | null>(null); + const [refresh, setRefresh] = useState(false); + const [paginationModel, setPaginationModel] = useState({page: 0, pageSize: 10}); + const [isNewRowAdded, setIsNewRowAdded] = useState(false); + const [shouldAddRowAfterFetch, setShouldAddRowAfterFetch] = useState(false); + const [isUploadModalOpen, setIsUploadModalOpen] = useState(false); + const {data: session} = useSession(); + const currentCensus = useOrgCensusContext(); + + const addNewRowToGrid = () => { + const id = randomId(); + const newRow = { + ...initialAllTaxonomiesViewRDSRow, + id, + isNew: true, + }; + + setRows(oldRows => [...oldRows ?? [], newRow]); + setRowModesModel(oldModel => ({ + ...oldModel, + [id]: {mode: 'edit', fieldToFocus: 'speciesCode'}, + })); + console.log('alltaxonomiesview addnewrowtogrid triggered'); + }; + + return ( + <> + + + + {session?.user.isAdmin && ( + + Note: ADMINISTRATOR VIEW + + )} + + + + {/* Upload Button */} + + + + + { + setIsUploadModalOpen(false); + setRefresh(true); + }} formType={"species"}/> + + + + ); +} \ No newline at end of file diff --git a/frontend/components/datagrids/applications/attributesdatagrid.tsx b/frontend/components/datagrids/applications/attributesdatagrid.tsx index 84726a57..86eea3b1 100644 --- a/frontend/components/datagrids/applications/attributesdatagrid.tsx +++ b/frontend/components/datagrids/applications/attributesdatagrid.tsx @@ -1,18 +1,18 @@ // attributes datagrid "use client"; -import {GridRowsProp} from "@mui/x-data-grid"; -import {AlertProps} from "@mui/material"; -import React, {useState} from "react"; -import {randomId} from "@mui/x-data-grid-generator"; +import { GridRowsProp } from "@mui/x-data-grid"; +import { AlertProps } from "@mui/material"; +import React, { useState } from "react"; +import { randomId } from "@mui/x-data-grid-generator"; import DataGridCommons from "@/components/datagrids/datagridcommons"; -import {AttributeGridColumns} from '@/config/sqlrdsdefinitions/tables/attributerds'; -import {Box, Button, Typography} from "@mui/joy"; -import {useSession} from "next-auth/react"; +import { AttributeGridColumns, initialAttributesRDSRow } from '@/config/sqlrdsdefinitions/tables/attributerds'; +import { Box, Button, Typography } from "@mui/joy"; +import { useSession } from "next-auth/react"; import UploadParentModal from "@/components/uploadsystemhelpers/uploadparentmodal"; +import { useOrgCensusContext } from "@/app/contexts/userselectionprovider"; export default function AttributesDataGrid() { - const initialRows: GridRowsProp = [{id: 0, code: '', description: '', status: ''}]; - const [rows, setRows] = useState(initialRows); + const [rows, setRows] = useState([initialAttributesRDSRow] as GridRowsProp); const [rowCount, setRowCount] = useState(0); const [rowModesModel, setRowModesModel] = useState({}); const [snackbar, setSnackbar] = React.useState | null>(null); const [refresh, setRefresh] = useState(false); - const [paginationModel, setPaginationModel] = useState({page: 0, pageSize: 10}); + const [paginationModel, setPaginationModel] = useState({ page: 0, pageSize: 10 }); const [isNewRowAdded, setIsNewRowAdded] = useState(false); const [shouldAddRowAfterFetch, setShouldAddRowAfterFetch] = useState(false); const [isUploadModalOpen, setIsUploadModalOpen] = useState(false); - const {data: session} = useSession(); + const { data: session } = useSession(); + const currentCensus = useOrgCensusContext(); const addNewRowToGrid = () => { const id = randomId(); - const newRow = {id, code: '', description: '', status: '', isNew: true}; + const newRow = { ...initialAttributesRDSRow, id, isNew: true }; setRows(oldRows => [...oldRows ?? [], newRow]); setRowModesModel(oldModel => ({ ...oldModel, - [id]: {mode: 'edit', fieldToFocus: 'code'}, + [id]: { mode: 'edit', fieldToFocus: 'code' }, })); console.log('attributes addnewrowtogrid triggered'); }; return ( <> - + - + {session?.user.isAdmin && ( - + Note: ADMINISTRATOR VIEW )} @@ -59,14 +60,17 @@ export default function AttributesDataGrid() { {/* Upload Button */} - + { setIsUploadModalOpen(false); setRefresh(true); - }} formType={"attributes"}/> + }} formType={"attributes"} /> (false); const [shouldAddRowAfterFetch, setShouldAddRowAfterFetch] = useState(false); - let currentPlot = usePlotContext(); - let currentSite = useSiteContext(); - let {setLoading} = useLoading(); + const currentPlot = usePlotContext(); + const currentSite = useSiteContext(); + const {setLoading} = useLoading(); const {data: session} = useSession(); const [openCensusId, setOpenCensusId] = useState(null); const [endDate, setEndDate] = useState(null); diff --git a/frontend/components/datagrids/applications/personneldatagrid.tsx b/frontend/components/datagrids/applications/personneldatagrid.tsx index 5a50b87e..bef03f47 100644 --- a/frontend/components/datagrids/applications/personneldatagrid.tsx +++ b/frontend/components/datagrids/applications/personneldatagrid.tsx @@ -1,26 +1,18 @@ "use client"; -import {GridRowModes, GridRowModesModel, GridRowsProp} from "@mui/x-data-grid"; -import {AlertProps} from "@mui/material"; -import React, {useState} from "react"; -import {PersonnelGridColumns} from '@/config/sqlrdsdefinitions/tables/personnelrds'; -import {usePlotContext} from "@/app/contexts/userselectionprovider"; -import {randomId} from "@mui/x-data-grid-generator"; +import { GridRowModes, GridRowModesModel, GridRowsProp } from "@mui/x-data-grid"; +import { AlertProps } from "@mui/material"; +import React, { useState } from "react"; +import { initialPersonnelRDSRow, PersonnelGridColumns } from '@/config/sqlrdsdefinitions/tables/personnelrds'; +import { randomId } from "@mui/x-data-grid-generator"; import DataGridCommons from "@/components/datagrids/datagridcommons"; -import {useSession} from "next-auth/react"; -import {Box, Button, Typography} from "@mui/joy"; +import { useSession } from "next-auth/react"; +import { Box, Button, Stack, Typography } from "@mui/joy"; import UploadParentModal from "@/components/uploadsystemhelpers/uploadparentmodal"; +import Link from 'next/link'; +import { useOrgCensusContext } from "@/app/contexts/userselectionprovider"; export default function PersonnelDataGrid() { - const initialRows: GridRowsProp = [ - { - id: 0, - personnelID: 0, - firstName: '', - lastName: '', - role: '', - }, - ]; - const [rows, setRows] = React.useState(initialRows); + const [rows, setRows] = React.useState([initialPersonnelRDSRow] as GridRowsProp); const [rowCount, setRowCount] = useState(0); // total number of rows const [rowModesModel, setRowModesModel] = React.useState({}); const [snackbar, setSnackbar] = React.useState(false); const [shouldAddRowAfterFetch, setShouldAddRowAfterFetch] = useState(false); const [isUploadModalOpen, setIsUploadModalOpen] = useState(false); - let currentPlot = usePlotContext(); - const {data: session} = useSession(); + const { data: session } = useSession(); + const currentCensus = useOrgCensusContext(); // Function to fetch paginated data const addNewRowToGrid = () => { const id = randomId(); @@ -46,24 +38,23 @@ export default function PersonnelDataGrid() { : 0) + 1; const newRow = { + ...initialPersonnelRDSRow, id: id, personnelID: nextPersonnelID, - firstName: '', - lastName: '', - role: '', isNew: true }; + // Add the new row to the state setRows(oldRows => [...oldRows, newRow]); // Set editing mode for the new row setRowModesModel(oldModel => ({ ...oldModel, - [id]: {mode: GridRowModes.Edit, fieldToFocus: 'firstName'}, + [id]: { mode: GridRowModes.Edit, fieldToFocus: 'firstName' }, })); }; return ( <> - + - + {session?.user.isAdmin && ( - + Note: ADMINISTRATOR VIEW )} - + Note: This is a locked view and will not allow modification. - + Please use this view as a way to confirm changes made to measurements. {/* Upload Button */} - - + + + {/* Link to Quadrat Personnel Data Grid */} + + + + { setIsUploadModalOpen(false); setRefresh(true); - }} formType={'personnel'}/> + }} formType={'personnel'} /> ({}); const [snackbar, setSnackbar] = useState(false); const [shouldAddRowAfterFetch, setShouldAddRowAfterFetch] = useState(false); - const { data: session } = useSession(); + const {data: session} = useSession(); + const router = useRouter(); const [quadratOptions, setQuadratOptions] = useState([]); const [personnelOptions, setPersonnelOptions] = useState([]); - let currentSite = useSiteContext(); - let currentPlot = usePlotContext(); - let currentCensus = useOrgCensusContext(); - let { validity } = useDataValidityContext(); + const currentSite = useSiteContext(); + const currentPlot = usePlotContext(); + const currentCensus = useOrgCensusContext(); + const {validity} = useDataValidityContext(); const addNewRowToGrid = () => { const id = randomId(); @@ -48,10 +49,9 @@ export default function QuadratPersonnelDataGrid() { ? rows.reduce((max, row) => Math.max(row.quadratPersonnelID, max), 0) : 0) + 1; const newRow = { + ...initialQuadratPersonnelRDSRow, id: id, quadratPersonnelID: nextQuadratPersonnelID + 1, - quadratID: null, - personnelID: null, isNew: true, }; // Add the new row to the state @@ -59,7 +59,7 @@ export default function QuadratPersonnelDataGrid() { // Set editing mode for the new row setRowModesModel(oldModel => ({ ...oldModel, - [id]: { mode: GridRowModes.Edit, fieldToFocus: 'quadratID' }, + [id]: {mode: GridRowModes.Edit, fieldToFocus: 'quadratID'}, })); }; @@ -85,7 +85,14 @@ export default function QuadratPersonnelDataGrid() { }, [currentSite, currentPlot, currentCensus]); const QuadratPersonnelGridColumns: GridColDef[] = [ - { field: 'quadratPersonnelID', headerName: 'ID', headerClassName: 'header', minWidth: 75, align: 'left', editable: false }, + { + field: 'quadratPersonnelID', + headerName: 'ID', + headerClassName: 'header', + minWidth: 75, + align: 'left', + editable: false + }, { field: 'quadratID', headerName: 'Quadrat ID', @@ -105,14 +112,14 @@ export default function QuadratPersonnelDataGrid() { minWidth: 140, align: 'left', type: 'singleSelect', - valueOptions: personnelOptions, + valueOptions: personnelOptions, editable: true, }, ]; return ( <> - + - + {session?.user.isAdmin && ( - + Note: ADMINISTRATOR VIEW )} - + {/* Back Button */} + ); -} +} \ No newline at end of file diff --git a/frontend/components/datagrids/applications/quadratsdatagrid.tsx b/frontend/components/datagrids/applications/quadratsdatagrid.tsx index 1a1e41af..8debd78c 100644 --- a/frontend/components/datagrids/applications/quadratsdatagrid.tsx +++ b/frontend/components/datagrids/applications/quadratsdatagrid.tsx @@ -1,38 +1,23 @@ "use client"; -import {GridColDef, GridRowId, GridRowModes, GridRowModesModel, GridRowsProp} from "@mui/x-data-grid"; +import {GridRowModes, GridRowModesModel, GridRowsProp} from "@mui/x-data-grid"; import {AlertProps} from "@mui/material"; -import React, {useCallback, useEffect, useState} from "react"; -import {QuadratsGridColumns as BaseQuadratsGridColumns, Quadrat} from '@/config/sqlrdsdefinitions/tables/quadratrds'; +import React, { useState} from "react"; +import {QuadratsGridColumns as BaseQuadratsGridColumns, initialQuadratRDSRow} from '@/config/sqlrdsdefinitions/tables/quadratrds'; import { useOrgCensusContext, usePlotContext, - useQuadratDispatch, } from "@/app/contexts/userselectionprovider"; import {randomId} from "@mui/x-data-grid-generator"; import DataGridCommons from "@/components/datagrids/datagridcommons"; -import {Box, Button, IconButton, Modal, ModalDialog, Stack, Typography} from "@mui/joy"; +import {Box, Button, Typography} from "@mui/joy"; import {useSession} from "next-auth/react"; import UploadParentModal from "@/components/uploadsystemhelpers/uploadparentmodal"; +import Link from 'next/link'; export default function QuadratsDataGrid() { - const initialRows: GridRowsProp = [ - { - id: 0, - quadratID: 0, - plotID: 0, - censusID: 0, - quadratName: '', - dimensionX: 0, - dimensionY: 0, - area: 0, - unit: '', - quadratShape: '', - }, - ]; - const [rows, setRows] = React.useState(initialRows); + const [rows, setRows] = React.useState([initialQuadratRDSRow] as GridRowsProp); const [rowCount, setRowCount] = useState(0); // total number of rows const [rowModesModel, setRowModesModel] = React.useState({}); - const [locked, setLocked] = useState(false); const [snackbar, setSnackbar] = React.useState(false); const [shouldAddRowAfterFetch, setShouldAddRowAfterFetch] = useState(false); - // const [censusOptions, setCensusOptions] = useState([]); const {data: session} = useSession(); const [isUploadModalOpen, setIsUploadModalOpen] = useState(false); - const [uploadFormType, setUploadFormType] = useState<'quadrats' | 'subquadrats'>('quadrats'); + const [uploadFormType, setUploadFormType] = useState<'quadrats'>('quadrats'); - let currentPlot = usePlotContext(); - let currentCensus = useOrgCensusContext(); - let quadratDispatch = useQuadratDispatch(); - - useEffect(() => { - if (currentCensus !== undefined) { - setLocked(currentCensus.dateRanges[0].endDate !== undefined); // if the end date is not undefined, then grid should be locked - } - }, [currentCensus]); - - const handleSelectQuadrat = useCallback((quadratID: number | null) => { - // we want to select a quadrat contextually when using this grid FOR subquadrats selection - // however, this information should not be retained, as the user might select a different quadrat or change quadrat information - // thus, we add the `| null` to the function and ensure that the context is properly reset when the user is done making changes or cancels their changes. - if (quadratID === null) quadratDispatch && quadratDispatch({quadrat: undefined}).catch(console.error); // dispatches are asynchronous - else { - const selectedQuadrat = rows.find(row => row.quadratID === quadratID) as Quadrat; // GridValidRowModel needs to be cast to Quadrat - if (selectedQuadrat && quadratDispatch) quadratDispatch({quadrat: selectedQuadrat}).catch(console.error); - } - }, [rows, quadratDispatch]); + const currentPlot = usePlotContext(); + const currentCensus = useOrgCensusContext(); const addNewRowToGrid = () => { const id = randomId(); @@ -76,16 +42,11 @@ export default function QuadratsDataGrid() { ? rows.reduce((max, row) => Math.max(row.quadratID, max), 0) : 0) + 1; const newRow = { + ...initialQuadratRDSRow, id: id, quadratID: nextQuadratID, plotID: currentPlot ? currentPlot.id : 0, censusID: currentCensus ? currentCensus.dateRanges[0].censusID : 0, - quadratName: '', - dimensionX: 0, - dimensionY: 0, - area: 0, - unit: '', - quadratShape: '', isNew: true, }; // Add the new row to the state @@ -96,91 +57,6 @@ export default function QuadratsDataGrid() { [id]: {mode: GridRowModes.Edit, fieldToFocus: 'quadratName'}, })); }; - - // const updatePersonnelInRows = useCallback((id: GridRowId, newPersonnel: PersonnelRDS[]) => { - // setRows(rows => rows.map(row => - // row.id === id - // ? {...row, personnel: newPersonnel.map(person => ({...person}))} - // : row - // )); - // }, []); - - // const handlePersonnelChange = useCallback( - // async (rowId: GridRowId, selectedPersonnel: PersonnelRDS[]) => { - // console.log(rows); - // const row = rows.find((row) => row.id === rowId); - // const quadratId = row?.quadratID; - // const personnelIds = selectedPersonnel.map(person => person.personnelID); - // console.log('new personnel ids: ', personnelIds); - - // // Check if quadratID is valid and not equal to the initial row's quadratID - // if (quadratId === undefined || quadratId === initialRows[0].quadratID) { - // console.error("Invalid quadratID, personnel update skipped."); - // setSnackbar({children: "Personnel update skipped due to invalid quadratID.", severity: 'error'}); - // return; - // } - - // try { - // const response = await fetch(`/api/formsearch/personnelblock?quadratID=${quadratId}&schema=${currentSite?.schemaName ?? ''}`, { - // method: 'PUT', - // headers: { - // 'Content-Type': 'application/json' - // }, - // body: JSON.stringify(personnelIds) - // }); - - // if (!response.ok) { - // setSnackbar({children: `Personnel updates failed!`, severity: 'error'}); - // throw new Error('Failed to update personnel'); - // } - - // // Handle successful response - // const responseData = await response.json(); - // updatePersonnelInRows(rowId, selectedPersonnel); - // setRefresh(true); - // setSnackbar({children: `${responseData.message}`, severity: 'success'}); - // } catch (error) { - // console.error("Error updating personnel:", error); - // } - // }, - // [rows, currentSite?.schemaName, setSnackbar, setRefresh, updatePersonnelInRows] - // ); - - - const quadratsGridColumns: GridColDef[] = [...BaseQuadratsGridColumns, - // { - // field: 'personnel', - // headerName: 'Personnel', - // flex: 1, - // renderCell: (params) => ( - // handlePersonnelChange(params.id, newPersonnel)} - // locked={!rowModesModel[params.id] || rowModesModel[params.id].mode !== GridRowModes.Edit} - // /> - // ), - // }, - // { - // field: 'subquadrats', - // headerName: 'Subquadrats', - // flex: 1, - // renderCell: (params) => ( - // - // - // - // ), - // } - ]; - return ( <> @@ -214,44 +90,19 @@ export default function QuadratsDataGrid() { }} color={'primary'}> Upload Quadrats - {/* */} + {/* Link to Quadrat Personnel Data Grid */} + + + { setIsUploadModalOpen(false); setRefresh(true); }} formType={uploadFormType}/> - {/* { - }} - aria-labelledby="upload-dialog-title" - sx={{display: 'flex', alignItems: 'center', justifyContent: 'center'}} - > - - setIsSubquadratDialogOpen(false)} - sx={{position: 'absolute', top: 8, right: 8}} - > - - - - - */} ); -} \ No newline at end of file +} diff --git a/frontend/components/datagrids/applications/speciesdatagrid.tsx b/frontend/components/datagrids/applications/speciesdatagrid.tsx index c4c77ace..87cba085 100644 --- a/frontend/components/datagrids/applications/speciesdatagrid.tsx +++ b/frontend/components/datagrids/applications/speciesdatagrid.tsx @@ -3,7 +3,7 @@ import {GridRowModes, GridRowModesModel, GridRowsProp} from "@mui/x-data-grid"; import {AlertProps} from "@mui/material"; import React, {useState} from "react"; import {SpeciesGridColumns} from '@/config/sqlrdsdefinitions/tables/speciesrds'; -import {usePlotContext} from "@/app/contexts/userselectionprovider"; +import {useOrgCensusContext, usePlotContext} from "@/app/contexts/userselectionprovider"; import {randomId} from "@mui/x-data-grid-generator"; import DataGridCommons from "@/components/datagrids/datagridcommons"; import {useSession} from "next-auth/react"; @@ -13,19 +13,19 @@ import UploadParentModal from "@/components/uploadsystemhelpers/uploadparentmoda export default function SpeciesDataGrid() { /** * id?: number; - speciesID?: number; - genusID?: number; - currentTaxonFlag?: boolean; - obsoleteTaxonFlag?: boolean; - speciesName?: string; - subspeciesName?: string; - speciesCode?: string; - idLevel?: string; - speciesAuthority?: string; - subspeciesAuthority?: string; - fieldFamily?: string; - description?: string; - referenceID?: number; + speciesID?: number; + genusID?: number; + currentTaxonFlag?: boolean; + obsoleteTaxonFlag?: boolean; + speciesName?: string; + subspeciesName?: string; + speciesCode?: string; + idLevel?: string; + speciesAuthority?: string; + subspeciesAuthority?: string; + fieldFamily?: string; + description?: string; + referenceID?: number; */ const initialRows: GridRowsProp = [ { @@ -58,8 +58,7 @@ export default function SpeciesDataGrid() { const [shouldAddRowAfterFetch, setShouldAddRowAfterFetch] = useState(false); const [isUploadModalOpen, setIsUploadModalOpen] = useState(false); const {data: session} = useSession(); - let currentPlot = usePlotContext(); - + const currentCensus = useOrgCensusContext(); const addNewRowToGrid = () => { const id = randomId(); // New row object @@ -115,14 +114,17 @@ export default function SpeciesDataGrid() { {/* Upload Button */} - + { - setIsUploadModalOpen(false); - setRefresh(true); - }} formType={'species'} /> + setIsUploadModalOpen(false); + setRefresh(true); + }} formType={'species'}/> { const id = randomId(); const newRow = { + ...initialStemDimensionsViewRDSRow, id, - speciesID: 0, - speciesCode: '', - familyID: 0, - family: '', - genusID: 0, - genus: '', - genusAuthority: '', - speciesName: '', - subspeciesName: '', - speciesIDLevel: '', - speciesAuthority: '', - subspeciesAuthority: '', - currentTaxonFlag: null, - obsoleteTaxonFlag: null, - fieldFamily: '', - speciesDescription: '', - referenceID: 0, - publicationTitle: '', - dateOfPublication: null, - citation: '', isNew: true, }; + setRows(oldRows => [...oldRows ?? [], newRow]); setRowModesModel(oldModel => ({ ...oldModel, - [id]: { mode: 'edit', fieldToFocus: 'code' }, + [id]: { mode: 'edit', fieldToFocus: 'stemTag' }, })); console.log('attributes addnewrowtogrid triggered'); }; + // stem, subquadrats, quadrat, plot + const stemUnitsColumn: GridColDef = { + field: 'stemUnits', + headerName: 'U', + headerClassName: 'header', + flex: 0.4, + renderHeader: () => U, + align: 'left', + editable: true, + type: 'singleSelect', + valueOptions: unitSelectionOptions + }; + const subquadratUnitsColumn: GridColDef = { + field: 'subquadratUnits', + headerName: 'U', + headerClassName: 'header', + flex: 0.4, + renderHeader: () => U, + align: 'left', + editable: true, + type: 'singleSelect', + valueOptions: unitSelectionOptions + }; + const quadratUnitsColumn: GridColDef = { + field: 'quadratUnits', + headerName: 'U', + headerClassName: 'header', + flex: 0.4, + renderHeader: () => U, + align: 'left', + editable: true, + type: 'singleSelect', + valueOptions: unitSelectionOptions + }; + const plotUnitsColumn: GridColDef = { + field: 'plotUnits', + headerName: 'U', + headerClassName: 'header', + flex: 0.4, + renderHeader: () => U, + align: 'left', + editable: true, + type: 'singleSelect', + valueOptions: unitSelectionOptions + }; return ( <> @@ -106,7 +112,10 @@ export default function AllTaxonomiesViewDataGrid() { {/* Upload Button */} - + @@ -116,8 +125,8 @@ export default function AllTaxonomiesViewDataGrid() { }} formType={"species"} /> | null>(null); + const [refresh, setRefresh] = useState(false); + const [paginationModel, setPaginationModel] = useState({page: 0, pageSize: 10}); + const [isNewRowAdded, setIsNewRowAdded] = useState(false); + const [shouldAddRowAfterFetch, setShouldAddRowAfterFetch] = useState(false); + const [isUploadModalOpen, setIsUploadModalOpen] = useState(false); + const {data: session} = useSession(); + const currentCensus = useOrgCensusContext(); + + const addNewRowToGrid = () => { + const id = randomId(); + const newRow = { + ...initialStemTaxonomiesViewRDSRow, + id, + isNew: true, + }; + + setRows(oldRows => [...oldRows ?? [], newRow]); + setRowModesModel(oldModel => ({ + ...oldModel, + [id]: {mode: 'edit', fieldToFocus: 'stemTag'}, + })); + console.log('attributes addnewrowtogrid triggered'); + }; + + return ( + <> + + + + {session?.user.isAdmin && ( + + Note: ADMINISTRATOR VIEW + + )} + + + + {/* Upload Button */} + + + + + { + setIsUploadModalOpen(false); + setRefresh(true); + }} formType={"species"}/> + + + + ); +} \ No newline at end of file diff --git a/frontend/components/datagrids/confirmationdialog.tsx b/frontend/components/datagrids/confirmationdialog.tsx new file mode 100644 index 00000000..270672da --- /dev/null +++ b/frontend/components/datagrids/confirmationdialog.tsx @@ -0,0 +1,39 @@ +"use client"; +import React from 'react'; +import { Dialog, DialogActions, DialogContent, DialogContentText, DialogTitle, Button } from '@mui/material'; + +interface ConfirmationDialogProps { + open: boolean; + onClose: () => void; + onConfirm: () => void; + title: string; + content: string; +} + +const ConfirmationDialog: React.FC = ({ open, onClose, onConfirm, title, content }) => { + return ( + + {title} + + + {content} + + + + + + + + ); +}; + +export default ConfirmationDialog; diff --git a/frontend/components/datagrids/datagridcommons.tsx b/frontend/components/datagrids/datagridcommons.tsx index 47c80266..87533b72 100644 --- a/frontend/components/datagrids/datagridcommons.tsx +++ b/frontend/components/datagrids/datagridcommons.tsx @@ -1,5 +1,5 @@ "use client"; -import React, { Dispatch, SetStateAction, useCallback, useEffect, useMemo, useState } from 'react'; +import React, { useCallback, useEffect, useMemo, useState } from 'react'; import { GridActionsCellItem, GridColDef, @@ -9,7 +9,6 @@ import { GridRowModel, GridRowModes, GridRowModesModel, - GridRowsProp, GridToolbar, GridToolbarContainer, GridToolbarProps, @@ -17,13 +16,7 @@ import { } from '@mui/x-data-grid'; import { Alert, - AlertProps, Button, - Dialog, - DialogActions, - DialogContent, - DialogContentText, - DialogTitle, Snackbar } from '@mui/material'; import AddIcon from '@mui/icons-material/Add'; @@ -33,13 +26,13 @@ import SaveIcon from '@mui/icons-material/Save'; import CancelIcon from '@mui/icons-material/Close'; import RefreshIcon from '@mui/icons-material/Refresh'; import Box from "@mui/joy/Box"; -import { Stack, Typography } from "@mui/joy"; +import { Tooltip, Typography } from "@mui/joy"; import { StyledDataGrid } from "@/config/styleddatagrid"; import { - computeMutation, createDeleteQuery, createFetchQuery, createPostPatchQuery, + getColumnVisibilityModel, getGridID, } from "@/config/datagridhelpers"; import { useSession } from "next-auth/react"; @@ -50,93 +43,28 @@ import { useSiteContext } from "@/app/contexts/userselectionprovider"; import { redirect } from 'next/navigation'; -import UpdateContextsFromIDB from '@/config/updatecontextsfromidb'; -import { UnifiedValidityFlags } from '@/config/macros'; +import { HTTPResponses, UnifiedValidityFlags } from '@/config/macros'; import { useDataValidityContext } from '@/app/contexts/datavalidityprovider'; - -interface EditToolbarCustomProps { - handleAddNewRow?: () => void; - handleRefresh?: () => Promise; - locked?: boolean; -} +import { useLockAnimation } from '@/app/contexts/lockanimationcontext'; +import { useLoading } from '@/app/contexts/loadingprovider'; +import { EditToolbarCustomProps, DataGridCommonProps, PendingAction, CellItemContainer, filterColumns } from './datagridmacros'; +import ReEnterDataModal from './reentrydatamodal'; +import ConfirmationDialog from './confirmationdialog'; type EditToolbarProps = EditToolbarCustomProps & GridToolbarProps & ToolbarPropsOverrides; -export function EditToolbar(props: EditToolbarProps) { - const { handleAddNewRow, handleRefresh, locked = false } = props; - - return ( - - - {!locked && ( - - )} - - - ); -} +const EditToolbar = ({ handleAddNewRow, handleRefresh, locked }: EditToolbarProps) => ( + + + + + +); -export interface DataGridCommonProps { - gridType: string; - gridColumns: GridColDef[]; - rows: GridRowsProp; - setRows: Dispatch>; - rowCount: number; - setRowCount: Dispatch>; - rowModesModel: GridRowModesModel; - setRowModesModel: Dispatch>; - snackbar: Pick | null; - setSnackbar: Dispatch | null>>; - refresh: boolean; - setRefresh: Dispatch>; - paginationModel: { pageSize: number, page: number }; - setPaginationModel: Dispatch>; - isNewRowAdded: boolean; - setIsNewRowAdded: Dispatch>; - shouldAddRowAfterFetch: boolean; - setShouldAddRowAfterFetch: Dispatch>; - addNewRowToGrid: () => void; - locked?: boolean; - handleSelectQuadrat?: (quadratID: number | null) => void; -} - -// Define types for the new states and props -type PendingAction = { - actionType: 'save' | 'delete' | ''; - actionId: GridRowId | null; -}; - -interface ConfirmationDialogProps { - isOpen: boolean; - onConfirm: () => void; - onCancel: () => void; - message: string; -} - -/** - * Function to determine if all entries in a column are null - */ -function allValuesAreNull(rows: GridRowsProp, field: string): boolean { - return rows.length > 0 && rows.every(row => row[field] === null || row[field] === undefined); -} - -/** - * Function to filter out columns where all entries are null, except the actions column. - */ -function filterColumns(rows: GridRowsProp, columns: GridColDef[]): GridColDef[] { - return columns.filter(col => col.field === 'actions' || !allValuesAreNull(rows, col.field)); -} -/** - * Renders common UI components for data grids. - * - * Handles state and logic for editing, saving, deleting rows, pagination, - * validation errors and more. Renders a DataGrid component with customized - * columns and cell renderers. - */ export default function DataGridCommons(props: Readonly) { const { addNewRowToGrid, @@ -156,87 +84,129 @@ export default function DataGridCommons(props: Readonly) { setPaginationModel, isNewRowAdded, setIsNewRowAdded, - shouldAddRowAfterFetch, setShouldAddRowAfterFetch, - locked = false, handleSelectQuadrat, } = props; - const [newLastPage, setNewLastPage] = useState(null); // new state to track the new last page + const [newLastPage, setNewLastPage] = useState(null); const [isDialogOpen, setIsDialogOpen] = useState(false); + const [isDeleteDialogOpen, setIsDeleteDialogOpen] = useState(false); + const [locked, setLocked] = useState(false); + const [pendingAction, setPendingAction] = useState({ actionType: '', actionId: null }); + const [promiseArguments, setPromiseArguments] = useState<{ resolve: (value: GridRowModel) => void, reject: (reason?: any) => void, newRow: GridRowModel, oldRow: GridRowModel } | null>(null); const currentPlot = usePlotContext(); const currentCensus = useOrgCensusContext(); const currentQuadrat = useQuadratContext(); + const currentSite = useSiteContext(); + const { setLoading } = useLoading(); const { triggerRefresh } = useDataValidityContext(); + const { triggerPulse } = useLockAnimation(); + const handleLockedClick = () => triggerPulse(); - const [pendingAction, setPendingAction] = useState({ - actionType: '', - actionId: null - }); + useSession(); - const { data: session } = useSession(); - const currentSite = useSiteContext(); + useEffect(() => { + if (currentCensus !== undefined) { + setLocked(currentCensus.dateRanges[0].endDate !== undefined); + } + }, [currentCensus]); + + useEffect(() => { + if (!isNewRowAdded) { + fetchPaginatedData(paginationModel.page).catch(console.error); + } + }, [paginationModel.page]); + + useEffect(() => { + if (currentPlot?.plotID || currentCensus?.plotCensusNumber) { + fetchPaginatedData(paginationModel.page).catch(console.error); + } + }, [currentPlot, currentCensus, paginationModel.page]); + + useEffect(() => { + if (refresh && currentSite) { + handleRefresh().then(() => { + setRefresh(false); + }); + } + }, [refresh, setRefresh]); const openConfirmationDialog = ( actionType: 'save' | 'delete', actionId: GridRowId ) => { setPendingAction({ actionType, actionId }); - setIsDialogOpen(true); + const row = rows.find(row => String(row.id) === String(actionId)); + if (row) { + if (actionType === 'delete') { + setIsDeleteDialogOpen(true); + } else { + setIsDialogOpen(true); + setRowModesModel(oldModel => ({ + ...oldModel, + [actionId]: { mode: GridRowModes.View } + })); + } + } }; const handleConfirmAction = async () => { setIsDialogOpen(false); - if ( - pendingAction.actionType === 'save' && - pendingAction.actionId !== null - ) { + setIsDeleteDialogOpen(false); + if (pendingAction.actionType === 'save' && pendingAction.actionId !== null && promiseArguments) { await performSaveAction(pendingAction.actionId); - } else if ( - pendingAction.actionType === 'delete' && - pendingAction.actionId !== null - ) { + } else if (pendingAction.actionType === 'delete' && pendingAction.actionId !== null) { await performDeleteAction(pendingAction.actionId); } setPendingAction({ actionType: '', actionId: null }); + setPromiseArguments(null); // Clear promise arguments after handling }; const handleCancelAction = () => { setIsDialogOpen(false); + setIsDeleteDialogOpen(false); + if (promiseArguments) { + promiseArguments.reject(new Error('Action cancelled by user')); + } setPendingAction({ actionType: '', actionId: null }); + setPromiseArguments(null); // Clear promise arguments after handling }; const performSaveAction = async (id: GridRowId) => { - if (locked) return; - // console.log('save confirmed'); - setRowModesModel(oldModel => ({ - ...oldModel, - [id]: { mode: GridRowModes.View } - })); - // console.log('Updated rowModesModel:', rowModesModel); + if (locked || !promiseArguments) return; + setLoading(true, "Saving changes..."); + try { + const updatedRow = await updateRow( + gridType, + currentSite?.schemaName, + promiseArguments.newRow, + promiseArguments.oldRow, + setSnackbar, + setIsNewRowAdded, + setShouldAddRowAfterFetch, + fetchPaginatedData, + paginationModel + ); + promiseArguments.resolve(updatedRow); + } catch (error) { + promiseArguments.reject(error); + } const row = rows.find(row => String(row.id) === String(id)); if (row?.isNew) { setIsNewRowAdded(false); setShouldAddRowAfterFetch(false); } if (handleSelectQuadrat) handleSelectQuadrat(null); - switch (gridType) { - case 'stemtaxonomiesview': - case 'alltaxonomiesview': - triggerRefresh(["species" as keyof UnifiedValidityFlags]); - case 'stemdimensionsview': - triggerRefresh(["quadrats" as keyof UnifiedValidityFlags]); - default: - triggerRefresh([gridType as keyof UnifiedValidityFlags]); - break; - } + triggerRefresh(); + setLoading(false); await fetchPaginatedData(paginationModel.page); }; const performDeleteAction = async (id: GridRowId) => { if (locked) return; + setLoading(true, "Deleting..."); const deletionID = rows.find(row => String(row.id) === String(id))?.id; if (!deletionID) return; const deleteQuery = createDeleteQuery( @@ -252,8 +222,14 @@ export default function DataGridCommons(props: Readonly) { }, body: JSON.stringify({ oldRow: undefined, newRow: rows.find(row => String(row.id) === String(id))! }) }); + setLoading(false); if (!response.ok) { - setSnackbar({ children: 'Error: Deletion failed', severity: 'error' }); + const error = await response.json(); + if (response.status === HTTPResponses.FOREIGN_KEY_CONFLICT) { + setSnackbar({ children: `Error: Cannot delete row due to foreign key constraint in table ${error.referencingTable}`, severity: 'error' }); + } else { + setSnackbar({ children: `Error: ${error.message || 'Deletion failed'}`, severity: 'error' }); + } } else { if (handleSelectQuadrat) handleSelectQuadrat(null); setSnackbar({ children: 'Row successfully deleted', severity: 'success' }); @@ -286,11 +262,8 @@ export default function DataGridCommons(props: Readonly) { const handleAddNewRow = async () => { if (locked) { - // console.log('rowCount: ', rowCount); return; } - // console.log('handleAddNewRow triggered'); - const newRowCount = rowCount + 1; const calculatedNewLastPage = Math.ceil(newRowCount / paginationModel.pageSize) - 1; const existingLastPage = Math.ceil(rowCount / paginationModel.pageSize) - 1; @@ -300,10 +273,8 @@ export default function DataGridCommons(props: Readonly) { setShouldAddRowAfterFetch(isNewPageNeeded); setNewLastPage(calculatedNewLastPage); - // console.log('newRowCount:', newRowCount, 'calculatedNewLastPage:', calculatedNewLastPage, 'isNewPageNeeded:', isNewPageNeeded); - if (isNewPageNeeded) { - await setPaginationModel({ ...paginationModel, page: calculatedNewLastPage }); + setPaginationModel({ ...paginationModel, page: calculatedNewLastPage }); addNewRowToGrid(); } else { setPaginationModel({ ...paginationModel, page: existingLastPage }); @@ -316,8 +287,8 @@ export default function DataGridCommons(props: Readonly) { }; const fetchPaginatedData = async (pageToFetch: number) => { - // console.log('fetchPaginatedData triggered'); - let paginatedQuery = createFetchQuery( + setLoading(true, "Loading data..."); + const paginatedQuery = createFetchQuery( currentSite?.schemaName ?? '', gridType, pageToFetch, @@ -329,7 +300,6 @@ export default function DataGridCommons(props: Readonly) { try { const response = await fetch(paginatedQuery, { method: 'GET' }); const data = await response.json(); - // console.log('fetchPaginatedData data (json-converted): ', data); if (!response.ok) throw new Error(data.message || 'Error fetching data'); setRows(data.output); setRowCount(data.totalCount); @@ -339,70 +309,77 @@ export default function DataGridCommons(props: Readonly) { addNewRowToGrid(); setIsNewRowAdded(false); } - - // console.log('Data validity refresh triggered for gridType:', gridType); } catch (error) { console.error('Error fetching data:', error); setSnackbar({ children: 'Error fetching data', severity: 'error' }); + } finally { + setLoading(false); } }; - useEffect(() => { - if (!isNewRowAdded) { - fetchPaginatedData(paginationModel.page).catch(console.error); - } - }, [paginationModel.page]); - - useEffect(() => { - if (currentPlot?.plotID || currentCensus?.plotCensusNumber) { - fetchPaginatedData(paginationModel.page).catch(console.error); - } - }, [currentPlot, currentCensus, paginationModel.page]); - - useEffect(() => { - if (refresh && currentSite) { - handleRefresh().then(() => { - setRefresh(false); - }); - } - }, [refresh, setRefresh]); - - const processRowUpdate = useCallback(async (newRow: GridRowModel, oldRow: GridRowModel): Promise => { + const updateRow = async ( + gridType: string, + schemaName: string | undefined, + newRow: GridRowModel, + oldRow: GridRowModel, + setSnackbar: (value: { children: string; severity: 'error' | 'success' }) => void, + setIsNewRowAdded: (value: boolean) => void, + setShouldAddRowAfterFetch: (value: boolean) => void, + fetchPaginatedData: (page: number) => Promise, + paginationModel: { page: number } + ): Promise => { const gridID = getGridID(gridType); - const fetchProcessQuery = createPostPatchQuery(currentSite?.schemaName ?? '', gridType, gridID); - - if (newRow.id === '') { - throw new Error(`Primary key id cannot be empty!`); - } + const fetchProcessQuery = createPostPatchQuery(schemaName ?? '', gridType, gridID); try { - let response, responseJSON; - response = await fetch(fetchProcessQuery, { + const response = await fetch(fetchProcessQuery, { method: oldRow.isNew ? 'POST' : 'PATCH', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ oldRow: oldRow, newRow: newRow }) }); - responseJSON = await response.json(); + + const responseJSON = await response.json(); + if (!response.ok) { setSnackbar({ children: `Error: ${responseJSON.message}`, severity: 'error' }); - return Promise.reject(responseJSON.row); // Return the problematic row + return Promise.reject(responseJSON.row); } + setSnackbar({ children: oldRow.isNew ? 'New row added!' : 'Row updated!', severity: 'success' }); + if (oldRow.isNew) { setIsNewRowAdded(false); setShouldAddRowAfterFetch(false); await fetchPaginatedData(paginationModel.page); } + return newRow; } catch (error: any) { setSnackbar({ children: `Error: ${error.message}`, severity: 'error' }); return Promise.reject(newRow); } - }, [setSnackbar, setIsNewRowAdded, fetchPaginatedData, paginationModel.page, gridType]); + }; + + const processRowUpdate = useCallback((newRow: GridRowModel, oldRow: GridRowModel) => new Promise((resolve, reject) => { + setLoading(true, "Processing changes..."); + if (newRow.id === '') { + setLoading(false); + return reject(new Error('Primary key id cannot be empty!')); + } + + setPromiseArguments({ resolve, reject, newRow, oldRow }); + setLoading(false); + }), [gridType, currentSite?.schemaName, setSnackbar, setIsNewRowAdded, setShouldAddRowAfterFetch, fetchPaginatedData, paginationModel]); const handleRowModesModelChange = (newRowModesModel: GridRowModesModel) => { - // console.log('New Row Modes Model:', newRowModesModel); setRowModesModel(newRowModesModel); + + const rowInEditMode = Object.entries(newRowModesModel).find(([id, mode]) => mode.mode === GridRowModes.Edit); + if (rowInEditMode) { + const [id] = rowInEditMode; + const row = rows.find(row => String(row.id) === String(id)); + console.log('handleRowModesModelChange triggered on row: ', row); + } }; const handleCloseSnackbar = () => setSnackbar(null); @@ -419,24 +396,21 @@ export default function DataGridCommons(props: Readonly) { const handleEditClick = (id: GridRowId) => () => { if (locked) return; const row = rows.find(row => String(row.id) === String(id)); + console.log('handleEditClick row: ', row); if (row && handleSelectQuadrat) { handleSelectQuadrat(row.quadratID); } - // console.log('Edit Click - Before Set:', rowModesModel); setRowModesModel((prevModel) => ({ ...prevModel, [id]: { mode: GridRowModes.Edit }, })); - // console.log('Edit Click - After Set:', rowModesModel); }; const handleCancelClick = (id: GridRowId, event?: React.MouseEvent) => { if (locked) return; event?.preventDefault(); - // console.log('Cancel clicked for row ID:', id); const row = rows.find(row => String(row.id) === String(id)); if (row?.isNew) { - // console.log('New row cancelled, removing from rows'); setRows(oldRows => oldRows.filter(row => row.id !== id)); setIsNewRowAdded(false); if (rowCount % paginationModel.pageSize === 1 && isNewRowAdded) { @@ -452,6 +426,31 @@ export default function DataGridCommons(props: Readonly) { if (handleSelectQuadrat) handleSelectQuadrat(null); }; + const getEnhancedCellAction = (type: string, icon: any, onClick: any) => ( + + + { + if (locked) { + handleLockedClick(); + const iconElement = e.currentTarget.querySelector('svg'); + if (iconElement) { + iconElement.classList.add('animate-shake'); + setTimeout(() => { + iconElement.classList.remove('animate-shake'); + }, 500); + } + } else { + onClick(); + } + }} + > + + + + + ); + function getGridActionsColumn(): GridColDef { return { field: 'actions', @@ -460,53 +459,27 @@ export default function DataGridCommons(props: Readonly) { width: 100, cellClassName: 'actions', getActions: ({ id }) => { - if (locked) return []; - // console.log('rowModesModel:', rowModesModel); const isInEditMode = rowModesModel[id]?.mode === 'edit'; - // console.log('Row ID:', id, 'Is in edit mode:', isInEditMode); - if (isInEditMode) { + if (isInEditMode && !locked) { return [ - } - label='Save' - key={'save'} - onClick={handleSaveClick(id)} - />, - } - label='Cancel' - key={'cancel'} - onClick={event => handleCancelClick(id, event)} - /> + getEnhancedCellAction('Save', , handleSaveClick(id)), + getEnhancedCellAction('Cancel', , (e: any) => handleCancelClick(id, e)), ]; } return [ - } - label='Edit' - key={'edit'} - onClick={handleEditClick(id)} - />, - } - label='Delete' - key={'delete'} - onClick={handleDeleteClick(id)} - /> + getEnhancedCellAction('Edit', , handleEditClick(id)), + getEnhancedCellAction('Delete', , handleDeleteClick(id)), ]; - } + }, }; } const columns = useMemo(() => { const commonColumns = gridColumns; - if (locked) { - return commonColumns; - } return [...commonColumns, getGridActionsColumn()]; - }, [gridColumns, locked, rowModesModel]); + }, [gridColumns, rowModesModel]); const filteredColumns = useMemo(() => { if (gridType !== 'quadratpersonnel') return filterColumns(rows, columns); @@ -558,6 +531,11 @@ export default function DataGridCommons(props: Readonly) { paginationModel={paginationModel} rowCount={rowCount} pageSizeOptions={[paginationModel.pageSize]} + initialState={{ + columns: { + columnVisibilityModel: getColumnVisibilityModel(gridType), + }, + }} slots={{ toolbar: EditToolbar }} @@ -582,40 +560,25 @@ export default function DataGridCommons(props: Readonly) { )} - + {isDialogOpen && promiseArguments && ( + + )} + {isDeleteDialogOpen && ( + + )} ); } } - -// ConfirmationDialog component with TypeScript types -const ConfirmationDialog: React.FC = (props) => { - const { isOpen, onConfirm, onCancel, message } = props; - return ( - - Confirm Action - - - {message} - - - - - - - - ); -}; diff --git a/frontend/components/datagrids/datagridmacros.ts b/frontend/components/datagrids/datagridmacros.ts new file mode 100644 index 00000000..f262ddf9 --- /dev/null +++ b/frontend/components/datagrids/datagridmacros.ts @@ -0,0 +1,142 @@ +import styled from "@emotion/styled"; +import { AlertProps } from "@mui/material"; +import { GridColDef, GridRowsProp, GridRowModesModel, GridRowId, GridSortDirection, GridRowModel } from "@mui/x-data-grid"; +import { Dispatch, SetStateAction } from "react"; + +export interface EditToolbarCustomProps { + handleAddNewRow?: () => void; + handleRefresh?: () => Promise; + locked?: boolean; +} + +export interface DataGridCommonProps { + gridType: string; + gridColumns: GridColDef[]; + rows: GridRowsProp; + setRows: Dispatch>; + rowCount: number; + setRowCount: Dispatch>; + rowModesModel: GridRowModesModel; + setRowModesModel: Dispatch>; + snackbar: Pick | null; + setSnackbar: Dispatch | null>>; + refresh: boolean; + setRefresh: Dispatch>; + paginationModel: { pageSize: number, page: number }; + setPaginationModel: Dispatch>; + isNewRowAdded: boolean; + setIsNewRowAdded: Dispatch>; + shouldAddRowAfterFetch: boolean; + setShouldAddRowAfterFetch: Dispatch>; + addNewRowToGrid: () => void; + handleSelectQuadrat?: (quadratID: number | null) => void; + initialRow?: GridRowModel; +} + +// Define types for the new states and props +export type PendingAction = { + actionType: 'save' | 'delete' | ''; + actionId: GridRowId | null; +}; + +export interface ConfirmationDialogProps { + isOpen: boolean; + onConfirm: () => void; + onCancel: () => void; + message: string; +} + +export const CellItemContainer = styled('div')({ + display: 'flex', + flexDirection: 'column', + alignItems: 'center', + justifyContent: 'center', + height: '100%', +}); + +/** + * Function to determine if all entries in a column are null + */ +export function allValuesAreNull(rows: GridRowsProp, field: string): boolean { + return rows.length > 0 && rows.every(row => row[field] === null || row[field] === undefined); +} + +/** + * Function to filter out columns where all entries are null, except the actions column. + */ +export function filterColumns(rows: GridRowsProp, columns: GridColDef[]): GridColDef[] { + return columns.filter(col => col.field === 'actions' || !allValuesAreNull(rows, col.field)); +} + +/** + * Function to filter out columns where all entries are null, except the actions column. + */ +export function filterMSVColumns(rows: GridRowsProp, columns: GridColDef[]): GridColDef[] { + return columns.filter(col => col.field === 'actions' || col.field === 'subquadrats' || col.field === "isValidated" || !allValuesAreNull(rows, col.field)); +} + + +export interface MeasurementSummaryGridProps { + gridColumns: GridColDef[]; + rows: GridRowsProp; + setRows: Dispatch>; + rowCount: number; + setRowCount: Dispatch>; + rowModesModel: GridRowModesModel; + setRowModesModel: Dispatch>; + snackbar: Pick | null; + setSnackbar: Dispatch | null>>; + refresh: boolean; + setRefresh: Dispatch>; + paginationModel: { pageSize: number; page: number }; + setPaginationModel: Dispatch>; + isNewRowAdded: boolean; + setIsNewRowAdded: Dispatch>; + shouldAddRowAfterFetch: boolean; + setShouldAddRowAfterFetch: Dispatch>; + addNewRowToGrid: () => void; + handleSelectQuadrat?: (quadratID: number | null) => void; +} + +export const errorMapping: { [key: string]: string[] } = { + '1': ["attributes"], + '2': ["measuredDBH"], + '3': ["measuredHOM"], + '4': ["treeTag", "stemTag"], + '5': ["treeTag", "stemTag", "quadratName"], + '6': ["stemQuadX", "stemQuadY"], + '7': ["speciesName"], + '8': ["measurementDate"], + '9': ["treeTag", "stemTag", "plotCensusNumber"], + '10': ["treeTag", "stemTag", "plotCensusNumber"], + '11': ["quadratName"], + '12': ["speciesName"], + '13': ["measuredDBH"], + '14': ["measuredDBH"], + '15': ["treeTag"], + '16': ["quadratName"], +}; + +export const sortRowsByMeasurementDate = (rows: GridRowsProp, direction: GridSortDirection): GridRowsProp => { + return rows.slice().sort((a, b) => { + const dateA = new Date(a.measurementDate).getTime(); + const dateB = new Date(b.measurementDate).getTime(); + return direction === 'asc' ? dateA - dateB : dateB - dateA; + }); +}; + +export const areRowsDifferent = (row1: GridRowModel | null, row2: GridRowModel | null): boolean => { + if (!row1 || !row2) { + return true; // Consider them different if either row is null + } + + const keys = Object.keys(row1); + + for (const key of keys) { + if (row1[key] !== row2[key]) { + return true; // If any value differs, the rows are different + } + } + + return false; // All values are identical +}; \ No newline at end of file diff --git a/frontend/components/datagrids/msvdatagrid.tsx b/frontend/components/datagrids/msvdatagrid.tsx index 9eead417..6546de53 100644 --- a/frontend/components/datagrids/msvdatagrid.tsx +++ b/frontend/components/datagrids/msvdatagrid.tsx @@ -1,5 +1,5 @@ "use client"; -import React, { Dispatch, SetStateAction, useEffect, useMemo, useState } from 'react'; +import React, { useCallback, useEffect, useMemo, useState } from 'react'; import { GridActionsCellItem, GridCellParams, @@ -10,8 +10,7 @@ import { GridRowModel, GridRowModes, GridRowModesModel, - GridRowParams, - GridRowsProp, + GridSortModel, GridToolbar, GridToolbarContainer, GridToolbarProps, @@ -20,14 +19,10 @@ import { } from '@mui/x-data-grid'; import { Alert, - AlertProps, Button, Checkbox, - Dialog, - DialogActions, - DialogContent, - DialogContentText, - DialogTitle, + FormControlLabel, + FormGroup, Snackbar } from '@mui/material'; import AddIcon from '@mui/icons-material/Add'; @@ -37,10 +32,9 @@ import SaveIcon from '@mui/icons-material/Save'; import CancelIcon from '@mui/icons-material/Close'; import RefreshIcon from '@mui/icons-material/Refresh'; import Box from "@mui/joy/Box"; -import { Stack, Typography } from "@mui/joy"; +import { Stack, Tooltip, Typography } from "@mui/joy"; import { StyledDataGrid } from "@/config/styleddatagrid"; import { - computeMutation, createDeleteQuery, createFetchQuery, createPostPatchQuery, @@ -57,102 +51,33 @@ import { saveAs } from 'file-saver'; import { redirect } from 'next/navigation'; import { CoreMeasurementsRDS } from '@/config/sqlrdsdefinitions/tables/coremeasurementsrds'; import moment from 'moment'; - -const errorMapping: { [key: string]: string[] } = { - '1': ["attributes"], - '2': ["measuredDBH"], - '3': ["measuredHOM"], - '4': ["treeTag", "stemTag"], - '5': ["treeTag", "stemTag", "quadratName"], - '6': ["stemQuadX", "stemQuadY"], - '7': ["speciesName"], - '8': ["measurementDate"], - '9': ["treeTag", "stemTag", "plotCensusNumber"], - '10': ["treeTag", "stemTag", "plotCensusNumber"], - '11': ["quadratName"], - '12': ["speciesName"], - '13': ["measuredDBH"], - '14': ["measuredDBH"], - '15': ["treeTag"], - '16': ["quadratName"], -}; - -interface EditToolbarCustomProps { - handleAddNewRow?: () => void; - handleRefresh?: () => Promise; - locked?: boolean; -} +import { useLockAnimation } from '@/app/contexts/lockanimationcontext'; +import CheckIcon from '@mui/icons-material/Check'; +import ErrorIcon from '@mui/icons-material/Error'; +import HourglassEmptyIcon from '@mui/icons-material/HourglassEmpty'; +import BlockIcon from '@mui/icons-material/Block'; +import { CensusDateRange, OrgCensusRDS } from '@/config/sqlrdsdefinitions/orgcensusrds'; +import { HTTPResponses, unitSelectionOptions } from '@/config/macros'; +import { gridColumnsArrayMSVRDS } from '@/config/sqlrdsdefinitions/views/measurementssummaryviewrds'; +import { MeasurementSummaryGridProps, sortRowsByMeasurementDate, PendingAction, CellItemContainer, errorMapping, filterColumns, EditToolbarCustomProps } from './datagridmacros'; +import ConfirmationDialog from './confirmationdialog'; +import ReEnterDataModal from './reentrydatamodal'; +import { useLoading } from '@/app/contexts/loadingprovider'; +import { useSession } from 'next-auth/react'; type EditToolbarProps = EditToolbarCustomProps & GridToolbarProps & ToolbarPropsOverrides; -export function EditToolbar(props: EditToolbarProps) { - const { handleAddNewRow, handleRefresh, locked = false } = props; - - return ( - - - {!locked && ( - - )} - - - ); -} - -export interface MeasurementSummaryGridProps { - gridColumns: GridColDef[]; - rows: GridRowsProp; - setRows: Dispatch>; - rowCount: number; - setRowCount: Dispatch>; - rowModesModel: GridRowModesModel; - setRowModesModel: Dispatch>; - snackbar: Pick | null; - setSnackbar: Dispatch | null>>; - refresh: boolean; - setRefresh: Dispatch>; - paginationModel: { pageSize: number; page: number }; - setPaginationModel: Dispatch>; - isNewRowAdded: boolean; - setIsNewRowAdded: Dispatch>; - shouldAddRowAfterFetch: boolean; - setShouldAddRowAfterFetch: Dispatch>; - addNewRowToGrid: () => void; - locked?: boolean; - handleSelectQuadrat?: (quadratID: number | null) => void; -} - -// Define types for the new states and props -type PendingAction = { - actionType: 'save' | 'delete' | ''; - actionId: GridRowId | null; -}; - -interface ConfirmationDialogProps { - isOpen: boolean; - onConfirm: () => void; - onCancel: () => void; - message: string; -} - -/** - * Function to determine if all entries in a column are null - */ -function allValuesAreNull(rows: GridRowsProp, field: string): boolean { - return rows.length > 0 && rows.every(row => row[field] === null || row[field] === undefined); -} - -/** - * Function to filter out columns where all entries are null, except the actions column. - */ -function filterColumns(rows: GridRowsProp, columns: GridColDef[]): GridColDef[] { - return columns.filter(col => col.field === 'actions' || col.field === 'subquadrats' || !allValuesAreNull(rows, col.field)); -} - +const EditToolbar = ({ handleAddNewRow, handleRefresh, locked }: EditToolbarProps) => ( + + + + + +); /** * Renders custom UI components for measurement summary view. * @@ -179,25 +104,88 @@ export default function MeasurementSummaryGrid(props: Readonly(null); // new state to track the new last page + const [isDialogOpen, setIsDialogOpen] = useState(false); + const [pendingAction, setPendingAction] = useState({ actionType: '', actionId: null }); + const [isDeleteDialogOpen, setIsDeleteDialogOpen] = useState(false); + const [locked, setLocked] = useState(false); + const [promiseArguments, setPromiseArguments] = useState<{ resolve: (value: GridRowModel) => void, reject: (reason?: any) => void, newRow: GridRowModel, oldRow: GridRowModel } | null>(null); + + // custom states -- msvdatagrid + const [deprecatedRows, setDeprecatedRows] = useState([]); // new state to track deprecated rows const [validationErrors, setValidationErrors] = useState<{ [key: number]: CMError }>({}); const [showErrorRows, setShowErrorRows] = useState(true); const [showValidRows, setShowValidRows] = useState(true); - const [isDialogOpen, setIsDialogOpen] = useState(false); + const [showDeprecatedRows, setShowDeprecatedRows] = useState(true); const [errorRowsForExport, setErrorRowsForExport] = useState([]); + const [sortModel, setSortModel] = useState([{ field: 'measurementDate', sort: 'asc' }]); + const [selectedDateRanges, setSelectedDateRanges] = useState([]); + // context pulls and definitions + const currentSite = useSiteContext(); const currentPlot = usePlotContext(); const currentCensus = useOrgCensusContext(); const currentQuadrat = useQuadratContext(); + const { setLoading } = useLoading(); + const { triggerPulse } = useLockAnimation(); + const handleLockedClick = () => triggerPulse(); - const [pendingAction, setPendingAction] = useState({ actionType: '', actionId: null }); + // column destructuring -- applying custom formats to columns + const [a, b, c, d] = gridColumnsArrayMSVRDS; - const currentSite = useSiteContext(); + // use the session + useSession(); + + // helper functions for usage: + const handleSortModelChange = (newModel: GridSortModel) => { + setSortModel(newModel); + if (newModel.length > 0) { + const { field, sort } = newModel[0]; + if (field === 'measurementDate') { + const sortedRows = sortRowsByMeasurementDate(rows, sort); + setRows(sortedRows); + } + } + }; + const getDateRangesForCensus = (census: OrgCensusRDS | undefined) => { + return census?.dateRanges ?? []; + }; + const handleDateRangeChange = (censusID: number) => (event: React.ChangeEvent) => { + if (event.target.checked) { + setSelectedDateRanges(prev => [...prev, censusID]); + } else { + setSelectedDateRanges(prev => prev.filter(id => id !== censusID)); + } + }; + const renderDateRangeFilters = (dateRanges: CensusDateRange[]) => ( + + {dateRanges.map(range => ( + } + label={ + + ID: {range.censusID} + + {moment(range.startDate).format('ddd, MMM D, YYYY')} - {moment(range.endDate).format('ddd, MMM D, YYYY')} + + + } + /> + ))} + + ); + const handleShowDeprecatedRowsChange = (event: any) => { + setShowDeprecatedRows(event.target.checked); + }; + const rowIsDeprecated = (rowId: GridRowId) => { + return deprecatedRows.some(depRow => depRow.id === rowId); + }; const extractErrorRows = () => { if (errorRowsForExport.length > 0) return; @@ -205,19 +193,23 @@ export default function MeasurementSummaryGrid(props: Readonly { + const error = validationErrors[Number(rowId)]; + if (!error) return false; + const errorFields = error.ValidationErrorIDs.flatMap( + id => errorMapping[id.toString()] || [] + ); + return errorFields.includes(colField); + }; + const rowHasError = (rowId: GridRowId) => { + if (!rows || rows.length === 0) return false; + return gridColumns.some(column => cellHasError(column.field, rowId)); + }; const fetchErrorRows = async () => { if (!rows || rows.length === 0) return []; const errorRows = rows.filter(row => rowHasError(row.id)); return errorRows; }; - - useEffect(() => { - if (errorRowsForExport && errorRowsForExport.length > 0) { - printErrorRows(); - } - }, [errorRowsForExport]); - const getRowErrorDescriptions = (rowId: GridRowId): string[] => { const error = validationErrors[Number(rowId)]; if (!error) return []; @@ -227,61 +219,102 @@ export default function MeasurementSummaryGrid(props: Readonly { - let csvContent = "data:text/csv;charset=utf-8,"; - csvContent += gridColumns.map(col => col.headerName).concat("Validation Errors").join(",") + "\r\n"; // Add "Validation Errors" column header + const errorRowCount = useMemo(() => { + return rows.filter(row => rowHasError(row.id)).length; + }, [rows, gridColumns]); - errorRowsForExport.forEach(row => { - const rowValues = gridColumns.map(col => row[col.field] ?? '').join(","); - const errorDescriptions = getRowErrorDescriptions(row.id).join("; "); // Function to retrieve error descriptions for a row - csvContent += `${rowValues},"${errorDescriptions}"\r\n`; - }); + // use effect loops, pulled from datagridcommons: + useEffect(() => { + if (currentCensus !== undefined) { + setLocked(currentCensus.dateRanges[0].endDate !== undefined); + } + }, [currentCensus]); - const blob = new Blob([csvContent], { type: "text/csv;charset=utf-8;" }); - saveAs(blob, `validation_errors_${currentPlot?.plotName}.csv`); - }; + useEffect(() => { + if (!isNewRowAdded) { + fetchPaginatedData(paginationModel.page).catch(console.error); + } + }, [paginationModel.page, sortModel]); - const printErrorRows = () => { - if (!errorRowsForExport || errorRowsForExport.length === 0) { - extractErrorRows(); - return; + useEffect(() => { + if (currentPlot?.plotID || currentCensus?.plotCensusNumber) { + fetchPaginatedData(paginationModel.page).catch(console.error); } + }, [currentPlot, currentCensus, paginationModel.page]); - let printContent = ""; - printContent += `
Site: ${currentSite?.schemaName} | Plot: ${currentPlot?.plotName} | Census: ${currentCensus?.plotCensusNumber}
`; - printContent += ""; + useEffect(() => { + if (refresh && currentSite) { + handleRefresh().then(() => { + setRefresh(false); + }); + } + }, [refresh, setRefresh]); - gridColumns.forEach(col => { - printContent += ``; - }); - printContent += ""; // Add error header - printContent += ""; - - errorRowsForExport.forEach(row => { - printContent += ""; - gridColumns.forEach(col => { - console.log('column fields: ', col.field); - if (col.field === "measurementDate") { - printContent += ``; - } else { - printContent += ``; - } + // custom useEffect loops for msvdatagrid --> setting date range filters + useEffect(() => { + if (currentCensus) { + const allDateRangeIDs = currentCensus.dateRanges.map(range => range.censusID); + setSelectedDateRanges(allDateRangeIDs); + } + }, [currentCensus]); + + useEffect(() => { + fetchValidationErrors() + .catch(console.error) + .then(() => setRefresh(false)); + }, [refresh]); + + useEffect(() => { + if (errorRowCount > 0) { + setSnackbar({ + children: `${errorRowCount} row(s) with validation errors detected.`, + severity: 'warning' }); - const errorDescriptions = getRowErrorDescriptions(row.id).join("; "); - printContent += ``; // Print error descriptions - printContent += ""; - }); + } + }, [errorRowCount]); + + // main system begins here: + + const updateRow = async ( + gridType: string, + schemaName: string | undefined, + newRow: GridRowModel, + oldRow: GridRowModel, + setSnackbar: (value: { children: string; severity: 'error' | 'success' }) => void, + setIsNewRowAdded: (value: boolean) => void, + setShouldAddRowAfterFetch: (value: boolean) => void, + fetchPaginatedData: (page: number) => Promise, + paginationModel: { page: number } + ): Promise => { + const gridID = getGridID(gridType); + const fetchProcessQuery = createPostPatchQuery(schemaName ?? '', gridType, gridID); - printContent += "
${col.headerName}Validation Errors
${moment(new Date(row[col.field])).utc().toDate().toDateString() ?? ''}${row[col.field] ?? ''}${errorDescriptions}
"; + try { + const response = await fetch(fetchProcessQuery, { + method: oldRow.isNew ? 'POST' : 'PATCH', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ oldRow: oldRow, newRow: newRow }) + }); - const printWindow = window.open('', '', 'height=600,width=800'); - if (printWindow) { - printWindow.document.write(printContent); - printWindow.document.close(); - printWindow.print(); + const responseJSON = await response.json(); + + if (!response.ok) { + setSnackbar({ children: `Error: ${responseJSON.message}`, severity: 'error' }); + return Promise.reject(responseJSON.row); + } + + setSnackbar({ children: oldRow.isNew ? 'New row added!' : 'Row updated!', severity: 'success' }); + + if (oldRow.isNew) { + setIsNewRowAdded(false); + setShouldAddRowAfterFetch(false); + await fetchPaginatedData(paginationModel.page); + } + + return newRow; + } catch (error: any) { + setSnackbar({ children: `Error: ${error.message}`, severity: 'error' }); + return Promise.reject(newRow); } }; @@ -290,69 +323,101 @@ export default function MeasurementSummaryGrid(props: Readonly { setPendingAction({ actionType, actionId }); - setIsDialogOpen(true); + const row = rows.find(row => String(row.id) === String(actionId)); + if (row) { + if (actionType === 'delete') { + setIsDeleteDialogOpen(true); + } else { + setIsDialogOpen(true); + setRowModesModel(oldModel => ({ + ...oldModel, + [actionId]: { mode: GridRowModes.View } + })); + } + } }; const handleConfirmAction = async () => { setIsDialogOpen(false); - if ( - pendingAction.actionType === 'save' && - pendingAction.actionId !== null - ) { + setIsDeleteDialogOpen(false); + if (pendingAction.actionType === 'save' && pendingAction.actionId !== null && promiseArguments) { await performSaveAction(pendingAction.actionId); - } else if ( - pendingAction.actionType === 'delete' && - pendingAction.actionId !== null - ) { + } else if (pendingAction.actionType === 'delete' && pendingAction.actionId !== null) { await performDeleteAction(pendingAction.actionId); } setPendingAction({ actionType: '', actionId: null }); + setPromiseArguments(null); // Clear promise arguments after handling }; const handleCancelAction = () => { setIsDialogOpen(false); + setIsDeleteDialogOpen(false); + if (promiseArguments) { + promiseArguments.reject(new Error('Action cancelled by user')); + } setPendingAction({ actionType: '', actionId: null }); + setPromiseArguments(null); // Clear promise arguments after handling }; const performSaveAction = async (id: GridRowId) => { - if (locked) return; - console.log('save confirmed'); - setRowModesModel(oldModel => ({ - ...oldModel, - [id]: { mode: GridRowModes.View } - })); - const row = rows.find(row => row.id === id); + if (locked || !promiseArguments) return; + setLoading(true, "Saving changes..."); + try { + const updatedRow = await updateRow( + 'measurementssummaryview', + currentSite?.schemaName, + promiseArguments.newRow, + promiseArguments.oldRow, + setSnackbar, + setIsNewRowAdded, + setShouldAddRowAfterFetch, + fetchPaginatedData, + paginationModel + ); + promiseArguments.resolve(updatedRow); + } catch (error) { + promiseArguments.reject(error); + } + const row = rows.find(row => String(row.id) === String(id)); if (row?.isNew) { setIsNewRowAdded(false); setShouldAddRowAfterFetch(false); } if (handleSelectQuadrat) handleSelectQuadrat(null); + setLoading(false); await fetchPaginatedData(paginationModel.page); }; const performDeleteAction = async (id: GridRowId) => { if (locked) return; - let gridID = getGridID('measurementssummaryview'); - const deletionID = rows.find(row => row.id == id)?.[gridID]; + setLoading(true, "Deleting..."); + const deletionID = rows.find(row => String(row.id) === String(id))?.id; + if (!deletionID) return; const deleteQuery = createDeleteQuery( currentSite?.schemaName ?? '', 'measurementssummaryview', - gridID, - deletionID, + getGridID('measurementssummaryview'), + deletionID ); const response = await fetch(deleteQuery, { method: 'DELETE', headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ oldRow: undefined, newRow: rows.find(row => row.id === id)! }) + body: JSON.stringify({ oldRow: undefined, newRow: rows.find(row => String(row.id) === String(id))! }) }); - if (!response.ok) - setSnackbar({ children: 'Error: Deletion failed', severity: 'error' }); - else { + setLoading(false); + if (!response.ok) { + const error = await response.json(); + if (response.status === HTTPResponses.FOREIGN_KEY_CONFLICT) { + setSnackbar({ children: `Error: Cannot delete row due to foreign key constraint in table ${error.referencingTable}`, severity: 'error' }); + } else { + setSnackbar({ children: `Error: ${error.message || 'Deletion failed'}`, severity: 'error' }); + } + } else { if (handleSelectQuadrat) handleSelectQuadrat(null); setSnackbar({ children: 'Row successfully deleted', severity: 'success' }); - setRows(rows.filter(row => row.id !== id)); + setRows(rows.filter(row => String(row.id) !== String(id))); await fetchPaginatedData(paginationModel.page); } }; @@ -381,11 +446,8 @@ export default function MeasurementSummaryGrid(props: Readonly { if (locked) { - console.log('rowCount: ', rowCount); return; } - console.log('handleAddNewRow triggered'); - const newRowCount = rowCount + 1; const calculatedNewLastPage = Math.ceil(newRowCount / paginationModel.pageSize) - 1; const existingLastPage = Math.ceil(rowCount / paginationModel.pageSize) - 1; @@ -396,7 +458,7 @@ export default function MeasurementSummaryGrid(props: Readonly { setRefresh(true); console.log('fetchPaginatedData triggered'); - let paginatedQuery = createFetchQuery( + const paginatedQuery = createFetchQuery( currentSite?.schemaName ?? '', 'measurementssummaryview', pageToFetch, @@ -426,7 +488,33 @@ export default function MeasurementSummaryGrid(props: Readonly 0 ? data.output : []); + if (data.deprecated) setDeprecatedRows(data.deprecated); + + // Apply date range filtering with UTC handling + const filteredRows = data.output.filter((row: any) => { + if (selectedDateRanges.length === 0) { + // If no date ranges are selected, show no rows + return false; + } + return selectedDateRanges.some(id => { + const range = currentCensus?.dateRanges.find(r => r.censusID === id); + const measurementDate = moment.utc(row.measurementDate); + if (range) { + const startDate = moment.utc(range.startDate); + const endDate = moment.utc(range.endDate); + console.log('measurementDate:', measurementDate.toISOString()); + console.log('range startDate:', startDate.toISOString(), 'endDate:', endDate.toISOString()); + return measurementDate.isBetween(startDate, endDate, undefined, '[]'); + } + return false; + }); + }); + console.log('filtered rows: ', filteredRows); + + // Sort rows by measurementDate before setting them + const sortedRows = sortRowsByMeasurementDate(filteredRows, sortModel[0]?.sort || 'asc'); + + setRows(sortedRows.length > 0 ? sortedRows : []); setRowCount(data.totalCount); if (isNewRowAdded && pageToFetch === newLastPage) { @@ -442,98 +530,26 @@ export default function MeasurementSummaryGrid(props: Readonly { - if (!isNewRowAdded) { - fetchPaginatedData(paginationModel.page).catch(console.error); + const processRowUpdate = useCallback((newRow: GridRowModel, oldRow: GridRowModel) => new Promise((resolve, reject) => { + setLoading(true, "Processing changes..."); + if (newRow.id === '') { + setLoading(false); + return reject(new Error('Primary key id cannot be empty!')); } - }, [paginationModel.page]); - - useEffect(() => { - if (currentPlot?.plotID || currentCensus?.plotCensusNumber) { - fetchPaginatedData(paginationModel.page).catch(console.error); - } - }, [currentPlot, currentCensus, paginationModel.page]); - - useEffect(() => { - if (refresh && currentSite) { - handleRefresh().then(() => { - setRefresh(false); - }); - } - }, [refresh, setRefresh]); - - const processRowUpdate = React.useCallback( - async ( - newRow: GridRowModel, - oldRow: GridRowModel - ): Promise => { - console.log( - 'Processing row update. Old row:', - oldRow, - '; New row:', - newRow - ); - - const gridID = getGridID('measurementssummaryview'); - const fetchProcessQuery = createPostPatchQuery( - currentSite?.schemaName ?? '', - 'measurementssummaryview', - gridID - ); - - if (newRow[gridID] === '') { - throw new Error(`Primary key ${gridID} cannot be empty!`); - } - - try { - let response, responseJSON; - if (oldRow.isNew) { - response = await fetch(fetchProcessQuery, { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ oldRow: oldRow, newRow: newRow }) - }); - responseJSON = await response.json(); - if (response.status > 299 || response.status < 200) - throw new Error(responseJSON.message || 'Insertion failed'); - setSnackbar({ children: `New row added!`, severity: 'success' }); - } else { - const mutation = computeMutation('measurementssummaryview', newRow, oldRow); - if (mutation) { - response = await fetch(fetchProcessQuery, { - method: 'PATCH', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ oldRow: oldRow, newRow: newRow }) - }); - responseJSON = await response.json(); - if (response.status > 299 || response.status < 200) - throw new Error(responseJSON.message || 'Update failed'); - setSnackbar({ children: `Row updated!`, severity: 'success' }); - } - } - // if (['attributes', 'personnel', 'species', 'quadrats', 'subquadrats'].includes('measurementssummaryview')) triggerRefresh(['measurementssummaryview' as keyof RefreshFixedDataFlags]); - if (oldRow.isNew) { - setIsNewRowAdded(false); - setShouldAddRowAfterFetch(false); - await fetchPaginatedData(paginationModel.page); - } - return newRow; - } catch (error: any) { - setSnackbar({ children: error.message, severity: 'error' }); - throw error; - } - }, - [ - setSnackbar, - setIsNewRowAdded, - fetchPaginatedData, - paginationModel.page - ] - ); + setPromiseArguments({ resolve, reject, newRow, oldRow }); + setLoading(false); + }), [currentSite?.schemaName, setSnackbar, setIsNewRowAdded, setShouldAddRowAfterFetch, fetchPaginatedData, paginationModel]); const handleRowModesModelChange = (newRowModesModel: GridRowModesModel) => { setRowModesModel(newRowModesModel); + + const rowInEditMode = Object.entries(newRowModesModel).find(([id, mode]) => mode.mode === GridRowModes.Edit); + if (rowInEditMode) { + const [id] = rowInEditMode; + const row = rows.find(row => String(row.id) === String(id)); + console.log('handleRowModesModelChange triggered on row: ', row); + } }; const handleCloseSnackbar = () => setSnackbar(null); @@ -559,12 +575,10 @@ export default function MeasurementSummaryGrid(props: Readonly { if (locked) return; event?.preventDefault(); - const row = rows.find(r => r.id === id); if (row?.isNew) { setRows(oldRows => oldRows.filter(row => row.id !== id)); setIsNewRowAdded(false); - if (rowCount % paginationModel.pageSize === 1 && isNewRowAdded) { const newPage = paginationModel.page - 1 >= 0 ? paginationModel.page - 1 : 0; @@ -579,6 +593,33 @@ export default function MeasurementSummaryGrid(props: Readonly { + return ( + + + { + if (locked) { + handleLockedClick(); + const iconElement = e.currentTarget.querySelector('svg'); + if (iconElement) { + iconElement.classList.add('animate-shake'); + setTimeout(() => { + iconElement.classList.remove('animate-shake'); + }, 500); + } + } else { + onClick(); + } + }} + > + + + + + ); + }; + function getGridActionsColumn(): GridColDef { return { field: 'actions', @@ -587,41 +628,20 @@ export default function MeasurementSummaryGrid(props: Readonly { - if (locked) return []; const isInEditMode = rowModesModel[id]?.mode === 'edit'; if (isInEditMode) { return [ - } - label='Save' - key={'save'} - onClick={handleSaveClick(id)} - />, - } - label='Cancel' - key={'cancel'} - onClick={event => handleCancelClick(id, event)} - /> + getEnhancedCellAction('Save', , handleSaveClick(id)), + getEnhancedCellAction('Cancel', , (e: any) => handleCancelClick(id, e)), ]; } return [ - } - label='Edit' - key={'edit'} - onClick={handleEditClick(id)} - />, - } - label='Delete' - key={'delete'} - onClick={handleDeleteClick(id)} - /> + getEnhancedCellAction('Edit', , handleEditClick(id)), + getEnhancedCellAction('Delete', , handleDeleteClick(id)), ]; - } + }, }; } @@ -641,28 +661,13 @@ export default function MeasurementSummaryGrid(props: Readonly { - fetchValidationErrors() - .catch(console.error) - .then(() => setRefresh(false)); - }, [refresh]); - - const cellHasError = (colField: string, rowId: GridRowId) => { - const error = validationErrors[Number(rowId)]; - if (!error) return false; - const errorFields = error.ValidationErrorIDs.flatMap( - id => errorMapping[id.toString()] || [] - ); - return errorFields.includes(colField); - }; - const getCellErrorMessages = (colField: string, rowId: GridRowId) => { const error = validationErrors[Number(rowId)]; if (!error) return ''; @@ -730,52 +735,138 @@ export default function MeasurementSummaryGrid(props: Readonly { + const rowId = params.row.id; + const isDeprecated = deprecatedRows.some(row => row.id === rowId); + const validationError = validationErrors[Number(rowId)]; + const isPendingValidation = !params.row.isValidated && !validationError; + const isValidated = params.row.isValidated; + + if (isDeprecated) { + return ( + + + + ); + } else if (validationError) { + return ( + + + + ); + } else if (isPendingValidation) { + return ( + + + + ); + } else if (isValidated) { + return ( + + + + ); + } else { + return null; + } + }, + }; + const measurementDateColumn: GridColDef = { + field: 'measurementDate', + headerName: 'Date', + headerClassName: 'header', + flex: 0.8, + sortable: true, + editable: true, + type: 'date', + renderHeader: () => + Date + YYYY-MM-DD + , + valueFormatter: (value) => { + console.log('params: ', value); + // Check if the date is present and valid + if (!value || !moment(value).utc().isValid()) { + console.log('value: ', value); + console.log('moment-converted: ', moment(value).utc().format('YYYY-MM-DD')); + return ''; + } + // Format the date as a dash-separated set of numbers + return moment(value).utc().format('YYYY-MM-DD'); + }, + }; + const stemUnitsColumn: GridColDef = { + field: 'stemUnits', + headerName: 'U', + headerClassName: 'header', + flex: 0.4, + renderHeader: () => U, + align: 'left', + editable: true, + type: 'singleSelect', + valueOptions: unitSelectionOptions + }; + const dbhUnitsColumn: GridColDef = { + field: 'dbhUnits', + headerName: 'U', + headerClassName: 'header', + flex: 0.4, + renderHeader: () => U, + align: 'left', + editable: true, + type: 'singleSelect', + valueOptions: unitSelectionOptions + }; + const homUnitsColumn: GridColDef = { + field: 'homUnits', + headerName: 'U', + headerClassName: 'header', + flex: 0.4, + renderHeader: () => U, + align: 'left', + editable: true, + type: 'singleSelect', + valueOptions: unitSelectionOptions + }; + const columns = useMemo(() => { const commonColumns = modifiedColumns; if (locked) { - return commonColumns; + return [validationStatusColumn, measurementDateColumn, ...commonColumns, ...b, dbhUnitsColumn, ...c, homUnitsColumn, ...d]; } - return [...commonColumns, getGridActionsColumn()]; + return [validationStatusColumn, + measurementDateColumn, ...commonColumns, stemUnitsColumn, ...b, dbhUnitsColumn, ...c, homUnitsColumn, ...d, getGridActionsColumn()]; }, [modifiedColumns, locked]); const filteredColumns = useMemo(() => filterColumns(rows, columns), [rows, columns]); - const rowHasError = (rowId: GridRowId) => { - if (!rows || rows.length === 0) return false; - return gridColumns.some(column => cellHasError(column.field, rowId)); - }; - const visibleRows = useMemo(() => { - if (showErrorRows && showValidRows) { - console.log('Showing all rows, including those with errors.'); - return rows; - } else if (showValidRows && !showErrorRows) { - console.log('Filtering out rows with errors.'); - return rows.filter(row => !rowHasError(row.id)); - } else if (!showValidRows && showErrorRows) { - return rows.filter(row => rowHasError(row.id)); - } else { - return []; + let filteredRows = rows; + if (!showValidRows) { + filteredRows = filteredRows.filter(row => rowHasError(row.id)); } - }, [rows, showErrorRows, showValidRows, gridColumns]); - - const errorRowCount = useMemo(() => { - return rows.filter(row => rowHasError(row.id)).length; - }, [rows, gridColumns]); - - useEffect(() => { - if (errorRowCount > 0) { - setSnackbar({ - children: `${errorRowCount} row(s) with validation errors detected.`, - severity: 'warning' - }); + if (!showErrorRows) { + filteredRows = filteredRows.filter(row => !rowHasError(row.id)); } - }, [errorRowCount]); - - const getRowClassName = (params: GridRowParams) => { - if (!params.row.isValidated) { - if (rowHasError(params.id)) return 'error-row'; - else return 'pending-validation'; + if (!showDeprecatedRows) { + filteredRows = filteredRows.filter(row => !rowIsDeprecated(row.id)); + } + return filteredRows; + }, [rows, showErrorRows, showValidRows, showDeprecatedRows]); + + const getRowClassName = (params: any) => { + const rowId = params.id; + if (rowIsDeprecated(rowId)) { + return 'deprecated'; + } else if (rowHasError(rowId)) { + return 'error-row'; } else { return 'validated'; } @@ -816,7 +907,7 @@ export default function MeasurementSummaryGrid(props: Readonly - + Show rows without errors: ({rows.length - errorRowCount}) + + + Show deprecated rows: ({deprecatedRows.length}) + - - - - - - - Note: The Grid is filtered by your selected Plot and Plot ID - + + Filtering — + Select or deselect filters to filter by date ranges within a census + + {renderDateRangeFilters(getDateRangesForCensus(currentCensus))} 'auto'} getRowClassName={getRowClassName} + isCellEditable={() => !locked} /> {!!snackbar && ( @@ -886,40 +997,25 @@ export default function MeasurementSummaryGrid(props: Readonly )} - + {isDialogOpen && promiseArguments && ( + + )} + {isDeleteDialogOpen && ( + + )}
); } -} - -// ConfirmationDialog component with TypeScript types -const ConfirmationDialog: React.FC = (props) => { - const { isOpen, onConfirm, onCancel, message } = props; - return ( - - Confirm Action - - - {message} - - - - - - - - ); -}; +} \ No newline at end of file diff --git a/frontend/components/datagrids/reentrydatamodal.tsx b/frontend/components/datagrids/reentrydatamodal.tsx new file mode 100644 index 00000000..272f090c --- /dev/null +++ b/frontend/components/datagrids/reentrydatamodal.tsx @@ -0,0 +1,139 @@ +"use client"; +import React, { useState, useEffect } from 'react'; +import { GridRowModel, GridColDef } from "@mui/x-data-grid"; +import moment from 'moment'; +import { unitSelectionOptions } from '@/config/macros'; +import { AttributeStatusOptions } from '@/config/sqlrdsdefinitions/tables/attributerds'; +import { Button, DialogActions, DialogContent, DialogTitle, FormControl, FormLabel, Input, Modal, ModalDialog, Option, Select, Stack } from '@mui/joy'; +import { DatePicker } from '@mui/x-date-pickers'; + +interface ReEnterDataModalProps { + row: GridRowModel; + reEnterData: GridRowModel | null; + handleClose: () => void; + handleSave: () => void; + columns: GridColDef[]; +} + +const ReEnterDataModal: React.FC = ({ + row, + reEnterData, + handleClose, + handleSave, + columns, +}) => { + const [localData, setLocalData] = useState({ ...reEnterData }); + + useEffect(() => { + console.log('row: ', row); + console.log('reEnterData: ', reEnterData); + const initialData = { ...row }; + columns.forEach((column) => { + const { field, editable } = column; + if (editable && initialData[field] !== localData[field]) { + initialData[field] = ''; + } + }); + setLocalData({ ...initialData }); + }, [row, reEnterData, columns]); + + const handleInputChange = (field: string, value: any) => { + setLocalData((prevData) => ({ + ...prevData, + [field]: value, + })); + }; + + const validateData = () => { + if (!reEnterData) return false; + const allFieldsMatch = Object.keys(reEnterData).every((field) => reEnterData[field] === localData[field]); + return allFieldsMatch; + }; + + const handleConfirm = () => { + if (validateData()) { + handleSave(); + } else { + alert("Values do not match. Please re-enter correctly."); + } + }; + + return ( + + + Confirm Changes + + + {columns.map((column) => { + const { field, type, editable } = column; + const value = localData[field]; + if (!editable) { + return null; + } + if (type === 'singleSelect') { + const valueOptions = field !== 'status' ? unitSelectionOptions : AttributeStatusOptions; + return ( + + {column.headerName} + + + ); + } + if (type === 'date') { + return ( + + {column.headerName} + { + if (newValue) handleInputChange(field, moment(newValue).utc().format('YYYY-MM-DD')); + }} + /> + + ); + } + return ( + + {column.headerName} + handleInputChange(field, e.target.value)} + fullWidth + /> + + ); + })} + + + + + + + + + + ); +}; + +export default ReEnterDataModal; diff --git a/frontend/components/datagrids/usedatagridcommons.ts b/frontend/components/datagrids/usedatagridcommons.ts new file mode 100644 index 00000000..b184b5e3 --- /dev/null +++ b/frontend/components/datagrids/usedatagridcommons.ts @@ -0,0 +1,149 @@ +import { useState, useCallback } from 'react'; +import { GridRowModel, GridRowId } from '@mui/x-data-grid'; +import { createFetchQuery } from '@/config/datagridhelpers'; +import { DataGridCommonProps, PendingAction } from './datagridmacros'; +import { Site } from '@/config/sqlrdsdefinitions/tables/sitesrds'; +import { Plot } from '@/config/sqlrdsdefinitions/tables/plotrds'; +import { OrgCensus } from '@/config/sqlrdsdefinitions/orgcensusrds'; +import { Quadrat } from '@/config/sqlrdsdefinitions/tables/quadratrds'; + +export interface DataGridCommonHookProps { + currentSite: Site; + currentPlot: Plot; + currentCensus: OrgCensus; + currentQuadrat: Quadrat; +} + +export const useDataGridCommons = ( + props: DataGridCommonProps & { setLoading: (loading: boolean, message?: string) => void } & DataGridCommonHookProps +) => { + const { + gridType, + currentQuadrat, + rowCount, + currentSite, + currentPlot, + currentCensus, + setSnackbar, + setIsNewRowAdded, + paginationModel, + setPaginationModel, + setLoading, + setRows, + setRowCount, + isNewRowAdded, + setShouldAddRowAfterFetch, + addNewRowToGrid, + } = props; + + const [newLastPage, setNewLastPage] = useState(null); + const [isDialogOpen, setIsDialogOpen] = useState(false); + const [isDeleteDialogOpen, setIsDeleteDialogOpen] = useState(false); + const [locked, setLocked] = useState(false); + const [pendingAction, setPendingAction] = useState({ actionType: '', actionId: null }); + const [promiseArguments, setPromiseArguments] = useState<{ + resolve: (value: GridRowModel) => void; + reject: (reason?: any) => void; + newRow: GridRowModel; + oldRow: GridRowModel; + } | null>(null); + + const openConfirmationDialog = (actionType: 'save' | 'delete', actionId: GridRowId) => { + setPendingAction({ actionType, actionId }); + setIsDialogOpen(true); + }; + + const handleSaveClick = (id: GridRowId) => () => { + if (locked) return; + openConfirmationDialog('save', id); + }; + + const handleDeleteClick = (id: GridRowId) => () => { + if (locked) return; + openConfirmationDialog('delete', id); + }; + + const handleAddNewRow = async () => { + if (locked) return; + + const newRowCount = rowCount + 1; + const calculatedNewLastPage = Math.ceil(newRowCount / paginationModel.pageSize) - 1; + const existingLastPage = Math.ceil(rowCount / paginationModel.pageSize) - 1; + const isNewPageNeeded = newRowCount % paginationModel.pageSize === 1; + + setIsNewRowAdded(true); + setShouldAddRowAfterFetch(isNewPageNeeded); + setNewLastPage(calculatedNewLastPage); + + if (isNewPageNeeded) { + setPaginationModel({ ...paginationModel, page: calculatedNewLastPage }); + addNewRowToGrid(); + } else { + setPaginationModel({ ...paginationModel, page: existingLastPage }); + addNewRowToGrid(); + } + }; + + const fetchPaginatedData = async (pageToFetch: number) => { + setLoading(true, "Loading data..."); + const paginatedQuery = createFetchQuery( + currentSite?.schemaName ?? '', + gridType, + pageToFetch, + paginationModel.pageSize, + currentPlot?.plotID, + currentCensus?.plotCensusNumber, + currentQuadrat?.quadratID + ); + try { + const response = await fetch(paginatedQuery, { method: 'GET' }); + const data = await response.json(); + if (!response.ok) throw new Error(data.message || 'Error fetching data'); + setRows(data.output); + setRowCount(data.totalCount); + + if (isNewRowAdded && pageToFetch === newLastPage) { + addNewRowToGrid(); + setIsNewRowAdded(false); + } + } catch (error) { + console.error('Error fetching data:', error); + setSnackbar({ children: 'Error fetching data', severity: 'error' }); + } finally { + setLoading(false); + } + }; + + const processRowUpdate = useCallback( + (newRow: GridRowModel, oldRow: GridRowModel) => + new Promise((resolve, reject) => { + setLoading(true, "Processing changes..."); + if (newRow.id === '') { + setLoading(false); + return reject(new Error('Primary key id cannot be empty!')); + } + + setPromiseArguments({ resolve, reject, newRow, oldRow }); + setLoading(false); + }), + [gridType, currentSite?.schemaName, setSnackbar, setIsNewRowAdded, setShouldAddRowAfterFetch, fetchPaginatedData, paginationModel] + ); + + return { + handleSaveClick, + handleDeleteClick, + handleAddNewRow, + fetchPaginatedData, + processRowUpdate, + isDialogOpen, + setIsDialogOpen, + isDeleteDialogOpen, + setIsDeleteDialogOpen, + locked, + setLocked, + pendingAction, + setPendingAction, + promiseArguments, + setPromiseArguments + }; +}; diff --git a/frontend/components/deprecated/quadrats_dep.tsx b/frontend/components/deprecated/quadrats_dep.tsx new file mode 100644 index 00000000..65a09771 --- /dev/null +++ b/frontend/components/deprecated/quadrats_dep.tsx @@ -0,0 +1,276 @@ +// "use client"; +// import {GridColDef, GridRowModes, GridRowModesModel, GridRowsProp} from "@mui/x-data-grid"; +// import {AlertProps} from "@mui/material"; +// import React, {useCallback, useEffect, useState} from "react"; +// import {QuadratsGridColumns as BaseQuadratsGridColumns, Quadrat} from '@/config/sqlrdsdefinitions/tables/quadratrds'; +// import { +// useOrgCensusContext, +// usePlotContext, +// useQuadratDispatch, +// } from "@/app/contexts/userselectionprovider"; +// import {randomId} from "@mui/x-data-grid-generator"; +// import DataGridCommons from "@/components/datagrids/datagridcommons"; +// import {Box, Button, Typography} from "@mui/joy"; +// import {useSession} from "next-auth/react"; +// import UploadParentModal from "@/components/uploadsystemhelpers/uploadparentmodal"; + +// export default function QuadratsDataGrid() { +// const initialRows: GridRowsProp = [ +// { +// id: 0, +// quadratID: 0, +// plotID: 0, +// censusID: 0, +// quadratName: '', +// dimensionX: 0, +// dimensionY: 0, +// area: 0, +// unit: '', +// quadratShape: '', +// }, +// ]; +// const [rows, setRows] = React.useState(initialRows); +// const [rowCount, setRowCount] = useState(0); // total number of rows +// const [rowModesModel, setRowModesModel] = React.useState({}); +// const [locked, setLocked] = useState(false); +// const [snackbar, setSnackbar] = React.useState | null>(null); +// const [refresh, setRefresh] = useState(false); +// const [paginationModel, setPaginationModel] = useState({ +// page: 0, +// pageSize: 10, +// }); +// const [isNewRowAdded, setIsNewRowAdded] = useState(false); +// const [shouldAddRowAfterFetch, setShouldAddRowAfterFetch] = useState(false); +// // const [censusOptions, setCensusOptions] = useState([]); +// const {data: session} = useSession(); +// const [isUploadModalOpen, setIsUploadModalOpen] = useState(false); +// const [uploadFormType, setUploadFormType] = useState<'quadrats' | 'subquadrats'>('quadrats'); + +// const currentPlot = usePlotContext(); +// const currentCensus = useOrgCensusContext(); +// const quadratDispatch = useQuadratDispatch(); + +// useEffect(() => { +// if (currentCensus !== undefined) { +// setLocked(currentCensus.dateRanges[0].endDate !== undefined); // if the end date is not undefined, then grid should be locked +// } +// }, [currentCensus]); + +// const handleSelectQuadrat = useCallback((quadratID: number | null) => { +// // we want to select a quadrat contextually when using this grid FOR subquadrats selection +// // however, this information should not be retained, as the user might select a different quadrat or change quadrat information +// // thus, we add the `| null` to the function and ensure that the context is properly reset when the user is done making changes or cancels their changes. +// if (quadratID === null) quadratDispatch && quadratDispatch({quadrat: undefined}).catch(console.error); // dispatches are asynchronous +// else { +// const selectedQuadrat = rows.find(row => row.quadratID === quadratID) as Quadrat; // GridValidRowModel needs to be cast to Quadrat +// if (selectedQuadrat && quadratDispatch) quadratDispatch({quadrat: selectedQuadrat}).catch(console.error); +// } +// }, [rows, quadratDispatch]); + +// const addNewRowToGrid = () => { +// const id = randomId(); +// const nextQuadratID = (rows.length > 0 +// ? rows.reduce((max, row) => Math.max(row.quadratID, max), 0) +// : 0) + 1; +// const newRow = { +// id: id, +// quadratID: nextQuadratID, +// plotID: currentPlot ? currentPlot.id : 0, +// censusID: currentCensus ? currentCensus.dateRanges[0].censusID : 0, +// quadratName: '', +// dimensionX: 0, +// dimensionY: 0, +// area: 0, +// unit: '', +// quadratShape: '', +// isNew: true, +// }; +// // Add the new row to the state +// setRows(oldRows => [...oldRows, newRow]); +// // Set editing mode for the new row +// setRowModesModel(oldModel => ({ +// ...oldModel, +// [id]: {mode: GridRowModes.Edit, fieldToFocus: 'quadratName'}, +// })); +// }; + +// // const updatePersonnelInRows = useCallback((id: GridRowId, newPersonnel: PersonnelRDS[]) => { +// // setRows(rows => rows.map(row => +// // row.id === id +// // ? {...row, personnel: newPersonnel.map(person => ({...person}))} +// // : row +// // )); +// // }, []); + +// // const handlePersonnelChange = useCallback( +// // async (rowId: GridRowId, selectedPersonnel: PersonnelRDS[]) => { +// // console.log(rows); +// // const row = rows.find((row) => row.id === rowId); +// // const quadratId = row?.quadratID; +// // const personnelIds = selectedPersonnel.map(person => person.personnelID); +// // console.log('new personnel ids: ', personnelIds); + +// // // Check if quadratID is valid and not equal to the initial row's quadratID +// // if (quadratId === undefined || quadratId === initialRows[0].quadratID) { +// // console.error("Invalid quadratID, personnel update skipped."); +// // setSnackbar({children: "Personnel update skipped due to invalid quadratID.", severity: 'error'}); +// // return; +// // } + +// // try { +// // const response = await fetch(`/api/formsearch/personnelblock?quadratID=${quadratId}&schema=${currentSite?.schemaName ?? ''}`, { +// // method: 'PUT', +// // headers: { +// // 'Content-Type': 'application/json' +// // }, +// // body: JSON.stringify(personnelIds) +// // }); + +// // if (!response.ok) { +// // setSnackbar({children: `Personnel updates failed!`, severity: 'error'}); +// // throw new Error('Failed to update personnel'); +// // } + +// // // Handle successful response +// // const responseData = await response.json(); +// // updatePersonnelInRows(rowId, selectedPersonnel); +// // setRefresh(true); +// // setSnackbar({children: `${responseData.message}`, severity: 'success'}); +// // } catch (error) { +// // console.error("Error updating personnel:", error); +// // } +// // }, +// // [rows, currentSite?.schemaName, setSnackbar, setRefresh, updatePersonnelInRows] +// // ); + + +// const quadratsGridColumns: GridColDef[] = [...BaseQuadratsGridColumns, +// // { +// // field: 'personnel', +// // headerName: 'Personnel', +// // flex: 1, +// // renderCell: (params) => ( +// // handlePersonnelChange(params.id, newPersonnel)} +// // locked={!rowModesModel[params.id] || rowModesModel[params.id].mode !== GridRowModes.Edit} +// // /> +// // ), +// // }, +// // { +// // field: 'subquadrats', +// // headerName: 'Subquadrats', +// // flex: 1, +// // renderCell: (params) => ( +// // +// // +// // +// // ), +// // } +// ]; + +// return ( +// <> +// +// +// +// {session?.user.isAdmin && ( +// +// Note: ADMINISTRATOR VIEW +// +// )} +// +// Note: This is a locked view and will not allow modification. +// +// +// Please use this view as a way to confirm changes made to measurements. +// +// + +// {/* Upload Button */} +// +// {/* */} +// +// +// { +// setIsUploadModalOpen(false); +// setRefresh(true); +// }} formType={uploadFormType}/> +// {/* { +// }} +// aria-labelledby="upload-dialog-title" +// sx={{display: 'flex', alignItems: 'center', justifyContent: 'center'}} +// > +// +// setIsSubquadratDialogOpen(false)} +// sx={{position: 'absolute', top: 8, right: 8}} +// > +// +// +// +// +// */} +// +// +// ); +// } \ No newline at end of file diff --git a/frontend/components/sidebar_deprecated.tsx b/frontend/components/deprecated/sidebar_deprecated.tsx similarity index 79% rename from frontend/components/sidebar_deprecated.tsx rename to frontend/components/deprecated/sidebar_deprecated.tsx index 6133a864..a9f781ac 100644 --- a/frontend/components/sidebar_deprecated.tsx +++ b/frontend/components/deprecated/sidebar_deprecated.tsx @@ -1,20 +1,20 @@ "use client"; import * as React from 'react'; -import { Dispatch, SetStateAction, useEffect, useState } from 'react'; +import {Dispatch, SetStateAction, useEffect, useState} from 'react'; import GlobalStyles from '@mui/joy/GlobalStyles'; import Box from '@mui/joy/Box'; import Divider from '@mui/joy/Divider'; import List from '@mui/joy/List'; import ListItem from '@mui/joy/ListItem'; -import ListItemButton, { listItemButtonClasses } from '@mui/joy/ListItemButton'; +import ListItemButton, {listItemButtonClasses} from '@mui/joy/ListItemButton'; import ListItemContent from '@mui/joy/ListItemContent'; import Typography from '@mui/joy/Typography'; import KeyboardArrowDownIcon from '@mui/icons-material/KeyboardArrowDown'; -import { LoginLogout } from "@/components/loginlogout"; -import { siteConfigNav, validityMapping } from "@/config/macros/siteconfigs"; -import { SiteConfigProps } from "@/config/macros/siteconfigs"; -import { Site } from "@/config/sqlrdsdefinitions/tables/sitesrds"; -import { Plot } from "@/config/sqlrdsdefinitions/tables/plotrds"; +import {LoginLogout} from "@/components/loginlogout"; +import {siteConfigNav, validityMapping} from "@/config/macros/siteconfigs"; +import {SiteConfigProps} from "@/config/macros/siteconfigs"; +import {Site} from "@/config/sqlrdsdefinitions/tables/sitesrds"; +import {Plot} from "@/config/sqlrdsdefinitions/tables/plotrds"; import { useOrgCensusContext, useOrgCensusDispatch, @@ -23,7 +23,7 @@ import { useSiteContext, useSiteDispatch } from "@/app/contexts/userselectionprovider"; -import { usePathname, useRouter } from "next/navigation"; +import {usePathname, useRouter} from "next/navigation"; import { Button, DialogActions, @@ -40,17 +40,21 @@ import { import WarningRoundedIcon from "@mui/icons-material/WarningRounded"; import Select from "@mui/joy/Select"; import Option from '@mui/joy/Option'; -import { useOrgCensusListContext, useOrgCensusListDispatch, usePlotListContext, useSiteListContext } from "@/app/contexts/listselectionprovider"; -import { getData, setData } from "@/config/db"; -import { useSession } from "next-auth/react"; -import { SlideToggle, TransitionComponent } from "@/components/client/clientmacros"; +import { + useOrgCensusListContext, + useOrgCensusListDispatch, + usePlotListContext, + useSiteListContext +} from "@/app/contexts/listselectionprovider"; +import {useSession} from "next-auth/react"; +import {SlideToggle, TransitionComponent} from "@/components/client/clientmacros"; import ListDivider from "@mui/joy/ListDivider"; import TravelExploreIcon from '@mui/icons-material/TravelExplore'; import Avatar from "@mui/joy/Avatar"; -import { CensusLogo, PlotLogo } from "@/components/icons"; -import { RainbowIcon } from '@/styles/rainbowicon'; -import { useDataValidityContext } from '@/app/contexts/datavalidityprovider'; -import { OrgCensus, OrgCensusRDS, OrgCensusToCensusResultMapper } from '@/config/sqlrdsdefinitions/orgcensusrds'; +import {CensusLogo, PlotLogo} from "@/components/icons"; +import {RainbowIcon} from '@/styles/rainbowicon'; +import {useDataValidityContext} from '@/app/contexts/datavalidityprovider'; +import {OrgCensus, OrgCensusRDS, OrgCensusToCensusResultMapper} from '@/config/sqlrdsdefinitions/orgcensusrds'; export interface SimpleTogglerProps { isOpen: boolean; @@ -58,7 +62,7 @@ export interface SimpleTogglerProps { renderToggle: any; } -export function SimpleToggler({ isOpen, renderToggle, children, }: Readonly) { +export function SimpleToggler({isOpen, renderToggle, children,}: Readonly) { return ( {renderToggle} @@ -87,10 +91,10 @@ interface MRTProps { function MenuRenderToggle(props: MRTProps, siteConfigProps: SiteConfigProps, menuOpen: boolean | undefined, setMenuOpen: Dispatch> | undefined) { const Icon = siteConfigProps.icon; - const { plotSelectionRequired, censusSelectionRequired, pathname, isParentDataIncomplete } = props; - let currentSite = useSiteContext(); - let currentPlot = usePlotContext(); - let currentCensus = useOrgCensusContext(); + const {plotSelectionRequired, censusSelectionRequired, pathname, isParentDataIncomplete} = props; + const currentSite = useSiteContext(); + const currentPlot = usePlotContext(); + const currentCensus = useOrgCensusContext(); return ( - + {siteConfigProps.label} ); @@ -128,18 +132,18 @@ interface SidebarProps { } export default function SidebarDeprecated(props: SidebarProps) { - const { data: session } = useSession(); - let currentSite = useSiteContext(); - let siteDispatch = useSiteDispatch(); - let currentPlot = usePlotContext(); - let plotDispatch = usePlotDispatch(); - let currentCensus = useOrgCensusContext(); - let censusDispatch = useOrgCensusDispatch(); - let censusListContext = useOrgCensusListContext(); - let censusListDispatch = useOrgCensusListDispatch(); - let siteListContext = useSiteListContext(); - let plotListContext = usePlotListContext(); - const { validity } = useDataValidityContext(); + const {data: session} = useSession(); + const currentSite = useSiteContext(); + const siteDispatch = useSiteDispatch(); + const currentPlot = usePlotContext(); + const plotDispatch = usePlotDispatch(); + const currentCensus = useOrgCensusContext(); + const censusDispatch = useOrgCensusDispatch(); + const censusListContext = useOrgCensusListContext(); + const censusListDispatch = useOrgCensusListDispatch(); + const siteListContext = useSiteListContext(); + const plotListContext = usePlotListContext(); + const {validity} = useDataValidityContext(); const isAllValiditiesTrue = Object.values(validity).every(Boolean); const [plot, setPlot] = useState(currentPlot); @@ -161,7 +165,7 @@ export default function SidebarDeprecated(props: SidebarProps) { const [isPlotSelectionRequired, setIsPlotSelectionRequired] = useState(true); const [isCensusSelectionRequired, setIsCensusSelectionRequired] = useState(true); - const { coreDataLoaded, setManualReset, siteListLoaded, setCensusListLoaded } = props; + const {coreDataLoaded, setManualReset, siteListLoaded, setCensusListLoaded} = props; const [openSiteSelectionModal, setOpenSiteSelectionModal] = useState(false); const [openPlotSelectionModal, setOpenPlotSelectionModal] = useState(false); @@ -252,7 +256,7 @@ export default function SidebarDeprecated(props: SidebarProps) { const handleSiteSelection = async (selectedSite: Site | undefined) => { setSite(selectedSite); if (siteDispatch) { - await siteDispatch({ site: selectedSite }); + await siteDispatch({site: selectedSite}); } if (selectedSite === undefined) { await handlePlotSelection(undefined); @@ -262,7 +266,7 @@ export default function SidebarDeprecated(props: SidebarProps) { const handlePlotSelection = async (selectedPlot: Plot) => { setPlot(selectedPlot); if (plotDispatch) { - await plotDispatch({ plot: selectedPlot }); + await plotDispatch({plot: selectedPlot}); } if (selectedPlot === undefined) { await handleCensusSelection(undefined); @@ -272,7 +276,7 @@ export default function SidebarDeprecated(props: SidebarProps) { const handleCensusSelection = async (selectedCensus: OrgCensus) => { setCensus(selectedCensus); if (censusDispatch) { - await censusDispatch({ census: selectedCensus }); + await censusDispatch({census: selectedCensus}); } }; @@ -312,10 +316,10 @@ export default function SidebarDeprecated(props: SidebarProps) { type ToggleArray = ToggleObject[]; const toggleArray: ToggleArray = [ - { toggle: undefined, setToggle: undefined }, - { toggle: measurementsToggle, setToggle: setMeasurementsToggle }, - { toggle: propertiesToggle, setToggle: setPropertiesToggle }, - { toggle: formsToggle, setToggle: setFormsToggle } + {toggle: undefined, setToggle: undefined}, + {toggle: measurementsToggle, setToggle: setMeasurementsToggle}, + {toggle: propertiesToggle, setToggle: setPropertiesToggle}, + {toggle: formsToggle, setToggle: setFormsToggle} ]; const renderSiteOptions = () => { @@ -349,8 +353,9 @@ export default function SidebarDeprecated(props: SidebarProps) { - - + + Allowed Sites ({allowedSites?.length}) @@ -362,8 +367,9 @@ export default function SidebarDeprecated(props: SidebarProps) { ))} - - + + Other Sites ({otherSites?.length}) @@ -381,7 +387,7 @@ export default function SidebarDeprecated(props: SidebarProps) { return ( <> - + - - + + - - - + + + ForestGEO {session?.user.isAdmin && ( - (Admin) + (Admin) )} - + { if (siteListLoaded) { setOpenSiteSelectionModal(true); @@ -436,7 +442,7 @@ export default function SidebarDeprecated(props: SidebarProps) { display: 'flex', alignItems: 'center', paddingBottom: '0.25em', width: '100%', textAlign: 'left' }}> - + - + + level="h3" + sx={{marginLeft: 1, display: 'flex', flexGrow: 1}}> {plot !== undefined ? `Plot: ${plot.plotName}` : "Select Plot"} - + { setOpenCensusSelectionModal(true); }} - sx={{ display: 'flex', alignItems: 'center', width: '100%', textAlign: 'left' }}> + sx={{display: 'flex', alignItems: 'center', width: '100%', textAlign: 'left'}}> - + + level="h4" + sx={{marginLeft: 1}}> {census !== undefined ? `Census: ${census.plotCensusNumber}` : 'Select Census'} + sx={{marginLeft: '2.5em'}}> + level="body-md" + sx={{textAlign: 'left', paddingLeft: '1em'}}> {(census !== undefined) && ( <>{(census.dateRanges[0]?.startDate) ? `Starting: ${new Date(census?.dateRanges[0]?.startDate).toDateString()}` : ''} )} + level="body-md" + sx={{textAlign: 'left', paddingLeft: '1em'}}> {(census !== undefined) && ( <>{(census.dateRanges[0]?.endDate) ? `Ending ${new Date(census.dateRanges[0]?.endDate).toDateString()}` : `Ongoing`} )} - + {siteConfigNav.map((item, index: number) => { const Icon = item.icon; - const { toggle, setToggle } = toggleArray[index]; + const {toggle, setToggle} = toggleArray[index]; const delay = (index) * 200; const getTooltipMessage = (href: string, isDataIncomplete: boolean) => { @@ -559,25 +565,25 @@ export default function SidebarDeprecated(props: SidebarProps) { return ( + style={{transitionDelay: `${delay}ms`}} direction="down"> - - { - if (!isLinkDisabled) { - router.push(item.href); - } - }}> + + { + if (!isLinkDisabled) { + router.push(item.href); + } + }}> - + {item.label} @@ -589,13 +595,13 @@ export default function SidebarDeprecated(props: SidebarProps) { ); } else { - let isParentDataIncomplete = item.expanded.some(subItem => { + const isParentDataIncomplete = item.expanded.some(subItem => { const dataKey = validityMapping[subItem.href]; return dataKey !== undefined && !validity[dataKey]; }); return ( + style={{transitionDelay: `${delay}ms`}} direction="down"> - + {item.expanded.map((link, subIndex) => { const SubIcon = link.icon; const delay = (subIndex + 1) * 200; - let dataValidityKey = validityMapping[link.href]; - let isDataIncomplete = dataValidityKey ? !validity[dataValidityKey] : false; + const dataValidityKey = validityMapping[link.href]; + const isDataIncomplete = dataValidityKey ? !validity[dataValidityKey] : false; const isLinkDisabled = getDisabledState(link.href); const tooltipMessage = getTooltipMessage(link.href, isDataIncomplete || (link.href === '/summary' && !isAllValiditiesTrue)); return ( - + style={{transitionDelay: `${delay}ms`}} direction="down"> + - - { - if (!isLinkDisabled) { - router.push((item.href + link.href)); - } - }}> + + { + if (!isLinkDisabled) { + router.push((item.href + link.href)); + } + }}> - + {link.label} @@ -658,39 +664,53 @@ export default function SidebarDeprecated(props: SidebarProps) { - - + + {census && census.dateRanges[0].endDate ? ( - - - ) : ( - - )} - - + + { setSite(currentSite); setOpenSiteSelectionModal(false); }}> - + Site Selection - - - + + + Select Site: {renderSiteOptions()} @@ -698,7 +718,7 @@ export default function SidebarDeprecated(props: SidebarProps) { - }> + }> @@ -912,18 +933,18 @@ export default function SidebarDeprecated(props: SidebarProps) { setOpenCloseCensusModal(false)}> - + Close Census - + Select an end date for the current census: - setCloseEndDate(new Date(e.target.value))} /> + setCloseEndDate(new Date(e.target.value))}/> - }> + }> @@ -935,7 +956,7 @@ export default function SidebarDeprecated(props: SidebarProps) { - + ); } diff --git a/frontend/components/forms/censusacinputform.tsx b/frontend/components/forms/censusacinputform.tsx index 581efd12..7427e57d 100644 --- a/frontend/components/forms/censusacinputform.tsx +++ b/frontend/components/forms/censusacinputform.tsx @@ -294,9 +294,9 @@ const CensusAutocompleteInputForm = () => { const [reviewState, setReviewState] = useState(ReviewStates.UPLOAD_SQL); // placeholder - let currentPlot = usePlotContext(); - let currentCensus = useOrgCensusContext(); - let currentSite = useSiteContext(); + const currentPlot = usePlotContext(); + const currentCensus = useOrgCensusContext(); + const currentSite = useSiteContext(); const {data: session} = useSession(); const handleQuadratChange = (id: number | string, newValue: string) => { diff --git a/frontend/components/forms/censusinlinevalidationform.tsx b/frontend/components/forms/censusinlinevalidationform.tsx index ac684340..cf35d3cb 100644 --- a/frontend/components/forms/censusinlinevalidationform.tsx +++ b/frontend/components/forms/censusinlinevalidationform.tsx @@ -276,9 +276,9 @@ const CensusAutocompleteInputForm = () => { const [validationErrors, setValidationErrors] = useState<{ [key: string]: string | null }>({}); const [isFormComplete, setIsFormComplete] = useState(false); - let currentPlot = usePlotContext(); - let currentCensus = useOrgCensusContext(); - let currentSite = useSiteContext(); + const currentPlot = usePlotContext(); + const currentCensus = useOrgCensusContext(); + const currentSite = useSiteContext(); const {data: session} = useSession(); @@ -311,7 +311,7 @@ const CensusAutocompleteInputForm = () => { }; const validateAllFields = async (row: GridValidRowModel) => { - let [firstName = '', lastName = ''] = row.personnel.split(' '); + const [firstName = '', lastName = ''] = row.personnel.split(' '); const validations = [ validateField('stems', 'StemTag', row.stemTag, row.id), validateField('trees', 'TreeTag', row.treeTag, row.id), diff --git a/frontend/components/loginlogout.tsx b/frontend/components/loginlogout.tsx index 070a29e2..d959ce3f 100644 --- a/frontend/components/loginlogout.tsx +++ b/frontend/components/loginlogout.tsx @@ -57,12 +57,12 @@ export const LoginLogout = () => { - {session?.user?.name!} + {session?.user?.name ? session.user.name : ''} - {session?.user?.email!} + {session?.user?.email ? session?.user?.email : ''} diff --git a/frontend/components/processors/processcensus.tsx b/frontend/components/processors/processcensus.tsx index de226cde..5bb3d8a7 100644 --- a/frontend/components/processors/processcensus.tsx +++ b/frontend/components/processors/processcensus.tsx @@ -1,4 +1,3 @@ -import { booleanToBit } from '@/config/macros'; import { runQuery, SpecialProcessingProps } from '@/components/processors/processormacros'; import { getPersonnelIDByName } from './processorhelperfunctions'; import moment from 'moment'; @@ -10,82 +9,147 @@ export async function processCensus(props: Readonly): Pr try { await connection.beginTransaction(); + let coreMeasurementID: number | undefined = undefined; + + // Fetch the necessary foreign key IDs + let speciesID: number | null = null; + if (rowData.spcode) { + let query = `SELECT SpeciesID FROM ${schema}.species WHERE SpeciesCode = ?`; + const rows = await runQuery(connection, query, [rowData.spcode]); + if (rows.length === 0) throw new Error(`SpeciesCode ${rowData.spcode} not found.`); + console.log('SpeciesCode found:', rowData.spcode); + speciesID = rows[0].SpeciesID; + } + + let quadratIDFromDB: number | null = null; + if (rowData.quadrat) { + let query = `SELECT QuadratID FROM ${schema}.quadrats WHERE QuadratName = ?`; + const rows = await runQuery(connection, query, [rowData.quadrat]); + if (rows.length === 0) throw new Error(`QuadratName ${rowData.quadrat} not found.`); + console.log('QuadratName found:', rowData.quadrat); + quadratIDFromDB = rows[0].QuadratID; + } + + let subquadratID: number | null = null; + if (rowData.subquadrat) { + let query = `SELECT SubquadratID FROM ${schema}.subquadrats WHERE SubquadratName = ?`; + const rows = await runQuery(connection, query, [rowData.subquadrat]); + if (rows.length > 0) subquadratID = rows[0].SubquadratID; + console.log('SubquadratName not found:', rowData.subquadrat); + } + // Upsert into trees if (rowData.tag) { - let query = `INSERT INTO ${schema}.trees (TreeTag) VALUES (?) ON DUPLICATE KEY UPDATE TreeID = LAST_INSERT_ID(TreeID)`; - let result = await runQuery(connection, query, [rowData.tag]); + let query = `INSERT INTO ${schema}.trees (TreeTag, SpeciesID) VALUES (?, ?) ON DUPLICATE KEY UPDATE TreeID = LAST_INSERT_ID(TreeID), SpeciesID = VALUES(SpeciesID)`; + let result = await runQuery(connection, query, [rowData.tag, speciesID ?? null]); + console.log('TreeTag upserted:', rowData.tag, 'Result:', result); const treeID = result.insertId; - // Fetch the necessary foreign key IDs - let rows; - if (rowData.spcode) { - query = `SELECT SpeciesID FROM ${schema}.species WHERE SpeciesCode = ?`; - rows = await runQuery(connection, query, [rowData.spcode]); - if (rows.length === 0) throw new Error(`SpeciesCode ${rowData.spcode} not found.`); - const speciesID = rows[0].SpeciesID; + // Upsert into stems + if (rowData.stemtag && rowData.lx && rowData.ly) { + query = ` + INSERT INTO ${schema}.stems (StemTag, TreeID, QuadratID, SubquadratID, LocalX, LocalY, Unit) + VALUES (?, ?, ?, ?, ?, ?, ?) + ON DUPLICATE KEY UPDATE StemID = LAST_INSERT_ID(StemID), TreeID = VALUES(TreeID), QuadratID = VALUES(QuadratID), SubquadratID = VALUES(SubquadratID), LocalX = VALUES(LocalX), LocalY = VALUES(LocalY), Unit = VALUES(Unit) + `; + result = await runQuery(connection, query, [ + rowData.stemtag, + treeID, + quadratIDFromDB ?? null, + subquadratID ?? null, + rowData.lx ?? null, + rowData.ly ?? null, + rowData.unit ?? null + ]); + console.log('Stem upserted:', rowData.stemtag, 'Result:', result); + const stemID = result.insertId; + + // Upsert into coremeasurements + if (rowData.dbh && rowData.hom && rowData.date) { + const personnelID = await getPersonnelIDByName(connection, schema, fullName); + console.log('Personnel ID:', personnelID); - query = `SELECT SubquadratID FROM ${schema}.subquadrats WHERE SubquadratName = ?`; - rows = await runQuery(connection, query, [rowData.subquadrat]); - if (rows.length === 0) throw new Error(`SubquadratName ${rowData.subquadrat} not found.`); - const subquadratID = rows[0].SubquadratID; + console.log('Preparing to upsert into coremeasurements with values:', { + censusID, + plotID, + quadratIDFromDB, + subquadratID, + treeID, + stemID, + personnelID, + date: moment(rowData.date).format('YYYY-MM-DD'), + dbh: rowData.dbh, + dbhunit: rowData.dbhunit, + hom: rowData.hom, + homunit: rowData.homunit + }); - // Upsert into stems - if (rowData.stemtag && rowData.lx && rowData.ly) { query = ` - INSERT INTO ${schema}.stems (StemTag, TreeID, SpeciesID, SubquadratID, LocalX, LocalY, Unit) - VALUES (?, ?, ?, ?, ?, ?, ?) - ON DUPLICATE KEY UPDATE StemID = LAST_INSERT_ID(StemID), TreeID = VALUES(TreeID), SpeciesID = VALUES(SpeciesID), SubquadratID = VALUES(SubquadratID), LocalX = VALUES(LocalX), LocalY = VALUES(LocalY), Unit = VALUES(Unit) + INSERT INTO ${schema}.coremeasurements + (CensusID, PlotID, QuadratID, SubquadratID, TreeID, StemID, PersonnelID, IsValidated, MeasurementDate, MeasuredDBH, DBHUnit, MeasuredHOM, HOMUnit) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) + ON DUPLICATE KEY UPDATE CoreMeasurementID = LAST_INSERT_ID(CoreMeasurementID), + MeasuredDBH = VALUES(MeasuredDBH), DBHUnit = VALUES(DBHUnit), + MeasuredHOM = VALUES(MeasuredHOM), HOMUnit = VALUES(HOMUnit), MeasurementDate = VALUES(MeasurementDate) `; - result = await runQuery(connection, query, [rowData.stemtag, treeID, speciesID, subquadratID, rowData.lx, rowData.ly, rowData.unit]); - const stemID = result.insertId; + result = await runQuery(connection, query, [ + censusID, + plotID, + quadratIDFromDB ?? null, + subquadratID ?? null, + treeID, + stemID, + personnelID, + 0, + moment(rowData.date).format('YYYY-MM-DD'), + rowData.dbh ?? null, + rowData.dbhunit ?? null, + rowData.hom ?? null, + rowData.homunit ?? null + ]); - // Upsert into coremeasurements - if (rowData.dbh && rowData.dbhunit && rowData.hom && rowData.homunit && rowData.date) { - query = ` - INSERT INTO ${schema}.coremeasurements - (CensusID, PlotID, QuadratID, SubquadratID, TreeID, StemID, PersonnelID, IsValidated, MeasurementDate, MeasuredDBH, DBHUnit, MeasuredHOM, HOMUnit) - VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) - ON DUPLICATE KEY UPDATE CoreMeasurementID = LAST_INSERT_ID(CoreMeasurementID), - MeasuredDBH = VALUES(MeasuredDBH), DBHUnit = VALUES(DBHUnit), - MeasuredHOM = VALUES(MeasuredHOM), HOMUnit = VALUES(HOMUnit), MeasurementDate = VALUES(MeasurementDate) - `; - const personnelID = await getPersonnelIDByName(connection, schema, fullName); - result = await runQuery(connection, query, [ - censusID, plotID, quadratID, subquadratID, treeID, stemID, personnelID, 0, moment(rowData.date).format('YYYY-MM-DD'), - rowData.dbh, rowData.dbhunit, rowData.hom, rowData.homunit - ]); - const coreMeasurementID = result.insertId; + console.log('CoreMeasurement upsert result:', result); - // Insert into cmattributes after verifying each code exists in attributes table - if (rowData.codes) { - const codes = rowData.codes.split(';').map(code => code.trim()).filter(Boolean); - for (const code of codes) { - query = `SELECT COUNT(*) as count FROM ${schema}.attributes WHERE Code = ?`; - const [attributeRows] = await runQuery(connection, query, [code]); - if (attributeRows[0].count === 0) { - throw new Error(`Attribute code ${code} not found.`); - } - query = ` - INSERT INTO ${schema}.cmattributes (CoreMeasurementID, Code) - VALUES (?, ?) - ON DUPLICATE KEY UPDATE CMAID = LAST_INSERT_ID(CMAID) - `; - await runQuery(connection, query, [coreMeasurementID, code]); + if (result && result.insertId) { + coreMeasurementID = result.insertId; + } else { + console.error('CoreMeasurement insertion did not return an insertId.'); + throw new Error('CoreMeasurement insertion failure'); + } + + // Insert into cmattributes after verifying each code exists in attributes table + if (rowData.codes) { + const codes = rowData.codes.split(';').map(code => code.trim()).filter(Boolean); + for (const code of codes) { + query = `SELECT COUNT(*) as count FROM ${schema}.attributes WHERE Code = ?`; + const attributeRows = await runQuery(connection, query, [code]); + if (!attributeRows || attributeRows.length === 0) { + throw new Error(`Attribute code ${code} not found or query failed.`); + } + if (!attributeRows[0] || !attributeRows[0].count) { + throw new Error(`Invalid response structure for attribute code ${code}.`); } + console.log('Attribute found:', code); + query = ` + INSERT INTO ${schema}.cmattributes (CoreMeasurementID, Code) + VALUES (?, ?) + ON DUPLICATE KEY UPDATE CMAID = LAST_INSERT_ID(CMAID) + `; + result = await runQuery(connection, query, [coreMeasurementID, code]); + console.log('CMAttribute upserted:', code, 'Result:', result); } - await connection.commit(); - return coreMeasurementID; } } } } await connection.commit(); - console.log('Upsert successful'); - return undefined; + console.log('Upsert successful. CoreMeasurement ID generated:', coreMeasurementID); + return coreMeasurementID; } catch (error: any) { await connection.rollback(); console.error('Upsert failed:', error.message); + console.error('Error object:', error); throw error; } } diff --git a/frontend/components/processors/processorhelperfunctions.tsx b/frontend/components/processors/processorhelperfunctions.tsx index 6ebc73dc..3cfe4de5 100644 --- a/frontend/components/processors/processorhelperfunctions.tsx +++ b/frontend/components/processors/processorhelperfunctions.tsx @@ -35,6 +35,7 @@ export async function getPersonnelIDByName( const rows = await runQuery(connection, query, [firstName.trim(), lastName.trim()]); if (rows.length > 0) { + console.log('getpersonnelidbyname: ', rows); return rows[0].PersonnelID as number; } return null; // No matching personnel found @@ -58,16 +59,17 @@ export async function insertOrUpdate(props: InsertUpdateProcessingProps): Promis await mapping.specialProcessing({...subProps, schema}); } else { const columns = Object.keys(mapping.columnMappings); + if (columns.includes('plotID')) rowData['plotID'] = subProps.plotID?.toString() ?? null; + if (columns.includes('censusID')) rowData['censusID'] = subProps.censusID?.toString() ?? null; const tableColumns = columns.map(fileColumn => mapping.columnMappings[fileColumn]).join(', '); const placeholders = columns.map(() => '?').join(', '); // Use '?' for placeholders in MySQL const values = columns.map(fileColumn => rowData[fileColumn]); - let query = ` + const query = ` INSERT INTO ${schema}.${mapping.tableName} (${tableColumns}) VALUES (${placeholders}) ON DUPLICATE KEY UPDATE ${tableColumns.split(', ').map(column => `${column} = VALUES(${column})`).join(', ')}; `; - try { // Execute the query using the provided connection await connection.beginTransaction(); @@ -250,7 +252,7 @@ interface UpdateQueryConfig { } export function generateUpdateOperations(schema: string, newRow: any, oldRow: any, config: UpdateQueryConfig): string[] { - const { fieldList, slices } = config; + const {fieldList, slices} = config; const changedFields = detectFieldChanges(newRow, oldRow, fieldList); const generateQueriesFromSlice = (type: keyof typeof slices): string[] => { @@ -258,7 +260,7 @@ export function generateUpdateOperations(schema: string, newRow: any, oldRow: an if (!sliceConfig) { return []; // Safety check in case of an undefined slice } - const { range, primaryKey } = sliceConfig; + const {range, primaryKey} = sliceConfig; const fieldsInSlice = fieldList.slice(range[0], range[1]); const changedInSlice = changedFields.filter(field => fieldsInSlice.includes(field)); return generateUpdateQueries(schema, type as string, changedInSlice, newRow, primaryKey); @@ -272,14 +274,14 @@ export function generateUpdateOperations(schema: string, newRow: any, oldRow: an } export function generateInsertOperations(schema: string, newRow: any, config: UpdateQueryConfig): string[] { - const { fieldList, slices } = config; + const {fieldList, slices} = config; const generateQueriesFromSlice = (type: keyof typeof slices): string => { const sliceConfig = slices[type]; if (!sliceConfig) { return ''; // Safety check in case of an undefined slice } - const { range } = sliceConfig; + const {range} = sliceConfig; const fieldsInSlice = fieldList.slice(range[0], range[1]); return generateInsertQuery(schema, type as string, fieldsInSlice, newRow); }; @@ -368,31 +370,31 @@ export const stemTaxonomiesViewFields = [ export const StemDimensionsViewQueryConfig: UpdateQueryConfig = { fieldList: stemDimensionsViewFields, slices: { - trees: { range: [0, 1], primaryKey: 'TreeID' }, - stems: { range: [1, 5], primaryKey: 'StemID' }, - subquadrats: { range: [5, 12], primaryKey: 'SubquadratID' }, - quadrats: { range: [12, 16], primaryKey: 'QuadratID' }, - plots: { range: [16, stemDimensionsViewFields.length], primaryKey: 'PlotID' }, + trees: {range: [0, 1], primaryKey: 'TreeID'}, + stems: {range: [1, 5], primaryKey: 'StemID'}, + subquadrats: {range: [5, 12], primaryKey: 'SubquadratID'}, + quadrats: {range: [12, 16], primaryKey: 'QuadratID'}, + plots: {range: [16, stemDimensionsViewFields.length], primaryKey: 'PlotID'}, } }; export const AllTaxonomiesViewQueryConfig: UpdateQueryConfig = { fieldList: allTaxonomiesFields, slices: { - family: { range: [0, 1], primaryKey: 'FamilyID' }, - genus: { range: [1, 3], primaryKey: 'GenusID' }, - species: { range: [3, 11], primaryKey: 'SpeciesID' }, - reference: { range: [11, allTaxonomiesFields.length], primaryKey: 'ReferenceID' }, + family: {range: [0, 1], primaryKey: 'FamilyID'}, + genus: {range: [1, 3], primaryKey: 'GenusID'}, + species: {range: [3, 11], primaryKey: 'SpeciesID'}, + reference: {range: [11, allTaxonomiesFields.length], primaryKey: 'ReferenceID'}, } }; export const StemTaxonomiesViewQueryConfig: UpdateQueryConfig = { fieldList: stemTaxonomiesViewFields, slices: { - trees: { range: [0, 1], primaryKey: 'TreeID' }, - stems: { range: [1, 2], primaryKey: 'StemID' }, - family: { range: [2, 3], primaryKey: 'FamilyID' }, - genus: { range: [3, 5], primaryKey: 'GenusID' }, - species: { range: [5, stemTaxonomiesViewFields.length], primaryKey: 'SpeciesID' }, + trees: {range: [0, 1], primaryKey: 'TreeID'}, + stems: {range: [1, 2], primaryKey: 'StemID'}, + family: {range: [2, 3], primaryKey: 'FamilyID'}, + genus: {range: [3, 5], primaryKey: 'GenusID'}, + species: {range: [5, stemTaxonomiesViewFields.length], primaryKey: 'SpeciesID'}, } }; \ No newline at end of file diff --git a/frontend/components/processors/processormacros.tsx b/frontend/components/processors/processormacros.tsx index 31062f54..4ad54049 100644 --- a/frontend/components/processors/processormacros.tsx +++ b/frontend/components/processors/processormacros.tsx @@ -1,4 +1,4 @@ -import {PoolConnection, PoolOptions, createPool} from 'mysql2/promise'; +import {PoolConnection, PoolOptions} from 'mysql2/promise'; import {booleanToBit} from "@/config/macros"; import {FileRow} from "@/config/macros/formdetails"; @@ -6,13 +6,13 @@ import {processSpecies} from "@/components/processors/processspecies"; import {NextRequest} from "next/server"; import {processCensus} from "@/components/processors/processcensus"; import {PoolMonitor} from "@/config/poolmonitor"; -import { AttributesResult } from '@/config/sqlrdsdefinitions/tables/attributerds'; -import { GridValidRowModel } from '@mui/x-data-grid'; +import {AttributesResult} from '@/config/sqlrdsdefinitions/tables/attributerds'; +import {GridValidRowModel} from '@mui/x-data-grid'; export async function getConn() { let conn: PoolConnection | null = null; try { - let i = 0; + const i = 0; conn = await getSqlConnection(i); } catch (error: any) { console.error("Error processing files:", error.message); @@ -81,6 +81,8 @@ export const fileMappings: Record = { // "quadrats": [{label: "quadrat"}, {label: "startx"}, {label: "starty"}, {label: "dimx"}, {label: "dimy"}, {label: "unit"}, {label: "quadratshape"}], columnMappings: { "quadrat": "QuadratName", + "plotID": "PlotID", + "censusID": "CensusID", "startx": "StartX", "starty": "StartY", "dimx": "DimensionX", @@ -95,6 +97,8 @@ export const fileMappings: Record = { columnMappings: { "subquadrat": "SubquadratName", "quadrat": "QuadratID", + "plotID": "PlotID", + "censusID": "CensusID", "dimx": "DimensionX", "dimy": "DimensionY", "xindex": "X", @@ -202,7 +206,7 @@ export async function parseCoreMeasurementsRequestBody(request: NextRequest) { } export async function parseAttributeRequestBody(request: NextRequest, parseType: string): Promise { - const {newRow: requestBody}: {newRow: GridValidRowModel} = await request.json(); + const {newRow: requestBody}: { newRow: GridValidRowModel } = await request.json(); switch (parseType) { case 'POST': case 'PATCH': { @@ -216,6 +220,7 @@ export async function parseAttributeRequestBody(request: NextRequest, parseType: throw new Error("Invalid parse type -- attributes"); } } + export function getCatalogSchema() { const catalogSchema = process.env.AZURE_SQL_CATALOG_SCHEMA; if (!catalogSchema) throw new Error('Environmental variable extraction for catalog schema failed'); @@ -249,10 +254,10 @@ export interface QueryConfig { } export function buildPaginatedQuery(config: QueryConfig): { query: string, params: any[] } { - const { schema, table, joins, conditionals, pagination, extraParams } = config; - const { page, pageSize } = pagination; + const {schema, table, joins, conditionals, pagination, extraParams} = config; + const {page, pageSize} = pagination; const startRow = page * pageSize; - let queryParams = extraParams || []; + const queryParams = extraParams || []; // Establish an alias for the primary table for consistency in joins and selections const tableAlias = table[0].toLowerCase(); // Simple default alias based on first letter of table name @@ -273,5 +278,5 @@ export function buildPaginatedQuery(config: QueryConfig): { query: string, param query += ` LIMIT ?, ?`; queryParams.push(startRow, pageSize); // Ensure these are the last parameters added - return { query, params: queryParams }; + return {query, params: queryParams}; } \ No newline at end of file diff --git a/frontend/components/processors/processspecies.tsx b/frontend/components/processors/processspecies.tsx index 34daae00..aec187b1 100644 --- a/frontend/components/processors/processspecies.tsx +++ b/frontend/components/processors/processspecies.tsx @@ -1,5 +1,5 @@ -import { runQuery, SpecialProcessingProps } from '@/components/processors/processormacros'; -import { booleanToBit } from '@/config/macros'; +import {runQuery, SpecialProcessingProps} from '@/components/processors/processormacros'; +import {booleanToBit} from '@/config/macros'; function cleanInputData(data: any) { const cleanedData: any = {}; @@ -12,7 +12,7 @@ function cleanInputData(data: any) { } export async function processSpecies(props: Readonly): Promise { - const { connection, rowData, schema } = props; + const {connection, rowData, schema} = props; console.log('rowData: ', rowData); try { diff --git a/frontend/components/sidebar.tsx b/frontend/components/sidebar.tsx index 3032d638..f5b19bb2 100644 --- a/frontend/components/sidebar.tsx +++ b/frontend/components/sidebar.tsx @@ -1,6 +1,6 @@ "use client"; import * as React from 'react'; -import { Dispatch, SetStateAction, useEffect, useState } from 'react'; +import { Dispatch, SetStateAction, useEffect, useRef, useState } from 'react'; import GlobalStyles from '@mui/joy/GlobalStyles'; import Box from '@mui/joy/Box'; import Divider from '@mui/joy/Divider'; @@ -29,21 +29,23 @@ import { DialogActions, DialogContent, DialogTitle, - Link, Modal, ModalDialog, SelectOption, Stack, Badge, Tooltip, - TextField, } from "@mui/joy"; import WarningRoundedIcon from "@mui/icons-material/WarningRounded"; import Select from "@mui/joy/Select"; import Option from '@mui/joy/Option'; -import { useOrgCensusListContext, useOrgCensusListDispatch, usePlotListContext, useSiteListContext } from "@/app/contexts/listselectionprovider"; +import { + useOrgCensusListContext, + usePlotListContext, + useSiteListContext +} from "@/app/contexts/listselectionprovider"; import { useSession } from "next-auth/react"; -import { SlideToggle, TransitionComponent } from "@/components/client/clientmacros"; +import { TransitionComponent } from "@/components/client/clientmacros"; import ListDivider from "@mui/joy/ListDivider"; import TravelExploreIcon from '@mui/icons-material/TravelExplore'; import Avatar from "@mui/joy/Avatar"; @@ -53,7 +55,7 @@ import { useDataValidityContext } from '@/app/contexts/datavalidityprovider'; import { OrgCensus, OrgCensusRDS, OrgCensusToCensusResultMapper } from '@/config/sqlrdsdefinitions/orgcensusrds'; import moment from 'moment'; import { DatePicker } from '@mui/x-date-pickers/DatePicker'; -import Joyride, { CallBackProps, Status, STATUS, Step } from 'react-joyride'; +import { useLockAnimation } from '@/app/contexts/lockanimationcontext'; // const initialSteps: Step[] = [ // { @@ -98,12 +100,17 @@ interface MRTProps { isParentDataIncomplete: boolean; } -function MenuRenderToggle(props: MRTProps, siteConfigProps: SiteConfigProps, menuOpen: boolean | undefined, setMenuOpen: Dispatch> | undefined) { +function MenuRenderToggle( + props: MRTProps, + siteConfigProps: SiteConfigProps, + menuOpen: boolean | undefined, + setMenuOpen: Dispatch> | undefined +) { const Icon = siteConfigProps.icon; const { plotSelectionRequired, censusSelectionRequired, pathname, isParentDataIncomplete } = props; - let currentSite = useSiteContext(); - let currentPlot = usePlotContext(); - let currentCensus = useOrgCensusContext(); + const currentSite = useSiteContext(); + const currentPlot = usePlotContext(); + const currentCensus = useOrgCensusContext(); return ( key !== 'subquadrats') @@ -162,7 +168,6 @@ export default function Sidebar(props: SidebarProps) { const [site, setSite] = useState(currentSite); const router = useRouter(); const pathname = usePathname(); - const containerRef = React.useRef(null); const [measurementsToggle, setMeasurementsToggle] = useState(false); const [propertiesToggle, setPropertiesToggle] = useState(false); @@ -172,87 +177,139 @@ export default function Sidebar(props: SidebarProps) { const [storedCensus, setStoredCensus] = useState(); const [storedSite, setStoredSite] = useState(); - const { coreDataLoaded, setManualReset, siteListLoaded, setCensusListLoaded } = props; + const { siteListLoaded, setCensusListLoaded } = props; const [openCloseCensusModal, setOpenCloseCensusModal] = useState(false); const [openReopenCensusModal, setOpenReopenCensusModal] = useState(false); + const [openNewCensusModal, setOpenNewCensusModal] = useState(false); const [reopenStartDate, setReopenStartDate] = useState(null); const [closeEndDate, setCloseEndDate] = useState(null); + const [newStartDate, setNewStartDate] = useState(null); - /** this has been postponed and marked as a future completion task. the joyride is not critical to the usage of the application. - const [run, setRun] = useState(false); - const [steps, setSteps] = useState(initialSteps); - const [stepIndex, setStepIndex] = useState(0); - const [joyridePrompted, setJoyridePrompted] = useState(false); - const [debounceTimer, setDebounceTimer] = useState(null); - const [dashboardLoaded, setDashboardLoaded] = useState(false); + const { triggerRefresh } = useDataValidityContext(); - useEffect(() => { - if (pathname === '/dashboard') { - setDashboardLoaded(true); - } - }, [pathname]); + const { isPulsing, triggerPulse } = useLockAnimation(); + const reopenButtonRef = useRef(null); + const addButtonRef = useRef(null); + const sidebarRef = useRef(null); + const [sidebarWidth, setSidebarWidth] = useState(340); // Default width useEffect(() => { - if (session && siteListLoaded && dashboardLoaded && !joyridePrompted) { - const startJoyride = confirm("Would you like to start the tutorial?"); - if (startJoyride) { - setRun(true); - } - setJoyridePrompted(true); - } - }, [session, siteListLoaded, dashboardLoaded, joyridePrompted]); - - const handleJoyrideCallback = (data: CallBackProps) => { - const { status, action, index } = data; - const finishedStatuses: Status[] = [STATUS.FINISHED, STATUS.SKIPPED]; - - if (finishedStatuses.includes(status)) { - setRun(false); - } else if (action === 'next' && index === 0) { - // Move to the next step if site is selected - if (site) { - setSteps(prevSteps => [ - ...prevSteps, - { - target: '.plot-selection', - content: 'Select a plot from here.', - disableBeacon: true, - spotlightClicks: true, - placement: 'right', - }, - ]); - setStepIndex(1); - } else { - alert("Please select a site to proceed."); - } - } else if (action === 'next' && index === 1) { - // Move to the next step if plot is selected - if (plot) { - setSteps(prevSteps => [ - ...prevSteps, - { - target: '.census-select', - content: 'Select a census from here.', - disableBeacon: true, - spotlightClicks: true, - placement: 'right', - }, - ]); - setStepIndex(2); - } else { - alert("Please select a plot to proceed."); - } - } else if (action === 'next' && index === 2) { - // End the tour if census is selected - if (census) { - setRun(false); - } else { - alert("Please select a census to proceed."); + const updateSidebarWidth = () => { + if (sidebarRef.current) { + const sidebarElements = sidebarRef.current.querySelectorAll('*'); + let maxWidth = 340; // Minimum width + + sidebarElements.forEach(element => { + if (sidebarRef.current) { + const elementRect = element.getBoundingClientRect(); + const sidebarRect = sidebarRef.current.getBoundingClientRect(); + const elementWidth = elementRect.right - sidebarRect.left; + + if (elementWidth > maxWidth) { + maxWidth = elementWidth; + } + } + }); + + setSidebarWidth(maxWidth + 10); } + }; + + const resizeObserver = new ResizeObserver(() => { + updateSidebarWidth(); + }); + + if (sidebarRef.current) { + const sidebarElements = sidebarRef.current.querySelectorAll('*'); + sidebarElements.forEach(element => { + resizeObserver.observe(element); + }); } - }; */ + + // Initial calculation + updateSidebarWidth(); + + return () => { + resizeObserver.disconnect(); + }; + }, [site, plot, census]); + + + /** this has been postponed and marked as a future completion task. the joyride is not critical to the usage of the application. + const [run, setRun] = useState(false); + const [steps, setSteps] = useState(initialSteps); + const [stepIndex, setStepIndex] = useState(0); + const [joyridePrompted, setJoyridePrompted] = useState(false); + const [debounceTimer, setDebounceTimer] = useState(null); + const [dashboardLoaded, setDashboardLoaded] = useState(false); + + useEffect(() => { + if (pathname === '/dashboard') { + setDashboardLoaded(true); + } + }, [pathname]); + + useEffect(() => { + if (session && siteListLoaded && dashboardLoaded && !joyridePrompted) { + const startJoyride = confirm("Would you like to start the tutorial?"); + if (startJoyride) { + setRun(true); + } + setJoyridePrompted(true); + } + }, [session, siteListLoaded, dashboardLoaded, joyridePrompted]); + + const handleJoyrideCallback = (data: CallBackProps) => { + const { status, action, index } = data; + const finishedStatuses: Status[] = [STATUS.FINISHED, STATUS.SKIPPED]; + + if (finishedStatuses.includes(status)) { + setRun(false); + } else if (action === 'next' && index === 0) { + // Move to the next step if site is selected + if (site) { + setSteps(prevSteps => [ + ...prevSteps, + { + target: '.plot-selection', + content: 'Select a plot from here.', + disableBeacon: true, + spotlightClicks: true, + placement: 'right', + }, + ]); + setStepIndex(1); + } else { + alert("Please select a site to proceed."); + } + } else if (action === 'next' && index === 1) { + // Move to the next step if plot is selected + if (plot) { + setSteps(prevSteps => [ + ...prevSteps, + { + target: '.census-select', + content: 'Select a census from here.', + disableBeacon: true, + spotlightClicks: true, + placement: 'right', + }, + ]); + setStepIndex(2); + } else { + alert("Please select a plot to proceed."); + } + } else if (action === 'next' && index === 2) { + // End the tour if census is selected + if (census) { + setRun(false); + } else { + alert("Please select a census to proceed."); + } + } + }; */ const getOpenClosedCensusStartEndDate = (censusType: string, census: OrgCensus): Date | undefined => { if (!census) return undefined; @@ -271,32 +328,54 @@ export default function Sidebar(props: SidebarProps) { }; const handleReopenCensus = async () => { - if (currentCensus && reopenStartDate) { + if (census && reopenStartDate) { const mapper = new OrgCensusToCensusResultMapper(); const validCensusListContext = (censusListContext || []).filter((census): census is OrgCensusRDS => census !== undefined); - await mapper.reopenCensus(currentSite?.schemaName || '', currentCensus.plotCensusNumber, reopenStartDate, validCensusListContext); + await mapper.reopenCensus(site?.schemaName || '', census.plotCensusNumber, reopenStartDate, validCensusListContext); setCensusListLoaded(false); setOpenReopenCensusModal(false); - } + setReopenStartDate(null); + } else throw new Error("current census or reopen start date was not set"); }; const handleCloseCensus = async () => { - if (currentCensus && closeEndDate) { + if (census && closeEndDate) { const mapper = new OrgCensusToCensusResultMapper(); const validCensusListContext = (censusListContext || []).filter((census): census is OrgCensusRDS => census !== undefined); - await mapper.closeCensus(currentSite?.schemaName || '', currentCensus.plotCensusNumber, closeEndDate, validCensusListContext); + await mapper.closeCensus(site?.schemaName || '', census.plotCensusNumber, closeEndDate, validCensusListContext); setCensusListLoaded(false); setOpenCloseCensusModal(false); + setCloseEndDate(null); } }; + const handleOpenNewCensus = async () => { + if ((site === undefined || site.schemaName === undefined) || newStartDate === null || (plot === undefined || plot.plotID === undefined)) throw new Error("new census start date was not set OR plot is undefined"); + const validCensusListContext = (censusListContext || []).filter((census): census is OrgCensusRDS => census !== undefined); + const highestPlotCensusNumber = validCensusListContext.length > 0 + ? validCensusListContext.reduce((max, census) => + census.plotCensusNumber > max ? census.plotCensusNumber : max, validCensusListContext[0].plotCensusNumber) + : 0; + const mapper = new OrgCensusToCensusResultMapper(); + await mapper.startNewCensus(site.schemaName, plot.plotID, highestPlotCensusNumber + 1, newStartDate, census ? census.description : undefined); + setCensusListLoaded(false); + setOpenNewCensusModal(false); + setNewStartDate(null); + }; + useEffect(() => { setPlot(currentPlot); setCensus(currentCensus); setSite(currentSite); }, [currentPlot, currentCensus, currentSite]); + useEffect(() => { + if (census && reopenStartDate === null && getOpenClosedCensusStartEndDate('closed', census)) setReopenStartDate(getOpenClosedCensusStartEndDate('closed', census) ?? null); + if (census && newStartDate === null && getOpenClosedCensusStartEndDate('open', census)) setNewStartDate(getOpenClosedCensusStartEndDate('open', census) ?? null); + if (census && closeEndDate === null && getOpenClosedCensusStartEndDate('open', census)) setCloseEndDate(getOpenClosedCensusStartEndDate('open', census) ?? null); + }, [census, reopenStartDate, closeEndDate, newStartDate]); + // useEffect(() => { // if (siteListLoaded && session) { // const checkAndInitializeKey = async (key: string, defaultValue: any) => { @@ -382,15 +461,15 @@ export default function Sidebar(props: SidebarProps) { <> {selectedSite ? ( - {`Site: ${selectedSite?.siteName}`} - - + {`Site: ${selectedSite?.siteName}`} + + — Schema: {selectedSite.schemaName} ) : ( - Select a Site + Select a Site )} ); @@ -408,15 +487,15 @@ export default function Sidebar(props: SidebarProps) { <> {selectedPlot ? ( - {`Plot: ${selectedPlot?.plotName}`} - + {`Plot: ${selectedPlot?.plotName}`} + — Quadrats: {selectedPlot.numQuadrats} ) : ( - Select a Plot + Select a Plot )} ); @@ -435,14 +514,14 @@ export default function Sidebar(props: SidebarProps) { <> {selectedCensus ? ( - {`Census: ${selectedCensus?.plotCensusNumber}`} - - + {`Census: ${selectedCensus?.plotCensusNumber}`} + + {(census !== undefined) && ( <>{(census.dateRanges[0]?.startDate) ? `\u2014 Starting: ${new Date(census?.dateRanges[0]?.startDate).toDateString()}` : ''} )} - + {(census !== undefined) && ( <>{(census.dateRanges[0]?.endDate) ? `\u2014 Ending ${new Date(census.dateRanges[0]?.endDate).toDateString()}` : `\u2014 Ongoing`} )} @@ -450,7 +529,7 @@ export default function Sidebar(props: SidebarProps) { ) : ( - Select a Census + Select a Census )} ); @@ -471,7 +550,7 @@ export default function Sidebar(props: SidebarProps) { const renderCensusOptions = () => ( - + Deselect Site (will trigger app reset!): @@ -600,7 +681,7 @@ export default function Sidebar(props: SidebarProps) { - + Allowed Sites ({allowedSites?.length}) @@ -613,7 +694,7 @@ export default function Sidebar(props: SidebarProps) { - + Other Sites ({otherSites?.length}) @@ -628,6 +709,35 @@ export default function Sidebar(props: SidebarProps) { ); }; + const shouldApplyTooltip = (item: SiteConfigProps, linkHref?: string): boolean => { + if (linkHref) { + // Check for sub-links + switch (linkHref) { + case '/summary': + return !isAllValiditiesTrue; + case '/subquadrats': + return !validity['quadrats']; + case '/quadratpersonnel': + return !(validity['quadrats'] && validity['personnel']); + default: + const dataKey = validityMapping[linkHref]; + return dataKey !== undefined && !validity[dataKey]; + } + } else { + // Check for main links + switch (item.href) { + case '/summary': + return !isAllValiditiesTrue; + case '/subquadrats': + return !validity['quadrats']; + case '/quadratpersonnel': + return !(validity['quadrats'] && validity['personnel']); + default: + return false; + } + } + }; + return ( <> {/* */} ({ ':root': { - '--Sidebar-width': '340px', + '--Sidebar-width': `${sidebarWidth}px`, [theme.breakpoints.up('lg')]: { - '--Sidebar-width': '340px', + '--Sidebar-width': `${sidebarWidth}px`, }, }, })} /> - + @@ -723,7 +834,12 @@ export default function Sidebar(props: SidebarProps) { )} - {/* Added ml: -1 to adjust the position of the navigation menu */} + {/* Added ml: -1 to adjust the position of the navigation menu */} - - {(site !== undefined && plot !== undefined && census !== undefined) ? ( - + + + {site !== undefined && plot !== undefined && census !== undefined ? ( + - { - if (!isLinkDisabled) { - router.push(item.href); - } - }}> - + { + if (!isLinkDisabled) { + router.push(item.href); + } + }}> + @@ -814,14 +921,11 @@ export default function Sidebar(props: SidebarProps) { ) : ( - { - if (!isLinkDisabled) { - router.push(item.href); - } - }}> + { + if (!isLinkDisabled) { + router.push(item.href); + } + }}> {item.label} @@ -833,61 +937,48 @@ export default function Sidebar(props: SidebarProps) { ); } else { - let isParentDataIncomplete = item.expanded.some(subItem => { - // Skip validity check for subquadrats - if (subItem.href === '/subquadrats') { - return false; - } - + const isParentDataIncomplete = item.expanded.some(subItem => { const dataKey = validityMapping[subItem.href]; return dataKey !== undefined && !validity[dataKey]; }); + return ( - - + + {item.expanded.map((link, subIndex) => { - // Skip rendering for subquadrats - if (link.href === '/subquadrats') { - return null; - } const SubIcon = link.icon; const delay = (subIndex + 1) * 200; - let dataValidityKey = validityMapping[link.href]; - let isDataIncomplete = dataValidityKey ? !validity[dataValidityKey] : false; + const isDataIncomplete = shouldApplyTooltip(item, link.href); const isLinkDisabled = getDisabledState(link.href); const tooltipMessage = getTooltipMessage(link.href, isDataIncomplete || (link.href === '/summary' && !isAllValiditiesTrue)); return ( - - - {(site !== undefined && plot !== undefined && census !== undefined) ? ( - + + + {site !== undefined && plot !== undefined && census !== undefined ? ( + - { + { if (!isLinkDisabled) { - router.push((item.href + link.href)); + router.push(item.href + link.href); + if (setToggle) { + setToggle(false); // Close the menu + } } }}> - + invisible={link.href === '/summary' ? isAllValiditiesTrue : !isDataIncomplete}> @@ -898,12 +989,10 @@ export default function Sidebar(props: SidebarProps) { ) : ( - { + { if (!isLinkDisabled) { - router.push((item.href + link.href)); + router.push(item.href + link.href); } }}> @@ -930,27 +1019,89 @@ export default function Sidebar(props: SidebarProps) { - - {census && census.dateRanges[0].endDate ? ( - - - - - ) : ( - - - + + {site && plot && ( + <> + {!census && censusListContext?.length === 0 ? ( + + + + ) : ( + <> + {census && census.dateRanges[0].endDate ? ( + + + + + ) : ( + + + + )} + + )} + )} + {site && plot && census && ( + + )} - setOpenReopenCensusModal(false)}> + { + }}> @@ -959,9 +1110,11 @@ export default function Sidebar(props: SidebarProps) { - The most recent census ended on: {moment(getOpenClosedCensusStartEndDate('closed', census) ?? new Date()).utc().toDate().toDateString()} + The most recent census ended + on: {moment(getOpenClosedCensusStartEndDate('closed', census) ?? new Date()).utc().toDate().toDateString()} Select a start date for the new census: - NOTE: selected date will be converted to UTC for standardized handling + NOTE: selected date will be converted to UTC for + standardized handling - setOpenCloseCensusModal(false)}> + { + }}> + + + + Start New Census + + + + + {censusListContext && censusListContext?.length > 0 && ( + The most recent census ended + on: {moment(getOpenClosedCensusStartEndDate('closed', census) ?? new Date()).utc().toDate().toDateString()} + )} + Select a start date for the new census: + NOTE: selected date will be converted to UTC for + standardized handling + { + if (newValue) { + setNewStartDate(moment(newValue.toDate()).utc().toDate()); + } else { + setNewStartDate(null); + } + }} + /> + + + + }> + + + + + + + { + }}> @@ -996,9 +1192,11 @@ export default function Sidebar(props: SidebarProps) { - Start Date: {moment(getOpenClosedCensusStartEndDate('open', census) ?? new Date()).utc().toDate().toDateString()} + Start + Date: {moment(getOpenClosedCensusStartEndDate('open', census) ?? new Date()).utc().toDate().toDateString()} Select an end date for the current census: - NOTE: selected date will be converted to UTC for standardized handling + NOTE: selected date will be converted to UTC for + standardized handling - + ); } diff --git a/frontend/components/uploadsystem/segments/uploadcomplete.tsx b/frontend/components/uploadsystem/segments/uploadcomplete.tsx index a59571ae..8c93ed44 100644 --- a/frontend/components/uploadsystem/segments/uploadcomplete.tsx +++ b/frontend/components/uploadsystem/segments/uploadcomplete.tsx @@ -6,11 +6,75 @@ import {Box} from "@mui/joy"; import {redirect} from "next/navigation"; import React, {useEffect, useState} from "react"; import CircularProgress from "@mui/joy/CircularProgress"; +import { useDataValidityContext } from "@/app/contexts/datavalidityprovider"; +import { useOrgCensusListDispatch, usePlotListDispatch, useQuadratListDispatch } from "@/app/contexts/listselectionprovider"; +import { createAndUpdateCensusList } from "@/config/sqlrdsdefinitions/orgcensusrds"; +import { useLoading } from "@/app/contexts/loadingprovider"; +import { useOrgCensusContext, usePlotContext, useSiteContext } from "@/app/contexts/userselectionprovider"; export default function UploadComplete(props: Readonly) { const {uploadForm, handleCloseUploadModal} = props; const [countdown, setCountdown] = useState(5); + const {triggerRefresh} = useDataValidityContext(); + const {setLoading} = useLoading(); + + const currentPlot = usePlotContext(); + const currentSite = useSiteContext(); + const currentCensus = useOrgCensusContext(); + + const censusListDispatch = useOrgCensusListDispatch(); + const plotListDispatch = usePlotListDispatch(); + const quadratListDispatch = useQuadratListDispatch(); + + const loadCensusData = async () => { + if (!currentPlot) return; + + setLoading(true, 'Loading raw census data'); + const response = await fetch(`/api/fetchall/census/${currentPlot.plotID}?schema=${currentSite?.schemaName || ''}`); + const censusRDSLoad = await response.json(); + setLoading(false); + + setLoading(true, 'Converting raw census data...'); + const censusList = await createAndUpdateCensusList(censusRDSLoad); + if (censusListDispatch) { + censusListDispatch({ censusList }); + } + setLoading(false); + }; + + const loadPlotsData = async () => { + if (!currentSite) return; + + setLoading(true, "Loading plot list information..."); + const plotsResponse = await fetch(`/api/fetchall/plots?schema=${currentSite?.schemaName || ''}`); + const plotsData = await plotsResponse.json(); + if (!plotsData) return; + setLoading(false); + + setLoading(true, "Dispatching plot list information..."); + if (plotListDispatch) { + await plotListDispatch({ plotList: plotsData }); + } else return; + setLoading(false); + }; + + const loadQuadratsData = async () => { + if (!currentPlot || !currentCensus) return; + + setLoading(true, "Loading quadrat list information..."); + const quadratsResponse = await fetch(`/api/fetchall/quadrats/${currentPlot.plotID}/${currentCensus.plotCensusNumber}?schema=${currentSite?.schemaName || ''}`); + const quadratsData = await quadratsResponse.json(); + if (!quadratsData) return; + setLoading(false); + + setLoading(true, "Dispatching quadrat list information..."); + if (quadratListDispatch) { + await quadratListDispatch({ quadratList: quadratsData }); + } else return; + setLoading(false); + } + // Effect for handling countdown and state transition useEffect(() => { let timer: number; // Declare timer as a number @@ -19,7 +83,14 @@ export default function UploadComplete(props: Readonly) { timer = window.setTimeout(() => setCountdown(countdown - 1), 1000) as unknown as number; // Use 'window.setTimeout' and type assertion to treat the return as a number } else if (countdown === 0) { - handleCloseUploadModal(); + triggerRefresh(); + loadCensusData() + .catch(console.error) + .then(loadPlotsData) + .catch(console.error) + .then(loadQuadratsData) + .catch(console.error) + .then(handleCloseUploadModal); } return () => clearTimeout(timer); // Clear timeout using the timer variable }, [countdown, handleCloseUploadModal]); diff --git a/frontend/components/uploadsystem/segments/uploadfireazure.tsx b/frontend/components/uploadsystem/segments/uploadfireazure.tsx index 591b61be..d21aae6b 100644 --- a/frontend/components/uploadsystem/segments/uploadfireazure.tsx +++ b/frontend/components/uploadsystem/segments/uploadfireazure.tsx @@ -8,12 +8,18 @@ import {Box, Typography} from "@mui/material"; import {Stack} from "@mui/joy"; import {LinearProgressWithLabel} from "@/components/client/clientmacros"; import CircularProgress from "@mui/joy/CircularProgress"; -import { useOrgCensusContext, usePlotContext } from "@/app/contexts/userselectionprovider"; +import {useOrgCensusContext, usePlotContext} from "@/app/contexts/userselectionprovider"; const UploadFireAzure: React.FC = ({ - acceptedFiles, uploadForm, setIsDataUnsaved, user, setUploadError, - setErrorComponent, setReviewState, - allRowToCMID, cmErrors, + acceptedFiles, + uploadForm, + setIsDataUnsaved, + user, + setUploadError, + setErrorComponent, + setReviewState, + allRowToCMID, + cmErrors, }) => { const [loading, setLoading] = useState(true); const [results, setResults] = useState([]); @@ -24,8 +30,8 @@ const UploadFireAzure: React.FC = ({ const [countdown, setCountdown] = useState(5); const [startCountdown, setStartCountdown] = useState(false); - let currentPlot = usePlotContext(); - let currentCensus = useOrgCensusContext(); + const currentPlot = usePlotContext(); + const currentCensus = useOrgCensusContext(); const mapCMErrorsToFileRowErrors = (fileName: string) => { return cmErrors @@ -43,14 +49,14 @@ const UploadFireAzure: React.FC = ({ const uploadToStorage = useCallback(async (file: FileWithPath) => { try { setCurrentlyRunning(`File ${file.name} uploading to Azure Storage...`); - let formData = new FormData(); + const formData = new FormData(); formData.append(file.name, file); if (uploadForm === 'measurements') { const fileRowErrors = mapCMErrorsToFileRowErrors(file.name); formData.append('fileRowErrors', JSON.stringify(fileRowErrors)); // Append validation errors to formData } const response = await fetch( - `/api/filehandlers/storageload?fileName=${file.name}&plot=${currentPlot?.plotName?.trim()}&census=${currentCensus?.dateRanges[0].censusID ? currentCensus?.dateRanges[0].censusID.toString().trim() : 0}&user=${user}&formType=${uploadForm}`, { + `/api/filehandlers/storageload?fileName=${file.name}&plot=${currentPlot?.plotName?.trim().toLowerCase()}&census=${currentCensus?.dateRanges[0].censusID ? currentCensus?.dateRanges[0].censusID.toString().trim() : 0}&user=${user}&formType=${uploadForm}`, { method: 'POST', body: formData }); diff --git a/frontend/components/uploadsystem/segments/uploadfiresql.tsx b/frontend/components/uploadsystem/segments/uploadfiresql.tsx index eae7da3d..34e09cee 100644 --- a/frontend/components/uploadsystem/segments/uploadfiresql.tsx +++ b/frontend/components/uploadsystem/segments/uploadfiresql.tsx @@ -9,7 +9,7 @@ import {Stack} from "@mui/joy"; import {DetailedCMIDRow} from "@/components/uploadsystem/uploadparent"; import {LinearProgressWithLabel} from "@/components/client/clientmacros"; import CircularProgress from "@mui/joy/CircularProgress"; -import { useOrgCensusContext, usePlotContext, useQuadratContext } from '@/app/contexts/userselectionprovider'; +import {useOrgCensusContext, usePlotContext, useQuadratContext} from '@/app/contexts/userselectionprovider'; interface IDToRow { coreMeasurementID: number; @@ -17,21 +17,21 @@ interface IDToRow { } const UploadFireSQL: React.FC = ({ - personnelRecording, - acceptedFiles, - parsedData, - uploadForm, - setIsDataUnsaved, - schema, - uploadCompleteMessage, - setUploadCompleteMessage, - setUploadError, - setReviewState, - setAllRowToCMID, -}) => { - let currentPlot = usePlotContext(); - let currentCensus = useOrgCensusContext(); - let currentQuadrat = useQuadratContext(); + personnelRecording, + acceptedFiles, + parsedData, + uploadForm, + setIsDataUnsaved, + schema, + uploadCompleteMessage, + setUploadCompleteMessage, + setUploadError, + setReviewState, + setAllRowToCMID, + }) => { + const currentPlot = usePlotContext(); + const currentCensus = useOrgCensusContext(); + const currentQuadrat = useQuadratContext(); const [loading, setLoading] = useState(true); const [results, setResults] = useState([]); const [totalOperations, setTotalOperations] = useState(0); @@ -48,21 +48,21 @@ const UploadFireSQL: React.FC = ({ const response = await fetch( `/api/sqlload?schema=${schema}&formType=${uploadForm}&fileName=${fileName}&plot=${currentPlot?.plotID?.toString().trim()}&census=${currentCensus?.dateRanges[0].censusID.toString().trim()}&quadrat=${currentQuadrat?.quadratID?.toString().trim()}&user=${personnelRecording}`, { method: 'POST', - headers: { 'Content-Type': 'application/json' }, + headers: {'Content-Type': 'application/json'}, body: JSON.stringify(fileData[fileName]) }); setCompletedOperations((prevCompleted) => prevCompleted + 1); const result = await response.json(); if (result.idToRows) { if (uploadForm === 'measurements') { - Promise.all(result.idToRows.map(({ coreMeasurementID }: IDToRow) => + Promise.all(result.idToRows.map(({coreMeasurementID}: IDToRow) => fetch(`/api/details/cmid?schema=${schema}&cmid=${coreMeasurementID}`) .then(response => response.json()) )).then(details => { const newRowToCMID: DetailedCMIDRow[] = result.idToRows.map(({ - coreMeasurementID, - fileRow - }: IDToRow, index: number) => { + coreMeasurementID, + fileRow + }: IDToRow, index: number) => { const detailArray = details[index]; if (Array.isArray(detailArray) && detailArray.length > 0) { const detail = detailArray[0]; @@ -100,9 +100,9 @@ const UploadFireSQL: React.FC = ({ }); } else { const newRowToCMID: DetailedCMIDRow[] = result.idToRows.map(({ - coreMeasurementID, - fileRow - }: IDToRow) => ({ + coreMeasurementID, + fileRow + }: IDToRow) => ({ coreMeasurementID, fileName, row: fileRow @@ -195,7 +195,7 @@ const UploadFireSQL: React.FC = ({ return ( <> {loading ? ( - + {`Total Operations: ${totalOperations}`} = ({ ) : ( - - + + Upload Complete {startCountdown && ( - - + + {countdown} seconds remaining {uploadCompleteMessage} diff --git a/frontend/components/uploadsystem/segments/uploadparsefiles.tsx b/frontend/components/uploadsystem/segments/uploadparsefiles.tsx index 3e7c3c69..a03baee0 100644 --- a/frontend/components/uploadsystem/segments/uploadparsefiles.tsx +++ b/frontend/components/uploadsystem/segments/uploadparsefiles.tsx @@ -13,16 +13,16 @@ import { Tooltip, Typography } from "@mui/joy"; -import { UploadParseFilesProps } from "@/config/macros/uploadsystemmacros"; -import { getTableHeaders, TableHeadersByFormType } from "@/config/macros/formdetails"; -import { Button, Grid } from "@mui/material"; -import { DropzoneLogic } from "@/components/uploadsystemhelpers/dropzone"; -import { FileList } from "@/components/uploadsystemhelpers/filelist"; -import { LoadingButton } from "@mui/lab"; -import React, { useState } from "react"; -import { FileWithPath } from "react-dropzone"; +import {UploadParseFilesProps} from "@/config/macros/uploadsystemmacros"; +import {getTableHeaders} from "@/config/macros/formdetails"; +import {Button, Grid} from "@mui/material"; +import {DropzoneLogic} from "@/components/uploadsystemhelpers/dropzone"; +import {FileList} from "@/components/uploadsystemhelpers/filelist"; +import {LoadingButton} from "@mui/lab"; +import React, {useState} from "react"; +import {FileWithPath} from "react-dropzone"; import WarningIcon from "@mui/icons-material/Warning"; -import { usePlotContext } from "@/app/contexts/userselectionprovider"; +import {usePlotContext} from "@/app/contexts/userselectionprovider"; export default function UploadParseFiles(props: Readonly) { const { @@ -32,7 +32,7 @@ export default function UploadParseFiles(props: Readonly) } = props; const [fileToReplace, setFileToReplace] = useState(null); - let currentPlot = usePlotContext(); + const currentPlot = usePlotContext(); const handleFileChange = async (newFiles: FileWithPath[]) => { for (const file of newFiles) { @@ -47,13 +47,13 @@ export default function UploadParseFiles(props: Readonly) }; return ( - + - - + + setFileToReplace(null)}> + onClose={() => setFileToReplace(null)}> Confirm File Replace @@ -79,21 +79,21 @@ export default function UploadParseFiles(props: Readonly) - - + + You have selected {uploadForm}. Please ensure that your file has the following headers - before continuing:
+ before continuing:
{uploadForm !== '' && getTableHeaders(uploadForm, currentPlot?.usesSubquadrats ?? false).map(obj => obj.label).join(', ')} -
- +
+
{uploadForm === 'measurements' && ( <> } + startDecorator={} variant="soft" color="danger" - sx={{ mb: 2 }} + sx={{mb: 2}} > Please note: For date fields, accepted formats are @@ -109,24 +109,24 @@ export default function UploadParseFiles(props: Readonly)
- Hover over formats to see additionally accepted separators.
+ Hover over formats to see additionally accepted separators.
Please ensure your dates follow one of these formats.
- + The person recording the data is {personnelRecording}. )} - + + setDataViewActive={setDataViewActive}/> {acceptedFiles.length > 0 && @@ -137,7 +137,7 @@ export default function UploadParseFiles(props: Readonly) color="primary" disabled={acceptedFiles.length <= 0} onClick={handleInitialSubmit} - sx={{ mt: 2 }}> + sx={{mt: 2}}> Review Files diff --git a/frontend/components/uploadsystem/segments/uploadreviewfiles.tsx b/frontend/components/uploadsystem/segments/uploadreviewfiles.tsx index 8578f447..f4839c65 100644 --- a/frontend/components/uploadsystem/segments/uploadreviewfiles.tsx +++ b/frontend/components/uploadsystem/segments/uploadreviewfiles.tsx @@ -10,16 +10,17 @@ import { Grid, Pagination } from "@mui/material"; -import { Box, Checkbox, Modal, ModalDialog, Stack, Typography } from "@mui/joy"; -import { DisplayParsedDataGridInline } from "@/components/uploadsystemhelpers/displayparseddatagrid"; +import {Box, Checkbox, Modal, Typography} from "@mui/joy"; +import {DisplayParsedDataGridInline} from "@/components/uploadsystemhelpers/displayparseddatagrid"; import Divider from "@mui/joy/Divider"; -import React, { useState, useEffect } from "react"; -import { ReviewStates } from "@/config/macros/uploadsystemmacros"; -import { UploadReviewFilesProps } from "@/config/macros/uploadsystemmacros"; -import { FileCollectionRowSet, RequiredTableHeadersByFormType, TableHeadersByFormType } from "@/config/macros/formdetails"; -import { FileWithPath } from "react-dropzone"; -import { DropzoneLogic } from "@/components/uploadsystemhelpers/dropzone"; -import { FileList } from "@/components/uploadsystemhelpers/filelist"; +import React, {useState, useEffect} from "react"; +import {ReviewStates} from "@/config/macros/uploadsystemmacros"; +import {UploadReviewFilesProps} from "@/config/macros/uploadsystemmacros"; + + +import {FileWithPath} from "react-dropzone"; +import {DropzoneLogic} from "@/components/uploadsystemhelpers/dropzone"; +import {FileList} from "@/components/uploadsystemhelpers/filelist"; import CircularProgress from "@mui/joy/CircularProgress"; export default function UploadReviewFiles(props: Readonly) { @@ -80,7 +81,7 @@ export default function UploadReviewFiles(props: Readonly { - const { isValid, missingHeaders } = areHeadersValid(currentFileHeaders); + const {isValid, missingHeaders} = areHeadersValid(currentFileHeaders); setMissingHeaders(missingHeaders); if (!isValid) { @@ -129,7 +130,7 @@ export default function UploadReviewFiles(props: Readonly { const isChecked = currentFileHeaders.map(item => item.trim().toLowerCase()).includes(header.trim().toLowerCase()); return ( - + - + ); }); @@ -147,44 +148,47 @@ export default function UploadReviewFiles(props: Readonly 0; }; return ( <> - + {reuploadInProgress ? ( - + ) : ( <> - - + - - - - + + {currentFileHeaders.length > 0 ? ( renderHeaderCheckboxes() ) : ( No file selected or file has no headers. )} - + - + - + {acceptedFiles.length > 0 && acceptedFiles[dataViewActive - 1] && currentFileHeaders.length > 0 ? ( <> - + Form: {uploadForm} @@ -203,26 +207,30 @@ export default function UploadReviewFiles(props: Readonly ) : ( - The selected file is missing required headers or the headers cannot be read. Please check the file and re-upload.
- Your file's headers were: {currentFileHeaders.join(', ')}
+ The selected file is missing required headers or the headers cannot be read. Please check the file + and re-upload.
+ Your file's headers were: {currentFileHeaders.join(', ')}
The expected headers were {expectedHeaders.join(', ')}
)}
- setDataViewActive(page)} /> -
- + - - - + setIsReuploadDialogOpen(false)}> {requireReupload ? "Headers Missing or Corrupted" : "Re-upload Corrected File"} @@ -232,22 +240,25 @@ export default function UploadReviewFiles(props: Readonly - + {!requireReupload && } - + {"Do your files look correct?"} {missingHeaders.length > 0 ? ( <> - The following required headers are missing: {missingHeaders.join(', ')}.
- You can still proceed with the upload, but consider re-uploading the files with the required headers.
- If you would like to re-upload a corrected file, you can do so by clicking "Re-upload Corrected File". + The following required headers are missing: {missingHeaders.join(', ')}.
+ You can still proceed with the upload, but consider re-uploading the files with the required + headers.
+ If you would like to re-upload a corrected file, you can do so by clicking "Re-upload + Corrected File". ) : ( "Please press Confirm to upload your files to storage." diff --git a/frontend/components/uploadsystem/segments/uploadstart.tsx b/frontend/components/uploadsystem/segments/uploadstart.tsx index bb924731..b94165c4 100644 --- a/frontend/components/uploadsystem/segments/uploadstart.tsx +++ b/frontend/components/uploadsystem/segments/uploadstart.tsx @@ -2,11 +2,10 @@ import {ReviewStates} from "@/config/macros/uploadsystemmacros"; import {UploadStartProps} from "@/config/macros/uploadsystemmacros"; -import {Box, Button, ListSubheader, Stack, Tooltip, Typography} from "@mui/joy"; +import {Box, Button, Stack, Tooltip, Typography} from "@mui/joy"; import AutocompleteFixedData from "@/components/forms/autocompletefixeddata"; import React, {Dispatch, SetStateAction, useEffect, useState} from "react"; import Select, {SelectOption} from "@mui/joy/Select"; -import List from "@mui/joy/List"; import Option from '@mui/joy/Option'; import FinalizeSelectionsButton from "../../client/finalizeselectionsbutton"; import ArrowBackIcon from '@mui/icons-material/ArrowBack'; @@ -19,19 +18,17 @@ export default function UploadStart(props: Readonly) { uploadForm, personnelRecording, setPersonnelRecording, setReviewState, - dbhUnit, setDBHUnit, - homUnit, setHOMUnit, - coordUnit, setCoordUnit } = props; const [finish, setFinish] = useState(false); - let quadratListContext = useQuadratListContext(); - let currentQuadrat = useQuadratContext(); - let currentPlot = usePlotContext(); + const quadratListContext = useQuadratListContext(); + const currentQuadrat = useQuadratContext(); + const currentPlot = usePlotContext(); console.log('current quadrat: ', currentQuadrat); - const [quadrat, setQuadrat] = useState(); + const [quadrat, setQuadrat] = useState(currentQuadrat); const [quadratList, setQuadratList] = useState([]); const quadratDispatch = useQuadratDispatch(); - const [isQuadratConfirmed, setIsQuadratConfirmed] = useState(false); + const [isQuadratConfirmed, setIsQuadratConfirmed] = useState(!!currentQuadrat); + const handleChange = ( _event: React.SyntheticEvent | null, dispatcher: Dispatch>, @@ -44,15 +41,6 @@ export default function UploadStart(props: Readonly) { // Single function to handle "Back" action const handleBack = () => { - // if (dbhUnit !== '' || homUnit !== '' || coordUnit !== '') { - // setCoordUnit(''); - // setDBHUnit(''); - // setHOMUnit(''); - // } else if (personnelRecording !== '') { - // setPersonnelRecording(''); - // } else if (isQuadratConfirmed) { - // setIsQuadratConfirmed(false); - // } if (personnelRecording !== '') { setPersonnelRecording(''); } else if (isQuadratConfirmed) { @@ -66,7 +54,7 @@ export default function UploadStart(props: Readonly) { // ensure that selectable list is restricted by selected plot setQuadratList(quadratListContext?.filter(quadrat => quadrat?.plotID === currentPlot.id) || undefined); } - }, []); + }, [currentPlot]); useEffect(() => { if (finish) setReviewState(ReviewStates.UPLOAD_FILES); @@ -86,10 +74,8 @@ export default function UploadStart(props: Readonly) { const allSelectionsMade = uploadForm !== '' && (uploadForm !== 'measurements' || - // (personnelRecording !== '' && (dbhUnit !== '' && homUnit !== '' && coordUnit !== '') && isQuadratConfirmed)); (personnelRecording !== '' && isQuadratConfirmed)); - // const showBackButton = personnelRecording !== '' || (dbhUnit !== '' && homUnit !== '' && coordUnit !== '') || isQuadratConfirmed; const showBackButton = personnelRecording !== '' || isQuadratConfirmed; const renderQuadratValue = (option: SelectOption | null) => { @@ -97,7 +83,7 @@ export default function UploadStart(props: Readonly) { return Select a Quadrat; // or some placeholder JSX } - // Find the corresponding CensusRDS object + // Find the corresponding Quadrat object const selectedValue = option.value; // assuming option has a 'value' property const selectedQuadrat = quadratListContext?.find(c => c?.quadratName === selectedValue); @@ -151,89 +137,19 @@ export default function UploadStart(props: Readonly) { )} - {/* Unit of Measurement Selection for measurements -- DEPRECATED, UNITS INCORPORATED INTO FORM TYPE */} - {/* {uploadForm === 'measurements' && personnelRecording !== '' && (dbhUnit === '' || homUnit === '' || coordUnit === '') && ( - <> - - - - Select the DBH unit of measurement: - - - - - Select the HOM unit of measurement: - - - - - - Select the Coordinate unit of measurement: - - - - - - - )} */} - {/* {(uploadForm === "measurements" && personnelRecording !== '' && (dbhUnit !== '' && homUnit !== '' && coordUnit !== '') && !isQuadratConfirmed) && ( */} {(uploadForm === "measurements" && personnelRecording !== '' && !isQuadratConfirmed) && ( Select Quadrat: