diff --git a/backend/src/main/resources/db/migration/V1__create_schema.sql b/backend/src/main/resources/db/migration/V1__create_schema.sql index d40f9e76..660adddf 100644 --- a/backend/src/main/resources/db/migration/V1__create_schema.sql +++ b/backend/src/main/resources/db/migration/V1__create_schema.sql @@ -44,3 +44,19 @@ CREATE TABLE IF NOT EXISTS silva.oracle_extraction_logs ( CONSTRAINT oracle_extraction_logs_pk PRIMARY KEY(id) ); + +-- Create sequence if it doesn't exist for User Recent Openings +CREATE SEQUENCE IF NOT EXISTS silva.user_recent_openings_seq +START WITH 1 +INCREMENT BY 1 +MINVALUE 1 +NO MAXVALUE +CACHE 30; + +-- Use the sequence in your table creation or insert statements +CREATE TABLE IF NOT EXISTS silva.user_recent_openings ( + id BIGINT PRIMARY KEY DEFAULT nextval('silva.user_recent_openings_seq'), + opening_id VARCHAR(255) NOT NULL, + user_id VARCHAR(255) NOT NULL, + last_viewed TIMESTAMP DEFAULT NOW() +); diff --git a/frontend/package-lock.json b/frontend/package-lock.json index ccf6c592..86a1ab75 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -2706,7 +2706,6 @@ "version": "0.5.2", "resolved": "https://registry.npmjs.org/@mapbox/geojson-rewind/-/geojson-rewind-0.5.2.tgz", "integrity": "sha512-tJaT+RbYGJYStt7wI3cq4Nl4SXxG8W7JDG5DMJu97V25RnbNg3QtQtf+KD+VLjNpWKYsRvXDNmNrBgEETr1ifA==", - "license": "ISC", "peer": true, "dependencies": { "get-stream": "^6.0.1", @@ -2720,7 +2719,6 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/@mapbox/geojson-types/-/geojson-types-1.0.2.tgz", "integrity": "sha512-e9EBqHHv3EORHrSfbR9DqecPNn+AmuAoQxV6aL8Xu30bJMJR1o8PZLZzpk1Wq7/NfCbuhmakHTPYRhoqLsXRnw==", - "license": "ISC", "peer": true }, "node_modules/@mapbox/jsonlint-lines-primitives": { @@ -2736,7 +2734,6 @@ "version": "1.5.0", "resolved": "https://registry.npmjs.org/@mapbox/mapbox-gl-supported/-/mapbox-gl-supported-1.5.0.tgz", "integrity": "sha512-/PT1P6DNf7vjEEiPkVIRJkvibbqWtqnyGaBz3nfRdcxclNSnSdaLU5tfAgcD7I8Yt5i+L19s406YLl1koLnLbg==", - "license": "BSD-3-Clause", "peer": true, "peerDependencies": { "mapbox-gl": ">=0.32.1 <2.0.0" @@ -2746,28 +2743,24 @@ "version": "0.1.0", "resolved": "https://registry.npmjs.org/@mapbox/point-geometry/-/point-geometry-0.1.0.tgz", "integrity": "sha512-6j56HdLTwWGO0fJPlrZtdU/B13q8Uwmo18Ck2GnGgN9PCFyKTZ3UbXeEdRFh18i9XQ92eH2VdtpJHpBD3aripQ==", - "license": "ISC", "peer": true }, "node_modules/@mapbox/tiny-sdf": { "version": "1.2.5", "resolved": "https://registry.npmjs.org/@mapbox/tiny-sdf/-/tiny-sdf-1.2.5.tgz", "integrity": "sha512-cD8A/zJlm6fdJOk6DqPUV8mcpyJkRz2x2R+/fYcWDYG3oWbG7/L7Yl/WqQ1VZCjnL9OTIMAn6c+BC5Eru4sQEw==", - "license": "BSD-2-Clause", "peer": true }, "node_modules/@mapbox/unitbezier": { "version": "0.0.0", "resolved": "https://registry.npmjs.org/@mapbox/unitbezier/-/unitbezier-0.0.0.tgz", "integrity": "sha512-HPnRdYO0WjFjRTSwO3frz1wKaU649OBFPX3Zo/2WZvuRi6zMiRGui8SnPQiQABgqCf8YikDe5t3HViTVw1WUzA==", - "license": "BSD-2-Clause", "peer": true }, "node_modules/@mapbox/vector-tile": { "version": "1.3.1", "resolved": "https://registry.npmjs.org/@mapbox/vector-tile/-/vector-tile-1.3.1.tgz", "integrity": "sha512-MCEddb8u44/xfQ3oD+Srl/tNcQoqTw3goGk2oLsrFxOTc3dUp+kAnby3PvAeeBYSMSjSPD1nd1AJA6W49WnoUw==", - "license": "BSD-3-Clause", "peer": true, "dependencies": { "@mapbox/point-geometry": "~0.1.0" @@ -2777,7 +2770,6 @@ "version": "3.1.0", "resolved": "https://registry.npmjs.org/@mapbox/whoots-js/-/whoots-js-3.1.0.tgz", "integrity": "sha512-Es6WcD0nO5l+2BOQS4uLfNPYQaNDfbot3X1XUoloz+x0mPDS3eeORZJl06HXjwBG1fOGwCRnzK88LMdxKRrd6Q==", - "license": "ISC", "peer": true, "engines": { "node": ">=6.0.0" @@ -3118,6 +3110,14 @@ "url": "https://opencollective.com/unts" } }, + "node_modules/@polka/url": { + "version": "1.0.0-next.28", + "resolved": "https://registry.npmjs.org/@polka/url/-/url-1.0.0-next.28.tgz", + "integrity": "sha512-8LduaNlMZGwdZ6qWrKlfa+2M4gahzFkprZiAt2TF8uS0qQgBizKXpXURqvTJ4WtmupWxaLqjRb2UCTe72mu+Aw==", + "dev": true, + "optional": true, + "peer": true + }, "node_modules/@react-leaflet/core": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/@react-leaflet/core/-/core-2.1.0.tgz", @@ -3160,18 +3160,6 @@ } } }, - "node_modules/@rollup/pluginutils/node_modules/picomatch": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.2.tgz", - "integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==", - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" - } - }, "node_modules/@rollup/rollup-android-arm-eabi": { "version": "4.24.3", "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.24.3.tgz", @@ -4672,7 +4660,6 @@ "version": "2.1.2", "resolved": "https://registry.npmjs.org/@terraformer/arcgis/-/arcgis-2.1.2.tgz", "integrity": "sha512-IvdfqehcNAUtKU1OFMKwPT8EvdKlVFZ7q7ZKzkIF8XzYZIVsZLuXuOS1UBdRh5u/o+X5Gax7jiZhD8U/4TV+Jw==", - "license": "MIT", "peer": true, "dependencies": { "@terraformer/common": "^2.1.2" @@ -4682,7 +4669,6 @@ "version": "2.1.2", "resolved": "https://registry.npmjs.org/@terraformer/common/-/common-2.1.2.tgz", "integrity": "sha512-cwPdTFzIpekZhZRrgDEkqLKNPoqbyCBQHiemaovnGIeUx0Pl336MY/eCxzJ5zXkrQLVo9zPalq/vYW5HnyKevQ==", - "license": "MIT", "peer": true }, "node_modules/@testing-library/dom": { @@ -5725,6 +5711,29 @@ "url": "https://opencollective.com/vitest" } }, + "node_modules/@vitest/ui": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@vitest/ui/-/ui-2.1.4.tgz", + "integrity": "sha512-Zd9e5oU063c+j9N9XzGJagCLNvG71x/2tOme3Js4JEZKX55zsgxhJwUgLI8hkN6NjMLpdJO8d7nVUUuPGAA58Q==", + "dev": true, + "optional": true, + "peer": true, + "dependencies": { + "@vitest/utils": "2.1.4", + "fflate": "^0.8.2", + "flatted": "^3.3.1", + "pathe": "^1.1.2", + "sirv": "^3.0.0", + "tinyglobby": "^0.2.9", + "tinyrainbow": "^1.2.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "vitest": "2.1.4" + } + }, "node_modules/@vitest/utils": { "version": "2.1.4", "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-2.1.4.tgz", @@ -6626,7 +6635,6 @@ "version": "1.0.3", "resolved": "https://registry.npmjs.org/csscolorparser/-/csscolorparser-1.0.3.tgz", "integrity": "sha512-umPSgYwZkdFoUrH5hIq5kf0wPSXiro51nPw0j2K/c83KflkPSTBGMz6NJvMB+07VlL0y7VPo6QJcDjcgKTTm3w==", - "license": "MIT", "peer": true }, "node_modules/cssstyle": { @@ -7385,7 +7393,6 @@ "version": "2.2.4", "resolved": "https://registry.npmjs.org/earcut/-/earcut-2.2.4.tgz", "integrity": "sha512-/pjZsA1b4RPHbeWZQn66SWS8nZZWLQQ23oE3Eam7aroEFGEvwKAsJfZ9ytiEMycfzXWpca4FA9QIOehf7PocBQ==", - "license": "ISC", "peer": true }, "node_modules/eastasianwidth": { @@ -8386,7 +8393,6 @@ "version": "3.0.13", "resolved": "https://registry.npmjs.org/esri-leaflet/-/esri-leaflet-3.0.13.tgz", "integrity": "sha512-QP831w3vv2Hfy8aWUcUHJShSrg+EeIt5vxtTJZEHbgLzjS89QidEo1GrZ51u5KAIMuq8WPmNAurstU2AaCPS8g==", - "license": "Apache-2.0", "peer": true, "dependencies": { "@terraformer/arcgis": "^2.1.0", @@ -8400,7 +8406,6 @@ "version": "3.0.1", "resolved": "https://registry.npmjs.org/esri-leaflet-cluster/-/esri-leaflet-cluster-3.0.1.tgz", "integrity": "sha512-rqI4inhOzqZqGj0nfYNdfnDI51mGEpdefw4M4WoA0ig1FVh0V5W9ecRuHXbaBim+R7SSKMwY30S2wKipcYoF2w==", - "license": "Apache-2.0", "peer": true, "peerDependencies": { "esri-leaflet": "*", @@ -8412,7 +8417,6 @@ "version": "3.1.5", "resolved": "https://registry.npmjs.org/esri-leaflet-geocoder/-/esri-leaflet-geocoder-3.1.5.tgz", "integrity": "sha512-NYPPLJ2gYb8yf3wrUBxLNlyxv9yT1E3Q9rYGuaxRzCUaU/ULsRzs9vm6cE4xDWhO8g+9j5DUIi3zWMMKRyeBcw==", - "license": "Apache-2.0", "peer": true, "dependencies": { "esri-leaflet": "^3.0.2", @@ -8423,7 +8427,6 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/esri-leaflet-heatmap/-/esri-leaflet-heatmap-2.0.1.tgz", "integrity": "sha512-Yd6gNghJfG5Zv7+5A0SraqIMK6nqYSAkF925DXnpbi5BFbI8vbJFK8JyG9hFq+vC/iBtCCpQyGz0UjtHMzNrPg==", - "license": "Apache-2.0", "peer": true, "dependencies": { "leaflet": "^1.0.0-rc.3", @@ -8437,7 +8440,6 @@ "version": "3.1.5", "resolved": "https://registry.npmjs.org/esri-leaflet-vector/-/esri-leaflet-vector-3.1.5.tgz", "integrity": "sha512-XNRkrqfB4xKGQfRtjiMJsxwF4oiPnSNngQJrLBbMQMadLqcy+mZRAbDHDx/KEK6S0w0QoM4/A+A2rcNHHBQKlA==", - "license": "Apache-2.0", "peer": true, "dependencies": { "mapbox-gl": "1.13.1" @@ -8583,6 +8585,22 @@ "reusify": "^1.0.4" } }, + "node_modules/fdir": { + "version": "6.4.2", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.4.2.tgz", + "integrity": "sha512-KnhMXsKSPZlAhp7+IjUkRZKPb4fUyccpDrdFXbi4QL1qkmFh9kVY09Yox+n4MaOb3lHZ1Tv829C3oaaXoMYPDQ==", + "dev": true, + "optional": true, + "peer": true, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, "node_modules/fflate": { "version": "0.8.2", "resolved": "https://registry.npmjs.org/fflate/-/fflate-0.8.2.tgz", @@ -8792,7 +8810,6 @@ "version": "3.2.1", "resolved": "https://registry.npmjs.org/geojson-vt/-/geojson-vt-3.2.1.tgz", "integrity": "sha512-EvGQQi/zPrDA6zr6BnJD/YhwAkBP8nnJ9emh3EnHQKVMfg/MRVtPbMYdgVy/IaEmn4UfagD2a6fafPDL5hbtwg==", - "license": "ISC", "peer": true }, "node_modules/get-intrinsic": { @@ -8818,7 +8835,6 @@ "version": "6.0.1", "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz", "integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==", - "license": "MIT", "peer": true, "engines": { "node": ">=10" @@ -8862,7 +8878,6 @@ "version": "3.4.3", "resolved": "https://registry.npmjs.org/gl-matrix/-/gl-matrix-3.4.3.tgz", "integrity": "sha512-wcCp8vu8FT22BnvKVPjXa/ICBWRq/zjFfdofZy1WSpQZpphblv12/bOQLBC1rMM7SGOFS9ltVmKOHil5+Ml7gA==", - "license": "MIT", "peer": true }, "node_modules/glob": { @@ -9016,7 +9031,6 @@ "version": "1.1.0", "resolved": "https://registry.npmjs.org/grid-index/-/grid-index-1.1.0.tgz", "integrity": "sha512-HZRwumpOGUrHyxO5bqKZL0B0GlUpwtCAzZ42sgxUPniu33R1LSFH5yrIcBCHjkctCAh3mtWKcKd9J4vDDdeVHA==", - "license": "ISC", "peer": true }, "node_modules/has-bigints": { @@ -9999,6 +10013,18 @@ "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, + "node_modules/jest-util/node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, "node_modules/js-cookie": { "version": "3.0.5", "resolved": "https://registry.npmjs.org/js-cookie/-/js-cookie-3.0.5.tgz", @@ -10182,7 +10208,6 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/kdbush/-/kdbush-3.0.0.tgz", "integrity": "sha512-hRkd6/XW4HTsA9vjVpY9tuXJYLSlelnkTmVFu4M9/7MIYQtFcHpbugAU7UbOfjOiVSVYl2fqgBuJ32JUmRo5Ew==", - "license": "ISC", "peer": true }, "node_modules/keyv": { @@ -10231,7 +10256,6 @@ "version": "1.5.3", "resolved": "https://registry.npmjs.org/leaflet.markercluster/-/leaflet.markercluster-1.5.3.tgz", "integrity": "sha512-vPTw/Bndq7eQHjLBVlWpnGeLa3t+3zGiuM7fJwCkiMFq+nmRuG3RI3f7f4N4TDX7T4NpbAXpR2+NTRSEGfCSeA==", - "license": "MIT", "peer": true, "peerDependencies": { "leaflet": "^1.3.1" @@ -10424,7 +10448,6 @@ "version": "1.13.1", "resolved": "https://registry.npmjs.org/mapbox-gl/-/mapbox-gl-1.13.1.tgz", "integrity": "sha512-GSyubcoSF5MyaP8z+DasLu5v7KmDK2pp4S5+VQ5WdVQUOaAqQY4jwl4JpcdNho3uWm2bIKs7x1l7q3ynGmW60g==", - "license": "SEE LICENSE IN LICENSE.txt", "peer": true, "dependencies": { "@mapbox/geojson-rewind": "^0.5.0", @@ -10478,6 +10501,17 @@ "node": ">=8.6" } }, + "node_modules/micromatch/node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, "node_modules/mime-db": { "version": "1.52.0", "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", @@ -10544,6 +10578,17 @@ "node": ">=16 || 14 >=14.17" } }, + "node_modules/mrmime": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/mrmime/-/mrmime-2.0.0.tgz", + "integrity": "sha512-eu38+hdgojoyq63s+yTpN4XMBdt5l8HhMhc4VKLO9KM5caLIBvUm4thi7fFaxyTmCKeNnXZ5pAlBwCUnhA09uw==", + "dev": true, + "optional": true, + "peer": true, + "engines": { + "node": ">=10" + } + }, "node_modules/ms": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", @@ -10554,7 +10599,6 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/murmurhash-js/-/murmurhash-js-1.0.0.tgz", "integrity": "sha512-TvmkNhkv8yct0SVBSy+o8wYzXjE4Zz3PCesbfs8HiCXXdcTuocApFv11UWlNFWKYsP2okqrhb7JNlSm9InBhIw==", - "license": "MIT", "peer": true }, "node_modules/nanoid": { @@ -10950,7 +10994,6 @@ "version": "3.3.0", "resolved": "https://registry.npmjs.org/pbf/-/pbf-3.3.0.tgz", "integrity": "sha512-XDF38WCH3z5OV/OVa8GKUNtLAyneuzbCisx7QUCF8Q6Nutx0WnJrQe5O+kOtBlLfRNUws98Y58Lblp+NJG5T4Q==", - "license": "BSD-3-Clause", "peer": true, "dependencies": { "ieee754": "^1.1.12", @@ -10974,12 +11017,11 @@ "license": "ISC" }, "node_modules/picomatch": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", - "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", - "license": "MIT", + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.2.tgz", + "integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==", "engines": { - "node": ">=8.6" + "node": ">=12" }, "funding": { "url": "https://github.com/sponsors/jonschlinkert" @@ -11027,7 +11069,6 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/potpack/-/potpack-1.0.2.tgz", "integrity": "sha512-choctRBIV9EMT9WGAZHn3V7t0Z2pMQyl0EZE6pFc/6ml3ssw7Dlf/oAOvFwjm1HVsqfQN8GfeFyJ+d8tRzqueQ==", - "license": "ISC", "peer": true }, "node_modules/prelude-ls": { @@ -11096,7 +11137,6 @@ "version": "3.6.0", "resolved": "https://registry.npmjs.org/protocol-buffers-schema/-/protocol-buffers-schema-3.6.0.tgz", "integrity": "sha512-TdDRD+/QNdrCGCE7v8340QyuXd4kIWIgapsE2+n/SaGiSSbomYl4TjHlvIoCWRpE7wFt02EpB35VVA2ImcBVqw==", - "license": "MIT", "peer": true }, "node_modules/proxy-from-env": { @@ -11155,7 +11195,6 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/quickselect/-/quickselect-2.0.0.tgz", "integrity": "sha512-RKJ22hX8mHe3Y6wH/N3wCM6BWtjaxIyyUIkpHOvfFnxdI4yD4tBXEBKSbriGujF6jnSVkJrffuo6vxACiSSxIw==", - "license": "ISC", "peer": true }, "node_modules/raf": { @@ -11416,7 +11455,6 @@ "version": "2.1.0", "resolved": "https://registry.npmjs.org/resolve-protobuf-schema/-/resolve-protobuf-schema-2.1.0.tgz", "integrity": "sha512-kI5ffTiZWmJaS/huM8wZfEMer1eRd7oJQhDuxeCLe3t7N7mX3z94CN0xPxBQxFYQTSNz9T0i+v6inKqSdK8xrQ==", - "license": "MIT", "peer": true, "dependencies": { "protocol-buffers-schema": "^3.3.1" @@ -11796,6 +11834,22 @@ "is-arrayish": "^0.3.1" } }, + "node_modules/sirv": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/sirv/-/sirv-3.0.0.tgz", + "integrity": "sha512-BPwJGUeDaDCHihkORDchNyyTvWFhcusy1XMmhEVTQTwGeybFbp8YEmB+njbPnth1FibULBSBVwCQni25XlCUDg==", + "dev": true, + "optional": true, + "peer": true, + "dependencies": { + "@polka/url": "^1.0.0-next.24", + "mrmime": "^2.0.0", + "totalist": "^3.0.0" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/slash": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", @@ -12152,7 +12206,6 @@ "version": "7.1.5", "resolved": "https://registry.npmjs.org/supercluster/-/supercluster-7.1.5.tgz", "integrity": "sha512-EulshI3pGUM66o6ZdH3ReiFcvHpM3vAigyK+vcxdjpJyEbIIrtbmBdY23mGgnI24uXiGFvrGq9Gkum/8U7vJWg==", - "license": "ISC", "peer": true, "dependencies": { "kdbush": "^3.0.0" @@ -12303,7 +12356,6 @@ "version": "1.0.3", "resolved": "https://registry.npmjs.org/tiny-binary-search/-/tiny-binary-search-1.0.3.tgz", "integrity": "sha512-STSHX/L5nI9WTLv6wrzJbAPbO7OIISX83KFBh2GVbX1Uz/vgZOU/ANn/8iV6t35yMTpoPzzO+3OQid3mifE0CA==", - "license": "Apache-2.0", "peer": true }, "node_modules/tinybench": { @@ -12320,6 +12372,21 @@ "dev": true, "license": "MIT" }, + "node_modules/tinyglobby": { + "version": "0.2.10", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.10.tgz", + "integrity": "sha512-Zc+8eJlFMvgatPZTl6A9L/yht8QqdmUNtURHaKZLmKBE12hNPSrqNkUp2cs3M/UKmNVVAMFQYSjYIVHDjW5zew==", + "dev": true, + "optional": true, + "peer": true, + "dependencies": { + "fdir": "^6.4.2", + "picomatch": "^4.0.2" + }, + "engines": { + "node": ">=12.0.0" + } + }, "node_modules/tinypool": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/tinypool/-/tinypool-1.0.1.tgz", @@ -12334,7 +12401,6 @@ "version": "2.0.3", "resolved": "https://registry.npmjs.org/tinyqueue/-/tinyqueue-2.0.3.tgz", "integrity": "sha512-ppJZNDuKGgxzkHihX8v9v9G5f+18gzaTfrukGrq6ueg0lmH4nqVnA2IPG0AEH3jKEk2GRJCUhDoqpoiw3PHLBA==", - "license": "ISC", "peer": true }, "node_modules/tinyrainbow": { @@ -12415,6 +12481,17 @@ "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", "license": "MIT" }, + "node_modules/totalist": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/totalist/-/totalist-3.0.1.tgz", + "integrity": "sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ==", + "dev": true, + "optional": true, + "peer": true, + "engines": { + "node": ">=6" + } + }, "node_modules/tough-cookie": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-5.0.0.tgz", @@ -12914,7 +12991,6 @@ "version": "3.1.3", "resolved": "https://registry.npmjs.org/vt-pbf/-/vt-pbf-3.1.3.tgz", "integrity": "sha512-2LzDFzt0mZKZ9IpVF2r69G9bXaP2Q2sArJCmcCgvfTdCCZzSyz4aCLoQyUilu37Ll56tCblIZrXFIjNUpGIlmA==", - "license": "MIT", "peer": true, "dependencies": { "@mapbox/point-geometry": "0.1.0", diff --git a/frontend/src/__test__/components/ActionButtons.test.tsx b/frontend/src/__test__/components/ActionButtons.test.tsx new file mode 100644 index 00000000..6f554d9b --- /dev/null +++ b/frontend/src/__test__/components/ActionButtons.test.tsx @@ -0,0 +1,39 @@ +// ActionButtons.test.tsx +import React from "react"; +import { vi } from "vitest"; +import { render, screen, fireEvent } from "@testing-library/react"; +import ActionButtons from "../../components/ActionButtons"; + +// Mock console.log +const consoleLogMock = vi.spyOn(console, "log").mockImplementationOnce(() =>vi.fn()) ; + +afterEach(() => { + consoleLogMock.mockClear(); +}); + +afterAll(() => { + consoleLogMock.mockRestore(); +}); + +describe("ActionButtons", () => { + const rowId = "test-row-id"; + + it("renders the 'View' and 'Document Download' buttons", () => { + render(); + + // Check that both buttons are in the document + expect(screen.getByRole("button", { name: /View/i })).toBeInTheDocument(); + expect(screen.getByRole("button", { name: /Document Download/i })).toBeInTheDocument(); + }); + + it("calls console.log with rowId when the 'View' button is clicked", () => { + render(); + + // Find the "View" button and click it + const viewButton = screen.getByRole("button", { name: /View/i }); + fireEvent.click(viewButton); + + // Check if console.log was called with the correct rowId + expect(consoleLogMock).toHaveBeenCalledWith(rowId); + }); +}); diff --git a/frontend/src/__test__/components/BarChartGrouped.test.tsx b/frontend/src/__test__/components/BarChartGrouped.test.tsx index 16255636..f1d12e98 100644 --- a/frontend/src/__test__/components/BarChartGrouped.test.tsx +++ b/frontend/src/__test__/components/BarChartGrouped.test.tsx @@ -1,26 +1,40 @@ import React from 'react'; -import { render, screen, waitFor } from '@testing-library/react'; -import { describe, expect, it, vi } from 'vitest'; +import { render, screen } from '@testing-library/react'; +import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; import BarChartGrouped from '../../components/BarChartGrouped'; -import { fetchOpeningsPerYear } from '../../services/OpeningService'; - -vi.mock('../../services/OpeningService', () => ({ - fetchOpeningsPerYear: vi.fn(() => Promise.resolve([ - { group: '2022', key: 'Openings', value: 10 }, - { group: '2023', key: 'Openings', value: 15 }, - ])), +import { useDistrictListQuery, useFetchOpeningsPerYear } from '../../services/queries/dashboard/dashboardQueries'; +import { describe, expect, it } from 'vitest'; +import { vi } from 'vitest'; +import '@testing-library/jest-dom'; +// Mock the hook +vi.mock('../../services/queries/dashboard/dashboardQueries', () => ({ + useFetchOpeningsPerYear: vi.fn(), + useDistrictListQuery: vi.fn(), })); -describe('BarChartGrouped component tests', () => { - it('should render loading state while fetching data and clean it after', async () => { - render(); +const queryClient = new QueryClient(); - const element = await waitFor(() => screen.getByText('Loading...')); +describe('BarChartGrouped component', () => { + it('should display loading state when data is fetching', () => { + // Mock loading state for openings data + (useFetchOpeningsPerYear as any).mockReturnValue({ + data: [], + isLoading: true, + }); - expect(element).toBeDefined(); - - expect(fetchOpeningsPerYear).toHaveBeenCalled(); - expect(screen.queryByTestId('bar-chart')).toBeDefined(); - }); + // If you're using useDistrictListQuery, mock it too + (useDistrictListQuery as any).mockReturnValue({ + data: [], + isLoading: false, + }); -}); + render( + + + + ); + + // Check if loading text is displayed + expect(screen.getByText('Loading...')).toBeInTheDocument(); + }); +}); \ No newline at end of file diff --git a/frontend/src/__test__/components/Dashboard/Opening/RecentOpeningsDataTable.test.tsx b/frontend/src/__test__/components/Dashboard/Opening/RecentOpeningsDataTable.test.tsx new file mode 100644 index 00000000..cb8c535f --- /dev/null +++ b/frontend/src/__test__/components/Dashboard/Opening/RecentOpeningsDataTable.test.tsx @@ -0,0 +1,69 @@ +// src/__test__/components/SilvicultureSearch/Openings/OpeningsSearchBar.test.tsx + +import React from "react"; +import { render, screen } from "@testing-library/react"; +import "@testing-library/jest-dom"; +import { vi } from "vitest"; +import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; +import PaginationProvider from "../../../../contexts/PaginationProvider"; +import RecentOpeningsDataTable from "../../../../components/Dashboard/Opening/RecentOpeningsDataTable"; +import { MemoryRouter } from "react-router-dom"; +import exp from "constants"; + +describe("OpeningsSearchBar", () => { + // Create a new QueryClient instance for each test + const queryClient = new QueryClient(); + const handleCheckboxChange = vi.fn() + const setLoadId = vi.fn() + const toggleSpatial = vi.fn() + const showSpatial = false + const data = { data: [], perPage: 0, totalPages: 0 } + const headers = [] + + it("shows appropriate message when no data is in the table", () => { + render( + + + + + + + + ); + expect(screen.getByText(/There are no openings to show yet/i)).toBeInTheDocument(); + expect(screen.queryByText(/Your recent openings will appear here once you generate one/i)).toBeInTheDocument(); + }); + + it("renders a blank table when rows is empty array", () => { + render( + + + + + + + + ); + // Check if the table is present + const table = screen.getByRole('table'); + expect(table).toBeInTheDocument(); + }); +}); \ No newline at end of file diff --git a/frontend/src/__test__/components/OpeningScreenDataTable.test.tsx b/frontend/src/__test__/components/OpeningScreenDataTable.test.tsx deleted file mode 100644 index 8f8bec2f..00000000 --- a/frontend/src/__test__/components/OpeningScreenDataTable.test.tsx +++ /dev/null @@ -1,90 +0,0 @@ -import React from 'react'; -import { act, render } from '@testing-library/react'; -import { describe, expect, it, vi } from 'vitest'; -import OpeningScreenDataTable from '../../components/OpeningScreenDataTable'; -import PaginationContext from '../../contexts/PaginationContext'; -import { RecentOpening } from '../../types/RecentOpening'; - -const rows: RecentOpening[] = [{ - id: '123', - openingId: '123', - fileId: '1', - cuttingPermit: '1', - timberMark: '1', - cutBlock: '1', - grossAreaHa: 1, - statusDesc: 'Approved', - categoryDesc: 'Another:Another', - disturbanceStart: '1', - entryTimestamp: '1', - updateTimestamp: '1', -}]; - -const headers = [{ key: 'openingId', header: 'Opening Id', }, - { key: 'forestFileId', header: 'File Id', }, - { key: 'cuttingPermit', header: 'Cutting permit', }, - { key: 'timberMark', header: 'Timber mark', }, - { key: 'cutBlock', header: 'Cut block', }, - { key: 'grossAreaHa', header: 'Gross area (ha)', }, - { key: 'status', header: 'Status', }, - { key: 'category', header: 'Category', }, - { key: 'disturbanceStart', header: 'Disturbance start', }, - { key: 'createdAt', header: 'Created At', }, - { key: 'lastViewed', header: 'Last Viewed', }, - { key: 'actions', header: 'Actions', }, -]; - -const setOpeningId = vi.fn(); -const paginationValueMock = { - getCurrentData: () => rows, - currentPage: 0, - totalPages: 0, - handlePageChange: vi.fn(), - handleItemsPerPageChange: vi.fn(), - itemsPerPage: 5, - totalResultItems:100, - setTotalResultItems:vi.fn(), - setPageData: vi.fn(), - setInitialItemsPerPage: vi.fn(), -}; - -describe('Opening Screen Data table component test', () => { - it('should remove the row checkbox when showSpatial is false', () => { - const { queryByTestId } = render( - - - - ); - - act(() => { - const tableSelectionRow: HTMLElement | null = queryByTestId('checkbox__opening-screen-data-table_1'); - expect(tableSelectionRow).toBeNull(); - }); - }); - - it('should display the row checkbox when showSpatial is true', () => { - const { queryByTestId } = render( - - - - ); - - act(() => { - const tableSelectionRow: HTMLElement | null = queryByTestId('checkbox__opening-screen-data-table_1'); - // The next line should be "not.toBeNull()" however, Carbon React team forgot to add data-testid - // attribute to this component (TableSelectRow), making it impossible to get by testid value. - // Once we have that fixed, please get back here and update the next statement. - expect(tableSelectionRow).toBeNull(); - }); - }); -}); diff --git a/frontend/src/__test__/components/OpeningsTab.test.tsx b/frontend/src/__test__/components/OpeningsTab.test.tsx index 10bd1b32..d96aa034 100644 --- a/frontend/src/__test__/components/OpeningsTab.test.tsx +++ b/frontend/src/__test__/components/OpeningsTab.test.tsx @@ -4,8 +4,7 @@ import { render, act, waitFor, screen } from '@testing-library/react'; import OpeningsTab from '../../components/OpeningsTab'; import { AuthProvider } from '../../contexts/AuthProvider'; import { getWmsLayersWhitelistUsers } from '../../services/SecretsService'; -import { fetchRecentOpenings } from '../../services/OpeningService'; -import { RecentOpening } from '../../types/RecentOpening'; +import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; import PaginationProvider from '../../contexts/PaginationProvider'; @@ -20,32 +19,66 @@ vi.mock('../../services/OpeningService', async () => { fetchRecentOpenings: vi.fn(), }; }); - - -const rows: RecentOpening[] = [{ - id: '123', - openingId: '123', - fileId: '1', - cuttingPermit: '1', - timberMark: '1', - cutBlock: '1', - grossAreaHa: 1, - statusDesc: 'Approved', - categoryDesc: 'Another:Another', - disturbanceStart: '1', - entryTimestamp: '1', - updateTimestamp: '1', -}]; +const queryClient = new QueryClient(); describe('Openings Tab test',() => { it('should render properly',async () =>{ (getWmsLayersWhitelistUsers as vi.Mock).mockResolvedValue([{userName: 'TEST'}]); - (fetchRecentOpenings as vi.Mock).mockResolvedValue(rows); + await act(async () => { - render(); + render( + + + + + + + + ); }); expect(screen.getByText('Recent openings')).toBeInTheDocument(); }); + it('should have Hide map when the showSpatial is true',async () =>{ + (getWmsLayersWhitelistUsers as vi.Mock).mockResolvedValue([{userName: 'TEST'}]); + + await act(async () => { + render( + + + + + + + + ); + }); + expect(screen.getByText('Hide map')).toBeInTheDocument(); + }); + + it('should render the table', async () => { + // Mocking state values + vi.spyOn(React, 'useState') + .mockImplementationOnce(() => [null, vi.fn()]) // for loadId + .mockImplementationOnce(() => [true, vi.fn()]) // for openingPolygonNotFound + .mockImplementationOnce(() => [{ userName: 'TEST' }, vi.fn()]) // for wmsUsersWhitelist + .mockImplementationOnce(() => [[], vi.fn()]); // for headers + + (getWmsLayersWhitelistUsers as vi.Mock).mockResolvedValue([{ userName: 'TEST' }]); + + await act(async () => { + render( + + + + + + + + ); + }); + expect(screen.getByRole('table')).toBeInTheDocument(); + }); + }); \ No newline at end of file diff --git a/frontend/src/__test__/components/SilvicultureSearch/Openings/OpeningSearchTab.test.tsx b/frontend/src/__test__/components/SilvicultureSearch/Openings/OpeningSearchTab.test.tsx index 80866593..ed5e13d7 100644 --- a/frontend/src/__test__/components/SilvicultureSearch/Openings/OpeningSearchTab.test.tsx +++ b/frontend/src/__test__/components/SilvicultureSearch/Openings/OpeningSearchTab.test.tsx @@ -136,6 +136,6 @@ describe('OpeningSearchTab', () => { const searchInput = screen.getByPlaceholderText('Search by opening ID, opening number, timber mark or file ID'); await act(async () => await userEvent.type(searchInput, 'potato')); await act(async () => (await screen.findByTestId('search-button')).click()); - expect(screen.getByText('Results not found')).toBeInTheDocument(); + expect(screen.getByText('There are no openings to show yet')).toBeInTheDocument(); }); }); \ No newline at end of file diff --git a/frontend/src/__test__/components/SilvicultureSearch/Openings/SearchScreenDataTable.test.tsx b/frontend/src/__test__/components/SilvicultureSearch/Openings/SearchScreenDataTable.test.tsx index b2d2e752..7b8108dc 100644 --- a/frontend/src/__test__/components/SilvicultureSearch/Openings/SearchScreenDataTable.test.tsx +++ b/frontend/src/__test__/components/SilvicultureSearch/Openings/SearchScreenDataTable.test.tsx @@ -1,15 +1,245 @@ import React from 'react'; -import { render } from '@testing-library/react'; +import { render, screen } from '@testing-library/react'; import { describe, expect, it, vi } from 'vitest'; -import SearchScreenDataTable from '../../../../components/SilvicultureSearch/Openings/SearchScreenDataTable/index'; -import { columns, rows } from '../../../../components/SilvicultureSearch/Openings/SearchScreenDataTable/testData'; +import SearchScreenDataTable from '../../../../components/SilvicultureSearch/Openings/SearchScreenDataTable'; +import { columns } from '../../../../components/SilvicultureSearch/Openings/SearchScreenDataTable/headerData'; import PaginationProvider from '../../../../contexts/PaginationProvider'; import { NotificationProvider } from '../../../../contexts/NotificationProvider'; import { BrowserRouter } from 'react-router-dom'; +import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; import { OpeningsSearchProvider } from '../../../../contexts/search/OpeningsSearch'; const handleCheckboxChange = vi.fn(); const toggleSpatial = vi.fn(); +const queryClient = new QueryClient(); + +export const rows:any = [ + { + id: '114207', + openingId: '114207', + fileId: 'TFL47', + cuttingPermit: '12S', + timberMark: '47/12S', + cutBlock: '12-69', + grossAreaHa: '12.9', + status: 'Free growing', + category: 'FTML', + disturbanceStart: '-', + createdAt: '2022-10-27', + orgUnit: 'DCC - Cariboo chilcotin natural resources', + lastViewed: '2022-10-27' + }, + { + id: '114206', + openingId: '114206', + fileId: 'TFL47', + cuttingPermit: '12T', + timberMark: '47/12S', + cutBlock: '12-69', + grossAreaHa: '12.9', + status: 'Free growing', + category: 'FTML', + disturbanceStart: '-', + createdAt: '2022-09-04', + orgUnit: 'DCC - Cariboo chilcotin natural resources', + lastViewed: '2022-10-27' + }, + { + id: '114205', + openingId: '114205', + fileId: 'TFL47', + cuttingPermit: '12T', + timberMark: '47/12S', + cutBlock: '12-44A', + grossAreaHa: '12.9', + status: 'Free growing', + category: 'FTML', + disturbanceStart: '-', + createdAt: '2022-09-04', + orgUnit: 'DCC - Cariboo chilcotin natural resources', + lastViewed: '2022-10-27' + }, + { + id: '114204', + openingId: '114204', + fileId: 'TFL47', + cuttingPermit: '12T', + timberMark: '47/12S', + cutBlock: '12-44A', + grossAreaHa: '12.9', + status: 'Active', + category: 'FTML', + disturbanceStart: '-', + createdAt: '2022-01-16', + orgUnit: 'DCC - Cariboo chilcotin natural resources', + lastViewed: '2022-10-26' + }, + { + id: '114203', + openingId: '114203', + fileId: 'TFL47', + cuttingPermit: '12T', + timberMark: '47/12S', + cutBlock: '12-44A', + grossAreaHa: '12.9', + status: 'Active', + category: 'FTML', + disturbanceStart: '-', + createdAt: '2021-12-08', + orgUnit: 'DCC - Cariboo chilcotin natural resources', + lastViewed: '2022-10-26' + }, + { + id: '114202', + openingId: '114202', + fileId: 'TFL47', + cuttingPermit: '12T', + timberMark: '47/12S', + cutBlock: '12-44A', + grossAreaHa: '12.9', + status: 'Free growing', + category: 'FTML', + disturbanceStart: '-', + createdAt: '2021-11-15', + orgUnit: 'DCC - Cariboo chilcotin natural resources', + lastViewed: '2022-10-25' + }, + { + id: '114201', + openingId: '114201', + fileId: 'TFL47', + cuttingPermit: '12T', + timberMark: '47/12S', + cutBlock: '12-44A', + grossAreaHa: '12.9', + status: 'Free growing', + category: 'FTML', + disturbanceStart: '-', + createdAt: '2021-11-15', + orgUnit: 'DCC - Cariboo chilcotin natural resources', + lastViewed: '2022-10-25' + }, + { + id: '114200', + openingId: '114200', + fileId: 'TFL47', + cuttingPermit: '12T', + timberMark: '47/12S', + cutBlock: '12-44A', + grossAreaHa: '12.9', + status: 'Active', + category: 'FTML', + disturbanceStart: '-', + createdAt: '2021-10-20', + orgUnit: 'DCC - Cariboo chilcotin natural resources', + lastViewed: '2022-10-24' + }, + { + id: '114199', + openingId: '114199', + fileId: 'TFL47', + cuttingPermit: '12T', + timberMark: '47/12S', + cutBlock: '12-44A', + grossAreaHa: '12.9', + status: 'Active', + category: 'FTML', + disturbanceStart: '-', + createdAt: '2021-10-20', + orgUnit: 'DCC - Cariboo chilcotin natural resources', + lastViewed: '2022-10-24' + }, + { + id: '114198', + openingId: '114198', + fileId: 'TFL47', + cuttingPermit: '12T', + timberMark: '47/12S', + cutBlock: '12-44A', + grossAreaHa: '12.9', + status: 'Free growing', + category: 'FTML', + disturbanceStart: '-', + createdAt: '2021-09-12', + orgUnit: 'DCC - Cariboo chilcotin natural resources', + lastViewed: '2022-10-23' + }, + { + id: '114197', + openingId: '114197', + fileId: 'TFL47', + cuttingPermit: '12T', + timberMark: '47/12S', + cutBlock: '12-44A', + grossAreaHa: '12.9', + status: 'Free growing', + category: 'FTML', + disturbanceStart: '-', + createdAt: '2021-09-12', + orgUnit: 'DCC - Cariboo chilcotin natural resources', + lastViewed: '2022-10-23' + }, + { + id: '114196', + openingId: '114196', + fileId: 'TFL47', + cuttingPermit: '12T', + timberMark: '47/12S', + cutBlock: '12-44A', + grossAreaHa: '12.9', + status: 'Free growing', + category: 'FTML', + disturbanceStart: '-', + createdAt: '2021-08-05', + orgUnit: 'DCC - Cariboo chilcotin natural resources', + lastViewed: '2022-10-22' + }, + { + id: '114195', + openingId: '114195', + fileId: 'TFL47', + cuttingPermit: '12T', + timberMark: '47/12S', + cutBlock: '12-44A', + grossAreaHa: '12.9', + status: 'Free growing', + category: 'FTML', + disturbanceStart: '-', + createdAt: '2021-08-05', + orgUnit: 'DCC - Cariboo chilcotin natural resources', + lastViewed: '2022-10-22' + }, + { + id: '114194', + openingId: '114194', + fileId: 'TFL47', + cuttingPermit: '12T', + timberMark: '47/12S', + cutBlock: '12-44A', + grossAreaHa: '12.9', + status: 'Active', + category: 'FTML', + disturbanceStart: '-', + createdAt: '2021-07-10', + orgUnit: 'DCC - Cariboo chilcotin natural resources', + lastViewed: '2022-10-21' + }, + { + id: '114193', + openingId: '114193', + fileId: 'TFL47', + cuttingPermit: '12T', + timberMark: '47/12S', + cutBlock: '12-44A', + grossAreaHa: '12.9', + status: 'Active', + category: 'FTML', + disturbanceStart: '-', + createdAt: '2021-07-10', + orgUnit: 'DCC - Cariboo chilcotin natural resources', + lastViewed: '2022-10-21' + } +]; describe('Search Screen Data table test', () => { @@ -17,6 +247,7 @@ describe('Search Screen Data table test', () => { const { getByText, container } = render( + @@ -32,6 +263,7 @@ describe('Search Screen Data table test', () => { + ); @@ -45,6 +277,7 @@ describe('Search Screen Data table test', () => { const { getByText, container } = render( + @@ -60,6 +293,7 @@ describe('Search Screen Data table test', () => { + ); @@ -69,4 +303,32 @@ describe('Search Screen Data table test', () => { expect(container.querySelector('.total-search-results')).toContainHTML('0'); }); + it('should render the checkbox for showSPatial being true', () => { + render( + + + + + + + + + + + + ); + const checkbox = document.querySelector('.cds--checkbox-group'); + expect(checkbox).toBeInTheDocument(); + + }); + + }); \ No newline at end of file diff --git a/frontend/src/__test__/components/SpatialCheckbox.test.tsx b/frontend/src/__test__/components/SpatialCheckbox.test.tsx new file mode 100644 index 00000000..267bbdf8 --- /dev/null +++ b/frontend/src/__test__/components/SpatialCheckbox.test.tsx @@ -0,0 +1,61 @@ +// SpatialCheckbox.test.tsx +import React from "react"; +import { vi } from "vitest"; +import { render, screen, fireEvent } from "@testing-library/react"; +import SpatialCheckbox from "../../components/SpatialCheckbox"; + +describe("SpatialCheckbox", () => { + const rowId = "test-row-id"; + const handleRowSelectionChanged = vi.fn(); + + afterEach(() => { + handleRowSelectionChanged.mockClear(); + }); + + it("renders the checkbox with the tooltip", () => { + render( + + ); + + // Check if the checkbox is in the document + const checkbox = screen.getByRole("checkbox", { + name: /click to view this opening's map activity/i, + }); + expect(checkbox).toBeInTheDocument(); + }); + + it("sets the checkbox to 'checked' if rowId is in selectedRows", () => { + render( + + ); + + const checkbox = screen.getByRole("checkbox"); + expect(checkbox).toBeChecked(); + }); + + it("calls handleRowSelectionChanged with rowId when checkbox is toggled", () => { + render( + + ); + + const checkbox = screen.getByRole("checkbox"); + + // Simulate a click on the checkbox + fireEvent.click(checkbox); + + // Check if handleRowSelectionChanged was called with the correct rowId + expect(handleRowSelectionChanged).toHaveBeenCalledWith(rowId); + }); +}); diff --git a/frontend/src/__test__/components/TableCellContent.test.tsx b/frontend/src/__test__/components/TableCellContent.test.tsx new file mode 100644 index 00000000..3cebab09 --- /dev/null +++ b/frontend/src/__test__/components/TableCellContent.test.tsx @@ -0,0 +1,98 @@ +// TableCellContent.test.tsx +import React from "react"; +import { vi } from "vitest"; +import { render, screen } from "@testing-library/react"; +import TableCellContent from "../../components/TableCellContent"; +import { OpeningsSearch } from "../../types/OpeningsSearch"; + +// Mock components +vi.mock("../StatusTag", () => ({ + default: ({ code }: { code: string }) =>
{`Status: ${code}`}
, +})); +vi.mock("../ActionButtons", () => ({ + default: ({ rowId }: { rowId: string }) =>
{`Actions for ${rowId}`}
, +})); +vi.mock("../SpatialCheckbox", () => ({ + default: ({ rowId }: { rowId: string }) =>
{`Spatial Checkbox for ${rowId}`}
, +})); + +describe("TableCellContent", () => { + const row = { + openingId: 1, + statusDescription: "Active", + categoryCode: "A", + categoryDescription: "Category A", + } as OpeningsSearch; + const selectedRows: string[] = []; + const handleRowSelectionChanged = vi.fn(); + + it("renders StatusTag when headerKey is 'statusDescription'", () => { + render( + + ); + + expect(screen.getByText(/Active/i)).toBeInTheDocument(); + }); + + it("renders ActionButtons and optionally SpatialCheckbox when headerKey is 'actions'", () => { + const { rerender } = render( + + ); + console.log(screen.debug()); + expect(screen.queryByText(/View/i)).toBeInTheDocument(); + }); + + it("renders category code and description when headerKey is 'Category'", () => { + render( + + ); + expect(screen.getAllByText("A - Category A")[0]).toBeInTheDocument(); + }); + + it("renders default content for other headerKey values", () => { + render( + + ); + + expect(screen.getByText(/Unknown Value/i)).toBeInTheDocument(); + }); + + it("renders SpatialCheckbox when headerKey is 'actions' and showSpatial is true", () => { + render( + + ); + //check if the Checkbox text is present + expect(screen.getByText(/Click to view this opening's map activity./i)).toBeInTheDocument(); + }); + +}); diff --git a/frontend/src/__test__/components/TableRowComponent.test.tsx b/frontend/src/__test__/components/TableRowComponent.test.tsx new file mode 100644 index 00000000..8e0a4ba2 --- /dev/null +++ b/frontend/src/__test__/components/TableRowComponent.test.tsx @@ -0,0 +1,88 @@ +// TableRowComponent.test.tsx +import React from "react"; +import { vi } from "vitest"; +import { render, screen, fireEvent } from "@testing-library/react"; +import TableRowComponent from "../../components/TableRowComponent"; +import { OpeningsSearch } from "../../types/OpeningsSearch"; +import { ITableHeader } from "../../types/TableHeader"; + +describe("TableRowComponent", () => { + const row: OpeningsSearch = { + openingId: 1, + statusDescription: "Active", + categoryCode: "A", + categoryDescription: "Category A", + }; + + const headers: ITableHeader[] = [ + { key: "statusDescription", label: "Status", selected: true }, + { key: "categoryCode", label: "Category Code", selected: true }, + { key: "actions", label: "Actions", selected: false }, + ]; + + const showSpatial = false; + const selectedRows: string[] = []; + const handleRowSelectionChanged = vi.fn(); + const setOpeningDetails = vi.fn(); + + afterEach(() => { + handleRowSelectionChanged.mockClear(); + setOpeningDetails.mockClear(); + }); + + it("renders TableCellContent for each selected header", () => { + render( + + ); + + // Check that the expected content for each selected header is rendered + expect(screen.getByText("Active")).toBeInTheDocument(); + expect(screen.getByText("A")).toBeInTheDocument(); + expect(screen.queryByText("Category A")).not.toBeInTheDocument(); // CategoryDescription isn't selected + }); + + it("calls setOpeningDetails when the row is clicked", () => { + render( + + ); + + // Simulate click on the row + const tableRow = screen.getByRole("row"); + fireEvent.click(tableRow); + + // Verify that setOpeningDetails was called with true + expect(setOpeningDetails).toHaveBeenCalledWith(true); + }); + + it("renders TableCell components only for selected headers", () => { + render( + + ); + + // Verify content specific to selected headers + expect(screen.getByText("Active")).toBeInTheDocument(); + expect(screen.getByText("A")).toBeInTheDocument(); + expect(screen.queryByText("Actions")).not.toBeInTheDocument(); + }); +}); diff --git a/frontend/src/__test__/screens/Opening.test.tsx b/frontend/src/__test__/screens/Opening.test.tsx index c5c623e2..70d4ba8a 100644 --- a/frontend/src/__test__/screens/Opening.test.tsx +++ b/frontend/src/__test__/screens/Opening.test.tsx @@ -6,6 +6,7 @@ import PaginationContext from '../../contexts/PaginationContext'; import { NotificationProvider } from '../../contexts/NotificationProvider'; import { BrowserRouter } from 'react-router-dom'; import { RecentOpening } from '../../types/RecentOpening'; +import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; import { getWmsLayersWhitelistUsers } from '../../services/SecretsService'; import { fetchFreeGrowingMilestones, fetchOpeningsPerYear, fetchRecentOpenings, fetchRecentActions } from '../../services/OpeningService'; import { fetchOpeningFavourites } from '../../services/OpeningFavouriteService'; @@ -72,6 +73,7 @@ const paginationValueMock = { setPageData: vi.fn(), setInitialItemsPerPage: vi.fn(), }; +const queryClient = new QueryClient(); describe('Opening screen test cases', () => { @@ -96,6 +98,7 @@ describe('Opening screen test cases', () => { it('should renders Opening Page Title component', async () => { const { getByTestId } = render( + @@ -103,6 +106,7 @@ describe('Opening screen test cases', () => { + ); @@ -121,6 +125,7 @@ describe('Opening screen test cases', () => { await act(async () => { ({ container } = render( + @@ -128,6 +133,7 @@ describe('Opening screen test cases', () => { + )); }); @@ -160,6 +166,7 @@ describe('Opening screen test cases', () => { await act(async () => { ({ container, getByText } = render( + @@ -167,6 +174,7 @@ describe('Opening screen test cases', () => { + )); }); @@ -183,6 +191,7 @@ describe('Opening screen test cases', () => { await act(async () => { ({ container, getByText } = render( + @@ -190,6 +199,7 @@ describe('Opening screen test cases', () => { + )); }); diff --git a/frontend/src/__test__/services/OpeningService.test.ts b/frontend/src/__test__/services/OpeningService.test.ts index 665fc031..e3d050b2 100644 --- a/frontend/src/__test__/services/OpeningService.test.ts +++ b/frontend/src/__test__/services/OpeningService.test.ts @@ -1,7 +1,6 @@ import { describe, it, expect, vi } from 'vitest'; import axios from 'axios'; import { - fetchRecentOpenings, fetchOpeningsPerYear, fetchFreeGrowingMilestones, fetchRecentActions @@ -21,57 +20,6 @@ describe('OpeningService', () => { (getAuthIdToken as vi.Mock).mockReturnValue(authToken); }); - describe('fetchRecentOpenings', () => { - it('should fetch recent openings successfully', async () => { - const mockData = { - data: [ - { - openingId: 1, - forestFileId: '123', - cuttingPermit: '456', - timberMark: '789', - cutBlock: 'A', - grossAreaHa: 10, - status: { description: 'Active' }, - category: { description: 'Category1' }, - disturbanceStart: '2023-01-01', - entryTimestamp: '2023-01-01T00:00:00Z', - updateTimestamp: '2023-01-02T00:00:00Z' - } - ] - }; - (axios.get as vi.Mock).mockResolvedValue({ status: 200, data: mockData }); - - const result = await fetchRecentOpenings(); - - expect(axios.get).toHaveBeenCalledWith(`${backendUrl}/api/openings/recent-openings?page=0&perPage=100`, { - headers: { Authorization: `Bearer ${authToken}` } - }); - expect(result).toEqual([ - { - id: '1', - openingId: '1', - forestFileId: '123', - cuttingPermit: '456', - timberMark: '789', - cutBlock: 'A', - grossAreaHa: '10', - status: 'Active', - category: 'Category1', - disturbanceStart: '2023-01-01', - entryTimestamp: '2023-01-01', - updateTimestamp: '2023-01-02' - } - ]); - }); - - it('should handle error while fetching recent openings', async () => { - (axios.get as vi.Mock).mockRejectedValue(new Error('Network Error')); - - await expect(fetchRecentOpenings()).rejects.toThrow('Network Error'); - }); - }); - describe('fetchOpeningsPerYear', () => { it('should fetch openings per year successfully', async () => { const mockData = [ diff --git a/frontend/src/__test__/services/queries/dashboard/dashboardQueries.test.tsx b/frontend/src/__test__/services/queries/dashboard/dashboardQueries.test.tsx new file mode 100644 index 00000000..82d05cca --- /dev/null +++ b/frontend/src/__test__/services/queries/dashboard/dashboardQueries.test.tsx @@ -0,0 +1,112 @@ +import { render, screen, waitFor } from "@testing-library/react"; +import axios from "axios"; +import { usePostViewedOpening, postViewedOpening } from "../../../../services/queries/dashboard/dashboardQueries"; +import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; +import { vi, describe, it, expect } from "vitest"; +import { getAuthIdToken } from "../../../../services/AuthService"; + +// Mock axios and the AuthService function +vi.mock("axios"); +vi.mock("../../../../services/AuthService", () => ({ + getAuthIdToken: vi.fn(), +})); + +describe("postViewedOpening", () => { + const backendUrl = import.meta.env.VITE_BACKEND_URL; + const openingId = "123"; + + it("should send a PUT request and return data on success", async () => { + const mockResponse = { data: { message: "Success" } }; + (axios.put as vi.Mock).mockResolvedValue(mockResponse); + (getAuthIdToken as vi.Mock).mockReturnValue("testAuthToken"); + + const result = await postViewedOpening(openingId); + + expect(axios.put).toHaveBeenCalledWith(`${backendUrl}/api/openings/recent/${openingId}`, null, { + headers: { Authorization: `Bearer testAuthToken` }, + }); + expect(result).toEqual(mockResponse.data); + }); + + it("should throw a 403 error if the user is unauthorized", async () => { + (axios.put as vi.Mock).mockRejectedValue({ response: { status: 403 } }); + + await expect(postViewedOpening(openingId)).rejects.toThrow( + "Forbidden: You don't have permission to view this opening." + ); + }); + + it("should throw a specific error message for other server errors", async () => { + const errorMessage = "Server error occurred"; + (axios.put as vi.Mock).mockRejectedValue({ + response: { data: { message: errorMessage } }, + }); + + await expect(postViewedOpening(openingId)).rejects.toThrow(errorMessage); + }); +}); + +describe("usePostViewedOpening", () => { + const queryClient = new QueryClient(); + + const wrapper = ({ children }: { children: React.ReactNode }) => ( + {children} + ); + + // Mock Component to test the hook + const MockComponent = ({ openingId }: { openingId: string }) => { + const mutation = usePostViewedOpening(); + + return ( + + ); + }; + + it("should call postViewedOpening when mutate is invoked", async () => { + const mockResponse = { message: "Success" }; + const backendUrl = import.meta.env.VITE_BACKEND_URL; + (axios.put as vi.Mock).mockResolvedValue({ data: mockResponse }); + (getAuthIdToken as vi.Mock).mockReturnValue("testAuthToken"); + + render( + + + + ); + + // Trigger the mutation + screen.getByTestId("mutate-button").click(); + + // Wait for axios call + await waitFor(() => + expect(axios.put).toHaveBeenCalledWith(`${backendUrl}/api/openings/recent/123`, null, { + headers: { Authorization: `Bearer testAuthToken` }, + }) + ); + }); + + it("should handle errors when mutation fails", async () => { + const errorMessage = "Server error occurred"; + (axios.put as vi.Mock).mockRejectedValue({ + response: { data: { message: errorMessage } }, + }); + + render( + + + + ); + + // Trigger the mutation + screen.getByTestId("mutate-button").click(); + + await waitFor(() => { + expect(axios.put).toHaveBeenCalled(); + }); + }); +}); diff --git a/frontend/src/__test__/services/search/openings.test.tsx b/frontend/src/__test__/services/search/openings.test.tsx index 6ddbf33e..76a8c652 100644 --- a/frontend/src/__test__/services/search/openings.test.tsx +++ b/frontend/src/__test__/services/search/openings.test.tsx @@ -5,6 +5,7 @@ import { fetchOpenings, OpeningFilters } from "../../../services/search/openings import { getAuthIdToken } from "../../../services/AuthService"; import { createDateParams } from "../../../utils/searchUtils"; import { describe, it, beforeEach, afterEach, vi, expect } from "vitest"; +import exp from "constants"; // Mock dependencies vi.mock("axios"); @@ -53,10 +54,14 @@ const mockApiResponse = { orgUnitCode: "DPG", orgUnitName: "Prince George Natural Resource District", entryUserId: "Datafix107808", - statusCode: "APP", - statusDescription: "Approved", - categoryCode: "FTML", - categoryDescription: "Forest Tenure - Major Licensee", + category: { + code: "CONT", + description: "SP as a part of contractual agreement", + }, + status: { + code: "APP", + description: "Approved", + }, }, ], }, @@ -106,4 +111,29 @@ describe("fetchOpenings", () => { await expect(fetchOpenings(sampleFilters)).rejects.toThrow("Network error"); }); + + it("should return flattened data structure with specific fields", async () => { + // Arrange: setting up the mock response for axios + mockedAxios.get.mockResolvedValue(mockApiResponse); + + // Act: call the fetchOpenings function + const result = await fetchOpenings(sampleFilters); + + // Assert: check that the response data is correctly flattened + const firstOpening = result.data[0]; + expect(firstOpening.openingId).toEqual(9100129); + expect(firstOpening.categoryCode).toEqual("CONT"); + expect(firstOpening.categoryDescription).toEqual("SP as a part of contractual agreement"); + expect(firstOpening.statusCode).toEqual("APP"); + expect(firstOpening.statusDescription).toEqual("Approved"); + expect(firstOpening.timberMark).toEqual("W1729S"); + expect(firstOpening.cutBlockId).toEqual("06-03"); + expect(firstOpening.entryUserId).toEqual("Datafix107808"); + + + // Confirm that original nested properties were removed + expect(firstOpening.status).toBeUndefined(); + expect(firstOpening.category).toBeUndefined(); + }); + }); diff --git a/frontend/src/components/ActionButtons/index.tsx b/frontend/src/components/ActionButtons/index.tsx new file mode 100644 index 00000000..877cfcc5 --- /dev/null +++ b/frontend/src/components/ActionButtons/index.tsx @@ -0,0 +1,32 @@ +// ActionButtons.tsx + +import React from "react"; +import { Button } from "@carbon/react"; +import * as Icons from "@carbon/icons-react"; + +interface ActionButtonsProps { + rowId: string; +} + +const ActionButtons: React.FC = ({ rowId }) => ( + <> + - - - - - - - - {showSpatial && ( - - )} - {headers.map((header, i) => ( - - { header.header } - - ))} - - - - {rows.map((row, i) => ( - - {showSpatial && ( - selectRowEvent(row.id, row.isSelected), - }) - } /> - )} - {row.cells.map((cell: any, j: number) => ( - - {cell.info.header === "status" ? ( - - ) : cell.info.header === "actions" ? ( - <> -
- - ); - }} - - - {/* Check if there are no elements in the table, if not then print the Empty */} - {filteredRows.length <= 0 ? ( - - ) : null} - - {/* Check if there are no elements in the table, if not then print the Empty */} - {filteredRows.length > 0 ? ( - { - handlePageChange( page ); - handleItemsPerPageChange(page, pageSize); - }} - /> - ) : null} - - ); -} - -export default OpeningScreenDataTable; diff --git a/frontend/src/components/OpeningScreenDataTable/styles.scss b/frontend/src/components/OpeningScreenDataTable/styles.scss deleted file mode 100644 index 02b7d06e..00000000 --- a/frontend/src/components/OpeningScreenDataTable/styles.scss +++ /dev/null @@ -1,77 +0,0 @@ -@use '@bcgov-nr/nr-theme/design-tokens/colors.scss' as colors; -@use '@bcgov-nr/nr-theme/design-tokens/variables.scss' as vars; -.activity-table { - margin-bottom: 2.5rem; - - tr > th:first-child, - tr > td:first-child { - padding-left: 2.5rem; - } - - tr > th:last-child, - tr > td:last-child { - padding-right: 2.5rem; - } - - tr > th:last-child div, - tr > td:last-child { - text-align: center; - } - - .activities-table-cell svg { - position: relative; - margin-right: 0.5rem; - top: 0.1875rem; - } -} - -.#{vars.$bcgov-prefix}--data-table thead tr th#blank { - min-width:50px; -} - -.#{vars.$bcgov-prefix}--data-table thead tr th { - background-color: #F3F3F5; - border-top: 1px solid; - border-color: var(--#{vars.$bcgov-prefix}-border-subtle-01); - background-color: var(--#{vars.$bcgov-prefix}-layer-accent-01) !important; -} - -.#{vars.$bcgov-prefix}--data-table thead tr th { - min-width:158px; -} - -.#{vars.$bcgov-prefix}--data-table tr:nth-child(even) td { - background-color: var(--#{vars.$bcgov-prefix}-layer-01) !important; - height: 64px; -} -.#{vars.$bcgov-prefix}--data-table tr:nth-child(odd) td { - background-color: var(--#{vars.$bcgov-prefix}-layer-02) !important; - height: 64px; -} -.#{vars.$bcgov-prefix}--data-table tr:hover td { - background-color: var(--#{vars.$bcgov-prefix}-layer-accent-02) !important; - cursor: pointer; -} -.#{vars.$bcgov-prefix}--pagination { - background-color: var(--#{vars.$bcgov-prefix}-layer-02) !important; -} - -.table-toolbar{ - background-color: var(--#{vars.$bcgov-prefix}-layer-02); -} - -.#{vars.$bcgov-prefix}--search-input:disabled::placeholder { - color: var(--#{vars.$bcgov-prefix}-text-disabled) !important; -} - - -@media only screen and (max-width: 672px) { - .#{vars.$bcgov-prefix}--data-table-content { - width: 100%; - overflow-x: scroll; - } - - .activity-table { - width: 56.25rem; - } -} diff --git a/frontend/src/components/OpeningScreenDataTable/testData.ts b/frontend/src/components/OpeningScreenDataTable/testData.ts deleted file mode 100644 index f3e3eafc..00000000 --- a/frontend/src/components/OpeningScreenDataTable/testData.ts +++ /dev/null @@ -1,55 +0,0 @@ -import { ITableHeader } from "../../types/TableHeader"; - -export const headers: ITableHeader[] = [ - { - key: 'openingId', - header: 'Opening Id', - selected: false - }, - { - key: 'forestFileId', - header: 'File Id', - selected: false - }, - { - key: 'cuttingPermit', - header: 'Cutting permit', - selected: false - }, - { - key: 'timberMark', - header: 'Timber mark', - selected: false - }, - { - key: 'cutBlock', - header: 'Cut block', - selected: false - }, - { - key: 'grossAreaHa', - header: 'Gross area (ha)', - selected: false - }, - { - key: 'status', - header: 'Status', - selected: false - }, - { - key: 'category', - header: 'Category', - selected: false - }, - { - key: 'disturbanceStart', - header: 'Disturbance start', - selected: false - }, - { - key: 'actions', - header: 'Actions', - selected: false - } -]; - diff --git a/frontend/src/components/OpeningsTab/index.tsx b/frontend/src/components/OpeningsTab/index.tsx index d1f9642f..7df1bc3e 100644 --- a/frontend/src/components/OpeningsTab/index.tsx +++ b/frontend/src/components/OpeningsTab/index.tsx @@ -1,17 +1,14 @@ +// OpeningsTab.tsx import React, { useEffect, useState } from 'react'; import { Button, InlineNotification } from '@carbon/react'; -import './styles.scss' +import './styles.scss'; import { Location } from '@carbon/icons-react'; import OpeningsMap from '../OpeningsMap'; -import OpeningScreenDataTable from '../OpeningScreenDataTable/index'; -import { headers } from '../OpeningScreenDataTable/testData'; -import { fetchRecentOpenings } from '../../services/OpeningService'; +import RecentOpeningsDataTable from '../Dashboard/Opening/RecentOpeningsDataTable'; +import { useUserRecentOpeningQuery } from '../../services/queries/search/openingQueries'; import SectionTitle from '../SectionTitle'; import TableSkeleton from '../TableSkeleton'; -import { RecentOpening } from '../../types/RecentOpening'; -import { generateHtmlFile } from './layersGenerator'; -import { getWmsLayersWhitelistUsers, WmsLayersWhitelistUser } from '../../services/SecretsService'; -import { useGetAuth } from '../../contexts/AuthProvider'; +import { columns as headers } from '../Dashboard/Opening/RecentOpeningsDataTable/headerData'; interface Props { showSpatial: boolean; @@ -19,45 +16,12 @@ interface Props { } const OpeningsTab: React.FC = ({ showSpatial, setShowSpatial }) => { - const [loading, setLoading] = useState(true); - const [openingRows, setOpeningRows] = useState([]); - const [error, setError] = useState(null); const [loadId, setLoadId] = useState(null); const [openingPolygonNotFound, setOpeningPolygonNotFound] = useState(false); - const [wmsUsersWhitelist, setWmsUsersWhitelist] = useState([]); - const { user } = useGetAuth(); - - useEffect(() => { - const fetchData = async () => { - try { - const rows: RecentOpening[] = await fetchRecentOpenings(); - setOpeningRows(rows); - setLoading(false); - setError(null); - } catch (error) { - console.error('Error fetching recent openings:', error); - setLoading(false); - setError('Failed to fetch recent openings'); - } - }; - - const fetchAllowedPeople = async () => { - try { - const usersList: WmsLayersWhitelistUser[] = await getWmsLayersWhitelistUsers(); - setWmsUsersWhitelist(usersList); - } catch (error) { - console.error('Error fetching recent openings:', error); - } - }; - - fetchData(); - fetchAllowedPeople(); - }, []); - - useEffect(() => {}, [loadId, openingPolygonNotFound, wmsUsersWhitelist]); + const { data, isFetching } = useUserRecentOpeningQuery(10); const toggleSpatial = () => { - setShowSpatial(!showSpatial); + setShowSpatial(!showSpatial); }; return ( @@ -91,23 +55,27 @@ const OpeningsTab: React.FC = ({ showSpatial, setShowSpatial }) => {
- {openingPolygonNotFound? ( + {openingPolygonNotFound ? ( - ) : null } - {loading ? ( + ) : null} + {isFetching ? ( ) : ( - {}} setOpeningId={setLoadId} + toggleSpatial={toggleSpatial} showSpatial={showSpatial} + totalItems={(data?.perPage ?? 0) * (data?.totalPages ?? 0)} /> )}
diff --git a/frontend/src/components/SilvicultureSearch/Openings/OpeningsSearchTab/index.tsx b/frontend/src/components/SilvicultureSearch/Openings/OpeningsSearchTab/index.tsx index 89078bb9..4424c9e8 100644 --- a/frontend/src/components/SilvicultureSearch/Openings/OpeningsSearchTab/index.tsx +++ b/frontend/src/components/SilvicultureSearch/Openings/OpeningsSearchTab/index.tsx @@ -4,7 +4,7 @@ import EmptySection from "../../../EmptySection"; import OpeningsSearchBar from "../OpeningsSearchBar"; import TableSkeleton from "../../../TableSkeleton"; import SearchScreenDataTable from "../SearchScreenDataTable"; -import { columns } from "../SearchScreenDataTable/testData"; +import { columns } from "../SearchScreenDataTable/headerData"; import OpeningsMap from "../../../OpeningsMap"; import { useOpeningsQuery } from "../../../../services/queries/search/openingQueries"; import { useOpeningsSearch } from "../../../../contexts/search/OpeningsSearch"; diff --git a/frontend/src/components/SilvicultureSearch/Openings/SearchScreenDataTable/headerData.ts b/frontend/src/components/SilvicultureSearch/Openings/SearchScreenDataTable/headerData.ts new file mode 100644 index 00000000..7dcabb5d --- /dev/null +++ b/frontend/src/components/SilvicultureSearch/Openings/SearchScreenDataTable/headerData.ts @@ -0,0 +1,55 @@ +import { ITableHeader } from "../../../../types/TableHeader"; + +export const columns: ITableHeader[] = [ + { + key: 'actions', + header: 'Actions', + selected: true + }, + { + key: 'openingId', + header: 'Opening Id', + selected: true + }, + { + key: 'forestFileId', + header: 'File Id', + selected: true + }, + { + key: 'categoryDescription', + header: 'Category', + selected: true, + elipsis: true + }, + { + key: 'orgUnitName', + header: 'Org unit', + selected: true + }, + { + key: 'statusDescription', + header: 'Status', + selected: true + }, + { + key: 'cuttingPermitId', + header: 'Cutting permit', + selected: true + }, + { + key: 'cutBlockId', + header: 'Cut block', + selected: true + }, + { + key: 'openingGrossAreaHa', + header: 'Gross Area', + selected: true + }, + { + key: 'disturbanceStartDate', + header: 'Disturbance Date', + selected: true + } +]; \ No newline at end of file diff --git a/frontend/src/components/SilvicultureSearch/Openings/SearchScreenDataTable/index.tsx b/frontend/src/components/SilvicultureSearch/Openings/SearchScreenDataTable/index.tsx index 1b62d519..47e74668 100644 --- a/frontend/src/components/SilvicultureSearch/Openings/SearchScreenDataTable/index.tsx +++ b/frontend/src/components/SilvicultureSearch/Openings/SearchScreenDataTable/index.tsx @@ -22,9 +22,8 @@ import { Row, Column, MenuItemDivider, - Tooltip, - MenuItem, - FlexGrid + Modal, + ActionableNotification } from "@carbon/react"; import * as Icons from "@carbon/icons-react"; import StatusTag from "../../../StatusTag"; @@ -33,15 +32,18 @@ import EmptySection from "../../../EmptySection"; import PaginationContext from "../../../../contexts/PaginationContext"; import { OpeningsSearch } from "../../../../types/OpeningsSearch"; import { ITableHeader } from "../../../../types/TableHeader"; +import { FlexGrid } from "@carbon/react"; +import { MenuItem } from "@carbon/react"; import { convertToCSV, downloadCSV, downloadPDF, - downloadXLSX + downloadXLSX, } from "../../../../utils/fileConversions"; +import { Tooltip } from "@carbon/react"; import { useNavigate } from "react-router-dom"; -import { setOpeningFavorite } from '../../../../services/OpeningFavouriteService'; -import { useNotification } from "../../../../contexts/NotificationProvider"; +import { usePostViewedOpening } from "../../../../services/queries/dashboard/dashboardQueries"; +import { useNotification } from '../../../../contexts/NotificationProvider'; import TruncatedText from "../../../TruncatedText"; import FriendlyDate from "../../../FriendlyDate"; @@ -50,7 +52,7 @@ interface ISearchScreenDataTable { rows: OpeningsSearch[]; headers: ITableHeader[]; defaultColumns: ITableHeader[]; - handleCheckboxChange: (columnKey: string) => void; + handleCheckboxChange: (columnKey: string) => void; toggleSpatial: () => void; showSpatial: boolean; totalItems: number; @@ -80,12 +82,16 @@ const SearchScreenDataTable: React.FC = ({ const [openEdit, setOpenEdit] = useState(false); const [openDownload, setOpenDownload] = useState(false); const [selectedRows, setSelectedRows] = useState([]); // State to store selected rows + const [toastText, setToastText] = useState(null); + const [openingDetails, setOpeningDetails] = useState(false); + const { mutate: markAsViewedOpening, isError, error } = usePostViewedOpening(); const navigate = useNavigate(); // This ref is used to calculate the width of the container for each cell const cellRefs = useRef([]); // Holds the with of each cell in the table const [cellWidths, setCellWidths] = useState([]); + const { displayNotification } = useNotification(); useEffect(() => { const widths = cellRefs.current.map((cell: ICellRefs) => cell.offsetWidth || 0); @@ -105,36 +111,38 @@ const SearchScreenDataTable: React.FC = ({ }, [rows, totalItems]); // Function to handle row selection changes - const handleRowSelectionChanged = (openingId: string) => { + const handleRowSelectionChanged = (rowId: string) => { setSelectedRows((prevSelectedRows) => { - if (prevSelectedRows.includes(openingId)) { + if (prevSelectedRows.includes(rowId)) { // If the row is already selected, remove it from the selected rows - return prevSelectedRows.filter((id) => id !== openingId); + return prevSelectedRows.filter((id) => id !== rowId); } else { // If the row is not selected, add it to the selected rows - return [...prevSelectedRows, openingId]; + return [...prevSelectedRows, rowId]; } }); }; - const { displayNotification } = useNotification(); + const handleRowClick = (openingId: string) => { + // Call the mutation to mark as viewed + markAsViewedOpening(openingId, { + onSuccess: () => { + setOpeningDetails(true) + }, + onError: (err: any) => { + // Display error notification (UI needs to be designed for this) + } + }); + }; //Function to handle the favourite feature of the opening for a user - const handleFavouriteOpening = (openingId: string) => { - try{ - setOpeningFavorite(parseInt(openingId)); - displayNotification({ - title: `Opening Id ${openingId} favourited`, - subTitle: 'You can follow this opening ID on your dashboard', - type: "success", - buttonLabel: "Go to track openings", - onClose: () => { - navigate('/opening?tab=metrics&scrollTo=trackOpenings') - } - }) - } catch (error) { - console.error(`Failed to update favorite status for ${openingId}`); - } + const handleFavouriteOpening = (rowId: string) => { + displayNotification({ + title: `Following OpeningID ${rowId}`, + type: 'success', + dismissIn: 8000, + onClose: () => {} + }); } return ( @@ -307,7 +315,14 @@ const SearchScreenDataTable: React.FC = ({ {rows && rows.map((row: any, i: number) => ( - + { + //add the api call to send the viewed opening + handleRowClick(row.openingId); + } + } + > {headers.map((header) => header.selected ? ( = ({ )} - + e.stopPropagation()} // Stop row onClick from triggering + > - handleFavouriteOpening(row.openingId) - } + onClick={(e: any) => { + e.stopPropagation(); // Stop row onClick from triggering + handleFavouriteOpening(row.openingId); + }} /> - downloadPDF(defaultColumns, [row]) - } + onClick={(e: any) => { + e.stopPropagation(); // Stop row onClick from triggering + downloadPDF(defaultColumns, [row]); + }} /> { + onClick={(e: any) => { + e.stopPropagation(); // Stop row onClick from triggering const csvData = convertToCSV(defaultColumns, [ row, ]); @@ -392,9 +414,11 @@ const SearchScreenDataTable: React.FC = ({ {rows.length <= 0 ? ( ) : null} @@ -419,7 +443,15 @@ const SearchScreenDataTable: React.FC = ({ handleItemsPerPageChange(page, pageSize); }} /> - )} + )} + + setOpeningDetails(false)} + passiveModal + modalHeading="We are working hard to get this feature asap, unfortunately you cannot view the opening details from SILVA atm." + modalLabel="Opening Details" + /> ); }; diff --git a/frontend/src/components/SilvicultureSearch/Openings/SearchScreenDataTable/testData.ts b/frontend/src/components/SilvicultureSearch/Openings/SearchScreenDataTable/testData.ts deleted file mode 100644 index cd59d624..00000000 --- a/frontend/src/components/SilvicultureSearch/Openings/SearchScreenDataTable/testData.ts +++ /dev/null @@ -1,284 +0,0 @@ -import { ITableHeader } from "../../../../types/TableHeader"; - -export const columns: ITableHeader[] = [ - { - key: 'actions', - header: 'Actions', - selected: true - }, - { - key: 'openingId', - header: 'Opening Id', - selected: true - }, - { - key: 'forestFileId', - header: 'File Id', - selected: true - }, - { - key: 'categoryDescription', - header: 'Category', - selected: true, - elipsis: true - }, - { - key: 'orgUnitName', - header: 'Org unit', - selected: true - }, - { - key: 'statusDescription', - header: 'Status', - selected: true - }, - { - key: 'cuttingPermitId', - header: 'Cutting permit', - selected: true - }, - { - key: 'cutBlockId', - header: 'Cut block', - selected: true - }, - { - key: 'openingGrossAreaHa', - header: 'Gross Area', - selected: true - }, - { - key: 'disturbanceStartDate', - header: 'Disturbance Date', - selected: true - } -]; - - -export const rows:any = [ - { - id: '114207', - openingId: '114207', - fileId: 'TFL47', - cuttingPermit: '12S', - timberMark: '47/12S', - cutBlock: '12-69', - grossAreaHa: '12.9', - status: 'Free growing', - category: 'FTML', - disturbanceStart: '-', - createdAt: '2022-10-27', - orgUnit: 'DCC - Cariboo chilcotin natural resources', - lastViewed: '2022-10-27' - }, - { - id: '114206', - openingId: '114206', - fileId: 'TFL47', - cuttingPermit: '12T', - timberMark: '47/12S', - cutBlock: '12-69', - grossAreaHa: '12.9', - status: 'Free growing', - category: 'FTML', - disturbanceStart: '-', - createdAt: '2022-09-04', - orgUnit: 'DCC - Cariboo chilcotin natural resources', - lastViewed: '2022-10-27' - }, - { - id: '114205', - openingId: '114205', - fileId: 'TFL47', - cuttingPermit: '12T', - timberMark: '47/12S', - cutBlock: '12-44A', - grossAreaHa: '12.9', - status: 'Free growing', - category: 'FTML', - disturbanceStart: '-', - createdAt: '2022-09-04', - orgUnit: 'DCC - Cariboo chilcotin natural resources', - lastViewed: '2022-10-27' - }, - { - id: '114204', - openingId: '114204', - fileId: 'TFL47', - cuttingPermit: '12T', - timberMark: '47/12S', - cutBlock: '12-44A', - grossAreaHa: '12.9', - status: 'Active', - category: 'FTML', - disturbanceStart: '-', - createdAt: '2022-01-16', - orgUnit: 'DCC - Cariboo chilcotin natural resources', - lastViewed: '2022-10-26' - }, - { - id: '114203', - openingId: '114203', - fileId: 'TFL47', - cuttingPermit: '12T', - timberMark: '47/12S', - cutBlock: '12-44A', - grossAreaHa: '12.9', - status: 'Active', - category: 'FTML', - disturbanceStart: '-', - createdAt: '2021-12-08', - orgUnit: 'DCC - Cariboo chilcotin natural resources', - lastViewed: '2022-10-26' - }, - { - id: '114202', - openingId: '114202', - fileId: 'TFL47', - cuttingPermit: '12T', - timberMark: '47/12S', - cutBlock: '12-44A', - grossAreaHa: '12.9', - status: 'Free growing', - category: 'FTML', - disturbanceStart: '-', - createdAt: '2021-11-15', - orgUnit: 'DCC - Cariboo chilcotin natural resources', - lastViewed: '2022-10-25' - }, - { - id: '114201', - openingId: '114201', - fileId: 'TFL47', - cuttingPermit: '12T', - timberMark: '47/12S', - cutBlock: '12-44A', - grossAreaHa: '12.9', - status: 'Free growing', - category: 'FTML', - disturbanceStart: '-', - createdAt: '2021-11-15', - orgUnit: 'DCC - Cariboo chilcotin natural resources', - lastViewed: '2022-10-25' - }, - { - id: '114200', - openingId: '114200', - fileId: 'TFL47', - cuttingPermit: '12T', - timberMark: '47/12S', - cutBlock: '12-44A', - grossAreaHa: '12.9', - status: 'Active', - category: 'FTML', - disturbanceStart: '-', - createdAt: '2021-10-20', - orgUnit: 'DCC - Cariboo chilcotin natural resources', - lastViewed: '2022-10-24' - }, - { - id: '114199', - openingId: '114199', - fileId: 'TFL47', - cuttingPermit: '12T', - timberMark: '47/12S', - cutBlock: '12-44A', - grossAreaHa: '12.9', - status: 'Active', - category: 'FTML', - disturbanceStart: '-', - createdAt: '2021-10-20', - orgUnit: 'DCC - Cariboo chilcotin natural resources', - lastViewed: '2022-10-24' - }, - { - id: '114198', - openingId: '114198', - fileId: 'TFL47', - cuttingPermit: '12T', - timberMark: '47/12S', - cutBlock: '12-44A', - grossAreaHa: '12.9', - status: 'Free growing', - category: 'FTML', - disturbanceStart: '-', - createdAt: '2021-09-12', - orgUnit: 'DCC - Cariboo chilcotin natural resources', - lastViewed: '2022-10-23' - }, - { - id: '114197', - openingId: '114197', - fileId: 'TFL47', - cuttingPermit: '12T', - timberMark: '47/12S', - cutBlock: '12-44A', - grossAreaHa: '12.9', - status: 'Free growing', - category: 'FTML', - disturbanceStart: '-', - createdAt: '2021-09-12', - orgUnit: 'DCC - Cariboo chilcotin natural resources', - lastViewed: '2022-10-23' - }, - { - id: '114196', - openingId: '114196', - fileId: 'TFL47', - cuttingPermit: '12T', - timberMark: '47/12S', - cutBlock: '12-44A', - grossAreaHa: '12.9', - status: 'Free growing', - category: 'FTML', - disturbanceStart: '-', - createdAt: '2021-08-05', - orgUnit: 'DCC - Cariboo chilcotin natural resources', - lastViewed: '2022-10-22' - }, - { - id: '114195', - openingId: '114195', - fileId: 'TFL47', - cuttingPermit: '12T', - timberMark: '47/12S', - cutBlock: '12-44A', - grossAreaHa: '12.9', - status: 'Free growing', - category: 'FTML', - disturbanceStart: '-', - createdAt: '2021-08-05', - orgUnit: 'DCC - Cariboo chilcotin natural resources', - lastViewed: '2022-10-22' - }, - { - id: '114194', - openingId: '114194', - fileId: 'TFL47', - cuttingPermit: '12T', - timberMark: '47/12S', - cutBlock: '12-44A', - grossAreaHa: '12.9', - status: 'Active', - category: 'FTML', - disturbanceStart: '-', - createdAt: '2021-07-10', - orgUnit: 'DCC - Cariboo chilcotin natural resources', - lastViewed: '2022-10-21' - }, - { - id: '114193', - openingId: '114193', - fileId: 'TFL47', - cuttingPermit: '12T', - timberMark: '47/12S', - cutBlock: '12-44A', - grossAreaHa: '12.9', - status: 'Active', - category: 'FTML', - disturbanceStart: '-', - createdAt: '2021-07-10', - orgUnit: 'DCC - Cariboo chilcotin natural resources', - lastViewed: '2022-10-21' - } -]; diff --git a/frontend/src/components/SpatialCheckbox/index.tsx b/frontend/src/components/SpatialCheckbox/index.tsx new file mode 100644 index 00000000..7ccad63c --- /dev/null +++ b/frontend/src/components/SpatialCheckbox/index.tsx @@ -0,0 +1,28 @@ +// SpatialCheckbox.tsx + +import React from "react"; +import { Checkbox, CheckboxGroup, Tooltip } from "@carbon/react"; + +interface SpatialCheckboxProps { + rowId: string; + selectedRows: string[]; + handleRowSelectionChanged: (rowId: string) => void; +} + +const SpatialCheckbox: React.FC = ({ + rowId, + selectedRows, + handleRowSelectionChanged, +}) => ( + + + handleRowSelectionChanged(rowId)} + /> + + +); + +export default SpatialCheckbox; diff --git a/frontend/src/components/TableCellContent/index.tsx b/frontend/src/components/TableCellContent/index.tsx new file mode 100644 index 00000000..3cc7a2cd --- /dev/null +++ b/frontend/src/components/TableCellContent/index.tsx @@ -0,0 +1,50 @@ +// TableCellContent.tsx + +import React from "react"; +import StatusTag from "../StatusTag"; +import ActionButtons from "../ActionButtons"; +import SpatialCheckbox from "../SpatialCheckbox"; +import { OpeningsSearch } from "../../types/OpeningsSearch"; +import TruncatedText from "../TruncatedText"; + +interface TableCellContentProps { + headerKey: string; + row: OpeningsSearch; + showSpatial: boolean; + selectedRows: string[]; + handleRowSelectionChanged: (rowId: string) => void; +} + +const TableCellContent: React.FC = ({ + headerKey, + row, + showSpatial, + selectedRows, + handleRowSelectionChanged, +}) => { + switch (headerKey) { + case "statusDescription": + return ; + case "actions": + return ( + <> + + {showSpatial && ( + + )} + + ); + case "Category": + return ( + + ); + default: + return <>{row[headerKey as keyof OpeningsSearch]}; + } +}; + +export default TableCellContent; diff --git a/frontend/src/components/TableRowComponent/index.tsx b/frontend/src/components/TableRowComponent/index.tsx new file mode 100644 index 00000000..41e4950f --- /dev/null +++ b/frontend/src/components/TableRowComponent/index.tsx @@ -0,0 +1,43 @@ +// TableRowComponent.tsx + +import React from "react"; +import { TableRow, TableCell } from "@carbon/react"; +import TableCellContent from "../TableCellContent"; +import { OpeningsSearch } from "../..//types/OpeningsSearch"; +import { ITableHeader } from "../..//types/TableHeader"; + +interface TableRowComponentProps { + row: OpeningsSearch; + headers: ITableHeader[]; + showSpatial: boolean; + selectedRows: string[]; + handleRowSelectionChanged: (rowId: string) => void; + setOpeningDetails: (show: boolean) => void; +} + +const TableRowComponent: React.FC = ({ + row, + headers, + showSpatial, + selectedRows, + handleRowSelectionChanged, + setOpeningDetails, +}) => ( + setOpeningDetails(true)}> + {headers.map((header) => + header.selected ? ( + + + + ) : null + )} + +); + +export default TableRowComponent; diff --git a/frontend/src/services/OpeningService.ts b/frontend/src/services/OpeningService.ts index e6265936..2a1bc56d 100644 --- a/frontend/src/services/OpeningService.ts +++ b/frontend/src/services/OpeningService.ts @@ -13,50 +13,6 @@ import { const backendUrl = env.VITE_BACKEND_URL; -/** - * Fetch recent openings data from backend. - * - * @returns {Promise} Array of objects found - */ -export async function fetchRecentOpenings(): Promise { - const authToken = getAuthIdToken(); - try { - const response = await axios.get(backendUrl.concat("/api/openings/recent-openings?page=0&perPage=100"), { - headers: { - Authorization: `Bearer ${authToken}` - } - }); - - if (response.status >= 200 && response.status < 300) { - const { data } = response; - - if (data.data) { - // Extracting row information from the fetched data - const rows: RecentOpening[] = data.data.map((opening: RecentOpeningApi) => ({ - id: opening.openingId.toString(), - openingId: opening.openingId.toString(), - forestFileId: opening.forestFileId ? opening.forestFileId : '-', - cuttingPermit: opening.cuttingPermit ? opening.cuttingPermit : '-', - timberMark: opening.timberMark ? opening.timberMark : '-', - cutBlock: opening.cutBlock ? opening.cutBlock : '-', - grossAreaHa: opening.grossAreaHa ? opening.grossAreaHa.toString() : '-', - status: opening.status && opening.status.description? opening.status.description : '-', - category: opening.category && opening.category.description? opening.category.description : '-', - disturbanceStart: opening.disturbanceStart ? opening.disturbanceStart : '-', - entryTimestamp: opening.entryTimestamp ? opening.entryTimestamp.split('T')[0] : '-', - updateTimestamp: opening.updateTimestamp ? opening.updateTimestamp.split('T')[0] : '-' - })); - - return rows; - } - } - return []; - } catch (error) { - console.error('Error fetching recent openings:', error); - throw error; - } -} - /** * Fetch openings per year data from backend. * diff --git a/frontend/src/services/queries/dashboard/dashboardQueries.ts b/frontend/src/services/queries/dashboard/dashboardQueries.ts new file mode 100644 index 00000000..f4c3be7b --- /dev/null +++ b/frontend/src/services/queries/dashboard/dashboardQueries.ts @@ -0,0 +1,32 @@ +import { useMutation, useQuery } from "@tanstack/react-query"; +import axios from "axios"; +import { getAuthIdToken } from "../../AuthService"; +import { env } from "../../../env"; + +const backendUrl = env.VITE_BACKEND_URL; + +// Function to send the POST request +export const postViewedOpening = async (openingId: string): Promise => { + const authToken = getAuthIdToken(); + try { + const response = await axios.put(`${backendUrl}/api/openings/recent/${openingId}`, null, { + headers: { + Authorization: `Bearer ${authToken}`, + }, + }); + return response.data; + } catch (error:any) { + if (error.response?.status === 403) { + throw new Error("Forbidden: You don't have permission to view this opening."); + } else { + throw new Error(error.response.data.message); + } + } + }; + + // Hook for using the mutation + export const usePostViewedOpening = () => { + return useMutation({ + mutationFn: (openingId: string) => postViewedOpening(openingId), + }); + }; diff --git a/frontend/src/services/queries/search/openingQueries.ts b/frontend/src/services/queries/search/openingQueries.ts index 4f5a6ebe..bda30c83 100644 --- a/frontend/src/services/queries/search/openingQueries.ts +++ b/frontend/src/services/queries/search/openingQueries.ts @@ -1,5 +1,5 @@ import { useQuery } from "@tanstack/react-query"; -import { fetchOpeningFilters, fetchOpenings, OpeningFilters } from "../../search/openings"; +import { fetchOpeningFilters, fetchOpenings, fetchUserRecentOpenings, OpeningFilters } from "../../search/openings"; export const useOpeningsQuery = (filters: OpeningFilters, enabled: boolean) => { return useQuery({ @@ -9,6 +9,15 @@ export const useOpeningsQuery = (filters: OpeningFilters, enabled: boolean) => { }); }; +export const useUserRecentOpeningQuery = (limit:number) => { + return useQuery({ + queryKey: ["userRecentOpenings", limit], + queryFn: () => fetchUserRecentOpenings(limit), + enabled: true, + refetchOnMount: "always" + }); +}; + export const useOpeningFiltersQuery = () => { return useQuery({ queryKey: ["openingFilters"], diff --git a/frontend/src/services/search/openings.ts b/frontend/src/services/search/openings.ts index c8d33643..770f46f1 100644 --- a/frontend/src/services/search/openings.ts +++ b/frontend/src/services/search/openings.ts @@ -68,7 +68,7 @@ export const fetchOpenings = async (filters: OpeningFilters): Promise => { statusList: filters.status, // Keep it as an array entryUserId: filters.clientAcronym, cutBlockId: filters.cutBlock, - cuttinPermitId:filters.cuttingPermit, + cuttingPermitId:filters.cuttingPermit, timbermark:filters.timberMark, myOpenings: filters.openingFilters?.includes("Openings created by me") || undefined, @@ -119,6 +119,37 @@ export const fetchOpenings = async (filters: OpeningFilters): Promise => { }; }; +// Used to fetch the recent openings for a user based on a limit value +export const fetchUserRecentOpenings = async (limit: number): Promise => { + + // Retrieve the auth token + const authToken = getAuthIdToken(); + + // Make the API request with the Authorization header + const response = await axios.get(`${backendUrl}/api/openings/recent`, { + headers: { + Authorization: `Bearer ${authToken}` + } + }); + + // Flatten the data part of the response + const flattenedData = response.data.data.map((item: OpeningItem) => ({ + ...item, + statusCode: item.status?.code, + statusDescription: item.status?.description, + categoryCode: item.category?.code, + categoryDescription: item.category?.description, + status: undefined, // Remove the old nested status object + category: undefined // Remove the old nested category object + })); + + // Returning the modified response data with the flattened structure + return { + ...response.data, + data: flattenedData + }; +}; + export const fetchCategories = async (): Promise => { // Retrieve the auth token