diff --git a/.env.example b/.env.example index 05a32a5146..f03372f60c 100644 --- a/.env.example +++ b/.env.example @@ -9,6 +9,8 @@ FLASK_APP=src/app.py FLASK_DEBUG=1 DEBUG=TRUE + # Front-End Variables VITE_BASENAME=/ -#VITE_BACKEND_URL= +VITE_BACKEND_URL=http://127.0.0.1:3001 +BACKEND_URL=http://127.0.0.1:3001 diff --git a/.gitignore b/.gitignore index 80704f4378..a3c42774a3 100755 --- a/.gitignore +++ b/.gitignore @@ -71,7 +71,7 @@ dist/* !.gitkeep !.htaccess !.eslintrc -!.env.example +!.env .now # backend stuff @@ -80,3 +80,4 @@ database.database database.db diagram.png __pycache__/ +.env diff --git a/Pipfile b/Pipfile index 4d377014ae..fd1fda56da 100644 --- a/Pipfile +++ b/Pipfile @@ -7,7 +7,6 @@ verify_ssl = true [packages] flask = "*" -flask-sqlalchemy = "*" flask-migrate = "*" flask-swagger = "*" psycopg2-binary = "*" @@ -20,6 +19,8 @@ typing-extensions = "*" flask-jwt-extended = "==4.6.0" wtforms = "==3.1.2" sqlalchemy = "*" +flask-sqlalchemy = "*" +flask-bcrypt = "*" [requires] python_version = "3.13" @@ -34,3 +35,4 @@ downgrade="flask db downgrade" insert-test-data="flask insert-test-data" reset_db="bash ./docs/assets/reset_migrations.bash" deploy="echo 'Please follow this 3 steps to deploy: https://github.com/4GeeksAcademy/flask-rest-hello/blob/master/README.md#deploy-your-website-to-heroku' " + diff --git a/Pipfile.lock b/Pipfile.lock index d9e474e972..c01f7d709a 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "ffbfb32d0afa5e4bcaba5c2d08c81381a97abd90f22284d2b76647365df5dc50" + "sha256": "9c612f7eebcf717533779dc02b40fce5eeaf66bfab71c81d166828128c272240" }, "pipfile-spec": 6, "requires": { @@ -18,11 +18,80 @@ "default": { "alembic": { "hashes": [ - "sha256:8a289f6778262df31571d29cca4c7fbacd2f0f582ea0816f4c399b6da7528486", - "sha256:cbc2386e60f89608bb63f30d2d6cc66c7aaed1fe105bd862828600e5ad167023" + "sha256:a5ed4adcf6d8a4cb575f3d759f071b03cd6e5c7618eb796cb52497be25bfe19a", + "sha256:cb6e1fd84b6174ab8dbb2329f86d631ba9559dd78df550b57804d607672cedbc" ], "markers": "python_version >= '3.10'", - "version": "==1.17.1" + "version": "==1.18.4" + }, + "bcrypt": { + "hashes": [ + "sha256:046ad6db88edb3c5ece4369af997938fb1c19d6a699b9c1b27b0db432faae4c4", + "sha256:0c418ca99fd47e9c59a301744d63328f17798b5947b0f791e9af3c1c499c2d0a", + "sha256:0c8e093ea2532601a6f686edbc2c6b2ec24131ff5c52f7610dd64fa4553b5464", + "sha256:0cae4cb350934dfd74c020525eeae0a5f79257e8a201c0c176f4b84fdbf2a4b4", + "sha256:137c5156524328a24b9fac1cb5db0ba618bc97d11970b39184c1d87dc4bf1746", + "sha256:200af71bc25f22006f4069060c88ed36f8aa4ff7f53e67ff04d2ab3f1e79a5b2", + "sha256:212139484ab3207b1f0c00633d3be92fef3c5f0af17cad155679d03ff2ee1e41", + "sha256:2b732e7d388fa22d48920baa267ba5d97cca38070b69c0e2d37087b381c681fd", + "sha256:35a77ec55b541e5e583eb3436ffbbf53b0ffa1fa16ca6782279daf95d146dcd9", + "sha256:38cac74101777a6a7d3b3e3cfefa57089b5ada650dce2baf0cbdd9d65db22a9e", + "sha256:3abeb543874b2c0524ff40c57a4e14e5d3a66ff33fb423529c88f180fd756538", + "sha256:3ca8a166b1140436e058298a34d88032ab62f15aae1c598580333dc21d27ef10", + "sha256:3cf67a804fc66fc217e6914a5635000259fbbbb12e78a99488e4d5ba445a71eb", + "sha256:4870a52610537037adb382444fefd3706d96d663ac44cbb2f37e3919dca3d7ef", + "sha256:48f753100931605686f74e27a7b49238122aa761a9aefe9373265b8b7aa43ea4", + "sha256:4bfd2a34de661f34d0bda43c3e4e79df586e4716ef401fe31ea39d69d581ef23", + "sha256:560ddb6ec730386e7b3b26b8b4c88197aaed924430e7b74666a586ac997249ef", + "sha256:5b1589f4839a0899c146e8892efe320c0fa096568abd9b95593efac50a87cb75", + "sha256:5feebf85a9cefda32966d8171f5db7e3ba964b77fdfe31919622256f80f9cf42", + "sha256:611f0a17aa4a25a69362dcc299fda5c8a3d4f160e2abb3831041feb77393a14a", + "sha256:61afc381250c3182d9078551e3ac3a41da14154fbff647ddf52a769f588c4172", + "sha256:64d7ce196203e468c457c37ec22390f1a61c85c6f0b8160fd752940ccfb3a683", + "sha256:64ee8434b0da054d830fa8e89e1c8bf30061d539044a39524ff7dec90481e5c2", + "sha256:6b8f520b61e8781efee73cba14e3e8c9556ccfb375623f4f97429544734545b4", + "sha256:741449132f64b3524e95cd30e5cd3343006ce146088f074f31ab26b94e6c75ba", + "sha256:744d3c6b164caa658adcb72cb8cc9ad9b4b75c7db507ab4bc2480474a51989da", + "sha256:79cfa161eda8d2ddf29acad370356b47f02387153b11d46042e93a0a95127493", + "sha256:7aeef54b60ceddb6f30ee3db090351ecf0d40ec6e2abf41430997407a46d2254", + "sha256:7edda91d5ab52b15636d9c30da87d2cc84f426c72b9dba7a9b4fe142ba11f534", + "sha256:7f277a4b3390ab4bebe597800a90da0edae882c6196d3038a73adf446c4f969f", + "sha256:7f4c94dec1b5ab5d522750cb059bb9409ea8872d4494fd152b53cca99f1ddd8c", + "sha256:801cad5ccb6b87d1b430f183269b94c24f248dddbbc5c1f78b6ed231743e001c", + "sha256:83e787d7a84dbbfba6f250dd7a5efd689e935f03dd83b0f919d39349e1f23f83", + "sha256:89042e61b5e808b67daf24a434d89bab164d4de1746b37a8d173b6b14f3db9ff", + "sha256:92864f54fb48b4c718fc92a32825d0e42265a627f956bc0361fe869f1adc3e7d", + "sha256:9d52ed507c2488eddd6a95bccee4e808d3234fa78dd370e24bac65a21212b861", + "sha256:9fffdb387abe6aa775af36ef16f55e318dcda4194ddbf82007a6f21da29de8f5", + "sha256:a28bc05039bdf3289d757f49d616ab3efe8cf40d8e8001ccdd621cd4f98f4fc9", + "sha256:a5393eae5722bcef046a990b84dff02b954904c36a194f6cfc817d7dca6c6f0b", + "sha256:a71f70ee269671460b37a449f5ff26982a6f2ba493b3eabdd687b4bf35f875ac", + "sha256:b17366316c654e1ad0306a6858e189fc835eca39f7eb2cafd6aaca8ce0c40a2e", + "sha256:baade0a5657654c2984468efb7d6c110db87ea63ef5a4b54732e7e337253e44f", + "sha256:c2388ca94ffee269b6038d48747f4ce8df0ffbea43f31abfa18ac72f0218effb", + "sha256:c58b56cdfb03202b3bcc9fd8daee8e8e9b6d7e3163aa97c631dfcfcc24d36c86", + "sha256:cde08734f12c6a4e28dc6755cd11d3bdfea608d93d958fffbe95a7026ebe4980", + "sha256:d79e5c65dcc9af213594d6f7f1fa2c98ad3fc10431e7aa53c176b441943efbdd", + "sha256:d8d65b564ec849643d9f7ea05c6d9f0cd7ca23bdd4ac0c2dbef1104ab504543d", + "sha256:db99dca3b1fdc3db87d7c57eac0c82281242d1eabf19dcb8a6b10eb29a2e72d1", + "sha256:dcd58e2b3a908b5ecc9b9df2f0085592506ac2d5110786018ee5e160f28e0911", + "sha256:dd19cf5184a90c873009244586396a6a884d591a5323f0e8a5922560718d4993", + "sha256:ddb4e1500f6efdd402218ffe34d040a1196c072e07929b9820f363a1fd1f4191", + "sha256:e3cf5b2560c7b5a142286f69bde914494b6d8f901aaa71e453078388a50881c4", + "sha256:ed2e1365e31fc73f1825fa830f1c8f8917ca1b3ca6185773b349c20fd606cec2", + "sha256:edfcdcedd0d0f05850c52ba3127b1fce70b9f89e0fe5ff16517df7e81fa3cbb8", + "sha256:f0ce778135f60799d89c9693b9b398819d15f1921ba15fe719acb3178215a7db", + "sha256:f2347d3534e76bf50bca5500989d6c1d05ed64b440408057a37673282c654927", + "sha256:f3c08197f3039bec79cee59a606d62b96b16669cff3949f21e74796b6e3cd2be", + "sha256:f632fd56fc4e61564f78b46a2269153122db34988e78b6be8b32d28507b7eaeb", + "sha256:f6984a24db30548fd39a44360532898c33528b74aedf81c26cf29c51ee47057e", + "sha256:f70aadb7a809305226daedf75d90379c397b094755a710d7014b8b117df1ebbf", + "sha256:f748f7c2d6fd375cc93d3fba7ef4a9e3a092421b8dbf34d8d4dc06be9492dfdd", + "sha256:f8429e1c410b4073944f03bd778a9e066e7fad723564a52ff91841d278dfc822", + "sha256:fc746432b951e92b58317af8e0ca746efe93e66555f1b40888865ef5bf56446b" + ], + "markers": "python_version >= '3.8'", + "version": "==5.0.0" }, "blinker": { "hashes": [ @@ -34,36 +103,44 @@ }, "certifi": { "hashes": [ - "sha256:0f212c2744a9bb6de0c56639a6f68afe01ecd92d91f14ae897c4fe7bbeeef0de", - "sha256:47c09d31ccf2acf0be3f701ea53595ee7e0b8fa08801c6624be771df09ae7b43" + "sha256:024c88eeec92ca068db80f02b8b07c9cef7b9fe261d1d535abfd5abd6f6af432", + "sha256:2227dcbaafe0d2f59279d1762ddddc37783ed4354594f194ffc31d20f41fc3db" ], "markers": "python_version >= '3.7'", - "version": "==2025.10.5" + "version": "==2026.6.17" }, "click": { "hashes": [ - "sha256:9b9f285302c6e3064f4330c05f05b81945b2a39544279343e6e7c5f27a9baddc", - "sha256:e7b8232224eba16f4ebe410c25ced9f7875cb5f3263ffc93cc3e8da705e229c4" + "sha256:9a6cea6e60b17ebe0a44c5cc636d94f09bd66142c1cd7d8b4cd731c4917a15f6", + "sha256:e6f9f66136c816745b9d65817da91d61d957fb16e02e4dcd0552553c5a197b76" ], "markers": "python_version >= '3.10'", - "version": "==8.3.0" + "version": "==8.4.2" }, "cloudinary": { "hashes": [ - "sha256:62d4374b79d5476de2a86cb6a1da709a5429e02aef474bfc5d99f3e38a1a62ff", - "sha256:b4785031179a5ec7010f46665e5c8fad2cae022c18405546f01d257e02f78b1c" + "sha256:6624d06014c33f50c0c9e740fde1dfe4579d03311025d8bb7183824b7443aa09", + "sha256:bc9139c68f2ef6996ba62a5d0300e63c711d4f6564644c0c82aa31bf641ef3c7" ], "index": "pypi", - "version": "==1.44.1" + "version": "==1.44.2" + }, + "colorama": { + "hashes": [ + "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", + "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6" + ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4, 3.5, 3.6'", + "version": "==0.4.6" }, "flask": { "hashes": [ - "sha256:bf656c15c80190ed628ad08cdfd3aaa35beb087855e2f494910aa3774cc4fd87", - "sha256:ca1d8112ec8a6158cc29ea4858963350011b5c846a414cdb7a954aa9e967d03c" + "sha256:0ef0e52b8a9cd932855379197dd8f94047b359ca0a78695144304cb45f87c9eb", + "sha256:f4bcbefc124291925f1a26446da31a5178f9483862233b23c0c96a20701f670c" ], "index": "pypi", "markers": "python_version >= '3.9'", - "version": "==3.1.2" + "version": "==3.1.3" }, "flask-admin": { "hashes": [ @@ -74,14 +151,22 @@ "markers": "python_version >= '3.10'", "version": "==2.0.0" }, + "flask-bcrypt": { + "hashes": [ + "sha256:062fd991dc9118d05ac0583675507b9fe4670e44416c97e0e6819d03d01f808a", + "sha256:f07b66b811417ea64eb188ae6455b0b708a793d966e1a80ceec4a23bc42a4369" + ], + "index": "pypi", + "version": "==1.0.1" + }, "flask-cors": { "hashes": [ - "sha256:c7b2cbfb1a31aa0d2e5341eea03a6805349f7a61647daee1a15c46bbe981494c", - "sha256:d81bcb31f07b0985be7f48406247e9243aced229b7747219160a0559edd678db" + "sha256:30c5031552cd59f620ac0c8211dac45b345d3b2df310e7721879e4f46ef9c601", + "sha256:68fcf75693e961f3af26683b23c4b9a8fb6b64de17d20d0c37b95e8de7ab2ed8" ], "index": "pypi", "markers": "python_version >= '3.9' and python_version < '4.0'", - "version": "==6.0.1" + "version": "==6.0.5" }, "flask-jwt-extended": { "hashes": [ @@ -120,72 +205,97 @@ }, "greenlet": { "hashes": [ - "sha256:00fadb3fedccc447f517ee0d3fd8fe49eae949e1cd0f6a611818f4f6fb7dc83b", - "sha256:061dc4cf2c34852b052a8620d40f36324554bc192be474b9e9770e8c042fd735", - "sha256:0db5594dce18db94f7d1650d7489909b57afde4c580806b8d9203b6e79cdc079", - "sha256:0dca0d95ff849f9a364385f36ab49f50065d76964944638be9691e1832e9f86d", - "sha256:16458c245a38991aa19676900d48bd1a6f2ce3e16595051a4db9d012154e8433", - "sha256:18d9260df2b5fbf41ae5139e1be4e796d99655f023a636cd0e11e6406cca7d58", - "sha256:1987de92fec508535687fb807a5cea1560f6196285a4cde35c100b8cd632cc52", - "sha256:1a921e542453fe531144e91e1feedf12e07351b1cf6c9e8a3325ea600a715a31", - "sha256:1ee8fae0519a337f2329cb78bd7a8e128ec0f881073d43f023c7b8d4831d5246", - "sha256:20fb936b4652b6e307b8f347665e2c615540d4b42b3b4c8a321d8286da7e520f", - "sha256:23768528f2911bcd7e475210822ffb5254ed10d71f4028387e5a99b4c6699671", - "sha256:2523e5246274f54fdadbce8494458a2ebdcdbc7b802318466ac5606d3cded1f8", - "sha256:27890167f55d2387576d1f41d9487ef171849ea0359ce1510ca6e06c8bece11d", - "sha256:299fd615cd8fc86267b47597123e3f43ad79c9d8a22bebdce535e53550763e2f", - "sha256:3b3812d8d0c9579967815af437d96623f45c0f2ae5f04e366de62a12d83a8fb0", - "sha256:3b67ca49f54cede0186854a008109d6ee71f66bd57bb36abd6d0a0267b540cdd", - "sha256:44358b9bf66c8576a9f57a590d5f5d6e72fa4228b763d0e43fee6d3b06d3a337", - "sha256:49a30d5fda2507ae77be16479bdb62a660fa51b1eb4928b524975b3bde77b3c0", - "sha256:4d1378601b85e2e5171b99be8d2dc85f594c79967599328f95c1dc1a40f1c633", - "sha256:554b03b6e73aaabec3745364d6239e9e012d64c68ccd0b8430c64ccc14939a8b", - "sha256:55e9c5affaa6775e2c6b67659f3a71684de4c549b3dd9afca3bc773533d284fa", - "sha256:58b97143c9cc7b86fc458f215bd0932f1757ce649e05b640fea2e79b54cedb31", - "sha256:5c9320971821a7cb77cfab8d956fa8e39cd07ca44b6070db358ceb7f8797c8c9", - "sha256:65458b409c1ed459ea899e939f0e1cdb14f58dbc803f2f93c5eab5694d32671b", - "sha256:671df96c1f23c4a0d4077a325483c1503c96a1b7d9db26592ae770daa41233d4", - "sha256:710638eb93b1fa52823aa91bf75326f9ecdfd5e0466f00789246a5280f4ba0fc", - "sha256:73f49b5368b5359d04e18d15828eecc1806033db5233397748f4ca813ff1056c", - "sha256:81701fd84f26330f0d5f4944d4e92e61afe6319dcd9775e39396e39d7c3e5f98", - "sha256:8854167e06950ca75b898b104b63cc646573aa5fef1353d4508ecdd1ee76254f", - "sha256:8c68325b0d0acf8d91dde4e6f930967dd52a5302cd4062932a6b2e7c2969f47c", - "sha256:94385f101946790ae13da500603491f04a76b6e4c059dab271b3ce2e283b2590", - "sha256:94abf90142c2a18151632371140b3dba4dee031633fe614cb592dbb6c9e17bc3", - "sha256:96378df1de302bc38e99c3a9aa311967b7dc80ced1dcc6f171e99842987882a2", - "sha256:9c40adce87eaa9ddb593ccb0fa6a07caf34015a29bf8d344811665b573138db9", - "sha256:9fe0a28a7b952a21e2c062cd5756d34354117796c6d9215a87f55e38d15402c5", - "sha256:a7d4e128405eea3814a12cc2605e0e6aedb4035bf32697f72deca74de4105e02", - "sha256:abbf57b5a870d30c4675928c37278493044d7c14378350b3aa5d484fa65575f0", - "sha256:b4a1870c51720687af7fa3e7cda6d08d801dae660f75a76f3845b642b4da6ee1", - "sha256:b6a7c19cf0d2742d0809a4c05975db036fdff50cd294a93632d6a310bf9ac02c", - "sha256:b90654e092f928f110e0007f572007c9727b5265f7632c2fa7415b4689351594", - "sha256:c17b6b34111ea72fc5a4e4beec9711d2226285f0386ea83477cbb97c30a3f3a5", - "sha256:c2ca18a03a8cfb5b25bc1cbe20f3d9a4c80d8c3b13ba3df49ac3961af0b1018d", - "sha256:c5111ccdc9c88f423426df3fd1811bfc40ed66264d35aa373420a34377efc98a", - "sha256:c60a6d84229b271d44b70fb6e5fa23781abb5d742af7b808ae3f6efd7c9c60f6", - "sha256:c8c9e331e58180d0d83c5b7999255721b725913ff6bc6cf39fa2a45841a4fd4b", - "sha256:c9913f1a30e4526f432991f89ae263459b1c64d1608c0d22a5c79c287b3c70df", - "sha256:cd3c8e693bff0fff6ba55f140bf390fa92c994083f838fece0f63be121334945", - "sha256:d25c5091190f2dc0eaa3f950252122edbbadbb682aa7b1ef2f8af0f8c0afefae", - "sha256:d2e685ade4dafd447ede19c31277a224a239a0a1a4eca4e6390efedf20260cfb", - "sha256:d76383238584e9711e20ebe14db6c88ddcedc1829a9ad31a584389463b5aa504", - "sha256:ddf9164e7a5b08e9d22511526865780a576f19ddd00d62f8a665949327fde8bb", - "sha256:e37ab26028f12dbb0ff65f29a8d3d44a765c61e729647bf2ddfbbed621726f01", - "sha256:f10fd42b5ee276335863712fa3da6608e93f70629c631bf77145021600abc23c", - "sha256:f28588772bb5fb869a8eb331374ec06f24a83a9c25bfa1f38b6993afe9c1e968" + "sha256:01e32e9d2b1714a2b06184cb3071ff2a2fd9bc7d065e39198ab21f7253dad421", + "sha256:0488ca77c94da5e09d1d9958f98b58cebba1b8fd9664c24898499133de927574", + "sha256:049827baab63dda8ab8ec5a6d07fc6eb0f418319cfc757fc8737a605e99ca1ad", + "sha256:0629377725977252159de1ebd3c6e49c170a63856e585446797bb3d66d4d9c34", + "sha256:09201fa698768db245920b00fdc86ee3e73540f01ca6db162be9632642e1a473", + "sha256:0977af2df83136f81c1f76e76d4e2fe7d0dc56ea9c101a86af26a95190b9ca32", + "sha256:120b77c2a18ebf629c3a7886f68c6d01e065654844ad468f15bb93ace66f2094", + "sha256:1587ff8b58fdf806993ed1490a06ac19c22d47b219c68b30954380029045d8d4", + "sha256:1724499fc08388208408681c53c5062e9803c334e5a0bdaeb616228ba882aac8", + "sha256:1adc23c50f22b0f5979521909a8360ab4a3d3bef8b641ce633a04cf1b1c967ea", + "sha256:1c31219badba285858ba8ed117f403dea7fafee6bade9a1991875aae530c3ceb", + "sha256:1d554cd96841a68d464d75a3736f8e87408a7b02b1930a75fa32feb408ad62f8", + "sha256:1f052fff492c52fdfa99bd3b3c1389a53de37dae76a0562741417f0d018f02b3", + "sha256:24c59cb7db9d5c694cb8fd0c76eef8e456b2123afdfa7e4b8f2a67a0860d7682", + "sha256:26aed8d9503ca78889141a9739d71b383efea5f472a7c522b5410f7eb2a1b163", + "sha256:2c3b3311af72b3d3b03cc0f1ffd11f072e834be5d0444105cf715fc44434e39c", + "sha256:2c6d6bfa4fdd7c39a0dbf112cdf28edbd19c517c810eefb6e4e71b0d55933a4c", + "sha256:2debcd0ef9455b7d4879589903efc8e497d4b8fb8c0ae772309e44d1ca5e957f", + "sha256:2ee6288f1933d698b4f098127ed17bda2910a75d2807915bd16294a972055d6c", + "sha256:30252d191d6959df1d040b559a38fc017139606c5ecc2ad00416557c0355d742", + "sha256:36cfea2aa075d544617176b2e84450480f0797070ad8799a8c41ada2fe449d32", + "sha256:3be00501fb4a8c37f6b4b3c4773808ceb26ea65c7ea64fd5735d0f330b3786de", + "sha256:3c2315045f9983e2e50d7e89d95405c21bddb8745f2da4487bc080ab3525f904", + "sha256:3c417cd6c593bbbef6f7aa31a79f37d3db7d18832fc56b694a2150130bde784e", + "sha256:3dff6cd3aac35f6cd3fc23460105acf576f5faf6c378de0bc088bf37c913864a", + "sha256:423167363c510a75b649f5cd58d873c29498ea03598b9e4b1c3b73e0f899f3d5", + "sha256:4e554809538bd4867f24421b43abde170f9c9b8192149b30df5e164bcac6124f", + "sha256:537c5c4f30395020bb9f48f53146070e3b997c3c75da14011ab732aaa19ce3ef", + "sha256:561dd919c02236a613fbf226791cbd77ee5002cbd5cb7e838869aa3ac7a71e16", + "sha256:5795e883e915333c0d5648faaa691857fbc7180136883edc377f50f0d509c2a8", + "sha256:5930d3946ecae99fa7fc0e3f3ae515426ad85058ebd9bfc6c00cca8016e6206b", + "sha256:5eba55076d79e8a5176e6925295cfb901ebc95dae493342ede22230f75d8bee2", + "sha256:6d78b5c1c178dad90447f1b8452262709d3eef4c98f825569e74c9d0b2260ac9", + "sha256:6d9e19257794e28821c9ebd5e23f86d7c267cd9d390089374f068d2049f949e3", + "sha256:6e9e49d732ee92a189bb7035e293029244aeba648297a9b856dc733d17ca7f0d", + "sha256:6f1e473c06ae8be00c9034c2bb10fa277b08a93287e3111c395b839f01d27e1f", + "sha256:6f96ed6f4adc1066954ae95f45717657cb67468ef3b89e9a3632e14a625a8f39", + "sha256:711028c953cd6ce5dc01bbb5a1747e3ad6bd8b2f7ded73778bb936e8dab9e3b6", + "sha256:76dae33e97b52743a19210931ee3e78a88fe1438bc2fc4ee5e7512d289bfad4f", + "sha256:7a7bfc200be40d04961d7e80e8337d726c0c1a50777e588123c3ed8ba731dcb9", + "sha256:7bb811753703739ad318112f16eccfaabdac050037b6d092debaa8b23566b4ce", + "sha256:7fe6062b1f35534e1e8fb28dfed406cf4eeff3e0bca3a0d9f8ff69f20a4abb00", + "sha256:87359c23eb4e8f1b16da68faad29bf5aeb80e3628d7d8e4aa2e41c36879ddedd", + "sha256:89da99ee8345b458ea2f16831dad31c88ddcdec454b48704d569a0b8fb28f146", + "sha256:9558cae989faeab6fbb425cd98a0cfa4190a47fba6443973fbee0a1eb0b0b6c3", + "sha256:98a52d6a50d4deaba304331d83ee3e10ebbdc1517fcca40b2715d1de4534065c", + "sha256:9dc23f0e5ad76415457212a4b947d22ebe4dc80baf02adf7dd5647a90f38bb4e", + "sha256:9df9daae96848508450011d0d86ed7c95f8829a354ce438284a77b24896fd1f8", + "sha256:9e194b996aa1b89d933cfe136e5eb39b22a8b72ba59d376ef39a55bca4dbf47f", + "sha256:a0314aa832c94633355dc6f3ee54f195159533355a323f26926fc63b98b2ccbb", + "sha256:a1759fa4f14c398508cf20dc8037de55cc23ae8bd14c185c2718257837195ca5", + "sha256:a1789a6244ea1ba61fd4386c9a6a31873e9b0234762103364be98ef87dcb19f3", + "sha256:a207023f1cf8695fd82580b8099c09c5809be18bc2282362cdfb965dd884a317", + "sha256:a2ddf9eddc617681108dd071b3feabf3f4a4cd64846254aec4d4ceda098b639a", + "sha256:a3f76a94e2d6e1fee8f302265679d8cc47d71a203936dd03c6e2ace0f9cfd46d", + "sha256:a850f6224088ef7dcc70f1a545cb6b3d119c35d6dca63b925b9f35da0635cdad", + "sha256:a9476cbead736dc48ce89e3cd97acff95ecc48cbf21273603a438f9870c4a014", + "sha256:a96457a30384de52d9c5d2fd33abf6c1daae3db392cd556738f408b1a79a1cf0", + "sha256:b4ac902af825cbac8e9b2fccab8122236fd2ba6c8b71a080116d2c2ec72671b1", + "sha256:b4cad42662c796334c2d24607c411e3ed82481c1fb4e1e8ec3a5a8416060092e", + "sha256:b9318cdeb9abdbfdd8bc8464ee4a06dffde2c7846e1def138365a6240ab2c9a5", + "sha256:bc18b8d33e6976804b9b792fe11cb3b1fee8b646e8a9e20bf521a429ddf73520", + "sha256:bf493b3c1c0a2324c49b0472e2280ba4665f3510d8115f6f807759a6163b15f7", + "sha256:c0ea4eb3de23f0bac1d75205e10ccfa9b418b17b01a2d7bf19e3b69dda08900a", + "sha256:c1b906220d83c140361cdd12eef970fb5881a168b98ee58a43786426173da14c", + "sha256:c1c1e5ad80f1f38ea479b83b39dccb20874cfe9ad5e52f87225fa294ba4d39a1", + "sha256:c674a1dd4fe41f6a93febe7ab366ceabf15080ea31a9307811c56dac5f435f73", + "sha256:ca92411942154023c65851e6077d8ca0d00f19de5fa80bb2c6f196ff6c920ba9", + "sha256:d7792398872f89466c6671d5d193537eff163ecf7fac78d82e6ddc25017fb4f5", + "sha256:db548d5ab6c2a8ead82c013f875090d79b5d7d2b67fc513934ce6cf66492ad7f", + "sha256:dbebc038fcdda8f8f21cce985fd04e34e0f42007e7fc7ab7ad285caf77974b95", + "sha256:e063263ce9047878480d7e536012fc8b7c8e1922989eb5f03b9ab998a2ee7b7e", + "sha256:e4af5d4961818ab651d09c1448a03b1ba2a1726a076266ebb62330bab9f3238c", + "sha256:e976f9f6941f57d87a194c91868622c8b22a142a741d2fde31655c319133ade6", + "sha256:f41feb9f2b59e2e61ac9bea4e344ddd9396bf3cacb2583f73a3595ed7df6f8e7", + "sha256:f4d67c1684db3f9782c37ee4bade3f86f5a23a8fcf3f8359224106018ca40728", + "sha256:f9bbd6216c45a563c2a61e478e038b439d9f248bde44f775ea37d339da643af4", + "sha256:f9ed777c6891d8253e54468576f55e27f8fc1a662a664f946a191003574c0a74", + "sha256:feb721811d2754bfd16b48de151dd6b1f222c048e625151f2ca44cfdfd69f59c" ], - "markers": "python_version >= '3.9'", - "version": "==3.2.4" + "markers": "python_version >= '3.10'", + "version": "==3.5.2" }, "gunicorn": { "hashes": [ - "sha256:ec400d38950de4dfd418cff8328b2c8faed0edb0d517d3394e457c317908ca4d", - "sha256:f014447a0101dc57e294f6c18ca6b40227a4c90e9bdb586042628030cba004ec" + "sha256:40233d26a5f0d1872916188c276e21641155111c2853f0c2cd55260aec0d24fc", + "sha256:ca9346f85e3a4aeeb64d491045c16b9a35647abd37ea15efe53080eb8b090baf" ], "index": "pypi", - "markers": "python_version >= '3.7'", - "version": "==23.0.0" + "markers": "python_version >= '3.10'", + "version": "==26.0.0" }, "itsdangerous": { "hashes": [ @@ -205,11 +315,11 @@ }, "mako": { "hashes": [ - "sha256:99579a6f39583fa7e5630a28c3c1f440e4e97a414b80372649c0ce338da2ea28", - "sha256:baef24a52fc4fc514a0887ac600f9f1cff3d82c61d4d700a1fa84d597b88db59" + "sha256:8f61569480282dbf557145ce441e4ba888be453c30989f879f0d652e39f53ea9", + "sha256:9f778e93289bd410bb35daadeb4fc66d95a746f0b75777b942088b7fd7af550a" ], "markers": "python_version >= '3.8'", - "version": "==1.3.10" + "version": "==1.3.12" }, "markupsafe": { "hashes": [ @@ -308,102 +418,102 @@ }, "packaging": { "hashes": [ - "sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484", - "sha256:d443872c98d677bf60f6a1f2f8c1cb748e8fe762d2bf9d3148b5599295b0fc4f" + "sha256:5fc45236b9446107ff2415ce77c807cee2862cb6fac22b8a73826d0693b0980e", + "sha256:ff452ff5a3e828ce110190feff1178bb1f2ea2281fa2075aadb987c2fb221661" ], "markers": "python_version >= '3.8'", - "version": "==25.0" + "version": "==26.2" }, "psycopg2-binary": { "hashes": [ - "sha256:00ce1830d971f43b667abe4a56e42c1e2d594b32da4802e44a73bacacb25535f", - "sha256:04195548662fa544626c8ea0f06561eb6203f1984ba5b4562764fbeb4c3d14b1", - "sha256:0da4de5c1ac69d94ed4364b6cbe7190c1a70d325f112ba783d83f8440285f152", - "sha256:0e8480afd62362d0a6a27dd09e4ca2def6fa50ed3a4e7c09165266106b2ffa10", - "sha256:20e7fb94e20b03dcc783f76c0865f9da39559dcc0c28dd1a3fce0d01902a6b9c", - "sha256:2c226ef95eb2250974bf6fa7a842082b31f68385c4f3268370e3f3870e7859ee", - "sha256:2d11098a83cca92deaeaed3d58cfd150d49b3b06ee0d0852be466bf87596899e", - "sha256:2e164359396576a3cc701ba8af4751ae68a07235d7a380c631184a611220d9a4", - "sha256:304fd7b7f97eef30e91b8f7e720b3db75fee010b520e434ea35ed1ff22501d03", - "sha256:31b32c457a6025e74d233957cc9736742ac5a6cb196c6b68499f6bb51390bd6a", - "sha256:32770a4d666fbdafab017086655bcddab791d7cb260a16679cc5a7338b64343b", - "sha256:366df99e710a2acd90efed3764bb1e28df6c675d33a7fb40df9b7281694432ee", - "sha256:37d8412565a7267f7d79e29ab66876e55cb5e8e7b3bbf94f8206f6795f8f7e7e", - "sha256:4012c9c954dfaccd28f94e84ab9f94e12df76b4afb22331b1f0d3154893a6316", - "sha256:41360b01c140c2a03d346cec3280cf8a71aa07d94f3b1509fa0161c366af66b4", - "sha256:44fc5c2b8fa871ce7f0023f619f1349a0aa03a0857f2c96fbc01c657dcbbdb49", - "sha256:47f212c1d3be608a12937cc131bd85502954398aaa1320cb4c14421a0ffccf4c", - "sha256:4bdab48575b6f870f465b397c38f1b415520e9879fdf10a53ee4f49dcbdf8a21", - "sha256:4dca1f356a67ecb68c81a7bc7809f1569ad9e152ce7fd02c2f2036862ca9f66b", - "sha256:5c6ff3335ce08c75afaed19e08699e8aacf95d4a260b495a4a8545244fe2ceb3", - "sha256:5f3f2732cf504a1aa9e9609d02f79bea1067d99edf844ab92c247bbca143303b", - "sha256:62b6d93d7c0b61a1dd6197d208ab613eb7dcfdcca0a49c42ceb082257991de9d", - "sha256:691c807d94aecfbc76a14e1408847d59ff5b5906a04a23e12a89007672b9e819", - "sha256:763c93ef1df3da6d1a90f86ea7f3f806dc06b21c198fa87c3c25504abec9404a", - "sha256:84011ba3109e06ac412f95399b704d3d6950e386b7994475b231cf61eec2fc1f", - "sha256:865f9945ed1b3950d968ec4690ce68c55019d79e4497366d36e090327ce7db14", - "sha256:875039274f8a2361e5207857899706da840768e2a775bf8c65e82f60b197df02", - "sha256:8b81627b691f29c4c30a8f322546ad039c40c328373b11dff7490a3e1b517855", - "sha256:8c55b385daa2f92cb64b12ec4536c66954ac53654c7f15a203578da4e78105c0", - "sha256:91537a8df2bde69b1c1db01d6d944c831ca793952e4f57892600e96cee95f2cd", - "sha256:92e3b669236327083a2e33ccfa0d320dd01b9803b3e14dd986a4fc54aa00f4e1", - "sha256:9b52a3f9bb540a3e4ec0f6ba6d31339727b2950c9772850d6545b7eae0b9d7c5", - "sha256:9bd81e64e8de111237737b29d68039b9c813bdf520156af36d26819c9a979e5f", - "sha256:9c55460033867b4622cda1b6872edf445809535144152e5d14941ef591980edf", - "sha256:9d3a9edcfbe77a3ed4bc72836d466dfce4174beb79eda79ea155cc77237ed9e8", - "sha256:a1cf393f1cdaf6a9b57c0a719a1068ba1069f022a59b8b1fe44b006745b59757", - "sha256:a28d8c01a7b27a1e3265b11250ba7557e5f72b5ee9e5f3a2fa8d2949c29bf5d2", - "sha256:a311f1edc9967723d3511ea7d2708e2c3592e3405677bf53d5c7246753591fbb", - "sha256:a6c0e4262e089516603a09474ee13eabf09cb65c332277e39af68f6233911087", - "sha256:ab8905b5dcb05bf3fb22e0cf90e10f469563486ffb6a96569e51f897c750a76a", - "sha256:b31e90fdd0f968c2de3b26ab014314fe814225b6c324f770952f7d38abf17e3c", - "sha256:b33fabeb1fde21180479b2d4667e994de7bbf0eec22832ba5d9b5e4cf65b6c6d", - "sha256:b637d6d941209e8d96a072d7977238eea128046effbf37d1d8b2c0764750017d", - "sha256:b6aed9e096bf63f9e75edf2581aa9a7e7186d97ab5c177aa6c87797cd591236c", - "sha256:b8fb3db325435d34235b044b199e56cdf9ff41223a4b9752e8576465170bb38c", - "sha256:ba34475ceb08cccbdd98f6b46916917ae6eeb92b5ae111df10b544c3a4621dc4", - "sha256:be9b840ac0525a283a96b556616f5b4820e0526addb8dcf6525a0fa162730be4", - "sha256:bf940cd7e7fec19181fdbc29d76911741153d51cab52e5c21165f3262125685e", - "sha256:c0377174bf1dd416993d16edc15357f6eb17ac998244cca19bc67cdc0e2e5766", - "sha256:c3cb3a676873d7506825221045bd70e0427c905b9c8ee8d6acd70cfcbd6e576d", - "sha256:c47676e5b485393f069b4d7a811267d3168ce46f988fa602658b8bb901e9e64d", - "sha256:c665f01ec8ab273a61c62beeb8cce3014c214429ced8a308ca1fc410ecac3a39", - "sha256:cffe9d7697ae7456649617e8bb8d7a45afb71cd13f7ab22af3e5c61f04840908", - "sha256:d526864e0f67f74937a8fce859bd56c979f5e2ec57ca7c627f5f1071ef7fee60", - "sha256:d57c9c387660b8893093459738b6abddbb30a7eab058b77b0d0d1c7d521ddfd7", - "sha256:d6fe6b47d0b42ce1c9f1fa3e35bb365011ca22e39db37074458f27921dca40f2", - "sha256:db4fd476874ccfdbb630a54426964959e58da4c61c9feba73e6094d51303d7d8", - "sha256:e0deeb03da539fa3577fcb0b3f2554a97f7e5477c246098dbb18091a4a01c16f", - "sha256:e35b7abae2b0adab776add56111df1735ccc71406e56203515e228a8dc07089f", - "sha256:ebb415404821b6d1c47353ebe9c8645967a5235e6d88f914147e7fd411419e6f", - "sha256:edcb3aeb11cb4bf13a2af3c53a15b3d612edeb6409047ea0b5d6a21a9d744b34", - "sha256:ef7a6beb4beaa62f88592ccc65df20328029d721db309cb3250b0aae0fa146c3", - "sha256:efff12b432179443f54e230fdf60de1f6cc726b6c832db8701227d089310e8aa", - "sha256:f07c9c4a5093258a03b28fab9b4f151aa376989e7f35f855088234e656ee6a94", - "sha256:f090b7ddd13ca842ebfe301cd587a76a4cf0913b1e429eb92c1be5dbeb1a19bc", - "sha256:fa0f693d3c68ae925966f0b14b8edda71696608039f4ed61b1fe9ffa468d16db", - "sha256:fcf21be3ce5f5659daefd2b3b3b6e4727b028221ddc94e6c1523425579664747" + "sha256:00814e40fa23c2b37ef0a1e3c749d89982c73a9cb5046137f0752a22d432e82f", + "sha256:049366c6d884bdcd65d66e6ca1fdbebe670b56c6c9ba46f164e6667e90881964", + "sha256:0dc9228d47c46bda253d2ecd6bb93b56a9f2d7ad33b684a1fa3622bf74ffe30c", + "sha256:1006fb62f0f0bc5ce256a832356c6262e91be43f5e4eb15b5eaf38079464caf2", + "sha256:127467c6e476dd876634f17c3d870530e73ff454ff99bff73d36e80af28e1115", + "sha256:1c8ad4c08e00f7679559eaed7aff1edfffc60c086b976f93972f686384a95e2c", + "sha256:29d4d134bd0ab46ffb04e94aa3c5fa3ef582e9026609165e2f758ff76fc3a3be", + "sha256:3471336e1acfd9c7fe507b8bad5af9317b6a89294f9eb37bd9a030bb7bebcdc6", + "sha256:36512911ebb2b60a0c3e44d0bb5048c1980aced91235d133b7874f3d1d93487c", + "sha256:398fcd4db988c7d7d3713e2b8e18939776fd3fb447052daae4f24fa39daede4c", + "sha256:3d999bd982a723113c1a45b55a7a6a90d64d0ed2278020ed625c490ff7bef96c", + "sha256:40e7b28b63aaf737cb3a1edc3a9bbc9a9f4ad3dcb7152e8c1130e4050eddcb7d", + "sha256:411e85815652d13560fbe731878daa5d92378c4995a22302071890ec3397d019", + "sha256:4413d0caef93c5cf50b96863df4c2efe8c269bf2267df353225595e7e15e8df7", + "sha256:4766ab678563054d3f1d064a4db19cc4b5f9e3a8d9018592a8285cf200c248f3", + "sha256:4dfcf8e45ebb0c663be34a3442f65e17311f3367089cd4e5e3a3e8e62c978777", + "sha256:527e6342b3e44c2f0544f6b8e927d60de7f163f5723b8f1dfa7d2a84298738cd", + "sha256:54a0dfecab1b48731f934e06139dfe11e24219fb6d0ceb32177cf0375f14c7b5", + "sha256:5a0253224780c978746cb9be55a946bcdaf40fe3519c0f622924cdabdafe2c39", + "sha256:5ac9444edc768c02a6b6a591f070b8aae28ff3a99be57560ac996001580f294c", + "sha256:5c7cb4cbf894a1d36c720d713de507952c7c58f66d30834708f03dbe5c822ccf", + "sha256:5c8ce6c61bd1b1f6b9c24ee32211599f6166af2c55abb19456090a21fd16554b", + "sha256:5cdc05117180c5fa9c40eea8ea559ce64d73824c39d928b7da9fb5f6a9392433", + "sha256:612b965daee295ae2da8f8218ce1d274645dc76ef3f1abf6a0a94fd57eff876d", + "sha256:63a3ebbd543d3d1eda088ac99164e8c5bac15293ee91f20281fd17d050aee1c4", + "sha256:66a7685d7e548f10fb4ce32fb01a7b7f4aa702134de92a292c7bd9e0d3dbd290", + "sha256:6f3b3de8a74ef8db215f22edffb19e32dc6fa41340456de7ec99efdc8a7b3ec2", + "sha256:6f9cae1f848779b5b01f417e762c40d026ea93eb0648249a604728cda991dde3", + "sha256:718e1fc18edf573b02cb8aea868de8d8d33f99ce9620206aa9144b67b0985e94", + "sha256:77b348775efd4cdab410ec6609d81ccecd1139c90265fa583a7255c8064bc03d", + "sha256:7af18183109e23502c8b2ae7f6926c0882766f35b5175a4cd737ad825e4d7a1b", + "sha256:7c729a73c7b1b84de3582f73cdd27d905121dc2c531f3d9a3c32a3011033b965", + "sha256:83946ba43979ebfdc99a3cd0ee775c89f221df026984ba19d46133d8d75d3cd9", + "sha256:840066105706cd2eb29b9a1c2329620056582a4bf3e8169dec5c447042d0869f", + "sha256:863f5d12241ebe1c76a72a04c2113b6dc905f90b9cef0e9be0efd994affd9354", + "sha256:864c261b3690e1207d14bbfe0a61e27567981b80c47a778561e49f676f7ce433", + "sha256:89d19a9f7899e8eb0656a2b3a08e0da04c720a06db6e0033eab5928aabe60fa9", + "sha256:8ffdb59fe88f99589e34354a130217aa1fd2d615612402d6edc8b3dbc7a44463", + "sha256:96937c9c5d891f772430f418a7a8b4691a90c3e6b93cf72b5bd7cad8cbca32a5", + "sha256:98062447aebc20ed20add1f547a364fd0ef8933640d5372ff1873f8deb9b61be", + "sha256:995ce929eede89db6254b50827e2b7fd61e50d11f0b116b29fffe4a2e53c4580", + "sha256:9b818ceff717f98851a64bffd4c5eb5b3059ae280276dcecc52ac658dcf006a4", + "sha256:9fe06d93e72f1c048e731a2e3e7854a5bfaa58fc736068df90b352cefe66f03f", + "sha256:a46fe069b65255df410f856d842bc235f90e22ffdf532dda625fd4213d3fd9b1", + "sha256:a7e39a65b7d2a20e4ba2e0aaad1960b61cc2888d6ab047769f8347bd3c9ad915", + "sha256:a99eaab34a9010f1a086b126de467466620a750634d114d20455f3a824aae033", + "sha256:ab29414b25dcb698bf26bf213e3348abdcd07bbd5de032a5bec15bd75b298b03", + "sha256:ace94261f43850e9e79f6c56636c5e0147978ab79eda5e5e5ebf13ae146fc8fe", + "sha256:b4a9eaa6e7f4ff91bec10aa3fb296878e75187bced5cc4bafe17dc40915e1326", + "sha256:b6937f5fe4e180aeee87de907a2fa982ded6f7f15d7218f78a083e4e1d68f2a0", + "sha256:b9a339b79d37c1b45f3235265f07cdeb0cb5ad7acd2ac7720a5920989c17c24e", + "sha256:ba3df2fc42a1cfa45b72cf096d4acb2b885937eedc61461081d53538d4a82a86", + "sha256:c41321a14dd74aceb6a9a643b9253a334521babfa763fa873e33d89cfa122fb5", + "sha256:c5ee5213445dd45312459029b8c4c0a695461eb517b753d2582315bd07995f5e", + "sha256:c6528cefc8e50fcc6f4a107e27a672058b36cc5736d665476aeb413ba88dbb06", + "sha256:cb4a1dacdd48077150dc762a9e5ddbf32c256d66cb46f80839391aa458774936", + "sha256:cfa2517c94ea3af6deb46f81e1bbd884faa63e28481eb2f889989dd8d95e5f03", + "sha256:d2fa0d7caca8635c56e373055094eeda3208d901d55dd0ff5abc1d4e47f82b56", + "sha256:d3227a3bc228c10d21011a99245edca923e4e8bf461857e869a507d9a41fe9f6", + "sha256:d6fcbba8c9fed08a73b8ac61ea79e4821e45b1e92bb466230c5e746bbf3d5256", + "sha256:e4e184b1fb6072bf05388aa41c697e1b2d01b3473f107e7ec44f186a32cfd0b8", + "sha256:ee2d84ef5eb6c04702d2e9c372ad557fb027f26a5d82804f749dfb14c7fdd2ab", + "sha256:f12ae41fcafadb39b2785e64a40f9db05d6de2ac114077457e0e7c597f3af980", + "sha256:f625abb7020e4af3432d95342daa1aa0db3fa369eed19807aa596367ba791b10", + "sha256:f921f3cd87035ef7df233383011d7a53ea1d346224752c1385f1edfd790ceb6a", + "sha256:fb1828cf3da68f99e45ebce1355d65d2d12b6a78fb5dfb16247aad6bdef5f5d2", + "sha256:ffdd7dc5463ccd61845ac37b7012d0f35a1548df9febe14f8dd549be4a0bc81e" ], "index": "pypi", "markers": "python_version >= '3.9'", - "version": "==2.9.11" + "version": "==2.9.12" }, "pyjwt": { "hashes": [ - "sha256:3cc5772eb20009233caf06e9d8a0577824723b44e6648ee0a2aedb6cf9381953", - "sha256:dcdd193e30abefd5debf142f9adfcdd2b58004e644f25406ffaebd50bd98dacb" + "sha256:41571c89ca91598c79e8ef18a2d07367d4810fbbd6f637794879baf1b7703423", + "sha256:66adcc2aff09b3f1bbd95fc1e1577df8ac8723c978552fd43304c8a290ac5728" ], "markers": "python_version >= '3.9'", - "version": "==2.10.1" + "version": "==2.13.0" }, "python-dotenv": { "hashes": [ - "sha256:42667e897e16ab0d66954af0e60a9caa94f0fd4ecf3aaf6d2d260eec1aa36ad6", - "sha256:b81ee9561e9ca4004139c6cbba3a238c32b03e4894671e181b671e8cb8425d61" + "sha256:1d8214789a24de455a8b8bd8ae6fe3c6b69a5e3d64aa8a8e5d68e694bbcb285a", + "sha256:2c371a91fbd7ba082c2c1dc1f8bf89ca22564a087c2c287cd9b662adde799cf3" ], "index": "pypi", - "markers": "python_version >= '3.9'", - "version": "==1.2.1" + "markers": "python_version >= '3.10'", + "version": "==1.2.2" }, "pyyaml": { "hashes": [ @@ -494,67 +604,68 @@ }, "sqlalchemy": { "hashes": [ - "sha256:0765e318ee9179b3718c4fd7ba35c434f4dd20332fbc6857a5e8df17719c24d7", - "sha256:0ae7454e1ab1d780aee69fd2aae7d6b8670a581d8847f2d1e0f7ddfbf47e5a22", - "sha256:0b1af8392eb27b372ddb783b317dea0f650241cea5bd29199b22235299ca2e45", - "sha256:0fe3917059c7ab2ee3f35e77757062b1bea10a0b6ca633c58391e3f3c6c488dd", - "sha256:119dc41e7a7defcefc57189cfa0e61b1bf9c228211aba432b53fb71ef367fda1", - "sha256:11bac86b0deada30b6b5f93382712ff0e911fe8d31cb9bf46e6b149ae175eff0", - "sha256:15f3326f7f0b2bfe406ee562e17f43f36e16167af99c4c0df61db668de20002d", - "sha256:17835885016b9e4d0135720160db3095dc78c583e7b902b6be799fb21035e749", - "sha256:19de7ca1246fbef9f9d1bff8f1ab25641569df226364a0e40457dc5457c54b05", - "sha256:1df4763760d1de0dfc8192cc96d8aa293eb1a44f8f7a5fbe74caf1b551905c5e", - "sha256:1e77faf6ff919aa8cd63f1c4e561cac1d9a454a191bb864d5dd5e545935e5a40", - "sha256:22be14009339b8bc16d6b9dc8780bacaba3402aa7581658e246114abbd2236e3", - "sha256:253e2f29843fb303eca6b2fc645aca91fa7aa0aa70b38b6950da92d44ff267f3", - "sha256:2b61188657e3a2b9ac4e8f04d6cf8e51046e28175f79464c67f2fd35bceb0976", - "sha256:2bf4bb6b3d6228fcf3a71b50231199fb94d2dd2611b66d33be0578ea3e6c2726", - "sha256:2e7b5b079055e02d06a4308d0481658e4f06bc7ef211567edc8f7d5dce52018d", - "sha256:2f19644f27c76f07e10603580a47278abb2a70311136a7f8fd27dc2e096b9013", - "sha256:2fc44e5965ea46909a416fff0af48a219faefd5773ab79e5f8a5fcd5d62b2667", - "sha256:2fcc4901a86ed81dc76703f3b93ff881e08761c63263c46991081fd7f034b165", - "sha256:3255d821ee91bdf824795e936642bbf43a4c7cedf5d1aed8d24524e66843aa74", - "sha256:329aa42d1be9929603f406186630135be1e7a42569540577ba2c69952b7cf399", - "sha256:357bade0e46064f88f2c3a99808233e67b0051cdddf82992379559322dfeb183", - "sha256:3caef1ff89b1caefc28f0368b3bde21a7e3e630c2eddac16abd9e47bd27cc36a", - "sha256:3cf6872a23601672d61a68f390e44703442639a12ee9dd5a88bbce52a695e46e", - "sha256:3fe166c7d00912e8c10d3a9a0ce105569a31a3d0db1a6e82c4e0f4bf16d5eca9", - "sha256:471733aabb2e4848d609141a9e9d56a427c0a038f4abf65dd19d7a21fd563632", - "sha256:4848395d932e93c1595e59a8672aa7400e8922c39bb9b0668ed99ac6fa867822", - "sha256:48bf7d383a35e668b984c805470518b635d48b95a3c57cb03f37eaa3551b5f9f", - "sha256:4c26ef74ba842d61635b0152763d057c8d48215d5be9bb8b7604116a059e9985", - "sha256:4d18cd0e9a0f37c9f4088e50e3839fcb69a380a0ec957408e0b57cff08ee0a26", - "sha256:585c0c852a891450edbb1eaca8648408a3cc125f18cf433941fa6babcc359e29", - "sha256:70e03833faca7166e6a9927fbee7c27e6ecde436774cd0b24bbcc96353bce06b", - "sha256:72fea91746b5890f9e5e0997f16cbf3d53550580d76355ba2d998311b17b2250", - "sha256:78e6c137ba35476adb5432103ae1534f2f5295605201d946a4198a0dea4b38e7", - "sha256:7a8694107eb4308a13b425ca8c0e67112f8134c846b6e1f722698708741215d5", - "sha256:7c77f3080674fc529b1bd99489378c7f63fcb4ba7f8322b79732e0258f0ea3ce", - "sha256:7cbcb47fd66ab294703e1644f78971f6f2f1126424d2b300678f419aa73c7b6e", - "sha256:846541e58b9a81cce7dee8329f352c318de25aa2f2bbe1e31587eb1f057448b4", - "sha256:8e0e4e66fd80f277a8c3de016a81a554e76ccf6b8d881ee0b53200305a8433f6", - "sha256:9919e77403a483ab81e3423151e8ffc9dd992c20d2603bf17e4a8161111e55f5", - "sha256:9b94843a102efa9ac68a7a30cd46df3ff1ed9c658100d30a725d10d9c60a2f44", - "sha256:9e9018544ab07614d591a26c1bd4293ddf40752cc435caf69196740516af7100", - "sha256:b87e7b91a5d5973dda5f00cd61ef72ad75a1db73a386b62877d4875a8840959c", - "sha256:c1c80faaee1a6c3428cecf40d16a2365bcf56c424c92c2b6f0f9ad204b899e9e", - "sha256:c3678a0fb72c8a6a29422b2732fe423db3ce119c34421b5f9955873eb9b62c1e", - "sha256:cbe4f85f50c656d753890f39468fcd8190c5f08282caf19219f684225bfd5fd2", - "sha256:cc2856d24afa44295735e72f3c75d6ee7fdd4336d8d3a8f3d44de7aa6b766df2", - "sha256:d733dec0614bb8f4bcb7c8af88172b974f685a31dc3a65cca0527e3120de5606", - "sha256:dc8b3850d2a601ca2320d081874033684e246d28e1c5e89db0864077cfc8f5a9", - "sha256:de4387a354ff230bc979b46b2207af841dc8bf29847b6c7dbe60af186d97aefa", - "sha256:e998cf7c29473bd077704cea3577d23123094311f59bdc4af551923b168332b1", - "sha256:ebac3f0b5732014a126b43c2b7567f2f0e0afea7d9119a3378bde46d3dcad88e", - "sha256:ee51625c2d51f8baadf2829fae817ad0b66b140573939dd69284d2ba3553ae73", - "sha256:f4a172b31785e2f00780eccab00bc240ccdbfdb8345f1e6063175b3ff12ad1b0", - "sha256:f7027414f2b88992877573ab780c19ecb54d3a536bef3397933573d6b5068be4", - "sha256:f9480c0740aabd8cb29c329b422fb65358049840b34aba0adf63162371d2a96e", - "sha256:ff486e183d151e51b1d694c7aa1695747599bb00b9f5f604092b54b74c64a8e1" + "sha256:0378d055e9e8cd6ce4d8dff683bdd3d7d413533c4ee51d67a2b1e0f9eacc0f23", + "sha256:0592bdadf86ddcabfd72d9ab66ea8a5d8d2cc6be1cc51fa7e66c03868ac5eac1", + "sha256:08a204d8b5638717c26a24df18fcf40af45a6b22e35b70b1d62f0113c2e278e8", + "sha256:0c2c62877097e1a0db401fba5cb4debee33265e5b2a55c4ccb489c02c53b4f72", + "sha256:0e8203d2fbd5c6254692ef0a72c740d75b2f3c7ca345404f4c1a4604813c77c0", + "sha256:0f053118c30e53161857a953e4de667d90e274980dccbe5dd3829bbbeece72a5", + "sha256:0f6bcad487aee1c638d707235682fc96f741de00663619881ab235400d03289e", + "sha256:111604e637da87031255ddc26c7d7bc22bc6af6f5d459ccff3af1b4660233a85", + "sha256:1181256e0f16479691b5616d36375dc2620ad8332b25978763c3d206ad3f3f1d", + "sha256:159bb6ba32059f57ad7375a8f50d844dd2f19d14954ecf820cd33e20debd46b2", + "sha256:1aa10c0daee6705294d181daadaa793221e1a59ed55000a3fab1d42b088ce4ba", + "sha256:1af05726b3d0cdba1c55284bf408fd3b792e690fe2399bfb8304565551cda652", + "sha256:1bed1ee8b01da6088210aa9412023326fb98a599ba502e6118308601dcbef77f", + "sha256:1d21ce524ab86c23046e992a5b81cb54c21079c6df6e78b8fc77d77cac70a6b9", + "sha256:1e47b1199c2e832e325eacabc8d32d2487f58c9358f97e9a00f5eb93c5680d84", + "sha256:247acaa29ccef6250dfd6a3eedf8f94ddf23564180a39fe362e32ae9dbdbde46", + "sha256:2a97eaad21c84b4ef8010b11eeba9fe6153eb0b3df3ff8b6abc309df1b978ef7", + "sha256:2cf39aabdf48e87c1c2c2ed6d20d33ffa0733b3071ce9c5f66357947dd009080", + "sha256:2e54ff2dd657f2e3e0fbf2b097db1182f7bfea263eca4353f00065bae2a67c3d", + "sha256:39a76529db6305693d8d4affa58ad5b5e2e18edd62daea628b29b97930b3513d", + "sha256:4004ada0aafe8ae1991b2cd1d99c6d9146126e123bd6f883c260d974aa012e54", + "sha256:436728ce18a80f6951a1e11cc6112c2ede9faf20766f1a26195a7c441ca12dbd", + "sha256:483b11bd46bf35fc14c52faf338b04300c9e6ce554bce9b11be85bfec3bc3195", + "sha256:4a011ea4510683319ce4ed274b56ee05194b39b6da9d09ca7a39388f0fa84dcc", + "sha256:581921d849d6e6f994d560389192955e80e2950e18fcdfe2ccea863e01158e6e", + "sha256:59cab3686b1bc039dd9cded2f8d0c08a246e84e76bd4ab5b4f18c7cdae293825", + "sha256:6b588fd681ddf0c196b8df1ea49a8913514894b2b8f945a9511b4b48871f99c8", + "sha256:6e46fc36029eff666391e0531e5387b62ce6c4f1d8e50b3fb3099eaca1b42522", + "sha256:6ea306caaae6bd5afd0a46050003c88f6bf33227377a49298c498c3cb88ff491", + "sha256:72ca54c952107ba5cd58854b67a5a6268631289d21651a1235396f3b98b47400", + "sha256:740cf6f35351b1ac3d82369152acf1d51d37e3dcf85d4dc0a22ca01410eabe2a", + "sha256:7c2056838b6685b72fdb36c99996cf862753461a62f2e84f4196371d3b2d6a07", + "sha256:7c6b36ed71f41942bdcd2ad2522be46bfce09d5705be5640ecf19bbc7660e4b7", + "sha256:7d78702b26ba1c18b2d0fb2ea940ba7f17a9581b42e8361ff93920ebbee1235a", + "sha256:804dccd8a4a6242c4e30ad961e540e18a588f6527202f2d6791b01845d59fdc9", + "sha256:9161cfc9efce70d1715f47d6ff40f79c6778c00d53be4fbc09d70301e4b83ba7", + "sha256:96747bfbadb055466e5b46d572618170046b45ce5a4879167f50d70a5319a499", + "sha256:9f380393be5abeb6815f68fd39271b95127173511b6706b0a630a9995d53f8f5", + "sha256:a42ad6afcbaaa777241e347aa2e29155993045a0d6b7db74da61053ffe875fe0", + "sha256:a5b2ed6d828f1f09bd812861f4f59ca3bc3803f9df871f4555187f0faf018604", + "sha256:a6d26094615306d116dd5e4a51b0304c99dd2356fc569eed6922a80a6bd3b265", + "sha256:aa18ae738b5170e253ad0bb6c4b0f07585081e8a6e50893e4d911d47b39a0904", + "sha256:ad30ae663711786303fbcd46a47516302d201ee49a877cb3fac61f672895110a", + "sha256:b21f0e7efc7a5c509e953784e9d1575ebb8b4318960e7e7d7a93bb803626cf64", + "sha256:b3e693d15533a45cd5906f0589f9c35090bef6ef45bf1e8195c424aa0ae06a8d", + "sha256:b7f08588854bbb724041d9ae9d980d40040c922382e1d9a2ecb390edc4fd5032", + "sha256:b93ab07b5292dbe7e6b8da89475275e7042744283921344b56105f3eeb0f828b", + "sha256:bb024d8b621d0be75f4f44ecc7c950450026e76d66dc8f791bb5331d7fed59d5", + "sha256:bb1f5062f98b0b3290e72b707747fdd7e0f22d6956b236ba7ca7f5c9971d2da2", + "sha256:c45a496d6bc05dec41dcd4c3a2b183723f47473255c159cd80b503c8f246424d", + "sha256:c5d98a2709840027f5a347c3af0a7c3d5f6c1ff93af2ca1c54494e23cba8f389", + "sha256:c68568f3facf8f66fa76c60e0ced69b67666ffa9941d1d0a3756fda196049080", + "sha256:c95ef01f53233a305a874a44a63fbfb1d81cd79b49de0f8529b3548cde437e37", + "sha256:ca216e8af5c05e326efc7e28716ac2381a7cf9791749f5ee1849dccdc99c9b00", + "sha256:ca8435d13829b92f4a97362d91975154a4015db3a2634154e1754e9a915e6b86", + "sha256:dc261707bf5739aea8a541593f3cc1d463c2701fb05fbcbba0ce031b69a21260", + "sha256:e5ea1a213be1fcd5e49d9904c3b9939211ded90bc2a64e93f4c01963474285de", + "sha256:fa268106c8987639a17a18514cfe0cd9bf17420ab887e1e1bf486da8836135b1" ], "index": "pypi", "markers": "python_version >= '3.7'", - "version": "==2.0.44" + "version": "==2.0.51" }, "typing-extensions": { "hashes": [ @@ -567,19 +678,19 @@ }, "urllib3": { "hashes": [ - "sha256:3fc47733c7e419d4bc3f6b3dc2b4f890bb743906a30d56ba4a5bfa4bbff92760", - "sha256:e6b01673c0fa6a13e374b50871808eb3bf7046c4b125b216f6bf1cc604cff0dc" + "sha256:231e0ec3b63ceb14667c67be60f2f2c40a518cb38b03af60abc813da26505f4c", + "sha256:9fb4c81ebbb1ce9531cce37674bbc6f1360472bc18ca9a553ede278ef7276897" ], - "markers": "python_version >= '3.9'", - "version": "==2.5.0" + "markers": "python_version >= '3.10'", + "version": "==2.7.0" }, "werkzeug": { "hashes": [ - "sha256:54b78bf3716d19a65be4fceccc0d1d7b89e608834989dfae50ea87564639213e", - "sha256:60723ce945c19328679790e3282cc758aa4a6040e4bb330f53d30fa546d44746" + "sha256:63a77fb8892bf28ebc3178683445222aa500e48ebad5ec77b0ad80f8726b1f50", + "sha256:9bad61a4268dac112f1c5cd4630a56ede601b6ed420300677a869083d70a4c44" ], "markers": "python_version >= '3.9'", - "version": "==3.1.3" + "version": "==3.1.8" }, "wtforms": { "hashes": [ diff --git a/README.md b/README.md index 6b782b220d..b38dfd40b0 100644 --- a/README.md +++ b/README.md @@ -79,3 +79,15 @@ This boilerplate it's 100% read to deploy with Render.com and Heroku in a matter This template was built as part of the 4Geeks Academy [Coding Bootcamp](https://4geeksacademy.com/us/coding-bootcamp) by [Alejandro Sanchez](https://twitter.com/alesanchezr) and many other contributors. Find out more about our [Full Stack Developer Course](https://4geeksacademy.com/us/coding-bootcamps/part-time-full-stack-developer), and [Data Science Bootcamp](https://4geeksacademy.com/us/coding-bootcamps/datascience-machine-learning). You can find other templates and resources like this at the [school github page](https://github.com/4geeksacademy/). + + +##------------------------------------------------------------- + +## Pasos para correr el backend projecto kevin.michelle.gia + +- git clone URL_DEL_REPO +- cd final.project.kevin.michelle.gia +- pipenv install +- pipenv run flask --app src/app.py db upgrade +- cd src +- pipenv run python app.py diff --git a/Workshop API/opencollection.yml b/Workshop API/opencollection.yml new file mode 100644 index 0000000000..1158a1ec89 --- /dev/null +++ b/Workshop API/opencollection.yml @@ -0,0 +1,10 @@ +opencollection: 1.0.0 + +info: + name: Workshop API +bundled: false +extensions: + bruno: + ignore: + - node_modules + - .git diff --git a/dist/index.html b/dist/index.html index 1e8cb81dfe..676e138629 100644 --- a/dist/index.html +++ b/dist/index.html @@ -1,14 +1,18 @@ - - - - - - Hello Rigo with React + Flux + Context.js - - - - -
- - - + + + + + + + + + Hello Rigo + + + + +
+ + + + \ No newline at end of file diff --git a/migrations/versions/0763d677d453_.py b/migrations/versions/0763d677d453_.py deleted file mode 100644 index 88964176f1..0000000000 --- a/migrations/versions/0763d677d453_.py +++ /dev/null @@ -1,35 +0,0 @@ -"""empty message - -Revision ID: 0763d677d453 -Revises: -Create Date: 2025-02-25 14:47:16.337069 - -""" -from alembic import op -import sqlalchemy as sa - - -# revision identifiers, used by Alembic. -revision = '0763d677d453' -down_revision = None -branch_labels = None -depends_on = None - - -def upgrade(): - # ### commands auto generated by Alembic - please adjust! ### - op.create_table('user', - sa.Column('id', sa.Integer(), nullable=False), - sa.Column('email', sa.String(length=120), nullable=False), - sa.Column('password', sa.String(), nullable=False), - sa.Column('is_active', sa.Boolean(), nullable=False), - sa.PrimaryKeyConstraint('id'), - sa.UniqueConstraint('email') - ) - # ### end Alembic commands ### - - -def downgrade(): - # ### commands auto generated by Alembic - please adjust! ### - op.drop_table('user') - # ### end Alembic commands ### diff --git a/migrations/versions/6e228a5724e1_initial_sqlite_database.py b/migrations/versions/6e228a5724e1_initial_sqlite_database.py new file mode 100644 index 0000000000..450ca6e909 --- /dev/null +++ b/migrations/versions/6e228a5724e1_initial_sqlite_database.py @@ -0,0 +1,177 @@ +"""initial sqlite database + +Revision ID: 6e228a5724e1 +Revises: +Create Date: 2026-07-03 13:08:30.636198 + +""" +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision = '6e228a5724e1' +down_revision = None +branch_labels = None +depends_on = None + + +def upgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.create_table('workshops', + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('company_name', sa.String(length=120), nullable=False), + sa.Column('cif', sa.String(length=50), nullable=False), + sa.Column('phone', sa.String(length=20), nullable=False), + sa.Column('email', sa.String(length=120), nullable=False), + sa.Column('address', sa.String(length=200), nullable=True), + sa.Column('city', sa.String(length=80), nullable=True), + sa.Column('postal_code', sa.String(length=20), nullable=True), + sa.Column('is_active', sa.Boolean(), nullable=False), + sa.Column('created_at', sa.DateTime(), nullable=True), + sa.PrimaryKeyConstraint('id'), + sa.UniqueConstraint('cif'), + sa.UniqueConstraint('email') + ) + op.create_table('customers', + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('first_name', sa.String(length=80), nullable=False), + sa.Column('last_name', sa.String(length=120), nullable=False), + sa.Column('dni', sa.String(length=20), nullable=False), + sa.Column('driving_license', sa.String(length=20), nullable=False), + sa.Column('phone', sa.String(length=20), nullable=False), + sa.Column('email', sa.String(length=120), nullable=True), + sa.Column('address', sa.String(length=200), nullable=True), + sa.Column('workshop_id', sa.Integer(), nullable=False), + sa.Column('is_active', sa.Boolean(), nullable=False), + sa.Column('created_at', sa.DateTime(), nullable=True), + sa.ForeignKeyConstraint(['workshop_id'], ['workshops.id'], ), + sa.PrimaryKeyConstraint('id'), + sa.UniqueConstraint('dni'), + sa.UniqueConstraint('driving_license') + ) + op.create_table('employees', + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('first_name', sa.String(length=80), nullable=False), + sa.Column('last_name', sa.String(length=120), nullable=False), + sa.Column('dni', sa.String(length=20), nullable=False), + sa.Column('phone', sa.String(length=20), nullable=False), + sa.Column('address', sa.String(length=200), nullable=True), + sa.Column('city', sa.String(length=80), nullable=True), + sa.Column('postal_code', sa.String(length=20), nullable=True), + sa.Column('role', sa.String(length=20), nullable=False), + sa.Column('job_position', sa.String(length=80), nullable=True), + sa.Column('specialty', sa.String(length=120), nullable=True), + sa.Column('workshop_id', sa.Integer(), nullable=False), + sa.Column('is_active', sa.Boolean(), nullable=False), + sa.Column('created_at', sa.DateTime(), nullable=True), + sa.ForeignKeyConstraint(['workshop_id'], ['workshops.id'], ), + sa.PrimaryKeyConstraint('id'), + sa.UniqueConstraint('dni') + ) + op.create_table('users', + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('email', sa.String(length=120), nullable=False), + sa.Column('password_hash', sa.String(length=255), nullable=False), + sa.Column('employee_id', sa.Integer(), nullable=False), + sa.Column('created_at', sa.DateTime(), nullable=True), + sa.ForeignKeyConstraint(['employee_id'], ['employees.id'], ), + sa.PrimaryKeyConstraint('id'), + sa.UniqueConstraint('email'), + sa.UniqueConstraint('employee_id') + ) + op.create_table('vehicles', + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('plate', sa.String(length=20), nullable=False), + sa.Column('vin', sa.String(length=50), nullable=True), + sa.Column('brand', sa.String(length=80), nullable=False), + sa.Column('model', sa.String(length=80), nullable=False), + sa.Column('version', sa.String(length=80), nullable=True), + sa.Column('year', sa.Integer(), nullable=True), + sa.Column('fuel_type', sa.String(length=30), nullable=False), + sa.Column('power_hp', sa.Integer(), nullable=True), + sa.Column('engine_cc', sa.Integer(), nullable=True), + sa.Column('color', sa.String(length=50), nullable=True), + sa.Column('mileage', sa.Integer(), nullable=False), + sa.Column('first_registration_date', sa.Date(), nullable=True), + sa.Column('customer_id', sa.Integer(), nullable=False), + sa.Column('workshop_id', sa.Integer(), nullable=False), + sa.Column('is_active', sa.Boolean(), nullable=False), + sa.Column('created_at', sa.DateTime(), nullable=True), + sa.ForeignKeyConstraint(['customer_id'], ['customers.id'], ), + sa.ForeignKeyConstraint(['workshop_id'], ['workshops.id'], ), + sa.PrimaryKeyConstraint('id'), + sa.UniqueConstraint('plate'), + sa.UniqueConstraint('vin') + ) + op.create_table('password_reset_tokens', + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('user_id', sa.Integer(), nullable=False), + sa.Column('token_hash', sa.String(length=255), nullable=False), + sa.Column('expires_at', sa.DateTime(), nullable=False), + sa.Column('used_at', sa.DateTime(), nullable=True), + sa.Column('created_at', sa.DateTime(), nullable=True), + sa.ForeignKeyConstraint(['user_id'], ['users.id'], ), + sa.PrimaryKeyConstraint('id'), + sa.UniqueConstraint('token_hash') + ) + op.create_table('services', + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('title', sa.String(length=120), nullable=False), + sa.Column('description', sa.Text(), nullable=True), + sa.Column('service_type', sa.String(length=50), nullable=False), + sa.Column('status', sa.String(length=30), nullable=False), + sa.Column('priority', sa.String(length=20), nullable=False), + sa.Column('entry_mileage', sa.Integer(), nullable=True), + sa.Column('start_date', sa.DateTime(), nullable=True), + sa.Column('end_date', sa.DateTime(), nullable=True), + sa.Column('observations', sa.Text(), nullable=True), + sa.Column('workshop_id', sa.Integer(), nullable=False), + sa.Column('customer_id', sa.Integer(), nullable=False), + sa.Column('vehicle_id', sa.Integer(), nullable=False), + sa.Column('employee_id', sa.Integer(), nullable=True), + sa.Column('created_at', sa.DateTime(), nullable=True), + sa.ForeignKeyConstraint(['customer_id'], ['customers.id'], ), + sa.ForeignKeyConstraint(['employee_id'], ['employees.id'], ), + sa.ForeignKeyConstraint(['vehicle_id'], ['vehicles.id'], ), + sa.ForeignKeyConstraint(['workshop_id'], ['workshops.id'], ), + sa.PrimaryKeyConstraint('id') + ) + op.create_table('service_comments', + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('comment', sa.Text(), nullable=False), + sa.Column('comment_type', sa.String(length=50), nullable=False), + sa.Column('service_id', sa.Integer(), nullable=False), + sa.Column('employee_id', sa.Integer(), nullable=True), + sa.Column('created_at', sa.DateTime(), nullable=True), + sa.ForeignKeyConstraint(['employee_id'], ['employees.id'], ), + sa.ForeignKeyConstraint(['service_id'], ['services.id'], ), + sa.PrimaryKeyConstraint('id') + ) + op.create_table('service_status_logs', + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('service_id', sa.Integer(), nullable=False), + sa.Column('from_status', sa.String(length=30), nullable=True), + sa.Column('to_status', sa.String(length=30), nullable=False), + sa.Column('employee_id', sa.Integer(), nullable=True), + sa.Column('note', sa.Text(), nullable=True), + sa.Column('changed_at', sa.DateTime(), nullable=True), + sa.ForeignKeyConstraint(['employee_id'], ['employees.id'], ), + sa.ForeignKeyConstraint(['service_id'], ['services.id'], ), + sa.PrimaryKeyConstraint('id') + ) + # ### end Alembic commands ### + + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.drop_table('service_status_logs') + op.drop_table('service_comments') + op.drop_table('services') + op.drop_table('password_reset_tokens') + op.drop_table('vehicles') + op.drop_table('users') + op.drop_table('employees') + op.drop_table('customers') + op.drop_table('workshops') + # ### end Alembic commands ### diff --git a/migrations/versions/6e6483e4f080_add_image_fields_to_service_comments.py b/migrations/versions/6e6483e4f080_add_image_fields_to_service_comments.py new file mode 100644 index 0000000000..385ca615f8 --- /dev/null +++ b/migrations/versions/6e6483e4f080_add_image_fields_to_service_comments.py @@ -0,0 +1,34 @@ +"""add image fields to service comments + +Revision ID: 6e6483e4f080 +Revises: 6e228a5724e1 +Create Date: 2026-07-06 12:21:39.911589 + +""" +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision = '6e6483e4f080' +down_revision = '6e228a5724e1' +branch_labels = None +depends_on = None + + +def upgrade(): + # ### commands auto generated by Alembic - please adjust! ### + with op.batch_alter_table('service_comments', schema=None) as batch_op: + batch_op.add_column(sa.Column('image_url', sa.String(length=500), nullable=True)) + batch_op.add_column(sa.Column('image_public_id', sa.String(length=255), nullable=True)) + + # ### end Alembic commands ### + + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + with op.batch_alter_table('service_comments', schema=None) as batch_op: + batch_op.drop_column('image_public_id') + batch_op.drop_column('image_url') + + # ### end Alembic commands ### diff --git a/package-lock.json b/package-lock.json index 8d43d98ab7..17d10bb18d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,10 +9,12 @@ "version": "1.0.1", "license": "ISC", "dependencies": { + "lucide-react": "^1.21.0", "prop-types": "^15.8.1", "react": "^18.2.0", "react-dom": "^18.2.0", - "react-router-dom": "^6.18.0" + "react-router-dom": "^6.18.0", + "xlsx": "^0.18.5" }, "devDependencies": { "@types/react": "^18.2.18", @@ -877,19 +879,6 @@ "node": ">=6.0.0" } }, - "node_modules/@jridgewell/source-map": { - "version": "0.3.6", - "resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.6.tgz", - "integrity": "sha512-1ZJTZebgqllO79ue2bm3rIGud/bOe0pP5BjSRCRxxYkEZS8STV7zN84UBbiYu7jy+eCKSnVIUgoWWE/tt+shMQ==", - "dev": true, - "license": "MIT", - "optional": true, - "peer": true, - "dependencies": { - "@jridgewell/gen-mapping": "^0.3.5", - "@jridgewell/trace-mapping": "^0.3.25" - } - }, "node_modules/@jridgewell/sourcemap-codec": { "version": "1.5.0", "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz", @@ -997,14 +986,6 @@ "@babel/types": "^7.20.7" } }, - "node_modules/@types/node": { - "version": "16.11.12", - "resolved": "https://registry.npmjs.org/@types/node/-/node-16.11.12.tgz", - "integrity": "sha512-+2Iggwg7PxoO5Kyhvsq9VarmPbIelXP070HMImEpbtGCoyWNINQj4wzjbQCXzdHTRXnqufutJb5KAURZANNBAw==", - "dev": true, - "optional": true, - "peer": true - }, "node_modules/@types/prop-types": { "version": "15.7.14", "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.14.tgz", @@ -1083,6 +1064,15 @@ "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, + "node_modules/adler-32": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/adler-32/-/adler-32-1.3.1.tgz", + "integrity": "sha512-ynZ4w/nUUv5rrsR8UUGoe1VC9hZj6V5hU9Qw1HlMDJGEJw5S7TfTErWTjMys6M7vr0YWcPqs3qAr4ss0nDfP+A==", + "license": "Apache-2.0", + "engines": { + "node": ">=0.8" + } + }, "node_modules/ajv": { "version": "6.12.6", "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", @@ -1307,14 +1297,6 @@ "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" } }, - "node_modules/buffer-from": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", - "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", - "dev": true, - "optional": true, - "peer": true - }, "node_modules/call-bind": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.8.tgz", @@ -1395,12 +1377,46 @@ ], "license": "CC-BY-4.0" }, + "node_modules/cfb": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/cfb/-/cfb-1.2.2.tgz", + "integrity": "sha512-KfdUZsSOw19/ObEWasvBP/Ac4reZvAGauZhs6S/gqNhXhI7cKwvlH7ulj+dOEYnca4bm4SGo8C1bTAQvnTjgQA==", + "license": "Apache-2.0", + "dependencies": { + "adler-32": "~1.3.0", + "crc-32": "~1.2.0" + }, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/codepage": { + "version": "1.15.0", + "resolved": "https://registry.npmjs.org/codepage/-/codepage-1.15.0.tgz", + "integrity": "sha512-3g6NUTPd/YtuuGrhMnOMRjFc+LJw/bnMp3+0r/Wcz3IXUuCosKRJvMphm5+Q+bvTVGcJJuRvVLuYba+WojaFaA==", + "license": "Apache-2.0", + "engines": { + "node": ">=0.8" + } + }, "node_modules/concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=", "dev": true }, + "node_modules/crc-32": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/crc-32/-/crc-32-1.2.2.tgz", + "integrity": "sha512-ROmzCKrTnOwybPcJApAA6WBWij23HVfGVNKqqrZpuyZOHqK2CwHSvpGuyt/UNNvaIjEd8X5IFGp4Mh+Ie1IHJQ==", + "license": "Apache-2.0", + "bin": { + "crc32": "bin/crc32.njs" + }, + "engines": { + "node": ">=0.8" + } + }, "node_modules/cross-spawn": { "version": "7.0.6", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", @@ -2287,6 +2303,15 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/frac": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/frac/-/frac-1.1.2.tgz", + "integrity": "sha512-w/XBfkibaTl3YDqASwfDUqkna4Z2p9cFSr1aHDt0WoMTECnRfBOv2WArlZILlqgWlmdIlALXGpM2AOhEk5W3IA==", + "license": "Apache-2.0", + "engines": { + "node": ">=0.8" + } + }, "node_modules/fs.realpath": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", @@ -3130,6 +3155,15 @@ "loose-envify": "cli.js" } }, + "node_modules/lucide-react": { + "version": "1.21.0", + "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-1.21.0.tgz", + "integrity": "sha512-reEZMXq8Qdd5jg5XYkQ5TR1fB/GiQ7ih4vcrthYDtgjSDwh0i6/YLiGjsWsIwgN49gpAnd4J2elSNzncMEEUUQ==", + "license": "ISC", + "peerDependencies": { + "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, "node_modules/math-intrinsics": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", @@ -3915,27 +3949,16 @@ "node": ">=0.10.0" } }, - "node_modules/source-map-support": { - "version": "0.5.21", - "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", - "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", - "dev": true, - "optional": true, - "peer": true, + "node_modules/ssf": { + "version": "0.11.2", + "resolved": "https://registry.npmjs.org/ssf/-/ssf-0.11.2.tgz", + "integrity": "sha512-+idbmIXoYET47hH+d7dfm2epdOMUDjqcB4648sTZ+t2JwoyBFL/insLfB/racrDmsKB3diwsDA696pZMieAC5g==", + "license": "Apache-2.0", "dependencies": { - "buffer-from": "^1.0.0", - "source-map": "^0.6.0" - } - }, - "node_modules/source-map-support/node_modules/source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "dev": true, - "optional": true, - "peer": true, + "frac": "~1.1.2" + }, "engines": { - "node": ">=0.10.0" + "node": ">=0.8" } }, "node_modules/string.prototype.matchall": { @@ -4074,35 +4097,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/terser": { - "version": "5.38.1", - "resolved": "https://registry.npmjs.org/terser/-/terser-5.38.1.tgz", - "integrity": "sha512-GWANVlPM/ZfYzuPHjq0nxT+EbOEDDN3Jwhwdg1D8TU8oSkktp8w64Uq4auuGLxFSoNTRDncTq2hQHX1Ld9KHkA==", - "dev": true, - "license": "BSD-2-Clause", - "optional": true, - "peer": true, - "dependencies": { - "@jridgewell/source-map": "^0.3.3", - "acorn": "^8.8.2", - "commander": "^2.20.0", - "source-map-support": "~0.5.20" - }, - "bin": { - "terser": "bin/terser" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/terser/node_modules/commander": { - "version": "2.20.3", - "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", - "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", - "dev": true, - "optional": true, - "peer": true - }, "node_modules/text-table": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", @@ -4438,6 +4432,24 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/wmf": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wmf/-/wmf-1.0.2.tgz", + "integrity": "sha512-/p9K7bEh0Dj6WbXg4JG0xvLQmIadrner1bi45VMJTfnbVHsc7yIajZyoSoK60/dtVBs12Fm6WkUI5/3WAVsNMw==", + "license": "Apache-2.0", + "engines": { + "node": ">=0.8" + } + }, + "node_modules/word": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/word/-/word-0.3.0.tgz", + "integrity": "sha512-OELeY0Q61OXpdUfTp+oweA/vtLVg5VDOXh+3he3PNzLGG/y0oylSOC1xRVj0+l4vQ3tj/bB1HVHv1ocXkQceFA==", + "license": "Apache-2.0", + "engines": { + "node": ">=0.8" + } + }, "node_modules/word-wrap": { "version": "1.2.5", "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", @@ -4454,6 +4466,27 @@ "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=", "dev": true }, + "node_modules/xlsx": { + "version": "0.18.5", + "resolved": "https://registry.npmjs.org/xlsx/-/xlsx-0.18.5.tgz", + "integrity": "sha512-dmg3LCjBPHZnQp5/F/+nnTa+miPJxUXB6vtk42YjBBKayDNagxGEeIdWApkYPOf3Z3pm3k62Knjzp7lMeTEtFQ==", + "license": "Apache-2.0", + "dependencies": { + "adler-32": "~1.3.0", + "cfb": "~1.2.1", + "codepage": "~1.15.0", + "crc-32": "~1.2.1", + "ssf": "~0.11.2", + "wmf": "~1.0.1", + "word": "~0.3.0" + }, + "bin": { + "xlsx": "bin/xlsx.njs" + }, + "engines": { + "node": ">=0.8" + } + }, "node_modules/yocto-queue": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", @@ -4944,18 +4977,6 @@ "integrity": "sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==", "dev": true }, - "@jridgewell/source-map": { - "version": "0.3.6", - "resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.6.tgz", - "integrity": "sha512-1ZJTZebgqllO79ue2bm3rIGud/bOe0pP5BjSRCRxxYkEZS8STV7zN84UBbiYu7jy+eCKSnVIUgoWWE/tt+shMQ==", - "dev": true, - "optional": true, - "peer": true, - "requires": { - "@jridgewell/gen-mapping": "^0.3.5", - "@jridgewell/trace-mapping": "^0.3.25" - } - }, "@jridgewell/sourcemap-codec": { "version": "1.5.0", "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz", @@ -5044,14 +5065,6 @@ "@babel/types": "^7.20.7" } }, - "@types/node": { - "version": "16.11.12", - "resolved": "https://registry.npmjs.org/@types/node/-/node-16.11.12.tgz", - "integrity": "sha512-+2Iggwg7PxoO5Kyhvsq9VarmPbIelXP070HMImEpbtGCoyWNINQj4wzjbQCXzdHTRXnqufutJb5KAURZANNBAw==", - "dev": true, - "optional": true, - "peer": true - }, "@types/prop-types": { "version": "15.7.14", "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.14.tgz", @@ -5107,6 +5120,11 @@ "dev": true, "requires": {} }, + "adler-32": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/adler-32/-/adler-32-1.3.1.tgz", + "integrity": "sha512-ynZ4w/nUUv5rrsR8UUGoe1VC9hZj6V5hU9Qw1HlMDJGEJw5S7TfTErWTjMys6M7vr0YWcPqs3qAr4ss0nDfP+A==" + }, "ajv": { "version": "6.12.6", "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", @@ -5252,14 +5270,6 @@ "update-browserslist-db": "^1.1.1" } }, - "buffer-from": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", - "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", - "dev": true, - "optional": true, - "peer": true - }, "call-bind": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.8.tgz", @@ -5304,12 +5314,31 @@ "integrity": "sha512-GwNPlWJin8E+d7Gxq96jxM6w0w+VFeyyXRsjU58emtkYqnbwHqXm5uT2uCmO0RQE9htWknOP4xtBlLmM/gWxvQ==", "dev": true }, + "cfb": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/cfb/-/cfb-1.2.2.tgz", + "integrity": "sha512-KfdUZsSOw19/ObEWasvBP/Ac4reZvAGauZhs6S/gqNhXhI7cKwvlH7ulj+dOEYnca4bm4SGo8C1bTAQvnTjgQA==", + "requires": { + "adler-32": "~1.3.0", + "crc-32": "~1.2.0" + } + }, + "codepage": { + "version": "1.15.0", + "resolved": "https://registry.npmjs.org/codepage/-/codepage-1.15.0.tgz", + "integrity": "sha512-3g6NUTPd/YtuuGrhMnOMRjFc+LJw/bnMp3+0r/Wcz3IXUuCosKRJvMphm5+Q+bvTVGcJJuRvVLuYba+WojaFaA==" + }, "concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=", "dev": true }, + "crc-32": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/crc-32/-/crc-32-1.2.2.tgz", + "integrity": "sha512-ROmzCKrTnOwybPcJApAA6WBWij23HVfGVNKqqrZpuyZOHqK2CwHSvpGuyt/UNNvaIjEd8X5IFGp4Mh+Ie1IHJQ==" + }, "cross-spawn": { "version": "7.0.6", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", @@ -5941,6 +5970,11 @@ "is-callable": "^1.2.7" } }, + "frac": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/frac/-/frac-1.1.2.tgz", + "integrity": "sha512-w/XBfkibaTl3YDqASwfDUqkna4Z2p9cFSr1aHDt0WoMTECnRfBOv2WArlZILlqgWlmdIlALXGpM2AOhEk5W3IA==" + }, "fs.realpath": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", @@ -6475,6 +6509,12 @@ "js-tokens": "^3.0.0 || ^4.0.0" } }, + "lucide-react": { + "version": "1.21.0", + "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-1.21.0.tgz", + "integrity": "sha512-reEZMXq8Qdd5jg5XYkQ5TR1fB/GiQ7ih4vcrthYDtgjSDwh0i6/YLiGjsWsIwgN49gpAnd4J2elSNzncMEEUUQ==", + "requires": {} + }, "math-intrinsics": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", @@ -6982,26 +7022,12 @@ "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", "dev": true }, - "source-map-support": { - "version": "0.5.21", - "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", - "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", - "dev": true, - "optional": true, - "peer": true, + "ssf": { + "version": "0.11.2", + "resolved": "https://registry.npmjs.org/ssf/-/ssf-0.11.2.tgz", + "integrity": "sha512-+idbmIXoYET47hH+d7dfm2epdOMUDjqcB4648sTZ+t2JwoyBFL/insLfB/racrDmsKB3diwsDA696pZMieAC5g==", "requires": { - "buffer-from": "^1.0.0", - "source-map": "^0.6.0" - }, - "dependencies": { - "source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "dev": true, - "optional": true, - "peer": true - } + "frac": "~1.1.2" } }, "string.prototype.matchall": { @@ -7094,30 +7120,6 @@ "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", "dev": true }, - "terser": { - "version": "5.38.1", - "resolved": "https://registry.npmjs.org/terser/-/terser-5.38.1.tgz", - "integrity": "sha512-GWANVlPM/ZfYzuPHjq0nxT+EbOEDDN3Jwhwdg1D8TU8oSkktp8w64Uq4auuGLxFSoNTRDncTq2hQHX1Ld9KHkA==", - "dev": true, - "optional": true, - "peer": true, - "requires": { - "@jridgewell/source-map": "^0.3.3", - "acorn": "^8.8.2", - "commander": "^2.20.0", - "source-map-support": "~0.5.20" - }, - "dependencies": { - "commander": { - "version": "2.20.3", - "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", - "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", - "dev": true, - "optional": true, - "peer": true - } - } - }, "text-table": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", @@ -7312,6 +7314,16 @@ "has-tostringtag": "^1.0.2" } }, + "wmf": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wmf/-/wmf-1.0.2.tgz", + "integrity": "sha512-/p9K7bEh0Dj6WbXg4JG0xvLQmIadrner1bi45VMJTfnbVHsc7yIajZyoSoK60/dtVBs12Fm6WkUI5/3WAVsNMw==" + }, + "word": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/word/-/word-0.3.0.tgz", + "integrity": "sha512-OELeY0Q61OXpdUfTp+oweA/vtLVg5VDOXh+3he3PNzLGG/y0oylSOC1xRVj0+l4vQ3tj/bB1HVHv1ocXkQceFA==" + }, "word-wrap": { "version": "1.2.5", "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", @@ -7324,6 +7336,20 @@ "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=", "dev": true }, + "xlsx": { + "version": "0.18.5", + "resolved": "https://registry.npmjs.org/xlsx/-/xlsx-0.18.5.tgz", + "integrity": "sha512-dmg3LCjBPHZnQp5/F/+nnTa+miPJxUXB6vtk42YjBBKayDNagxGEeIdWApkYPOf3Z3pm3k62Knjzp7lMeTEtFQ==", + "requires": { + "adler-32": "~1.3.0", + "cfb": "~1.2.1", + "codepage": "~1.15.0", + "crc-32": "~1.2.1", + "ssf": "~0.11.2", + "wmf": "~1.0.1", + "word": "~0.3.0" + } + }, "yocto-queue": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", diff --git a/package.json b/package.json index 0caab10749..2ececad817 100755 --- a/package.json +++ b/package.json @@ -8,10 +8,10 @@ "main": "index.js", "scripts": { "dev": "vite", - "start": "vite", - "build": "vite build", - "lint": "eslint . --ext js,jsx --report-unused-disable-directives --max-warnings 0", - "preview": "vite preview" + "start": "vite", + "build": "vite build", + "lint": "eslint . --ext js,jsx --report-unused-disable-directives --max-warnings 0", + "preview": "vite preview" }, "author": { "name": "Alejandro Sanchez", @@ -30,13 +30,13 @@ "license": "ISC", "devDependencies": { "@types/react": "^18.2.18", - "@types/react-dom": "^18.2.7", - "@vitejs/plugin-react": "^4.0.4", - "eslint": "^8.46.0", - "eslint-plugin-react": "^7.33.1", - "eslint-plugin-react-hooks": "^4.6.0", - "eslint-plugin-react-refresh": "^0.4.3", - "vite": "^4.4.8" + "@types/react-dom": "^18.2.7", + "@vitejs/plugin-react": "^4.0.4", + "eslint": "^8.46.0", + "eslint-plugin-react": "^7.33.1", + "eslint-plugin-react-hooks": "^4.6.0", + "eslint-plugin-react-refresh": "^0.4.3", + "vite": "^4.4.8" }, "babel": { "presets": [ @@ -54,9 +54,11 @@ ] }, "dependencies": { + "lucide-react": "^1.21.0", "prop-types": "^15.8.1", - "react": "^18.2.0", - "react-dom": "^18.2.0", - "react-router-dom": "^6.18.0" + "react": "^18.2.0", + "react-dom": "^18.2.0", + "react-router-dom": "^6.18.0", + "xlsx": "^0.18.5" } } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml new file mode 100644 index 0000000000..c7c9e6f4da --- /dev/null +++ b/pnpm-lock.yaml @@ -0,0 +1,2905 @@ +lockfileVersion: '9.0' + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + +importers: + + .: + dependencies: + lucide-react: + specifier: ^1.21.0 + version: 1.21.0(react@18.3.1) + prop-types: + specifier: ^15.8.1 + version: 15.8.1 + react: + specifier: ^18.2.0 + version: 18.3.1 + react-dom: + specifier: ^18.2.0 + version: 18.3.1(react@18.3.1) + react-router-dom: + specifier: ^6.18.0 + version: 6.30.4(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + xlsx: + specifier: ^0.18.5 + version: 0.18.5 + devDependencies: + '@types/react': + specifier: ^18.2.18 + version: 18.3.31 + '@types/react-dom': + specifier: ^18.2.7 + version: 18.3.7(@types/react@18.3.31) + '@vitejs/plugin-react': + specifier: ^4.0.4 + version: 4.7.0(vite@4.5.14) + eslint: + specifier: ^8.46.0 + version: 8.57.1 + eslint-plugin-react: + specifier: ^7.33.1 + version: 7.37.5(eslint@8.57.1) + eslint-plugin-react-hooks: + specifier: ^4.6.0 + version: 4.6.2(eslint@8.57.1) + eslint-plugin-react-refresh: + specifier: ^0.4.3 + version: 0.4.26(eslint@8.57.1) + vite: + specifier: ^4.4.8 + version: 4.5.14 + +packages: + + '@babel/code-frame@7.29.7': + resolution: {integrity: sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw==} + engines: {node: '>=6.9.0'} + + '@babel/compat-data@7.29.7': + resolution: {integrity: sha512-locTkQyKvwIEgBzVrn8693ebc97F2U8ZHjbXwDXJ5Fn2TCpNwTlKcaKLkdHop5c/icOFE7qt7Q9JC5hnKNa6Gg==} + engines: {node: '>=6.9.0'} + + '@babel/core@7.29.7': + resolution: {integrity: sha512-RgHBCvtjbOK2gXSNBNIkNoEc9qoVEtau3hj8gEqKQuL3HZAibKarWFEI3Lfm6EYKkLalOh8eSrj9b+ch9H/VBA==} + engines: {node: '>=6.9.0'} + + '@babel/generator@7.29.7': + resolution: {integrity: sha512-DkXD5OJQaAQIdZ1bt3UZdEnHAn9Imd3IVBdX03UFe+ony9Ojw5pzr9YVKGDY1jt+Gcn/FnGkNf8r+Vj5NOJWtQ==} + engines: {node: '>=6.9.0'} + + '@babel/helper-compilation-targets@7.29.7': + resolution: {integrity: sha512-wem6WaBj4NaVYVdNhLPPVacES6ZJ+KBBfSkTMD3YZxbP3rm3Di85tJU5ljaUNhaOynt+Aj0xruhYuzQBt8n71g==} + engines: {node: '>=6.9.0'} + + '@babel/helper-globals@7.29.7': + resolution: {integrity: sha512-3nQVUAtvkKH9zahfWgw96Jc/uFOmjACE1kQz82E2lqWmHBgjzbNlsC22nuQTfahmWeQtTq5nQ/4Nnd2A1wj4zA==} + engines: {node: '>=6.9.0'} + + '@babel/helper-module-imports@7.29.7': + resolution: {integrity: sha512-ejHwrQQYcm9xnTivShn2IDOlIzInN34AXskvq9QicvCtEzq1Vzclu/tKF8Jq1Cg8JG2GL6/EmjgsCT7lXepE3g==} + engines: {node: '>=6.9.0'} + + '@babel/helper-module-transforms@7.29.7': + resolution: {integrity: sha512-UPUVSyXbOh627KiCIGQSgwWzGeBKLkaJ9PJEdrngIwMSzxLR4jS4+f1f1jb7VzBbg8nFLaYotvVPFCTqdrmTAg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + + '@babel/helper-plugin-utils@7.29.7': + resolution: {integrity: sha512-G7sHYigPY17oO5SYWnfD/0MTBwVR781S/JI643e/JhUYgVgWE/61SoW3NH9KWUKyKq5LVh3npif99Wkt6j86Jw==} + engines: {node: '>=6.9.0'} + + '@babel/helper-string-parser@7.29.7': + resolution: {integrity: sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==} + engines: {node: '>=6.9.0'} + + '@babel/helper-validator-identifier@7.29.7': + resolution: {integrity: sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==} + engines: {node: '>=6.9.0'} + + '@babel/helper-validator-option@7.29.7': + resolution: {integrity: sha512-N9ZErrD+yW5geCDtBqnOoxmR8+tNKiGuxKlDpuJxfsqpa2dFcexaziGAE/qoHLiDDreVNMupxGmSoNlyvsA3gw==} + engines: {node: '>=6.9.0'} + + '@babel/helpers@7.29.7': + resolution: {integrity: sha512-1k2lAGRMfHTcwuNYcCNUmaUffmQv8KWMfh2iJUUeRlwlwH4FdNG7mfPI10NPfLHJFThE4Tyr4mv7kTNZOiPuBg==} + engines: {node: '>=6.9.0'} + + '@babel/parser@7.29.7': + resolution: {integrity: sha512-hnORnjP/1P/zFEndoeX+n+t1RwWRJiJpM/jO7FW32Kn9r5+sJB2JWOdYo4L6k78j15eCwY3Gm/7364B1EMwtNg==} + engines: {node: '>=6.0.0'} + hasBin: true + + '@babel/plugin-transform-react-jsx-self@7.29.7': + resolution: {integrity: sha512-TL0hMc9xzy86VD31nUiwzd5otRAcyEPcsegCxolO0PvcXuH1v0kECe/UIznYFihpkvU5wg/jk4v0TTEFfm53fw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-react-jsx-source@7.29.7': + resolution: {integrity: sha512-06IyK09H3wi4cGbhDBwp5gUGo0IKtnYa8tyTiephirPCK6fbobVGiXMMI5zLQ4aKEYP3wZ3ArU44o+8KMrSG/Q==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/template@7.29.7': + resolution: {integrity: sha512-puq+Gf35oI24FeN11LkoUQFqv9uwNeWpxXZi/Ji3rRIoKAzKnxRaZ+Gkj0vKS9ZCiTESfng1N9LyOyXvo+m+Gg==} + engines: {node: '>=6.9.0'} + + '@babel/traverse@7.29.7': + resolution: {integrity: sha512-EhlfNQtZ+NK22w5BM61ciuiq1m58ed33Wr1Xan//ZRTy6hgjnwyCffRYwzsGXdASJSUJ1guZILsErh1eQcl+zw==} + engines: {node: '>=6.9.0'} + + '@babel/types@7.29.7': + resolution: {integrity: sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA==} + engines: {node: '>=6.9.0'} + + '@esbuild/android-arm64@0.18.20': + resolution: {integrity: sha512-Nz4rJcchGDtENV0eMKUNa6L12zz2zBDXuhj/Vjh18zGqB44Bi7MBMSXjgunJgjRhCmKOjnPuZp4Mb6OKqtMHLQ==} + engines: {node: '>=12'} + cpu: [arm64] + os: [android] + + '@esbuild/android-arm@0.18.20': + resolution: {integrity: sha512-fyi7TDI/ijKKNZTUJAQqiG5T7YjJXgnzkURqmGj13C6dCqckZBLdl4h7bkhHt/t0WP+zO9/zwroDvANaOqO5Sw==} + engines: {node: '>=12'} + cpu: [arm] + os: [android] + + '@esbuild/android-x64@0.18.20': + resolution: {integrity: sha512-8GDdlePJA8D6zlZYJV/jnrRAi6rOiNaCC/JclcXpB+KIuvfBN4owLtgzY2bsxnx666XjJx2kDPUmnTtR8qKQUg==} + engines: {node: '>=12'} + cpu: [x64] + os: [android] + + '@esbuild/darwin-arm64@0.18.20': + resolution: {integrity: sha512-bxRHW5kHU38zS2lPTPOyuyTm+S+eobPUnTNkdJEfAddYgEcll4xkT8DB9d2008DtTbl7uJag2HuE5NZAZgnNEA==} + engines: {node: '>=12'} + cpu: [arm64] + os: [darwin] + + '@esbuild/darwin-x64@0.18.20': + resolution: {integrity: sha512-pc5gxlMDxzm513qPGbCbDukOdsGtKhfxD1zJKXjCCcU7ju50O7MeAZ8c4krSJcOIJGFR+qx21yMMVYwiQvyTyQ==} + engines: {node: '>=12'} + cpu: [x64] + os: [darwin] + + '@esbuild/freebsd-arm64@0.18.20': + resolution: {integrity: sha512-yqDQHy4QHevpMAaxhhIwYPMv1NECwOvIpGCZkECn8w2WFHXjEwrBn3CeNIYsibZ/iZEUemj++M26W3cNR5h+Tw==} + engines: {node: '>=12'} + cpu: [arm64] + os: [freebsd] + + '@esbuild/freebsd-x64@0.18.20': + resolution: {integrity: sha512-tgWRPPuQsd3RmBZwarGVHZQvtzfEBOreNuxEMKFcd5DaDn2PbBxfwLcj4+aenoh7ctXcbXmOQIn8HI6mCSw5MQ==} + engines: {node: '>=12'} + cpu: [x64] + os: [freebsd] + + '@esbuild/linux-arm64@0.18.20': + resolution: {integrity: sha512-2YbscF+UL7SQAVIpnWvYwM+3LskyDmPhe31pE7/aoTMFKKzIc9lLbyGUpmmb8a8AixOL61sQ/mFh3jEjHYFvdA==} + engines: {node: '>=12'} + cpu: [arm64] + os: [linux] + + '@esbuild/linux-arm@0.18.20': + resolution: {integrity: sha512-/5bHkMWnq1EgKr1V+Ybz3s1hWXok7mDFUMQ4cG10AfW3wL02PSZi5kFpYKrptDsgb2WAJIvRcDm+qIvXf/apvg==} + engines: {node: '>=12'} + cpu: [arm] + os: [linux] + + '@esbuild/linux-ia32@0.18.20': + resolution: {integrity: sha512-P4etWwq6IsReT0E1KHU40bOnzMHoH73aXp96Fs8TIT6z9Hu8G6+0SHSw9i2isWrD2nbx2qo5yUqACgdfVGx7TA==} + engines: {node: '>=12'} + cpu: [ia32] + os: [linux] + + '@esbuild/linux-loong64@0.18.20': + resolution: {integrity: sha512-nXW8nqBTrOpDLPgPY9uV+/1DjxoQ7DoB2N8eocyq8I9XuqJ7BiAMDMf9n1xZM9TgW0J8zrquIb/A7s3BJv7rjg==} + engines: {node: '>=12'} + cpu: [loong64] + os: [linux] + + '@esbuild/linux-mips64el@0.18.20': + resolution: {integrity: sha512-d5NeaXZcHp8PzYy5VnXV3VSd2D328Zb+9dEq5HE6bw6+N86JVPExrA6O68OPwobntbNJ0pzCpUFZTo3w0GyetQ==} + engines: {node: '>=12'} + cpu: [mips64el] + os: [linux] + + '@esbuild/linux-ppc64@0.18.20': + resolution: {integrity: sha512-WHPyeScRNcmANnLQkq6AfyXRFr5D6N2sKgkFo2FqguP44Nw2eyDlbTdZwd9GYk98DZG9QItIiTlFLHJHjxP3FA==} + engines: {node: '>=12'} + cpu: [ppc64] + os: [linux] + + '@esbuild/linux-riscv64@0.18.20': + resolution: {integrity: sha512-WSxo6h5ecI5XH34KC7w5veNnKkju3zBRLEQNY7mv5mtBmrP/MjNBCAlsM2u5hDBlS3NGcTQpoBvRzqBcRtpq1A==} + engines: {node: '>=12'} + cpu: [riscv64] + os: [linux] + + '@esbuild/linux-s390x@0.18.20': + resolution: {integrity: sha512-+8231GMs3mAEth6Ja1iK0a1sQ3ohfcpzpRLH8uuc5/KVDFneH6jtAJLFGafpzpMRO6DzJ6AvXKze9LfFMrIHVQ==} + engines: {node: '>=12'} + cpu: [s390x] + os: [linux] + + '@esbuild/linux-x64@0.18.20': + resolution: {integrity: sha512-UYqiqemphJcNsFEskc73jQ7B9jgwjWrSayxawS6UVFZGWrAAtkzjxSqnoclCXxWtfwLdzU+vTpcNYhpn43uP1w==} + engines: {node: '>=12'} + cpu: [x64] + os: [linux] + + '@esbuild/netbsd-x64@0.18.20': + resolution: {integrity: sha512-iO1c++VP6xUBUmltHZoMtCUdPlnPGdBom6IrO4gyKPFFVBKioIImVooR5I83nTew5UOYrk3gIJhbZh8X44y06A==} + engines: {node: '>=12'} + cpu: [x64] + os: [netbsd] + + '@esbuild/openbsd-x64@0.18.20': + resolution: {integrity: sha512-e5e4YSsuQfX4cxcygw/UCPIEP6wbIL+se3sxPdCiMbFLBWu0eiZOJ7WoD+ptCLrmjZBK1Wk7I6D/I3NglUGOxg==} + engines: {node: '>=12'} + cpu: [x64] + os: [openbsd] + + '@esbuild/sunos-x64@0.18.20': + resolution: {integrity: sha512-kDbFRFp0YpTQVVrqUd5FTYmWo45zGaXe0X8E1G/LKFC0v8x0vWrhOWSLITcCn63lmZIxfOMXtCfti/RxN/0wnQ==} + engines: {node: '>=12'} + cpu: [x64] + os: [sunos] + + '@esbuild/win32-arm64@0.18.20': + resolution: {integrity: sha512-ddYFR6ItYgoaq4v4JmQQaAI5s7npztfV4Ag6NrhiaW0RrnOXqBkgwZLofVTlq1daVTQNhtI5oieTvkRPfZrePg==} + engines: {node: '>=12'} + cpu: [arm64] + os: [win32] + + '@esbuild/win32-ia32@0.18.20': + resolution: {integrity: sha512-Wv7QBi3ID/rROT08SABTS7eV4hX26sVduqDOTe1MvGMjNd3EjOz4b7zeexIR62GTIEKrfJXKL9LFxTYgkyeu7g==} + engines: {node: '>=12'} + cpu: [ia32] + os: [win32] + + '@esbuild/win32-x64@0.18.20': + resolution: {integrity: sha512-kTdfRcSiDfQca/y9QIkng02avJ+NCaQvrMejlsB3RRv5sE9rRoeBPISaZpKxHELzRxZyLvNts1P27W3wV+8geQ==} + engines: {node: '>=12'} + cpu: [x64] + os: [win32] + + '@eslint-community/eslint-utils@4.9.1': + resolution: {integrity: sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + peerDependencies: + eslint: ^6.0.0 || ^7.0.0 || >=8.0.0 + + '@eslint-community/regexpp@4.12.2': + resolution: {integrity: sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==} + engines: {node: ^12.0.0 || ^14.0.0 || >=16.0.0} + + '@eslint/eslintrc@2.1.4': + resolution: {integrity: sha512-269Z39MS6wVJtsoUl10L60WdkhJVdPG24Q4eZTH3nnF6lpvSShEK3wQjDX9JRWAUPvPh7COouPpU9IrqaZFvtQ==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + + '@eslint/js@8.57.1': + resolution: {integrity: sha512-d9zaMRSTIKDLhctzH12MtXvJKSSUhaHcjV+2Z+GK+EEY7XKpP5yR4x+N3TAcHTcu963nIr+TMcCb4DBCYX1z6Q==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + + '@humanwhocodes/config-array@0.13.0': + resolution: {integrity: sha512-DZLEEqFWQFiyK6h5YIeynKx7JlvCYWL0cImfSRXZ9l4Sg2efkFGTuFf6vzXjK1cq6IYkU+Eg/JizXw+TD2vRNw==} + engines: {node: '>=10.10.0'} + deprecated: Use @eslint/config-array instead + + '@humanwhocodes/module-importer@1.0.1': + resolution: {integrity: sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==} + engines: {node: '>=12.22'} + + '@humanwhocodes/object-schema@2.0.3': + resolution: {integrity: sha512-93zYdMES/c1D69yZiKDBj0V24vqNzB/koF26KPaagAfd3P/4gUlh3Dys5ogAK+Exi9QyzlD8x/08Zt7wIKcDcA==} + deprecated: Use @eslint/object-schema instead + + '@jridgewell/gen-mapping@0.3.13': + resolution: {integrity: sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==} + + '@jridgewell/remapping@2.3.5': + resolution: {integrity: sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==} + + '@jridgewell/resolve-uri@3.1.2': + resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==} + engines: {node: '>=6.0.0'} + + '@jridgewell/sourcemap-codec@1.5.5': + resolution: {integrity: sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==} + + '@jridgewell/trace-mapping@0.3.31': + resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==} + + '@nodelib/fs.scandir@2.1.5': + resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==} + engines: {node: '>= 8'} + + '@nodelib/fs.stat@2.0.5': + resolution: {integrity: sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==} + engines: {node: '>= 8'} + + '@nodelib/fs.walk@1.2.8': + resolution: {integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==} + engines: {node: '>= 8'} + + '@remix-run/router@1.23.3': + resolution: {integrity: sha512-4An71tdz9X8+3sI4Qqqd2LWd9vS39J7sqd9EU4Scw7TJE/qB10Flv/UuqbPVgfQV9XoK8Np6jNquZitnZq5i+Q==} + engines: {node: '>=14.0.0'} + + '@rolldown/pluginutils@1.0.0-beta.27': + resolution: {integrity: sha512-+d0F4MKMCbeVUJwG96uQ4SgAznZNSq93I3V+9NHA4OpvqG8mRCpGdKmK8l/dl02h2CCDHwW2FqilnTyDcAnqjA==} + + '@types/babel__core@7.20.5': + resolution: {integrity: sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==} + + '@types/babel__generator@7.27.0': + resolution: {integrity: sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==} + + '@types/babel__template@7.4.4': + resolution: {integrity: sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==} + + '@types/babel__traverse@7.28.0': + resolution: {integrity: sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==} + + '@types/prop-types@15.7.15': + resolution: {integrity: sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw==} + + '@types/react-dom@18.3.7': + resolution: {integrity: sha512-MEe3UeoENYVFXzoXEWsvcpg6ZvlrFNlOQ7EOsvhI3CfAXwzPfO8Qwuxd40nepsYKqyyVQnTdEfv68q91yLcKrQ==} + peerDependencies: + '@types/react': ^18.0.0 + + '@types/react@18.3.31': + resolution: {integrity: sha512-vfEqpXTvwT91yhmwdfouStN2hSKwTvyRs8qpLfADyrq/kxDw0hZM7Wk9Ug1FELj8hIby+S/+kQCSRFF32nv2Qw==} + + '@ungap/structured-clone@1.3.2': + resolution: {integrity: sha512-5jsZFwgR5rTdKwidH9Qmat75RKwqfpKlWWB1frDkljN127mwqBu8K0PYo7/hFpF03IEJpfVPpCQDY/eDx3iHvA==} + + '@vitejs/plugin-react@4.7.0': + resolution: {integrity: sha512-gUu9hwfWvvEDBBmgtAowQCojwZmJ5mcLn3aufeCsitijs3+f2NsrPtlAWIR6OPiqljl96GVCUbLe0HyqIpVaoA==} + engines: {node: ^14.18.0 || >=16.0.0} + peerDependencies: + vite: ^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 + + acorn-jsx@5.3.2: + resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==} + peerDependencies: + acorn: ^6.0.0 || ^7.0.0 || ^8.0.0 + + acorn@8.17.0: + resolution: {integrity: sha512-xRQbDb9BnwDafYNn6Vwl839DYVjqXYb1XVGtWAZ1kcDc6iwAL4hg3B1dZlRiuENFeO2H53gFG3in621AdERVAg==} + engines: {node: '>=0.4.0'} + hasBin: true + + adler-32@1.3.1: + resolution: {integrity: sha512-ynZ4w/nUUv5rrsR8UUGoe1VC9hZj6V5hU9Qw1HlMDJGEJw5S7TfTErWTjMys6M7vr0YWcPqs3qAr4ss0nDfP+A==} + engines: {node: '>=0.8'} + + ajv@6.15.0: + resolution: {integrity: sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw==} + + ansi-regex@5.0.1: + resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==} + engines: {node: '>=8'} + + ansi-styles@4.3.0: + resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==} + engines: {node: '>=8'} + + argparse@2.0.1: + resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==} + + array-buffer-byte-length@1.0.2: + resolution: {integrity: sha512-LHE+8BuR7RYGDKvnrmcuSq3tDcKv9OFEXQt/HpbZhY7V6h0zlUXutnAD82GiFx9rdieCMjkvtcsPqBwgUl1Iiw==} + engines: {node: '>= 0.4'} + + array-includes@3.1.9: + resolution: {integrity: sha512-FmeCCAenzH0KH381SPT5FZmiA/TmpndpcaShhfgEN9eCVjnFBqq3l1xrI42y8+PPLI6hypzou4GXw00WHmPBLQ==} + engines: {node: '>= 0.4'} + + array.prototype.findlast@1.2.5: + resolution: {integrity: sha512-CVvd6FHg1Z3POpBLxO6E6zr+rSKEQ9L6rZHAaY7lLfhKsWYUBBOuMs0e9o24oopj6H+geRCX0YJ+TJLBK2eHyQ==} + engines: {node: '>= 0.4'} + + array.prototype.flat@1.3.3: + resolution: {integrity: sha512-rwG/ja1neyLqCuGZ5YYrznA62D4mZXg0i1cIskIUKSiqF3Cje9/wXAls9B9s1Wa2fomMsIv8czB8jZcPmxCXFg==} + engines: {node: '>= 0.4'} + + array.prototype.flatmap@1.3.3: + resolution: {integrity: sha512-Y7Wt51eKJSyi80hFrJCePGGNo5ktJCslFuboqJsbf57CCPcm5zztluPlc4/aD8sWsKvlwatezpV4U1efk8kpjg==} + engines: {node: '>= 0.4'} + + array.prototype.tosorted@1.1.4: + resolution: {integrity: sha512-p6Fx8B7b7ZhL/gmUsAy0D15WhvDccw3mnGNbZpi3pmeJdxtWsj2jEaI4Y6oo3XiHfzuSgPwKc04MYt6KgvC/wA==} + engines: {node: '>= 0.4'} + + arraybuffer.prototype.slice@1.0.4: + resolution: {integrity: sha512-BNoCY6SXXPQ7gF2opIP4GBE+Xw7U+pHMYKuzjgCN3GwiaIR09UUeKfheyIry77QtrCBlC0KK0q5/TER/tYh3PQ==} + engines: {node: '>= 0.4'} + + async-function@1.0.0: + resolution: {integrity: sha512-hsU18Ae8CDTR6Kgu9DYf0EbCr/a5iGL0rytQDobUcdpYOKokk8LEjVphnXkDkgpi0wYVsqrXuP0bZxJaTqdgoA==} + engines: {node: '>= 0.4'} + + available-typed-arrays@1.0.7: + resolution: {integrity: sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==} + engines: {node: '>= 0.4'} + + balanced-match@1.0.2: + resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} + + baseline-browser-mapping@2.10.40: + resolution: {integrity: sha512-BSSLZ9/Cjjv7Gtj5B68ZzXcXUg8iOf3fme+FCuh8rC/Go+Kmh8cox7M3A8dolou16s64QjLPOSdngh7GxXvkSw==} + engines: {node: '>=6.0.0'} + hasBin: true + + brace-expansion@1.1.15: + resolution: {integrity: sha512-EwOCDEex4quD37XhqM3omwtMoJjr//isUZz1JopUNWms+4Z2ViyM/k1YIRePpoVNnQhENnxtFjLaxNHrT7xIUg==} + + browserslist@4.28.4: + resolution: {integrity: sha512-MTc8i/x9jBQd1iMw2CFGS+rwMa07eYjLR0CCTLDACl9xhxy+nIs3KeML/biicXtk9JrZ6dnnTatmc7ErPXIxqw==} + engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} + hasBin: true + + call-bind-apply-helpers@1.0.2: + resolution: {integrity: sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==} + engines: {node: '>= 0.4'} + + call-bind@1.0.9: + resolution: {integrity: sha512-a/hy+pNsFUTR+Iz8TCJvXudKVLAnz/DyeSUo10I5yvFDQJBFU2s9uqQpoSrJlroHUKoKqzg+epxyP9lqFdzfBQ==} + engines: {node: '>= 0.4'} + + call-bound@1.0.4: + resolution: {integrity: sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==} + engines: {node: '>= 0.4'} + + callsites@3.1.0: + resolution: {integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==} + engines: {node: '>=6'} + + caniuse-lite@1.0.30001799: + resolution: {integrity: sha512-hG1bReV+OUU+MOqK4t/ZWI0tZOyz3rqS9XuhOUz1cIcbwBKjOyJEJuw9ER5JuNyqxNk8u/JUVbGibBOL1yrjFw==} + + cfb@1.2.2: + resolution: {integrity: sha512-KfdUZsSOw19/ObEWasvBP/Ac4reZvAGauZhs6S/gqNhXhI7cKwvlH7ulj+dOEYnca4bm4SGo8C1bTAQvnTjgQA==} + engines: {node: '>=0.8'} + + chalk@4.1.2: + resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==} + engines: {node: '>=10'} + + codepage@1.15.0: + resolution: {integrity: sha512-3g6NUTPd/YtuuGrhMnOMRjFc+LJw/bnMp3+0r/Wcz3IXUuCosKRJvMphm5+Q+bvTVGcJJuRvVLuYba+WojaFaA==} + engines: {node: '>=0.8'} + + color-convert@2.0.1: + resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} + engines: {node: '>=7.0.0'} + + color-name@1.1.4: + resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} + + concat-map@0.0.1: + resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==} + + convert-source-map@2.0.0: + resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==} + + crc-32@1.2.2: + resolution: {integrity: sha512-ROmzCKrTnOwybPcJApAA6WBWij23HVfGVNKqqrZpuyZOHqK2CwHSvpGuyt/UNNvaIjEd8X5IFGp4Mh+Ie1IHJQ==} + engines: {node: '>=0.8'} + hasBin: true + + cross-spawn@7.0.6: + resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} + engines: {node: '>= 8'} + + csstype@3.2.3: + resolution: {integrity: sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==} + + data-view-buffer@1.0.2: + resolution: {integrity: sha512-EmKO5V3OLXh1rtK2wgXRansaK1/mtVdTUEiEI0W8RkvgT05kfxaH29PliLnpLP73yYO6142Q72QNa8Wx/A5CqQ==} + engines: {node: '>= 0.4'} + + data-view-byte-length@1.0.2: + resolution: {integrity: sha512-tuhGbE6CfTM9+5ANGf+oQb72Ky/0+s3xKUpHvShfiz2RxMFgFPjsXuRLBVMtvMs15awe45SRb83D6wH4ew6wlQ==} + engines: {node: '>= 0.4'} + + data-view-byte-offset@1.0.1: + resolution: {integrity: sha512-BS8PfmtDGnrgYdOonGZQdLZslWIeCGFP9tpan0hi1Co2Zr2NKADsvGYA8XxuG/4UWgJ6Cjtv+YJnB6MM69QGlQ==} + engines: {node: '>= 0.4'} + + debug@4.4.3: + resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==} + engines: {node: '>=6.0'} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + + deep-is@0.1.4: + resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==} + + define-data-property@1.1.4: + resolution: {integrity: sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==} + engines: {node: '>= 0.4'} + + define-properties@1.2.1: + resolution: {integrity: sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==} + engines: {node: '>= 0.4'} + + doctrine@2.1.0: + resolution: {integrity: sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==} + engines: {node: '>=0.10.0'} + + doctrine@3.0.0: + resolution: {integrity: sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==} + engines: {node: '>=6.0.0'} + + dunder-proto@1.0.1: + resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==} + engines: {node: '>= 0.4'} + + electron-to-chromium@1.5.380: + resolution: {integrity: sha512-W6d5AbuEoRayO447cqrg6lKJIlscgRnnxOZl/08kfV71BQDoEBC7Wwis68z87LjyK6f4kWyTaubuDbhHKrZkbA==} + + es-abstract-get@1.0.0: + resolution: {integrity: sha512-6PMWXpdhshVvFp+FoWYs1EvG1Nj0tvk0dZM+XcK0xMEM1czRVcP6ohqPWHy6qPagSpC8j4+p89WXlT+xXJs/fg==} + engines: {node: '>= 0.4'} + + es-abstract@1.24.2: + resolution: {integrity: sha512-2FpH9Q5i2RRwyEP1AylXe6nYLR5OhaJTZwmlcP0dL/+JCbgg7yyEo/sEK6HeGZRf3dFpWwThaRHVApXSkW3xeg==} + engines: {node: '>= 0.4'} + + es-define-property@1.0.1: + resolution: {integrity: sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==} + engines: {node: '>= 0.4'} + + es-errors@1.3.0: + resolution: {integrity: sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==} + engines: {node: '>= 0.4'} + + es-iterator-helpers@1.3.3: + resolution: {integrity: sha512-0PuBxFi+4uPanB97iDxCLWuHeYud2FALrw5HFZGtAF38UpJDbDC8frwp2cnDyae692CQ0dou60UwWfhgsa4U/g==} + engines: {node: '>= 0.4'} + + es-object-atoms@1.1.2: + resolution: {integrity: sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw==} + engines: {node: '>= 0.4'} + + es-set-tostringtag@2.1.0: + resolution: {integrity: sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==} + engines: {node: '>= 0.4'} + + es-shim-unscopables@1.1.0: + resolution: {integrity: sha512-d9T8ucsEhh8Bi1woXCf+TIKDIROLG5WCkxg8geBCbvk22kzwC5G2OnXVMO6FUsvQlgUUXQ2itephWDLqDzbeCw==} + engines: {node: '>= 0.4'} + + es-to-primitive@1.3.4: + resolution: {integrity: sha512-yPDz7wqpg1/mmHLmS3tcfTfbw5f1eryXvyghYBffGdERwe+mV7ZcWzTR8LR17Kvqt3qfPurjlonmnq3MKXIOXw==} + engines: {node: '>= 0.4'} + + esbuild@0.18.20: + resolution: {integrity: sha512-ceqxoedUrcayh7Y7ZX6NdbbDzGROiyVBgC4PriJThBKSVPWnnFHZAkfI1lJT8QFkOwH4qOS2SJkS4wvpGl8BpA==} + engines: {node: '>=12'} + hasBin: true + + escalade@3.2.0: + resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==} + engines: {node: '>=6'} + + escape-string-regexp@4.0.0: + resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==} + engines: {node: '>=10'} + + eslint-plugin-react-hooks@4.6.2: + resolution: {integrity: sha512-QzliNJq4GinDBcD8gPB5v0wh6g8q3SUi6EFF0x8N/BL9PoVs0atuGc47ozMRyOWAKdwaZ5OnbOEa3WR+dSGKuQ==} + engines: {node: '>=10'} + peerDependencies: + eslint: ^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 + + eslint-plugin-react-refresh@0.4.26: + resolution: {integrity: sha512-1RETEylht2O6FM/MvgnyvT+8K21wLqDNg4qD51Zj3guhjt433XbnnkVttHMyaVyAFD03QSV4LPS5iE3VQmO7XQ==} + peerDependencies: + eslint: '>=8.40' + + eslint-plugin-react@7.37.5: + resolution: {integrity: sha512-Qteup0SqU15kdocexFNAJMvCJEfa2xUKNV4CC1xsVMrIIqEy3SQ/rqyxCWNzfrd3/ldy6HMlD2e0JDVpDg2qIA==} + engines: {node: '>=4'} + peerDependencies: + eslint: ^3 || ^4 || ^5 || ^6 || ^7 || ^8 || ^9.7 + + eslint-scope@7.2.2: + resolution: {integrity: sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + + eslint-visitor-keys@3.4.3: + resolution: {integrity: sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + + eslint@8.57.1: + resolution: {integrity: sha512-ypowyDxpVSYpkXr9WPv2PAZCtNip1Mv5KTW0SCurXv/9iOpcrH9PaqUElksqEB6pChqHGDRCFTyrZlGhnLNGiA==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + deprecated: This version is no longer supported. Please see https://eslint.org/version-support for other options. + hasBin: true + + espree@9.6.1: + resolution: {integrity: sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + + esquery@1.7.0: + resolution: {integrity: sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==} + engines: {node: '>=0.10'} + + esrecurse@4.3.0: + resolution: {integrity: sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==} + engines: {node: '>=4.0'} + + estraverse@5.3.0: + resolution: {integrity: sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==} + engines: {node: '>=4.0'} + + esutils@2.0.3: + resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==} + engines: {node: '>=0.10.0'} + + fast-deep-equal@3.1.3: + resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} + + fast-json-stable-stringify@2.1.0: + resolution: {integrity: sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==} + + fast-levenshtein@2.0.6: + resolution: {integrity: sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==} + + fastq@1.20.1: + resolution: {integrity: sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==} + + file-entry-cache@6.0.1: + resolution: {integrity: sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==} + engines: {node: ^10.12.0 || >=12.0.0} + + find-up@5.0.0: + resolution: {integrity: sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==} + engines: {node: '>=10'} + + flat-cache@3.2.0: + resolution: {integrity: sha512-CYcENa+FtcUKLmhhqyctpclsq7QF38pKjZHsGNiSQF5r4FtoKDWabFDl3hzaEQMvT1LHEysw5twgLvpYYb4vbw==} + engines: {node: ^10.12.0 || >=12.0.0} + + flatted@3.4.2: + resolution: {integrity: sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA==} + + for-each@0.3.5: + resolution: {integrity: sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==} + engines: {node: '>= 0.4'} + + frac@1.1.2: + resolution: {integrity: sha512-w/XBfkibaTl3YDqASwfDUqkna4Z2p9cFSr1aHDt0WoMTECnRfBOv2WArlZILlqgWlmdIlALXGpM2AOhEk5W3IA==} + engines: {node: '>=0.8'} + + fs.realpath@1.0.0: + resolution: {integrity: sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==} + + fsevents@2.3.3: + resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} + engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} + os: [darwin] + + function-bind@1.1.2: + resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==} + + function.prototype.name@1.2.0: + resolution: {integrity: sha512-jObKIik1P2QjPHP5nz5BaOtUlfgS0fWo8IUByNXkM+o+02sJOi94em77GwJKQSJ3gfPHdgzLNrHc1uokV4P/ew==} + engines: {node: '>= 0.4'} + + functions-have-names@1.2.3: + resolution: {integrity: sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==} + + generator-function@2.0.1: + resolution: {integrity: sha512-SFdFmIJi+ybC0vjlHN0ZGVGHc3lgE0DxPAT0djjVg+kjOnSqclqmj0KQ7ykTOLP6YxoqOvuAODGdcHJn+43q3g==} + engines: {node: '>= 0.4'} + + gensync@1.0.0-beta.2: + resolution: {integrity: sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==} + engines: {node: '>=6.9.0'} + + get-intrinsic@1.3.0: + resolution: {integrity: sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==} + engines: {node: '>= 0.4'} + + get-proto@1.0.1: + resolution: {integrity: sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==} + engines: {node: '>= 0.4'} + + get-symbol-description@1.1.0: + resolution: {integrity: sha512-w9UMqWwJxHNOvoNzSJ2oPF5wvYcvP7jUvYzhp67yEhTi17ZDBBC1z9pTdGuzjD+EFIqLSYRweZjqfiPzQ06Ebg==} + engines: {node: '>= 0.4'} + + glob-parent@6.0.2: + resolution: {integrity: sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==} + engines: {node: '>=10.13.0'} + + glob@7.2.3: + resolution: {integrity: sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==} + deprecated: Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me + + globals@13.24.0: + resolution: {integrity: sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==} + engines: {node: '>=8'} + + globalthis@1.0.4: + resolution: {integrity: sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ==} + engines: {node: '>= 0.4'} + + gopd@1.2.0: + resolution: {integrity: sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==} + engines: {node: '>= 0.4'} + + graphemer@1.4.0: + resolution: {integrity: sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==} + + has-bigints@1.1.0: + resolution: {integrity: sha512-R3pbpkcIqv2Pm3dUwgjclDRVmWpTJW2DcMzcIhEXEx1oh/CEMObMm3KLmRJOdvhM7o4uQBnwr8pzRK2sJWIqfg==} + engines: {node: '>= 0.4'} + + has-flag@4.0.0: + resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==} + engines: {node: '>=8'} + + has-property-descriptors@1.0.2: + resolution: {integrity: sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==} + + has-proto@1.2.0: + resolution: {integrity: sha512-KIL7eQPfHQRC8+XluaIw7BHUwwqL19bQn4hzNgdr+1wXoU0KKj6rufu47lhY7KbJR2C6T6+PfyN0Ea7wkSS+qQ==} + engines: {node: '>= 0.4'} + + has-symbols@1.1.0: + resolution: {integrity: sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==} + engines: {node: '>= 0.4'} + + has-tostringtag@1.0.2: + resolution: {integrity: sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==} + engines: {node: '>= 0.4'} + + hasown@2.0.4: + resolution: {integrity: sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==} + engines: {node: '>= 0.4'} + + ignore@5.3.2: + resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==} + engines: {node: '>= 4'} + + import-fresh@3.3.1: + resolution: {integrity: sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==} + engines: {node: '>=6'} + + imurmurhash@0.1.4: + resolution: {integrity: sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==} + engines: {node: '>=0.8.19'} + + inflight@1.0.6: + resolution: {integrity: sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==} + deprecated: This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful. + + inherits@2.0.4: + resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} + + internal-slot@1.1.0: + resolution: {integrity: sha512-4gd7VpWNQNB4UKKCFFVcp1AVv+FMOgs9NKzjHKusc8jTMhd5eL1NqQqOpE0KzMds804/yHlglp3uxgluOqAPLw==} + engines: {node: '>= 0.4'} + + is-array-buffer@3.0.5: + resolution: {integrity: sha512-DDfANUiiG2wC1qawP66qlTugJeL5HyzMpfr8lLK+jMQirGzNod0B12cFB/9q838Ru27sBwfw78/rdoU7RERz6A==} + engines: {node: '>= 0.4'} + + is-async-function@2.1.1: + resolution: {integrity: sha512-9dgM/cZBnNvjzaMYHVoxxfPj2QXt22Ev7SuuPrs+xav0ukGB0S6d4ydZdEiM48kLx5kDV+QBPrpVnFyefL8kkQ==} + engines: {node: '>= 0.4'} + + is-bigint@1.1.0: + resolution: {integrity: sha512-n4ZT37wG78iz03xPRKJrHTdZbe3IicyucEtdRsV5yglwc3GyUfbAfpSeD0FJ41NbUNSt5wbhqfp1fS+BgnvDFQ==} + engines: {node: '>= 0.4'} + + is-boolean-object@1.2.2: + resolution: {integrity: sha512-wa56o2/ElJMYqjCjGkXri7it5FbebW5usLw/nPmCMs5DeZ7eziSYZhSmPRn0txqeW4LnAmQQU7FgqLpsEFKM4A==} + engines: {node: '>= 0.4'} + + is-callable@1.2.7: + resolution: {integrity: sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==} + engines: {node: '>= 0.4'} + + is-core-module@2.16.2: + resolution: {integrity: sha512-evOr8xfXKxE6qSR0hSXL2r3sd7ALj8+7jQEUvPYcm5sgZFdJ+AYzT6yNmJenvIYQBgIGwfwz08sL8zoL7yq2BA==} + engines: {node: '>= 0.4'} + + is-data-view@1.0.2: + resolution: {integrity: sha512-RKtWF8pGmS87i2D6gqQu/l7EYRlVdfzemCJN/P3UOs//x1QE7mfhvzHIApBTRf7axvT6DMGwSwBXYCT0nfB9xw==} + engines: {node: '>= 0.4'} + + is-date-object@1.1.0: + resolution: {integrity: sha512-PwwhEakHVKTdRNVOw+/Gyh0+MzlCl4R6qKvkhuvLtPMggI1WAHt9sOwZxQLSGpUaDnrdyDsomoRgNnCfKNSXXg==} + engines: {node: '>= 0.4'} + + is-document.all@1.0.0: + resolution: {integrity: sha512-+XSoyS05OdBbhFuELhgTCpFNHkpBOJqtsZfUFFpe5QTw+9Sjbh8zitxhQkYAo6wV7e1Vb8cAPvpCk9jGam/82g==} + engines: {node: '>= 0.4'} + + is-extglob@2.1.1: + resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} + engines: {node: '>=0.10.0'} + + is-finalizationregistry@1.1.1: + resolution: {integrity: sha512-1pC6N8qWJbWoPtEjgcL2xyhQOP491EQjeUo3qTKcmV8YSDDJrOepfG8pcC7h/QgnQHYSv0mJ3Z/ZWxmatVrysg==} + engines: {node: '>= 0.4'} + + is-generator-function@1.1.2: + resolution: {integrity: sha512-upqt1SkGkODW9tsGNG5mtXTXtECizwtS2kA161M+gJPc1xdb/Ax629af6YrTwcOeQHbewrPNlE5Dx7kzvXTizA==} + engines: {node: '>= 0.4'} + + is-glob@4.0.3: + resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==} + engines: {node: '>=0.10.0'} + + is-map@2.0.3: + resolution: {integrity: sha512-1Qed0/Hr2m+YqxnM09CjA2d/i6YZNfF6R2oRAOj36eUdS6qIV/huPJNSEpKbupewFs+ZsJlxsjjPbc0/afW6Lw==} + engines: {node: '>= 0.4'} + + is-negative-zero@2.0.3: + resolution: {integrity: sha512-5KoIu2Ngpyek75jXodFvnafB6DJgr3u8uuK0LEZJjrU19DrMD3EVERaR8sjz8CCGgpZvxPl9SuE1GMVPFHx1mw==} + engines: {node: '>= 0.4'} + + is-number-object@1.1.1: + resolution: {integrity: sha512-lZhclumE1G6VYD8VHe35wFaIif+CTy5SJIi5+3y4psDgWu4wPDoBhF8NxUOinEc7pHgiTsT6MaBb92rKhhD+Xw==} + engines: {node: '>= 0.4'} + + is-path-inside@3.0.3: + resolution: {integrity: sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==} + engines: {node: '>=8'} + + is-regex@1.2.1: + resolution: {integrity: sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g==} + engines: {node: '>= 0.4'} + + is-set@2.0.3: + resolution: {integrity: sha512-iPAjerrse27/ygGLxw+EBR9agv9Y6uLeYVJMu+QNCoouJ1/1ri0mGrcWpfCqFZuzzx3WjtwxG098X+n4OuRkPg==} + engines: {node: '>= 0.4'} + + is-shared-array-buffer@1.0.4: + resolution: {integrity: sha512-ISWac8drv4ZGfwKl5slpHG9OwPNty4jOWPRIhBpxOoD+hqITiwuipOQ2bNthAzwA3B4fIjO4Nln74N0S9byq8A==} + engines: {node: '>= 0.4'} + + is-string@1.1.1: + resolution: {integrity: sha512-BtEeSsoaQjlSPBemMQIrY1MY0uM6vnS1g5fmufYOtnxLGUZM2178PKbhsk7Ffv58IX+ZtcvoGwccYsh0PglkAA==} + engines: {node: '>= 0.4'} + + is-symbol@1.1.1: + resolution: {integrity: sha512-9gGx6GTtCQM73BgmHQXfDmLtfjjTUDSyoxTCbp5WtoixAhfgsDirWIcVQ/IHpvI5Vgd5i/J5F7B9cN/WlVbC/w==} + engines: {node: '>= 0.4'} + + is-typed-array@1.1.15: + resolution: {integrity: sha512-p3EcsicXjit7SaskXHs1hA91QxgTw46Fv6EFKKGS5DRFLD8yKnohjF3hxoju94b/OcMZoQukzpPpBE9uLVKzgQ==} + engines: {node: '>= 0.4'} + + is-weakmap@2.0.2: + resolution: {integrity: sha512-K5pXYOm9wqY1RgjpL3YTkF39tni1XajUIkawTLUo9EZEVUFga5gSQJF8nNS7ZwJQ02y+1YCNYcMh+HIf1ZqE+w==} + engines: {node: '>= 0.4'} + + is-weakref@1.1.1: + resolution: {integrity: sha512-6i9mGWSlqzNMEqpCp93KwRS1uUOodk2OJ6b+sq7ZPDSy2WuI5NFIxp/254TytR8ftefexkWn5xNiHUNpPOfSew==} + engines: {node: '>= 0.4'} + + is-weakset@2.0.4: + resolution: {integrity: sha512-mfcwb6IzQyOKTs84CQMrOwW4gQcaTOAWJ0zzJCl2WSPDrWk/OzDaImWFH3djXhb24g4eudZfLRozAvPGw4d9hQ==} + engines: {node: '>= 0.4'} + + isarray@2.0.5: + resolution: {integrity: sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==} + + isexe@2.0.0: + resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} + + iterator.prototype@1.1.5: + resolution: {integrity: sha512-H0dkQoCa3b2VEeKQBOxFph+JAbcrQdE7KC0UkqwpLmv2EC4P41QXP+rqo9wYodACiG5/WM5s9oDApTU8utwj9g==} + engines: {node: '>= 0.4'} + + js-tokens@4.0.0: + resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} + + js-yaml@4.3.0: + resolution: {integrity: sha512-1td788aAnnZ5qs7V2QIRl1owjtYpbKt749Y3xauqQgwIIGF/xXWz1wMTEBx5O3LK3lXLVuqXPdPxj2BoFHaW9Q==} + hasBin: true + + jsesc@3.1.0: + resolution: {integrity: sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==} + engines: {node: '>=6'} + hasBin: true + + json-buffer@3.0.1: + resolution: {integrity: sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==} + + json-schema-traverse@0.4.1: + resolution: {integrity: sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==} + + json-stable-stringify-without-jsonify@1.0.1: + resolution: {integrity: sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==} + + json5@2.2.3: + resolution: {integrity: sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==} + engines: {node: '>=6'} + hasBin: true + + jsx-ast-utils@3.3.5: + resolution: {integrity: sha512-ZZow9HBI5O6EPgSJLUb8n2NKgmVWTwCvHGwFuJlMjvLFqlGG6pjirPhtdsseaLZjSibD8eegzmYpUZwoIlj2cQ==} + engines: {node: '>=4.0'} + + keyv@4.5.4: + resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==} + + levn@0.4.1: + resolution: {integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==} + engines: {node: '>= 0.8.0'} + + locate-path@6.0.0: + resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==} + engines: {node: '>=10'} + + lodash.merge@4.6.2: + resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==} + + loose-envify@1.4.0: + resolution: {integrity: sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==} + hasBin: true + + lru-cache@5.1.1: + resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==} + + lucide-react@1.21.0: + resolution: {integrity: sha512-reEZMXq8Qdd5jg5XYkQ5TR1fB/GiQ7ih4vcrthYDtgjSDwh0i6/YLiGjsWsIwgN49gpAnd4J2elSNzncMEEUUQ==} + peerDependencies: + react: ^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0 + + math-intrinsics@1.1.0: + resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==} + engines: {node: '>= 0.4'} + + minimatch@3.1.5: + resolution: {integrity: sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==} + + ms@2.1.3: + resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} + + nanoid@3.3.15: + resolution: {integrity: sha512-y7Wygv/7mEOvxTuEQDB8StXdMRBWf1kR/tlhAzBRUFkB2jfcLOAxO/SHmOO2zgz1pVgK29/kyupn059/bCHdjA==} + engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} + hasBin: true + + natural-compare@1.4.0: + resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==} + + node-exports-info@1.6.2: + resolution: {integrity: sha512-kXs9Go0cah0qHVV2v389IXQLdLCeE1xfFtjOAF+iobu0OIoG1pje8At2vMHyaPMiPMnG/LWP50twML21eMcAag==} + engines: {node: '>= 0.4'} + + node-releases@2.0.50: + resolution: {integrity: sha512-J6l92tKHX6w8Jy5nO1Vuc01NoIiRGi/d6qBKVxh+IQ8Cr3b6HbVNfKiF8ZpFKufTwpwxMmce2W3iQZ861ZRyTg==} + engines: {node: '>=18'} + + object-assign@4.1.1: + resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==} + engines: {node: '>=0.10.0'} + + object-inspect@1.13.4: + resolution: {integrity: sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==} + engines: {node: '>= 0.4'} + + object-keys@1.1.1: + resolution: {integrity: sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==} + engines: {node: '>= 0.4'} + + object.assign@4.1.7: + resolution: {integrity: sha512-nK28WOo+QIjBkDduTINE4JkF/UJJKyf2EJxvJKfblDpyg0Q+pkOHNTL0Qwy6NP6FhE/EnzV73BxxqcJaXY9anw==} + engines: {node: '>= 0.4'} + + object.entries@1.1.9: + resolution: {integrity: sha512-8u/hfXFRBD1O0hPUjioLhoWFHRmt6tKA4/vZPyckBr18l1KE9uHrFaFaUi8MDRTpi4uak2goyPTSNJLXX2k2Hw==} + engines: {node: '>= 0.4'} + + object.fromentries@2.0.8: + resolution: {integrity: sha512-k6E21FzySsSK5a21KRADBd/NGneRegFO5pLHfdQLpRDETUNJueLXs3WCzyQ3tFRDYgbq3KHGXfTbi2bs8WQ6rQ==} + engines: {node: '>= 0.4'} + + object.values@1.2.1: + resolution: {integrity: sha512-gXah6aZrcUxjWg2zR2MwouP2eHlCBzdV4pygudehaKXSGW4v2AsRQUK+lwwXhii6KFZcunEnmSUoYp5CXibxtA==} + engines: {node: '>= 0.4'} + + once@1.4.0: + resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==} + + optionator@0.9.4: + resolution: {integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==} + engines: {node: '>= 0.8.0'} + + own-keys@1.0.1: + resolution: {integrity: sha512-qFOyK5PjiWZd+QQIh+1jhdb9LpxTF0qs7Pm8o5QHYZ0M3vKqSqzsZaEB6oWlxZ+q2sJBMI/Ktgd2N5ZwQoRHfg==} + engines: {node: '>= 0.4'} + + p-limit@3.1.0: + resolution: {integrity: sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==} + engines: {node: '>=10'} + + p-locate@5.0.0: + resolution: {integrity: sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==} + engines: {node: '>=10'} + + parent-module@1.0.1: + resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==} + engines: {node: '>=6'} + + path-exists@4.0.0: + resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==} + engines: {node: '>=8'} + + path-is-absolute@1.0.1: + resolution: {integrity: sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==} + engines: {node: '>=0.10.0'} + + path-key@3.1.1: + resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==} + engines: {node: '>=8'} + + path-parse@1.0.7: + resolution: {integrity: sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==} + + picocolors@1.1.1: + resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} + + possible-typed-array-names@1.1.0: + resolution: {integrity: sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==} + engines: {node: '>= 0.4'} + + postcss@8.5.16: + resolution: {integrity: sha512-vuwillviilfKZsg0VGj5R/YwwcHx4SLsIOI/7K6mQkWx+l5cUHTjj5g0AasTBcyXsbfTgrwsUNmVUb5xVwyPwg==} + engines: {node: ^10 || ^12 || >=14} + + prelude-ls@1.2.1: + resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==} + engines: {node: '>= 0.8.0'} + + prop-types@15.8.1: + resolution: {integrity: sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==} + + punycode@2.3.1: + resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} + engines: {node: '>=6'} + + queue-microtask@1.2.3: + resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} + + react-dom@18.3.1: + resolution: {integrity: sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==} + peerDependencies: + react: ^18.3.1 + + react-is@16.13.1: + resolution: {integrity: sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==} + + react-refresh@0.17.0: + resolution: {integrity: sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ==} + engines: {node: '>=0.10.0'} + + react-router-dom@6.30.4: + resolution: {integrity: sha512-q4HvNl+mmDdkS0g+MqiBZNteQJCuimWoOyHMy4T/RQLAn9Z29+E91QXRaxOujeMl2HTzRSS0KFPd7lxX3PjV0Q==} + engines: {node: '>=14.0.0'} + peerDependencies: + react: '>=16.8' + react-dom: '>=16.8' + + react-router@6.30.4: + resolution: {integrity: sha512-SVUsDe+DybHM/WmYKIVYhZh1o5Dcuf16yM6WjG02Q9XVFMZIJyHYhwrr6bFBXZkVP6z69kNkMyBCujt8FaFLJA==} + engines: {node: '>=14.0.0'} + peerDependencies: + react: '>=16.8' + + react@18.3.1: + resolution: {integrity: sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==} + engines: {node: '>=0.10.0'} + + reflect.getprototypeof@1.0.10: + resolution: {integrity: sha512-00o4I+DVrefhv+nX0ulyi3biSHCPDe+yLv5o/p6d/UVlirijB8E16FtfwSAi4g3tcqrQ4lRAqQSoFEZJehYEcw==} + engines: {node: '>= 0.4'} + + regexp.prototype.flags@1.5.4: + resolution: {integrity: sha512-dYqgNSZbDwkaJ2ceRd9ojCGjBq+mOm9LmtXnAnEGyHhN/5R7iDW2TRw3h+o/jCFxus3P2LfWIIiwowAjANm7IA==} + engines: {node: '>= 0.4'} + + resolve-from@4.0.0: + resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==} + engines: {node: '>=4'} + + resolve@2.0.0-next.7: + resolution: {integrity: sha512-tqt+NBWwyaMgw3zDsnygx4CByWjQEJHOPMdslYhppaQSJUtL/D4JO9CcBBlhPoI8lz9oJIDXkwXfhF4aWqP8xQ==} + engines: {node: '>= 0.4'} + hasBin: true + + reusify@1.1.0: + resolution: {integrity: sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==} + engines: {iojs: '>=1.0.0', node: '>=0.10.0'} + + rimraf@3.0.2: + resolution: {integrity: sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==} + deprecated: Rimraf versions prior to v4 are no longer supported + hasBin: true + + rollup@3.30.0: + resolution: {integrity: sha512-kQvGasUgN+AlWGliFn2POSajRQEsULVYFGTvOZmK06d7vCD+YhZztt70kGk3qaeAXeWYL5eO7zx+rAubBc55eA==} + engines: {node: '>=14.18.0', npm: '>=8.0.0'} + hasBin: true + + run-parallel@1.2.0: + resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==} + + safe-array-concat@1.1.4: + resolution: {integrity: sha512-wtZlHyOje6OZTGqAoaDKxFkgRtkF9CnHAVnCHKfuj200wAgL+bSJhdsCD2l0Qx/2ekEXjPWcyKkfGb5CPboslg==} + engines: {node: '>=0.4'} + + safe-push-apply@1.0.0: + resolution: {integrity: sha512-iKE9w/Z7xCzUMIZqdBsp6pEQvwuEebH4vdpjcDWnyzaI6yl6O9FHvVpmGelvEHNsoY6wGblkxR6Zty/h00WiSA==} + engines: {node: '>= 0.4'} + + safe-regex-test@1.1.0: + resolution: {integrity: sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw==} + engines: {node: '>= 0.4'} + + scheduler@0.23.2: + resolution: {integrity: sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==} + + semver@6.3.1: + resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==} + hasBin: true + + set-function-length@1.2.2: + resolution: {integrity: sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==} + engines: {node: '>= 0.4'} + + set-function-name@2.0.2: + resolution: {integrity: sha512-7PGFlmtwsEADb0WYyvCMa1t+yke6daIG4Wirafur5kcf+MhUnPms1UeR0CKQdTZD81yESwMHbtn+TR+dMviakQ==} + engines: {node: '>= 0.4'} + + set-proto@1.0.0: + resolution: {integrity: sha512-RJRdvCo6IAnPdsvP/7m6bsQqNnn1FCBX5ZNtFL98MmFF/4xAIJTIg1YbHW5DC2W5SKZanrC6i4HsJqlajw/dZw==} + engines: {node: '>= 0.4'} + + shebang-command@2.0.0: + resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==} + engines: {node: '>=8'} + + shebang-regex@3.0.0: + resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==} + engines: {node: '>=8'} + + side-channel-list@1.0.1: + resolution: {integrity: sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w==} + engines: {node: '>= 0.4'} + + side-channel-map@1.0.1: + resolution: {integrity: sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==} + engines: {node: '>= 0.4'} + + side-channel-weakmap@1.0.2: + resolution: {integrity: sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==} + engines: {node: '>= 0.4'} + + side-channel@1.1.1: + resolution: {integrity: sha512-6x6dK6zJdpTzF4sQeNYxwtvBzf6Eg4GtlesS94HOvTudUeyK2WXAaIfmDgsyslYrRBeFIlsi54AYsFGUuhmvrQ==} + engines: {node: '>= 0.4'} + + source-map-js@1.2.1: + resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} + engines: {node: '>=0.10.0'} + + ssf@0.11.2: + resolution: {integrity: sha512-+idbmIXoYET47hH+d7dfm2epdOMUDjqcB4648sTZ+t2JwoyBFL/insLfB/racrDmsKB3diwsDA696pZMieAC5g==} + engines: {node: '>=0.8'} + + stop-iteration-iterator@1.1.0: + resolution: {integrity: sha512-eLoXW/DHyl62zxY4SCaIgnRhuMr6ri4juEYARS8E6sCEqzKpOiE521Ucofdx+KnDZl5xmvGYaaKCk5FEOxJCoQ==} + engines: {node: '>= 0.4'} + + string.prototype.matchall@4.0.12: + resolution: {integrity: sha512-6CC9uyBL+/48dYizRf7H7VAYCMCNTBeM78x/VTUe9bFEaxBepPJDa1Ow99LqI/1yF7kuy7Q3cQsYMrcjGUcskA==} + engines: {node: '>= 0.4'} + + string.prototype.repeat@1.0.0: + resolution: {integrity: sha512-0u/TldDbKD8bFCQ/4f5+mNRrXwZ8hg2w7ZR8wa16e8z9XpePWl3eGEcUD0OXpEH/VJH/2G3gjUtR3ZOiBe2S/w==} + + string.prototype.trim@1.2.11: + resolution: {integrity: sha512-PwvK7BU+CMTJGYQCTZb5RWXIML92lftJLhQz1tBzgKiqGxJaMlBAa48POXaNAC2s4y8jr3EFqrkF9+44neS46w==} + engines: {node: '>= 0.4'} + + string.prototype.trimend@1.0.10: + resolution: {integrity: sha512-2+3aDAOmPTmuFwjDnmJG2ctEkQKVki7vOSqaxkv42Mowj1V6PnvuwFCRrR5lChUux1TBskPjfkeTOhqczDMxTw==} + engines: {node: '>= 0.4'} + + string.prototype.trimstart@1.0.8: + resolution: {integrity: sha512-UXSH262CSZY1tfu3G3Secr6uGLCFVPMhIqHjlgCUtCCcgihYc/xKs9djMTMUOb2j1mVSeU8EU6NWc/iQKU6Gfg==} + engines: {node: '>= 0.4'} + + strip-ansi@6.0.1: + resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==} + engines: {node: '>=8'} + + strip-json-comments@3.1.1: + resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==} + engines: {node: '>=8'} + + supports-color@7.2.0: + resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==} + engines: {node: '>=8'} + + supports-preserve-symlinks-flag@1.0.0: + resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==} + engines: {node: '>= 0.4'} + + text-table@0.2.0: + resolution: {integrity: sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==} + + type-check@0.4.0: + resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==} + engines: {node: '>= 0.8.0'} + + type-fest@0.20.2: + resolution: {integrity: sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==} + engines: {node: '>=10'} + + typed-array-buffer@1.0.3: + resolution: {integrity: sha512-nAYYwfY3qnzX30IkA6AQZjVbtK6duGontcQm1WSG1MD94YLqK0515GNApXkoxKOWMusVssAHWLh9SeaoefYFGw==} + engines: {node: '>= 0.4'} + + typed-array-byte-length@1.0.3: + resolution: {integrity: sha512-BaXgOuIxz8n8pIq3e7Atg/7s+DpiYrxn4vdot3w9KbnBhcRQq6o3xemQdIfynqSeXeDrF32x+WvfzmOjPiY9lg==} + engines: {node: '>= 0.4'} + + typed-array-byte-offset@1.0.4: + resolution: {integrity: sha512-bTlAFB/FBYMcuX81gbL4OcpH5PmlFHqlCCpAl8AlEzMz5k53oNDvN8p1PNOWLEmI2x4orp3raOFB51tv9X+MFQ==} + engines: {node: '>= 0.4'} + + typed-array-length@1.0.8: + resolution: {integrity: sha512-phPGCwqr2+Qo0fwniCE8e4pKnGu/yFb5nD5Y8bf0EEeiI5GklnACYA9GFy/DrAeRrKHXvHn+1SUsOWgJp6RO+g==} + engines: {node: '>= 0.4'} + + unbox-primitive@1.1.0: + resolution: {integrity: sha512-nWJ91DjeOkej/TA8pXQ3myruKpKEYgqvpw9lz4OPHj/NWFNluYrjbz9j01CJ8yKQd2g4jFoOkINCTW2I5LEEyw==} + engines: {node: '>= 0.4'} + + update-browserslist-db@1.2.3: + resolution: {integrity: sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==} + hasBin: true + peerDependencies: + browserslist: '>= 4.21.0' + + uri-js@4.4.1: + resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==} + + vite@4.5.14: + resolution: {integrity: sha512-+v57oAaoYNnO3hIu5Z/tJRZjq5aHM2zDve9YZ8HngVHbhk66RStobhb1sqPMIPEleV6cNKYK4eGrAbE9Ulbl2g==} + engines: {node: ^14.18.0 || >=16.0.0} + hasBin: true + peerDependencies: + '@types/node': '>= 14' + less: '*' + lightningcss: ^1.21.0 + sass: '*' + stylus: '*' + sugarss: '*' + terser: ^5.4.0 + peerDependenciesMeta: + '@types/node': + optional: true + less: + optional: true + lightningcss: + optional: true + sass: + optional: true + stylus: + optional: true + sugarss: + optional: true + terser: + optional: true + + which-boxed-primitive@1.1.1: + resolution: {integrity: sha512-TbX3mj8n0odCBFVlY8AxkqcHASw3L60jIuF8jFP78az3C2YhmGvqbHBpAjTRH2/xqYunrJ9g1jSyjCjpoWzIAA==} + engines: {node: '>= 0.4'} + + which-builtin-type@1.2.1: + resolution: {integrity: sha512-6iBczoX+kDQ7a3+YJBnh3T+KZRxM/iYNPXicqk66/Qfm1b93iu+yOImkg0zHbj5LNOcNv1TEADiZ0xa34B4q6Q==} + engines: {node: '>= 0.4'} + + which-collection@1.0.2: + resolution: {integrity: sha512-K4jVyjnBdgvc86Y6BkaLZEN933SwYOuBFkdmBu9ZfkcAbdVbpITnDmjvZ/aQjRXQrv5EPkTnD1s39GiiqbngCw==} + engines: {node: '>= 0.4'} + + which-typed-array@1.1.22: + resolution: {integrity: sha512-fvO4ExWMFsqyhG3AiPAObMuY1lxaqgYcxbc49CNdWDDECOJNgQyvsOWVwbZc+qf3rzRtxojBK+CMEv0Ld5CYpw==} + engines: {node: '>= 0.4'} + + which@2.0.2: + resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==} + engines: {node: '>= 8'} + hasBin: true + + wmf@1.0.2: + resolution: {integrity: sha512-/p9K7bEh0Dj6WbXg4JG0xvLQmIadrner1bi45VMJTfnbVHsc7yIajZyoSoK60/dtVBs12Fm6WkUI5/3WAVsNMw==} + engines: {node: '>=0.8'} + + word-wrap@1.2.5: + resolution: {integrity: sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==} + engines: {node: '>=0.10.0'} + + word@0.3.0: + resolution: {integrity: sha512-OELeY0Q61OXpdUfTp+oweA/vtLVg5VDOXh+3he3PNzLGG/y0oylSOC1xRVj0+l4vQ3tj/bB1HVHv1ocXkQceFA==} + engines: {node: '>=0.8'} + + wrappy@1.0.2: + resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==} + + xlsx@0.18.5: + resolution: {integrity: sha512-dmg3LCjBPHZnQp5/F/+nnTa+miPJxUXB6vtk42YjBBKayDNagxGEeIdWApkYPOf3Z3pm3k62Knjzp7lMeTEtFQ==} + engines: {node: '>=0.8'} + hasBin: true + + yallist@3.1.1: + resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==} + + yocto-queue@0.1.0: + resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==} + engines: {node: '>=10'} + +snapshots: + + '@babel/code-frame@7.29.7': + dependencies: + '@babel/helper-validator-identifier': 7.29.7 + js-tokens: 4.0.0 + picocolors: 1.1.1 + + '@babel/compat-data@7.29.7': {} + + '@babel/core@7.29.7': + dependencies: + '@babel/code-frame': 7.29.7 + '@babel/generator': 7.29.7 + '@babel/helper-compilation-targets': 7.29.7 + '@babel/helper-module-transforms': 7.29.7(@babel/core@7.29.7) + '@babel/helpers': 7.29.7 + '@babel/parser': 7.29.7 + '@babel/template': 7.29.7 + '@babel/traverse': 7.29.7 + '@babel/types': 7.29.7 + '@jridgewell/remapping': 2.3.5 + convert-source-map: 2.0.0 + debug: 4.4.3 + gensync: 1.0.0-beta.2 + json5: 2.2.3 + semver: 6.3.1 + transitivePeerDependencies: + - supports-color + + '@babel/generator@7.29.7': + dependencies: + '@babel/parser': 7.29.7 + '@babel/types': 7.29.7 + '@jridgewell/gen-mapping': 0.3.13 + '@jridgewell/trace-mapping': 0.3.31 + jsesc: 3.1.0 + + '@babel/helper-compilation-targets@7.29.7': + dependencies: + '@babel/compat-data': 7.29.7 + '@babel/helper-validator-option': 7.29.7 + browserslist: 4.28.4 + lru-cache: 5.1.1 + semver: 6.3.1 + + '@babel/helper-globals@7.29.7': {} + + '@babel/helper-module-imports@7.29.7': + dependencies: + '@babel/traverse': 7.29.7 + '@babel/types': 7.29.7 + transitivePeerDependencies: + - supports-color + + '@babel/helper-module-transforms@7.29.7(@babel/core@7.29.7)': + dependencies: + '@babel/core': 7.29.7 + '@babel/helper-module-imports': 7.29.7 + '@babel/helper-validator-identifier': 7.29.7 + '@babel/traverse': 7.29.7 + transitivePeerDependencies: + - supports-color + + '@babel/helper-plugin-utils@7.29.7': {} + + '@babel/helper-string-parser@7.29.7': {} + + '@babel/helper-validator-identifier@7.29.7': {} + + '@babel/helper-validator-option@7.29.7': {} + + '@babel/helpers@7.29.7': + dependencies: + '@babel/template': 7.29.7 + '@babel/types': 7.29.7 + + '@babel/parser@7.29.7': + dependencies: + '@babel/types': 7.29.7 + + '@babel/plugin-transform-react-jsx-self@7.29.7(@babel/core@7.29.7)': + dependencies: + '@babel/core': 7.29.7 + '@babel/helper-plugin-utils': 7.29.7 + + '@babel/plugin-transform-react-jsx-source@7.29.7(@babel/core@7.29.7)': + dependencies: + '@babel/core': 7.29.7 + '@babel/helper-plugin-utils': 7.29.7 + + '@babel/template@7.29.7': + dependencies: + '@babel/code-frame': 7.29.7 + '@babel/parser': 7.29.7 + '@babel/types': 7.29.7 + + '@babel/traverse@7.29.7': + dependencies: + '@babel/code-frame': 7.29.7 + '@babel/generator': 7.29.7 + '@babel/helper-globals': 7.29.7 + '@babel/parser': 7.29.7 + '@babel/template': 7.29.7 + '@babel/types': 7.29.7 + debug: 4.4.3 + transitivePeerDependencies: + - supports-color + + '@babel/types@7.29.7': + dependencies: + '@babel/helper-string-parser': 7.29.7 + '@babel/helper-validator-identifier': 7.29.7 + + '@esbuild/android-arm64@0.18.20': + optional: true + + '@esbuild/android-arm@0.18.20': + optional: true + + '@esbuild/android-x64@0.18.20': + optional: true + + '@esbuild/darwin-arm64@0.18.20': + optional: true + + '@esbuild/darwin-x64@0.18.20': + optional: true + + '@esbuild/freebsd-arm64@0.18.20': + optional: true + + '@esbuild/freebsd-x64@0.18.20': + optional: true + + '@esbuild/linux-arm64@0.18.20': + optional: true + + '@esbuild/linux-arm@0.18.20': + optional: true + + '@esbuild/linux-ia32@0.18.20': + optional: true + + '@esbuild/linux-loong64@0.18.20': + optional: true + + '@esbuild/linux-mips64el@0.18.20': + optional: true + + '@esbuild/linux-ppc64@0.18.20': + optional: true + + '@esbuild/linux-riscv64@0.18.20': + optional: true + + '@esbuild/linux-s390x@0.18.20': + optional: true + + '@esbuild/linux-x64@0.18.20': + optional: true + + '@esbuild/netbsd-x64@0.18.20': + optional: true + + '@esbuild/openbsd-x64@0.18.20': + optional: true + + '@esbuild/sunos-x64@0.18.20': + optional: true + + '@esbuild/win32-arm64@0.18.20': + optional: true + + '@esbuild/win32-ia32@0.18.20': + optional: true + + '@esbuild/win32-x64@0.18.20': + optional: true + + '@eslint-community/eslint-utils@4.9.1(eslint@8.57.1)': + dependencies: + eslint: 8.57.1 + eslint-visitor-keys: 3.4.3 + + '@eslint-community/regexpp@4.12.2': {} + + '@eslint/eslintrc@2.1.4': + dependencies: + ajv: 6.15.0 + debug: 4.4.3 + espree: 9.6.1 + globals: 13.24.0 + ignore: 5.3.2 + import-fresh: 3.3.1 + js-yaml: 4.3.0 + minimatch: 3.1.5 + strip-json-comments: 3.1.1 + transitivePeerDependencies: + - supports-color + + '@eslint/js@8.57.1': {} + + '@humanwhocodes/config-array@0.13.0': + dependencies: + '@humanwhocodes/object-schema': 2.0.3 + debug: 4.4.3 + minimatch: 3.1.5 + transitivePeerDependencies: + - supports-color + + '@humanwhocodes/module-importer@1.0.1': {} + + '@humanwhocodes/object-schema@2.0.3': {} + + '@jridgewell/gen-mapping@0.3.13': + dependencies: + '@jridgewell/sourcemap-codec': 1.5.5 + '@jridgewell/trace-mapping': 0.3.31 + + '@jridgewell/remapping@2.3.5': + dependencies: + '@jridgewell/gen-mapping': 0.3.13 + '@jridgewell/trace-mapping': 0.3.31 + + '@jridgewell/resolve-uri@3.1.2': {} + + '@jridgewell/sourcemap-codec@1.5.5': {} + + '@jridgewell/trace-mapping@0.3.31': + dependencies: + '@jridgewell/resolve-uri': 3.1.2 + '@jridgewell/sourcemap-codec': 1.5.5 + + '@nodelib/fs.scandir@2.1.5': + dependencies: + '@nodelib/fs.stat': 2.0.5 + run-parallel: 1.2.0 + + '@nodelib/fs.stat@2.0.5': {} + + '@nodelib/fs.walk@1.2.8': + dependencies: + '@nodelib/fs.scandir': 2.1.5 + fastq: 1.20.1 + + '@remix-run/router@1.23.3': {} + + '@rolldown/pluginutils@1.0.0-beta.27': {} + + '@types/babel__core@7.20.5': + dependencies: + '@babel/parser': 7.29.7 + '@babel/types': 7.29.7 + '@types/babel__generator': 7.27.0 + '@types/babel__template': 7.4.4 + '@types/babel__traverse': 7.28.0 + + '@types/babel__generator@7.27.0': + dependencies: + '@babel/types': 7.29.7 + + '@types/babel__template@7.4.4': + dependencies: + '@babel/parser': 7.29.7 + '@babel/types': 7.29.7 + + '@types/babel__traverse@7.28.0': + dependencies: + '@babel/types': 7.29.7 + + '@types/prop-types@15.7.15': {} + + '@types/react-dom@18.3.7(@types/react@18.3.31)': + dependencies: + '@types/react': 18.3.31 + + '@types/react@18.3.31': + dependencies: + '@types/prop-types': 15.7.15 + csstype: 3.2.3 + + '@ungap/structured-clone@1.3.2': {} + + '@vitejs/plugin-react@4.7.0(vite@4.5.14)': + dependencies: + '@babel/core': 7.29.7 + '@babel/plugin-transform-react-jsx-self': 7.29.7(@babel/core@7.29.7) + '@babel/plugin-transform-react-jsx-source': 7.29.7(@babel/core@7.29.7) + '@rolldown/pluginutils': 1.0.0-beta.27 + '@types/babel__core': 7.20.5 + react-refresh: 0.17.0 + vite: 4.5.14 + transitivePeerDependencies: + - supports-color + + acorn-jsx@5.3.2(acorn@8.17.0): + dependencies: + acorn: 8.17.0 + + acorn@8.17.0: {} + + adler-32@1.3.1: {} + + ajv@6.15.0: + dependencies: + fast-deep-equal: 3.1.3 + fast-json-stable-stringify: 2.1.0 + json-schema-traverse: 0.4.1 + uri-js: 4.4.1 + + ansi-regex@5.0.1: {} + + ansi-styles@4.3.0: + dependencies: + color-convert: 2.0.1 + + argparse@2.0.1: {} + + array-buffer-byte-length@1.0.2: + dependencies: + call-bound: 1.0.4 + is-array-buffer: 3.0.5 + + array-includes@3.1.9: + dependencies: + call-bind: 1.0.9 + call-bound: 1.0.4 + define-properties: 1.2.1 + es-abstract: 1.24.2 + es-object-atoms: 1.1.2 + get-intrinsic: 1.3.0 + is-string: 1.1.1 + math-intrinsics: 1.1.0 + + array.prototype.findlast@1.2.5: + dependencies: + call-bind: 1.0.9 + define-properties: 1.2.1 + es-abstract: 1.24.2 + es-errors: 1.3.0 + es-object-atoms: 1.1.2 + es-shim-unscopables: 1.1.0 + + array.prototype.flat@1.3.3: + dependencies: + call-bind: 1.0.9 + define-properties: 1.2.1 + es-abstract: 1.24.2 + es-shim-unscopables: 1.1.0 + + array.prototype.flatmap@1.3.3: + dependencies: + call-bind: 1.0.9 + define-properties: 1.2.1 + es-abstract: 1.24.2 + es-shim-unscopables: 1.1.0 + + array.prototype.tosorted@1.1.4: + dependencies: + call-bind: 1.0.9 + define-properties: 1.2.1 + es-abstract: 1.24.2 + es-errors: 1.3.0 + es-shim-unscopables: 1.1.0 + + arraybuffer.prototype.slice@1.0.4: + dependencies: + array-buffer-byte-length: 1.0.2 + call-bind: 1.0.9 + define-properties: 1.2.1 + es-abstract: 1.24.2 + es-errors: 1.3.0 + get-intrinsic: 1.3.0 + is-array-buffer: 3.0.5 + + async-function@1.0.0: {} + + available-typed-arrays@1.0.7: + dependencies: + possible-typed-array-names: 1.1.0 + + balanced-match@1.0.2: {} + + baseline-browser-mapping@2.10.40: {} + + brace-expansion@1.1.15: + dependencies: + balanced-match: 1.0.2 + concat-map: 0.0.1 + + browserslist@4.28.4: + dependencies: + baseline-browser-mapping: 2.10.40 + caniuse-lite: 1.0.30001799 + electron-to-chromium: 1.5.380 + node-releases: 2.0.50 + update-browserslist-db: 1.2.3(browserslist@4.28.4) + + call-bind-apply-helpers@1.0.2: + dependencies: + es-errors: 1.3.0 + function-bind: 1.1.2 + + call-bind@1.0.9: + dependencies: + call-bind-apply-helpers: 1.0.2 + es-define-property: 1.0.1 + get-intrinsic: 1.3.0 + set-function-length: 1.2.2 + + call-bound@1.0.4: + dependencies: + call-bind-apply-helpers: 1.0.2 + get-intrinsic: 1.3.0 + + callsites@3.1.0: {} + + caniuse-lite@1.0.30001799: {} + + cfb@1.2.2: + dependencies: + adler-32: 1.3.1 + crc-32: 1.2.2 + + chalk@4.1.2: + dependencies: + ansi-styles: 4.3.0 + supports-color: 7.2.0 + + codepage@1.15.0: {} + + color-convert@2.0.1: + dependencies: + color-name: 1.1.4 + + color-name@1.1.4: {} + + concat-map@0.0.1: {} + + convert-source-map@2.0.0: {} + + crc-32@1.2.2: {} + + cross-spawn@7.0.6: + dependencies: + path-key: 3.1.1 + shebang-command: 2.0.0 + which: 2.0.2 + + csstype@3.2.3: {} + + data-view-buffer@1.0.2: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + is-data-view: 1.0.2 + + data-view-byte-length@1.0.2: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + is-data-view: 1.0.2 + + data-view-byte-offset@1.0.1: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + is-data-view: 1.0.2 + + debug@4.4.3: + dependencies: + ms: 2.1.3 + + deep-is@0.1.4: {} + + define-data-property@1.1.4: + dependencies: + es-define-property: 1.0.1 + es-errors: 1.3.0 + gopd: 1.2.0 + + define-properties@1.2.1: + dependencies: + define-data-property: 1.1.4 + has-property-descriptors: 1.0.2 + object-keys: 1.1.1 + + doctrine@2.1.0: + dependencies: + esutils: 2.0.3 + + doctrine@3.0.0: + dependencies: + esutils: 2.0.3 + + dunder-proto@1.0.1: + dependencies: + call-bind-apply-helpers: 1.0.2 + es-errors: 1.3.0 + gopd: 1.2.0 + + electron-to-chromium@1.5.380: {} + + es-abstract-get@1.0.0: + dependencies: + es-errors: 1.3.0 + es-object-atoms: 1.1.2 + is-callable: 1.2.7 + object-inspect: 1.13.4 + + es-abstract@1.24.2: + dependencies: + array-buffer-byte-length: 1.0.2 + arraybuffer.prototype.slice: 1.0.4 + available-typed-arrays: 1.0.7 + call-bind: 1.0.9 + call-bound: 1.0.4 + data-view-buffer: 1.0.2 + data-view-byte-length: 1.0.2 + data-view-byte-offset: 1.0.1 + es-define-property: 1.0.1 + es-errors: 1.3.0 + es-object-atoms: 1.1.2 + es-set-tostringtag: 2.1.0 + es-to-primitive: 1.3.4 + function.prototype.name: 1.2.0 + get-intrinsic: 1.3.0 + get-proto: 1.0.1 + get-symbol-description: 1.1.0 + globalthis: 1.0.4 + gopd: 1.2.0 + has-property-descriptors: 1.0.2 + has-proto: 1.2.0 + has-symbols: 1.1.0 + hasown: 2.0.4 + internal-slot: 1.1.0 + is-array-buffer: 3.0.5 + is-callable: 1.2.7 + is-data-view: 1.0.2 + is-negative-zero: 2.0.3 + is-regex: 1.2.1 + is-set: 2.0.3 + is-shared-array-buffer: 1.0.4 + is-string: 1.1.1 + is-typed-array: 1.1.15 + is-weakref: 1.1.1 + math-intrinsics: 1.1.0 + object-inspect: 1.13.4 + object-keys: 1.1.1 + object.assign: 4.1.7 + own-keys: 1.0.1 + regexp.prototype.flags: 1.5.4 + safe-array-concat: 1.1.4 + safe-push-apply: 1.0.0 + safe-regex-test: 1.1.0 + set-proto: 1.0.0 + stop-iteration-iterator: 1.1.0 + string.prototype.trim: 1.2.11 + string.prototype.trimend: 1.0.10 + string.prototype.trimstart: 1.0.8 + typed-array-buffer: 1.0.3 + typed-array-byte-length: 1.0.3 + typed-array-byte-offset: 1.0.4 + typed-array-length: 1.0.8 + unbox-primitive: 1.1.0 + which-typed-array: 1.1.22 + + es-define-property@1.0.1: {} + + es-errors@1.3.0: {} + + es-iterator-helpers@1.3.3: + dependencies: + call-bind: 1.0.9 + call-bound: 1.0.4 + define-properties: 1.2.1 + es-abstract: 1.24.2 + es-errors: 1.3.0 + es-set-tostringtag: 2.1.0 + function-bind: 1.1.2 + get-intrinsic: 1.3.0 + globalthis: 1.0.4 + gopd: 1.2.0 + has-property-descriptors: 1.0.2 + has-proto: 1.2.0 + has-symbols: 1.1.0 + internal-slot: 1.1.0 + iterator.prototype: 1.1.5 + math-intrinsics: 1.1.0 + + es-object-atoms@1.1.2: + dependencies: + es-errors: 1.3.0 + + es-set-tostringtag@2.1.0: + dependencies: + es-errors: 1.3.0 + get-intrinsic: 1.3.0 + has-tostringtag: 1.0.2 + hasown: 2.0.4 + + es-shim-unscopables@1.1.0: + dependencies: + hasown: 2.0.4 + + es-to-primitive@1.3.4: + dependencies: + es-abstract-get: 1.0.0 + es-define-property: 1.0.1 + es-errors: 1.3.0 + is-callable: 1.2.7 + is-date-object: 1.1.0 + is-symbol: 1.1.1 + + esbuild@0.18.20: + optionalDependencies: + '@esbuild/android-arm': 0.18.20 + '@esbuild/android-arm64': 0.18.20 + '@esbuild/android-x64': 0.18.20 + '@esbuild/darwin-arm64': 0.18.20 + '@esbuild/darwin-x64': 0.18.20 + '@esbuild/freebsd-arm64': 0.18.20 + '@esbuild/freebsd-x64': 0.18.20 + '@esbuild/linux-arm': 0.18.20 + '@esbuild/linux-arm64': 0.18.20 + '@esbuild/linux-ia32': 0.18.20 + '@esbuild/linux-loong64': 0.18.20 + '@esbuild/linux-mips64el': 0.18.20 + '@esbuild/linux-ppc64': 0.18.20 + '@esbuild/linux-riscv64': 0.18.20 + '@esbuild/linux-s390x': 0.18.20 + '@esbuild/linux-x64': 0.18.20 + '@esbuild/netbsd-x64': 0.18.20 + '@esbuild/openbsd-x64': 0.18.20 + '@esbuild/sunos-x64': 0.18.20 + '@esbuild/win32-arm64': 0.18.20 + '@esbuild/win32-ia32': 0.18.20 + '@esbuild/win32-x64': 0.18.20 + + escalade@3.2.0: {} + + escape-string-regexp@4.0.0: {} + + eslint-plugin-react-hooks@4.6.2(eslint@8.57.1): + dependencies: + eslint: 8.57.1 + + eslint-plugin-react-refresh@0.4.26(eslint@8.57.1): + dependencies: + eslint: 8.57.1 + + eslint-plugin-react@7.37.5(eslint@8.57.1): + dependencies: + array-includes: 3.1.9 + array.prototype.findlast: 1.2.5 + array.prototype.flatmap: 1.3.3 + array.prototype.tosorted: 1.1.4 + doctrine: 2.1.0 + es-iterator-helpers: 1.3.3 + eslint: 8.57.1 + estraverse: 5.3.0 + hasown: 2.0.4 + jsx-ast-utils: 3.3.5 + minimatch: 3.1.5 + object.entries: 1.1.9 + object.fromentries: 2.0.8 + object.values: 1.2.1 + prop-types: 15.8.1 + resolve: 2.0.0-next.7 + semver: 6.3.1 + string.prototype.matchall: 4.0.12 + string.prototype.repeat: 1.0.0 + + eslint-scope@7.2.2: + dependencies: + esrecurse: 4.3.0 + estraverse: 5.3.0 + + eslint-visitor-keys@3.4.3: {} + + eslint@8.57.1: + dependencies: + '@eslint-community/eslint-utils': 4.9.1(eslint@8.57.1) + '@eslint-community/regexpp': 4.12.2 + '@eslint/eslintrc': 2.1.4 + '@eslint/js': 8.57.1 + '@humanwhocodes/config-array': 0.13.0 + '@humanwhocodes/module-importer': 1.0.1 + '@nodelib/fs.walk': 1.2.8 + '@ungap/structured-clone': 1.3.2 + ajv: 6.15.0 + chalk: 4.1.2 + cross-spawn: 7.0.6 + debug: 4.4.3 + doctrine: 3.0.0 + escape-string-regexp: 4.0.0 + eslint-scope: 7.2.2 + eslint-visitor-keys: 3.4.3 + espree: 9.6.1 + esquery: 1.7.0 + esutils: 2.0.3 + fast-deep-equal: 3.1.3 + file-entry-cache: 6.0.1 + find-up: 5.0.0 + glob-parent: 6.0.2 + globals: 13.24.0 + graphemer: 1.4.0 + ignore: 5.3.2 + imurmurhash: 0.1.4 + is-glob: 4.0.3 + is-path-inside: 3.0.3 + js-yaml: 4.3.0 + json-stable-stringify-without-jsonify: 1.0.1 + levn: 0.4.1 + lodash.merge: 4.6.2 + minimatch: 3.1.5 + natural-compare: 1.4.0 + optionator: 0.9.4 + strip-ansi: 6.0.1 + text-table: 0.2.0 + transitivePeerDependencies: + - supports-color + + espree@9.6.1: + dependencies: + acorn: 8.17.0 + acorn-jsx: 5.3.2(acorn@8.17.0) + eslint-visitor-keys: 3.4.3 + + esquery@1.7.0: + dependencies: + estraverse: 5.3.0 + + esrecurse@4.3.0: + dependencies: + estraverse: 5.3.0 + + estraverse@5.3.0: {} + + esutils@2.0.3: {} + + fast-deep-equal@3.1.3: {} + + fast-json-stable-stringify@2.1.0: {} + + fast-levenshtein@2.0.6: {} + + fastq@1.20.1: + dependencies: + reusify: 1.1.0 + + file-entry-cache@6.0.1: + dependencies: + flat-cache: 3.2.0 + + find-up@5.0.0: + dependencies: + locate-path: 6.0.0 + path-exists: 4.0.0 + + flat-cache@3.2.0: + dependencies: + flatted: 3.4.2 + keyv: 4.5.4 + rimraf: 3.0.2 + + flatted@3.4.2: {} + + for-each@0.3.5: + dependencies: + is-callable: 1.2.7 + + frac@1.1.2: {} + + fs.realpath@1.0.0: {} + + fsevents@2.3.3: + optional: true + + function-bind@1.1.2: {} + + function.prototype.name@1.2.0: + dependencies: + call-bind: 1.0.9 + call-bound: 1.0.4 + es-define-property: 1.0.1 + es-errors: 1.3.0 + functions-have-names: 1.2.3 + has-property-descriptors: 1.0.2 + hasown: 2.0.4 + is-callable: 1.2.7 + is-document.all: 1.0.0 + + functions-have-names@1.2.3: {} + + generator-function@2.0.1: {} + + gensync@1.0.0-beta.2: {} + + get-intrinsic@1.3.0: + dependencies: + call-bind-apply-helpers: 1.0.2 + es-define-property: 1.0.1 + es-errors: 1.3.0 + es-object-atoms: 1.1.2 + function-bind: 1.1.2 + get-proto: 1.0.1 + gopd: 1.2.0 + has-symbols: 1.1.0 + hasown: 2.0.4 + math-intrinsics: 1.1.0 + + get-proto@1.0.1: + dependencies: + dunder-proto: 1.0.1 + es-object-atoms: 1.1.2 + + get-symbol-description@1.1.0: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + get-intrinsic: 1.3.0 + + glob-parent@6.0.2: + dependencies: + is-glob: 4.0.3 + + glob@7.2.3: + dependencies: + fs.realpath: 1.0.0 + inflight: 1.0.6 + inherits: 2.0.4 + minimatch: 3.1.5 + once: 1.4.0 + path-is-absolute: 1.0.1 + + globals@13.24.0: + dependencies: + type-fest: 0.20.2 + + globalthis@1.0.4: + dependencies: + define-properties: 1.2.1 + gopd: 1.2.0 + + gopd@1.2.0: {} + + graphemer@1.4.0: {} + + has-bigints@1.1.0: {} + + has-flag@4.0.0: {} + + has-property-descriptors@1.0.2: + dependencies: + es-define-property: 1.0.1 + + has-proto@1.2.0: + dependencies: + dunder-proto: 1.0.1 + + has-symbols@1.1.0: {} + + has-tostringtag@1.0.2: + dependencies: + has-symbols: 1.1.0 + + hasown@2.0.4: + dependencies: + function-bind: 1.1.2 + + ignore@5.3.2: {} + + import-fresh@3.3.1: + dependencies: + parent-module: 1.0.1 + resolve-from: 4.0.0 + + imurmurhash@0.1.4: {} + + inflight@1.0.6: + dependencies: + once: 1.4.0 + wrappy: 1.0.2 + + inherits@2.0.4: {} + + internal-slot@1.1.0: + dependencies: + es-errors: 1.3.0 + hasown: 2.0.4 + side-channel: 1.1.1 + + is-array-buffer@3.0.5: + dependencies: + call-bind: 1.0.9 + call-bound: 1.0.4 + get-intrinsic: 1.3.0 + + is-async-function@2.1.1: + dependencies: + async-function: 1.0.0 + call-bound: 1.0.4 + get-proto: 1.0.1 + has-tostringtag: 1.0.2 + safe-regex-test: 1.1.0 + + is-bigint@1.1.0: + dependencies: + has-bigints: 1.1.0 + + is-boolean-object@1.2.2: + dependencies: + call-bound: 1.0.4 + has-tostringtag: 1.0.2 + + is-callable@1.2.7: {} + + is-core-module@2.16.2: + dependencies: + hasown: 2.0.4 + + is-data-view@1.0.2: + dependencies: + call-bound: 1.0.4 + get-intrinsic: 1.3.0 + is-typed-array: 1.1.15 + + is-date-object@1.1.0: + dependencies: + call-bound: 1.0.4 + has-tostringtag: 1.0.2 + + is-document.all@1.0.0: + dependencies: + call-bound: 1.0.4 + + is-extglob@2.1.1: {} + + is-finalizationregistry@1.1.1: + dependencies: + call-bound: 1.0.4 + + is-generator-function@1.1.2: + dependencies: + call-bound: 1.0.4 + generator-function: 2.0.1 + get-proto: 1.0.1 + has-tostringtag: 1.0.2 + safe-regex-test: 1.1.0 + + is-glob@4.0.3: + dependencies: + is-extglob: 2.1.1 + + is-map@2.0.3: {} + + is-negative-zero@2.0.3: {} + + is-number-object@1.1.1: + dependencies: + call-bound: 1.0.4 + has-tostringtag: 1.0.2 + + is-path-inside@3.0.3: {} + + is-regex@1.2.1: + dependencies: + call-bound: 1.0.4 + gopd: 1.2.0 + has-tostringtag: 1.0.2 + hasown: 2.0.4 + + is-set@2.0.3: {} + + is-shared-array-buffer@1.0.4: + dependencies: + call-bound: 1.0.4 + + is-string@1.1.1: + dependencies: + call-bound: 1.0.4 + has-tostringtag: 1.0.2 + + is-symbol@1.1.1: + dependencies: + call-bound: 1.0.4 + has-symbols: 1.1.0 + safe-regex-test: 1.1.0 + + is-typed-array@1.1.15: + dependencies: + which-typed-array: 1.1.22 + + is-weakmap@2.0.2: {} + + is-weakref@1.1.1: + dependencies: + call-bound: 1.0.4 + + is-weakset@2.0.4: + dependencies: + call-bound: 1.0.4 + get-intrinsic: 1.3.0 + + isarray@2.0.5: {} + + isexe@2.0.0: {} + + iterator.prototype@1.1.5: + dependencies: + define-data-property: 1.1.4 + es-object-atoms: 1.1.2 + get-intrinsic: 1.3.0 + get-proto: 1.0.1 + has-symbols: 1.1.0 + set-function-name: 2.0.2 + + js-tokens@4.0.0: {} + + js-yaml@4.3.0: + dependencies: + argparse: 2.0.1 + + jsesc@3.1.0: {} + + json-buffer@3.0.1: {} + + json-schema-traverse@0.4.1: {} + + json-stable-stringify-without-jsonify@1.0.1: {} + + json5@2.2.3: {} + + jsx-ast-utils@3.3.5: + dependencies: + array-includes: 3.1.9 + array.prototype.flat: 1.3.3 + object.assign: 4.1.7 + object.values: 1.2.1 + + keyv@4.5.4: + dependencies: + json-buffer: 3.0.1 + + levn@0.4.1: + dependencies: + prelude-ls: 1.2.1 + type-check: 0.4.0 + + locate-path@6.0.0: + dependencies: + p-locate: 5.0.0 + + lodash.merge@4.6.2: {} + + loose-envify@1.4.0: + dependencies: + js-tokens: 4.0.0 + + lru-cache@5.1.1: + dependencies: + yallist: 3.1.1 + + lucide-react@1.21.0(react@18.3.1): + dependencies: + react: 18.3.1 + + math-intrinsics@1.1.0: {} + + minimatch@3.1.5: + dependencies: + brace-expansion: 1.1.15 + + ms@2.1.3: {} + + nanoid@3.3.15: {} + + natural-compare@1.4.0: {} + + node-exports-info@1.6.2: + dependencies: + array.prototype.flatmap: 1.3.3 + es-errors: 1.3.0 + object.entries: 1.1.9 + semver: 6.3.1 + + node-releases@2.0.50: {} + + object-assign@4.1.1: {} + + object-inspect@1.13.4: {} + + object-keys@1.1.1: {} + + object.assign@4.1.7: + dependencies: + call-bind: 1.0.9 + call-bound: 1.0.4 + define-properties: 1.2.1 + es-object-atoms: 1.1.2 + has-symbols: 1.1.0 + object-keys: 1.1.1 + + object.entries@1.1.9: + dependencies: + call-bind: 1.0.9 + call-bound: 1.0.4 + define-properties: 1.2.1 + es-object-atoms: 1.1.2 + + object.fromentries@2.0.8: + dependencies: + call-bind: 1.0.9 + define-properties: 1.2.1 + es-abstract: 1.24.2 + es-object-atoms: 1.1.2 + + object.values@1.2.1: + dependencies: + call-bind: 1.0.9 + call-bound: 1.0.4 + define-properties: 1.2.1 + es-object-atoms: 1.1.2 + + once@1.4.0: + dependencies: + wrappy: 1.0.2 + + optionator@0.9.4: + dependencies: + deep-is: 0.1.4 + fast-levenshtein: 2.0.6 + levn: 0.4.1 + prelude-ls: 1.2.1 + type-check: 0.4.0 + word-wrap: 1.2.5 + + own-keys@1.0.1: + dependencies: + get-intrinsic: 1.3.0 + object-keys: 1.1.1 + safe-push-apply: 1.0.0 + + p-limit@3.1.0: + dependencies: + yocto-queue: 0.1.0 + + p-locate@5.0.0: + dependencies: + p-limit: 3.1.0 + + parent-module@1.0.1: + dependencies: + callsites: 3.1.0 + + path-exists@4.0.0: {} + + path-is-absolute@1.0.1: {} + + path-key@3.1.1: {} + + path-parse@1.0.7: {} + + picocolors@1.1.1: {} + + possible-typed-array-names@1.1.0: {} + + postcss@8.5.16: + dependencies: + nanoid: 3.3.15 + picocolors: 1.1.1 + source-map-js: 1.2.1 + + prelude-ls@1.2.1: {} + + prop-types@15.8.1: + dependencies: + loose-envify: 1.4.0 + object-assign: 4.1.1 + react-is: 16.13.1 + + punycode@2.3.1: {} + + queue-microtask@1.2.3: {} + + react-dom@18.3.1(react@18.3.1): + dependencies: + loose-envify: 1.4.0 + react: 18.3.1 + scheduler: 0.23.2 + + react-is@16.13.1: {} + + react-refresh@0.17.0: {} + + react-router-dom@6.30.4(react-dom@18.3.1(react@18.3.1))(react@18.3.1): + dependencies: + '@remix-run/router': 1.23.3 + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + react-router: 6.30.4(react@18.3.1) + + react-router@6.30.4(react@18.3.1): + dependencies: + '@remix-run/router': 1.23.3 + react: 18.3.1 + + react@18.3.1: + dependencies: + loose-envify: 1.4.0 + + reflect.getprototypeof@1.0.10: + dependencies: + call-bind: 1.0.9 + define-properties: 1.2.1 + es-abstract: 1.24.2 + es-errors: 1.3.0 + es-object-atoms: 1.1.2 + get-intrinsic: 1.3.0 + get-proto: 1.0.1 + which-builtin-type: 1.2.1 + + regexp.prototype.flags@1.5.4: + dependencies: + call-bind: 1.0.9 + define-properties: 1.2.1 + es-errors: 1.3.0 + get-proto: 1.0.1 + gopd: 1.2.0 + set-function-name: 2.0.2 + + resolve-from@4.0.0: {} + + resolve@2.0.0-next.7: + dependencies: + es-errors: 1.3.0 + is-core-module: 2.16.2 + node-exports-info: 1.6.2 + object-keys: 1.1.1 + path-parse: 1.0.7 + supports-preserve-symlinks-flag: 1.0.0 + + reusify@1.1.0: {} + + rimraf@3.0.2: + dependencies: + glob: 7.2.3 + + rollup@3.30.0: + optionalDependencies: + fsevents: 2.3.3 + + run-parallel@1.2.0: + dependencies: + queue-microtask: 1.2.3 + + safe-array-concat@1.1.4: + dependencies: + call-bind: 1.0.9 + call-bound: 1.0.4 + get-intrinsic: 1.3.0 + has-symbols: 1.1.0 + isarray: 2.0.5 + + safe-push-apply@1.0.0: + dependencies: + es-errors: 1.3.0 + isarray: 2.0.5 + + safe-regex-test@1.1.0: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + is-regex: 1.2.1 + + scheduler@0.23.2: + dependencies: + loose-envify: 1.4.0 + + semver@6.3.1: {} + + set-function-length@1.2.2: + dependencies: + define-data-property: 1.1.4 + es-errors: 1.3.0 + function-bind: 1.1.2 + get-intrinsic: 1.3.0 + gopd: 1.2.0 + has-property-descriptors: 1.0.2 + + set-function-name@2.0.2: + dependencies: + define-data-property: 1.1.4 + es-errors: 1.3.0 + functions-have-names: 1.2.3 + has-property-descriptors: 1.0.2 + + set-proto@1.0.0: + dependencies: + dunder-proto: 1.0.1 + es-errors: 1.3.0 + es-object-atoms: 1.1.2 + + shebang-command@2.0.0: + dependencies: + shebang-regex: 3.0.0 + + shebang-regex@3.0.0: {} + + side-channel-list@1.0.1: + dependencies: + es-errors: 1.3.0 + object-inspect: 1.13.4 + + side-channel-map@1.0.1: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + get-intrinsic: 1.3.0 + object-inspect: 1.13.4 + + side-channel-weakmap@1.0.2: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + get-intrinsic: 1.3.0 + object-inspect: 1.13.4 + side-channel-map: 1.0.1 + + side-channel@1.1.1: + dependencies: + es-errors: 1.3.0 + object-inspect: 1.13.4 + side-channel-list: 1.0.1 + side-channel-map: 1.0.1 + side-channel-weakmap: 1.0.2 + + source-map-js@1.2.1: {} + + ssf@0.11.2: + dependencies: + frac: 1.1.2 + + stop-iteration-iterator@1.1.0: + dependencies: + es-errors: 1.3.0 + internal-slot: 1.1.0 + + string.prototype.matchall@4.0.12: + dependencies: + call-bind: 1.0.9 + call-bound: 1.0.4 + define-properties: 1.2.1 + es-abstract: 1.24.2 + es-errors: 1.3.0 + es-object-atoms: 1.1.2 + get-intrinsic: 1.3.0 + gopd: 1.2.0 + has-symbols: 1.1.0 + internal-slot: 1.1.0 + regexp.prototype.flags: 1.5.4 + set-function-name: 2.0.2 + side-channel: 1.1.1 + + string.prototype.repeat@1.0.0: + dependencies: + define-properties: 1.2.1 + es-abstract: 1.24.2 + + string.prototype.trim@1.2.11: + dependencies: + call-bind: 1.0.9 + call-bound: 1.0.4 + define-data-property: 1.1.4 + define-properties: 1.2.1 + es-abstract: 1.24.2 + es-object-atoms: 1.1.2 + has-property-descriptors: 1.0.2 + safe-regex-test: 1.1.0 + + string.prototype.trimend@1.0.10: + dependencies: + call-bind: 1.0.9 + call-bound: 1.0.4 + define-properties: 1.2.1 + es-object-atoms: 1.1.2 + + string.prototype.trimstart@1.0.8: + dependencies: + call-bind: 1.0.9 + define-properties: 1.2.1 + es-object-atoms: 1.1.2 + + strip-ansi@6.0.1: + dependencies: + ansi-regex: 5.0.1 + + strip-json-comments@3.1.1: {} + + supports-color@7.2.0: + dependencies: + has-flag: 4.0.0 + + supports-preserve-symlinks-flag@1.0.0: {} + + text-table@0.2.0: {} + + type-check@0.4.0: + dependencies: + prelude-ls: 1.2.1 + + type-fest@0.20.2: {} + + typed-array-buffer@1.0.3: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + is-typed-array: 1.1.15 + + typed-array-byte-length@1.0.3: + dependencies: + call-bind: 1.0.9 + for-each: 0.3.5 + gopd: 1.2.0 + has-proto: 1.2.0 + is-typed-array: 1.1.15 + + typed-array-byte-offset@1.0.4: + dependencies: + available-typed-arrays: 1.0.7 + call-bind: 1.0.9 + for-each: 0.3.5 + gopd: 1.2.0 + has-proto: 1.2.0 + is-typed-array: 1.1.15 + reflect.getprototypeof: 1.0.10 + + typed-array-length@1.0.8: + dependencies: + call-bind: 1.0.9 + for-each: 0.3.5 + gopd: 1.2.0 + is-typed-array: 1.1.15 + possible-typed-array-names: 1.1.0 + reflect.getprototypeof: 1.0.10 + + unbox-primitive@1.1.0: + dependencies: + call-bound: 1.0.4 + has-bigints: 1.1.0 + has-symbols: 1.1.0 + which-boxed-primitive: 1.1.1 + + update-browserslist-db@1.2.3(browserslist@4.28.4): + dependencies: + browserslist: 4.28.4 + escalade: 3.2.0 + picocolors: 1.1.1 + + uri-js@4.4.1: + dependencies: + punycode: 2.3.1 + + vite@4.5.14: + dependencies: + esbuild: 0.18.20 + postcss: 8.5.16 + rollup: 3.30.0 + optionalDependencies: + fsevents: 2.3.3 + + which-boxed-primitive@1.1.1: + dependencies: + is-bigint: 1.1.0 + is-boolean-object: 1.2.2 + is-number-object: 1.1.1 + is-string: 1.1.1 + is-symbol: 1.1.1 + + which-builtin-type@1.2.1: + dependencies: + call-bound: 1.0.4 + function.prototype.name: 1.2.0 + has-tostringtag: 1.0.2 + is-async-function: 2.1.1 + is-date-object: 1.1.0 + is-finalizationregistry: 1.1.1 + is-generator-function: 1.1.2 + is-regex: 1.2.1 + is-weakref: 1.1.1 + isarray: 2.0.5 + which-boxed-primitive: 1.1.1 + which-collection: 1.0.2 + which-typed-array: 1.1.22 + + which-collection@1.0.2: + dependencies: + is-map: 2.0.3 + is-set: 2.0.3 + is-weakmap: 2.0.2 + is-weakset: 2.0.4 + + which-typed-array@1.1.22: + dependencies: + available-typed-arrays: 1.0.7 + call-bind: 1.0.9 + call-bound: 1.0.4 + for-each: 0.3.5 + get-proto: 1.0.1 + gopd: 1.2.0 + has-tostringtag: 1.0.2 + + which@2.0.2: + dependencies: + isexe: 2.0.0 + + wmf@1.0.2: {} + + word-wrap@1.2.5: {} + + word@0.3.0: {} + + wrappy@1.0.2: {} + + xlsx@0.18.5: + dependencies: + adler-32: 1.3.1 + cfb: 1.2.2 + codepage: 1.15.0 + crc-32: 1.2.2 + ssf: 0.11.2 + wmf: 1.0.2 + word: 0.3.0 + + yallist@3.1.1: {} + + yocto-queue@0.1.0: {} diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml new file mode 100644 index 0000000000..00f6fc4703 --- /dev/null +++ b/pnpm-workspace.yaml @@ -0,0 +1,2 @@ +allowBuilds: + esbuild: set this to true or false diff --git a/src/api/cloudinary_config.py b/src/api/cloudinary_config.py new file mode 100644 index 0000000000..4807b26272 --- /dev/null +++ b/src/api/cloudinary_config.py @@ -0,0 +1,11 @@ +import os +import cloudinary + + +def configure_cloudinary(): + cloudinary.config( + cloud_name=os.getenv("CLOUDINARY_CLOUD_NAME"), + api_key=os.getenv("CLOUDINARY_API_KEY"), + api_secret=os.getenv("CLOUDINARY_API_SECRET"), + secure=True, + ) \ No newline at end of file diff --git a/src/api/models.py b/src/api/models.py index da515f6a1a..2497a4fe73 100644 --- a/src/api/models.py +++ b/src/api/models.py @@ -1,19 +1,405 @@ +from datetime import datetime, timezone from flask_sqlalchemy import SQLAlchemy -from sqlalchemy import String, Boolean -from sqlalchemy.orm import Mapped, mapped_column db = SQLAlchemy() + +# ============================================================ +# WORKSHOP +# ============================================================ + +class Workshop(db.Model): + + __tablename__ = "workshops" + + id = db.Column(db.Integer, primary_key=True) + + company_name = db.Column(db.String(120), nullable=False) + cif = db.Column(db.String(50), unique=True, nullable=False) + phone = db.Column(db.String(20), nullable=False) + email = db.Column(db.String(120), unique=True, nullable=False) + address = db.Column(db.String(200), nullable=True) + city = db.Column(db.String(80), nullable=True) + postal_code = db.Column(db.String(20), nullable=True) + is_active = db.Column(db.Boolean(), default=True, nullable=False) + created_at = db.Column(db.DateTime, default=lambda: datetime.now(timezone.utc)) + + employees = db.relationship("Employee", backref="workshop", lazy=True) + customers = db.relationship("Customer", backref="workshop", lazy=True) + vehicles = db.relationship("Vehicle", backref="workshop", lazy=True) + services = db.relationship("Service", backref="workshop", lazy=True) + + def serialize(self): + + return { + + "id": self.id, + "company_name": self.company_name, + "cif": self.cif, + "phone": self.phone, + "email": self.email, + "address": self.address, + "city": self.city, + "postal_code": self.postal_code, + "is_active": self.is_active, + "created_at": self.created_at.isoformat() if self.created_at else None + + } + + +# ============================================================ +# USER +# ============================================================ + class User(db.Model): - id: Mapped[int] = mapped_column(primary_key=True) - email: Mapped[str] = mapped_column(String(120), unique=True, nullable=False) - password: Mapped[str] = mapped_column(nullable=False) - is_active: Mapped[bool] = mapped_column(Boolean(), nullable=False) + __tablename__ = "users" + + id = db.Column(db.Integer, primary_key=True) + + email = db.Column(db.String(120), unique=True, nullable=False) + password_hash = db.Column(db.String(255), nullable=False) + employee_id = db.Column(db.Integer, db.ForeignKey("employees.id"), unique=True, nullable=False) + created_at = db.Column(db.DateTime, default=lambda: datetime.now(timezone.utc)) + + employee = db.relationship("Employee", backref=db.backref("user", uselist=False), uselist=False) + + def serialize(self): + + return { + + "id": self.id, + "email": self.email, + "employee_id": self.employee_id, + "created_at": self.created_at.isoformat() if self.created_at else None, + "employee": self.employee.serialize_basic() if self.employee else None + + } + + +# ============================================================ +# EMPLOYEE +# ============================================================ + +class Employee(db.Model): + + __tablename__ = "employees" + + id = db.Column(db.Integer, primary_key=True) + + first_name = db.Column(db.String(80), nullable=False) + last_name = db.Column(db.String(120), nullable=False) + dni = db.Column(db.String(20), unique=True, nullable=False) + phone = db.Column(db.String(20), nullable=False) + address = db.Column(db.String(200), nullable=True) + city = db.Column(db.String(80), nullable=True) + postal_code = db.Column(db.String(20), nullable=True) + role = db.Column(db.String(20), nullable=False, default="mechanic") + job_position = db.Column(db.String(80), nullable=True) + specialty = db.Column(db.String(120), nullable=True) + workshop_id = db.Column(db.Integer, db.ForeignKey("workshops.id"), nullable=False) + is_active = db.Column(db.Boolean(), default=True, nullable=False) + created_at = db.Column(db.DateTime, default=lambda: datetime.now(timezone.utc)) + + services = db.relationship("Service", backref="employee", lazy=True) + + def serialize_basic(self): + + return { + + "id": self.id, + "first_name": self.first_name, + "last_name": self.last_name, + "role": self.role, + "workshop_id": self.workshop_id, + "is_active": self.is_active + + } + + def serialize(self): + + return { + + "id": self.id, + "first_name": self.first_name, + "last_name": self.last_name, + "dni": self.dni, + "phone": self.phone, + "address": self.address, + "city": self.city, + "postal_code": self.postal_code, + "role": self.role, + "job_position": self.job_position, + "specialty": self.specialty, + "workshop_id": self.workshop_id, + "is_active": self.is_active, + "created_at": self.created_at.isoformat() if self.created_at else None, + "email": self.user.email if self.user else None + + } + + +# ============================================================ +# CUSTOMER +# ============================================================ + +class Customer(db.Model): + + __tablename__ = "customers" + + id = db.Column(db.Integer, primary_key=True) + + first_name = db.Column(db.String(80), nullable=False) + last_name = db.Column(db.String(120), nullable=False) + dni = db.Column(db.String(20), unique=True, nullable=False) + driving_license = db.Column(db.String(20), unique=True, nullable=False) + phone = db.Column(db.String(20), nullable=False) + email = db.Column(db.String(120), nullable=True) + address = db.Column(db.String(200), nullable=True) + + workshop_id = db.Column(db.Integer, db.ForeignKey("workshops.id"), nullable=False) + + is_active = db.Column(db.Boolean(), default=True, nullable=False) + created_at = db.Column(db.DateTime, default=lambda: datetime.now(timezone.utc)) + + vehicles = db.relationship("Vehicle", backref="customer", lazy=True) + services = db.relationship("Service", backref="customer", lazy=True) def serialize(self): + return { + "id": self.id, + "first_name": self.first_name, + "last_name": self.last_name, + "dni": self.dni, + "driving_license": self.driving_license, + "phone": self.phone, "email": self.email, - # do not serialize the password, its a security breach + "address": self.address, + "workshop_id": self.workshop_id, + "is_active": self.is_active, + "created_at": self.created_at.isoformat() if self.created_at else None + + } + + +# ============================================================ +# VEHICLE +# ============================================================ + +class Vehicle(db.Model): + + __tablename__ = "vehicles" + + id = db.Column(db.Integer, primary_key=True) + + plate = db.Column(db.String(20), unique=True, nullable=False) + vin = db.Column(db.String(50), unique=True, nullable=True) + brand = db.Column(db.String(80), nullable=False) + model = db.Column(db.String(80), nullable=False) + version = db.Column(db.String(80), nullable=True) + year = db.Column(db.Integer, nullable=True) + fuel_type = db.Column(db.String(30), nullable=False) + power_hp = db.Column(db.Integer, nullable=True) + engine_cc = db.Column(db.Integer, nullable=True) + color = db.Column(db.String(50), nullable=True) + mileage = db.Column(db.Integer, default=0, nullable=False) + first_registration_date = db.Column(db.Date, nullable=True) + + customer_id = db.Column(db.Integer, db.ForeignKey("customers.id"), nullable=False) + workshop_id = db.Column(db.Integer, db.ForeignKey("workshops.id"), nullable=False) + + is_active = db.Column(db.Boolean(), default=True, nullable=False) + created_at = db.Column(db.DateTime, default=lambda: datetime.now(timezone.utc)) + + services = db.relationship("Service", backref="vehicle", lazy=True) + + def serialize(self): + + return { + + "id": self.id, + "plate": self.plate, + "vin": self.vin, + "brand": self.brand, + "model": self.model, + "version": self.version, + "year": self.year, + "fuel_type": self.fuel_type, + "power_hp": self.power_hp, + "engine_cc": self.engine_cc, + "color": self.color, + "mileage": self.mileage, + "first_registration_date": self.first_registration_date.isoformat() if self.first_registration_date else None, + "customer_id": self.customer_id, + "workshop_id": self.workshop_id, + "is_active": self.is_active, + "created_at": self.created_at.isoformat() if self.created_at else None, + "customer_name": f"{self.customer.first_name} {self.customer.last_name}" if self.customer else None + + } + + +# ============================================================ +# SERVICE +# ============================================================ + +class Service(db.Model): + + __tablename__ = "services" + + id = db.Column(db.Integer, primary_key=True) + + title = db.Column(db.String(120), nullable=False) + description = db.Column(db.Text, nullable=True) + service_type = db.Column(db.String(50), nullable=False) + status = db.Column(db.String(30), default="pending", nullable=False) + priority = db.Column(db.String(20), default="normal", nullable=False) + + entry_mileage = db.Column(db.Integer, nullable=True) + start_date = db.Column(db.DateTime, default=lambda: datetime.now(timezone.utc)) + end_date = db.Column(db.DateTime, nullable=True) + observations = db.Column(db.Text, nullable=True) + + workshop_id = db.Column(db.Integer, db.ForeignKey("workshops.id"), nullable=False) + customer_id = db.Column(db.Integer, db.ForeignKey("customers.id"), nullable=False) + vehicle_id = db.Column(db.Integer, db.ForeignKey("vehicles.id"), nullable=False) + employee_id = db.Column(db.Integer, db.ForeignKey("employees.id"), nullable=True) + + created_at = db.Column(db.DateTime, default=lambda: datetime.now(timezone.utc)) + + comments = db.relationship("ServiceComment", backref="service", lazy=True) + status_logs = db.relationship("ServiceStatusLog", backref="service", lazy=True) + + def serialize(self): + + return { + + "id": self.id, + "title": self.title, + "description": self.description, + "service_type": self.service_type, + "status": self.status, + "priority": self.priority, + "entry_mileage": self.entry_mileage, + "start_date": self.start_date.isoformat() if self.start_date else None, + "end_date": self.end_date.isoformat() if self.end_date else None, + "observations": self.observations, + "workshop_id": self.workshop_id, + "customer_id": self.customer_id, + "vehicle_id": self.vehicle_id, + "employee_id": self.employee_id, + "created_at": self.created_at.isoformat() if self.created_at else None, + "vehicle_plate": self.vehicle.plate if self.vehicle else None, + "vehicle_brand": self.vehicle.brand if self.vehicle else None, + "vehicle_model": self.vehicle.model if self.vehicle else None, + "customer_name": f"{self.customer.first_name} {self.customer.last_name}" if self.customer else None, + "employee_name": f"{self.employee.first_name} {self.employee.last_name}" if self.employee else None + + } + + +# ============================================================ +# SERVICE COMMENT +# ============================================================ + +class ServiceComment(db.Model): + + __tablename__ = "service_comments" + + id = db.Column(db.Integer, primary_key=True) + + comment = db.Column(db.Text, nullable=False) + comment_type = db.Column(db.String(50), default="note", nullable=False) + + image_url = db.Column(db.String(500), nullable=True) + image_public_id = db.Column(db.String(255), nullable=True) + + service_id = db.Column(db.Integer, db.ForeignKey("services.id"), nullable=False) + employee_id = db.Column(db.Integer, db.ForeignKey("employees.id"), nullable=True) + + created_at = db.Column(db.DateTime, default=lambda: datetime.now(timezone.utc)) + + author = db.relationship("Employee", foreign_keys=[employee_id]) + + def serialize(self): + + return { + + "id": self.id, + "comment": self.comment, + "comment_type": self.comment_type, + "service_id": self.service_id, + "employee_id": self.employee_id, + "created_at": self.created_at.isoformat() if self.created_at else None, + "author_name": f"{self.author.first_name} {self.author.last_name}" if self.author else None, + "author_email": self.author.user.email if self.author and self.author.user else None, + "image_url": self.image_url, + "image_public_id": self.image_public_id, + } + + +# ============================================================ +# SERVICE STATUS +# ============================================================ + +class ServiceStatusLog(db.Model): + + __tablename__ = "service_status_logs" + + id = db.Column(db.Integer, primary_key=True) + + service_id = db.Column(db.Integer, db.ForeignKey("services.id"), nullable=False) + from_status = db.Column(db.String(30), nullable=True) + to_status = db.Column(db.String(30), nullable=False) + + employee_id = db.Column(db.Integer, db.ForeignKey("employees.id"), nullable=True) + + note = db.Column(db.Text, nullable=True) + changed_at = db.Column(db.DateTime, default=lambda: datetime.now(timezone.utc)) + + changed_by = db.relationship("Employee", foreign_keys=[employee_id]) + + def serialize(self): + + return { + + "id": self.id, + "service_id": self.service_id, + "from_status": self.from_status, + "to_status": self.to_status, + "employee_id": self.employee_id, + "changed_by": f"{self.changed_by.first_name} {self.changed_by.last_name}" if self.changed_by else None, + "note": self.note, + "changed_at": self.changed_at.isoformat() if self.changed_at else None + + } + +# ============================================================ +# PASSWORD RESET +# ============================================================ + +class PasswordResetToken(db.Model): + __tablename__ = "password_reset_tokens" + + id = db.Column(db.Integer, primary_key=True) + + user_id = db.Column(db.Integer, db.ForeignKey("users.id"), nullable=False) + token_hash = db.Column(db.String(255), nullable=False, unique=True) + + expires_at = db.Column(db.DateTime, nullable=False) + used_at = db.Column(db.DateTime, nullable=True) + + created_at = db.Column(db.DateTime, default=lambda: datetime.now(timezone.utc)) + + user = db.relationship("User", backref="password_reset_tokens") + + def serialize(self): + return { + "id": self.id, + "user_id": self.user_id, + "expires_at": self.expires_at.isoformat() if self.expires_at else None, + "used_at": self.used_at.isoformat() if self.used_at else None, + "created_at": self.created_at.isoformat() if self.created_at else None } \ No newline at end of file diff --git a/src/api/routes.py b/src/api/routes.py index 029589a3a1..a06f00a7a5 100644 --- a/src/api/routes.py +++ b/src/api/routes.py @@ -1,22 +1,2032 @@ -""" -This module takes care of starting the API Server, Loading the DB and Adding the endpoints -""" -from flask import Flask, request, jsonify, url_for, Blueprint -from api.models import db, User -from api.utils import generate_sitemap, APIException +from datetime import datetime, timezone, timedelta +import hashlib +import secrets +from flask import Blueprint, request, jsonify from flask_cors import CORS +from flask_bcrypt import Bcrypt +from flask_jwt_extended import create_access_token, jwt_required, get_jwt_identity +import cloudinary.uploader +from api.models import ( + db, + Workshop, + User, + Employee, + Customer, + Vehicle, + Service, + ServiceComment, + ServiceStatusLog, + PasswordResetToken +) -api = Blueprint('api', __name__) -# Allow CORS requests to this API +api = Blueprint("api", __name__) CORS(api) +bcrypt = Bcrypt() -@api.route('/hello', methods=['POST', 'GET']) -def handle_hello(): - response_body = { - "message": "Hello! I'm a message that came from the backend, check the network tab on the google inspector and you will see the GET request" - } +###---------------OPTIONS---------------------- - return jsonify(response_body), 200 +FUEL_TYPES = [ + "gasoline", + "diesel", + "hybrid", + "plug_in_hybrid", + "electric", + "lpg" +] + +SERVICE_TYPES = [ + "repair", + "maintenance", + "diagnostic", + "inspection", + "bodywork", + "painting", + "cleaning", + "detailing", + "other" +] + +SERVICE_STATUSES = [ + "pending", + "diagnosis", + "budget_pending", + "waiting_parts", + "in_repair", + "ready_to_deliver", + "delivered", + "cancelled" +] + +SERVICE_PRIORITIES = [ + "low", + "normal", + "high", + "urgent" +] + +COMMENT_TYPES = [ + "note", + "status_update", + "admin_alert" +] + + +###---------------HELPERS---------------------- + +def error_response(message, status_code): + + return jsonify({ + "message": message, + "error": message + }), status_code + + +def get_current_user(): + + current_user_id = get_jwt_identity() + + if not current_user_id: + return None + + return db.session.get(User, int(current_user_id)) + + +def get_current_employee(user): + + if not user: + return None + + return user.employee + + +def get_current_workshop_id(user): + + employee = get_current_employee(user) + + if not employee: + return None + + return employee.workshop_id + + +def get_current_workshop(user): + + employee = get_current_employee(user) + + if not employee: + return None + + return employee.workshop + + +def is_admin(user): + + employee = get_current_employee(user) + + return employee is not None and employee.role == "admin" and employee.is_active + + +def is_mechanic(user): + + employee = get_current_employee(user) + + return employee is not None and employee.role == "mechanic" and employee.is_active + + +def parse_date(value): + + if not value: + return None + + return datetime.strptime(value, "%Y-%m-%d").date() + + +def parse_datetime(value): + + if not value: + return None + + return datetime.fromisoformat(value.replace("Z", "+00:00")) + + +def create_status_log(service, employee_id, new_status, note=None): + + status_log = ServiceStatusLog( + service_id=service.id, + from_status=service.status, + to_status=new_status, + employee_id=employee_id, + note=note + ) + + service.status = new_status + + db.session.add(status_log) + + return status_log + + +def get_customer_or_error(customer_id, current_user, active_only=True): + + workshop_id = get_current_workshop_id(current_user) + + if not workshop_id: + return None, error_response("Workshop not found for this user", 404) + + query = Customer.query.filter_by( + id=customer_id, + workshop_id=workshop_id + ) + + if active_only: + query = query.filter_by(is_active=True) + + customer = query.first() + + if not customer: + return None, error_response("Customer not found", 404) + + return customer, None + + +def get_vehicle_or_error(vehicle_id, current_user, active_only=True): + + workshop_id = get_current_workshop_id(current_user) + + if not workshop_id: + return None, error_response("Workshop not found for this user", 404) + + query = Vehicle.query.filter_by( + id=vehicle_id, + workshop_id=workshop_id + ) + + if active_only: + query = query.filter_by(is_active=True) + + vehicle = query.first() + + if not vehicle: + return None, error_response("Vehicle not found", 404) + + return vehicle, None + + +def get_employee_or_error(employee_id, current_user, active_only=True): + + workshop_id = get_current_workshop_id(current_user) + + if not workshop_id: + return None, error_response("Workshop not found for this user", 404) + + query = Employee.query.filter_by( + id=employee_id, + workshop_id=workshop_id + ) + + if active_only: + query = query.filter_by(is_active=True) + + employee = query.first() + + if not employee: + return None, error_response("Employee not found", 404) + + return employee, None + + +def get_service_or_error(service_id, current_user): + + workshop_id = get_current_workshop_id(current_user) + + if not workshop_id: + return None, error_response("Workshop not found for this user", 404) + + service = Service.query.filter_by( + id=service_id, + workshop_id=workshop_id + ).first() + + if not service: + return None, error_response("Service not found", 404) + + return service, None + + + +###------------OPTIONS ENDPOINT---------------------- + +@api.route("/options", methods=["GET"]) +def get_options(): + + return jsonify({ + "fuel_types": FUEL_TYPES, + "service_types": SERVICE_TYPES, + "service_statuses": SERVICE_STATUSES, + "service_priorities": SERVICE_PRIORITIES, + "comment_types": COMMENT_TYPES + }), 200 + + +###---------------REGISTER---------------------- + +@api.route("/register", methods=["POST"]) +def register(): + + data = request.get_json() or {} + + company_name = data.get("company_name") + cif = data.get("cif") + + workshop_phone = data.get("workshop_phone") or data.get("phone") + workshop_email = data.get("workshop_email") or data.get("email") + + address = data.get("address") + city = data.get("city") + postal_code = data.get("postal_code") + + first_name = data.get("first_name") + last_name = data.get("last_name") + dni = data.get("dni") + employee_phone = data.get("employee_phone") or data.get("admin_phone") or data.get("phone") + + user_email = data.get("user_email") or data.get("email") + password = data.get("password") + password_confirm = data.get("password_confirm") + + if not company_name or not cif or not workshop_phone or not workshop_email: + return error_response("company_name, cif, workshop_phone and workshop_email are required", 400) + + if not first_name or not last_name or not dni or not employee_phone: + return error_response("first_name, last_name, dni and employee_phone are required for the admin employee", 400) + + if not user_email or not password: + return error_response("user_email and password are required", 400) + + if password != password_confirm: + return error_response("Passwords do not match", 400) + + existing_workshop = Workshop.query.filter( + (Workshop.email == workshop_email) | (Workshop.cif == cif) + ).first() + + if existing_workshop: + return error_response("A workshop with this email or CIF already exists", 409) + + existing_employee = Employee.query.filter_by(dni=dni).first() + + if existing_employee: + return error_response("An employee with this DNI already exists", 409) + + existing_user = User.query.filter_by(email=user_email).first() + + if existing_user: + return error_response("A user with this email already exists", 409) + + new_workshop = Workshop( + company_name=company_name, + cif=cif, + phone=workshop_phone, + email=workshop_email, + address=address, + city=city, + postal_code=postal_code + ) + + db.session.add(new_workshop) + db.session.flush() + + admin_employee = Employee( + first_name=first_name, + last_name=last_name, + dni=dni, + phone=employee_phone, + role="admin", + job_position="admin", + workshop_id=new_workshop.id + ) + + db.session.add(admin_employee) + db.session.flush() + + password_hash = bcrypt.generate_password_hash(password).decode("utf-8") + + admin_user = User( + email=user_email, + password_hash=password_hash, + employee_id=admin_employee.id + ) + + db.session.add(admin_user) + db.session.commit() + + token = create_access_token( + identity=str(admin_user.id), + additional_claims={ + "role": admin_employee.role, + "workshop_id": admin_employee.workshop_id, + "employee_id": admin_employee.id + } + ) + + return jsonify({ + "message": "Workshop created successfully", + "token": token, + "workshop": new_workshop.serialize(), + "user": admin_user.serialize(), + "employee": admin_employee.serialize() + }), 201 + +###--------------- LOGIN---------------------- + +@api.route("/login", methods=["POST"]) +def login(): + + data = request.get_json() or {} + + email = data.get("email") + password = data.get("password") + + if not email or not password: + return error_response("email and password are required", 400) + + user = User.query.filter_by(email=email).first() + + if not user: + return error_response("Invalid email or password", 401) + + employee = user.employee + + if not employee: + return error_response("This user account has no employee profile", 403) + + if not employee.is_active: + return error_response("This employee account is inactive", 403) + + workshop = employee.workshop + + if not workshop: + return error_response("Workshop not found", 404) + + if not workshop.is_active: + return error_response("This workshop account is inactive", 403) + + is_valid_password = bcrypt.check_password_hash(user.password_hash, password) + + if not is_valid_password: + return error_response("Invalid email or password", 401) + + token = create_access_token( + identity=str(user.id), + additional_claims={ + "role": employee.role, + "workshop_id": employee.workshop_id, + "employee_id": employee.id + } + ) + + return jsonify({ + "message": "Login successful", + "token": token, + "user": user.serialize(), + "employee": employee.serialize(), + "workshop": workshop.serialize() + }), 200 + +###---------------C CURRENT USER / SESSION---------------------- + +@api.route("/me", methods=["GET"]) +@jwt_required() +def get_me(): + + current_user = get_current_user() + + if not current_user: + return error_response("User not found", 404) + + employee = current_user.employee + + if not employee: + return error_response("Employee profile not found", 404) + + return jsonify({ + "user": current_user.serialize(), + "employee": employee.serialize(), + "workshop": employee.workshop.serialize() if employee.workshop else None + }), 200 + + +###---------------WORKSHOP---------------------- + + +@api.route("/workshop", methods=["GET"]) +@jwt_required() +def get_my_workshop(): + + current_user = get_current_user() + workshop = get_current_workshop(current_user) + + if not workshop: + return error_response("Workshop not found", 404) + + return jsonify({ + "workshop": workshop.serialize() + }), 200 + + +@api.route("/workshop", methods=["PUT"]) +@jwt_required() +def update_my_workshop(): + + current_user = get_current_user() + + if not is_admin(current_user): + return error_response("Only admin users can update workshop data", 403) + + workshop = get_current_workshop(current_user) + + if not workshop: + return error_response("Workshop not found", 404) + + data = request.get_json() or {} + + if "email" in data and data.get("email") != workshop.email: + existing_workshop = Workshop.query.filter( + Workshop.email == data.get("email"), + Workshop.id != workshop.id + ).first() + + if existing_workshop: + return error_response("A workshop with this email already exists", 409) + + editable_fields = [ + "company_name", + "phone", + "email", + "address", + "city", + "postal_code" + ] + + for field in editable_fields: + if field in data: + setattr(workshop, field, data[field]) + + db.session.commit() + + return jsonify({ + "message": "Workshop updated successfully", + "workshop": workshop.serialize() + }), 200 + +###---------------MECHANICS GET---------------------- + +@api.route("/mechanics", methods=["GET"]) +@jwt_required() +def get_mechanics(): + + current_user = get_current_user() + + if not is_admin(current_user): + return error_response("Only admin users can see mechanics", 403) + + workshop_id = get_current_workshop_id(current_user) + + mechanics = Employee.query.filter_by( + workshop_id=workshop_id, + role="mechanic", + is_active=True + ).all() + + return jsonify({ + "mechanics": [mechanic.serialize() for mechanic in mechanics] + }), 200 + +###------------MECHANICS CREATE---------------------- + +@api.route("/mechanics", methods=["POST"]) +@jwt_required() +def create_mechanic(): + + current_user = get_current_user() + + if not is_admin(current_user): + return error_response("Only admin users can create mechanics", 403) + + data = request.get_json() or {} + + first_name = data.get("first_name") + last_name = data.get("last_name") + dni = data.get("dni") + phone = data.get("phone") + email = data.get("email") + password = data.get("password") + password_confirm = data.get("password_confirm") + address = data.get("address") + city = data.get("city") + postal_code = data.get("postal_code") + specialty = data.get("specialty") + + if not first_name or not last_name or not dni or not phone or not email or not password: + return error_response("first_name, last_name, dni, phone, email and password are required", 400) + + if password != password_confirm: + return error_response("Passwords do not match", 400) + + existing_user = User.query.filter_by(email=email).first() + + if existing_user: + return error_response("A user with this email already exists", 409) + + existing_employee = Employee.query.filter_by(dni=dni).first() + + if existing_employee: + return error_response("An employee with this DNI already exists", 409) + + workshop_id = get_current_workshop_id(current_user) + + mechanic_employee = Employee( + first_name=first_name, + last_name=last_name, + dni=dni, + phone=phone, + address=address, + city=city, + postal_code=postal_code, + role="mechanic", + specialty=specialty, + workshop_id=workshop_id + ) + + db.session.add(mechanic_employee) + db.session.flush() + + password_hash = bcrypt.generate_password_hash(password).decode("utf-8") + + mechanic_user = User( + email=email, + password_hash=password_hash, + employee_id=mechanic_employee.id + ) + + db.session.add(mechanic_user) + db.session.commit() + + return jsonify({ + "message": "Mechanic created successfully", + "user": mechanic_user.serialize(), + "employee": mechanic_employee.serialize() + }), 201 +###---------------MECHANICS ID GETL---------------------- + +@api.route("/mechanics/", methods=["GET"]) +@jwt_required() +def get_mechanic_detail(employee_id): + + current_user = get_current_user() + + if not is_admin(current_user): + return error_response("Only admin users can see mechanic details", 403) + + employee, error = get_employee_or_error(employee_id, current_user) + + if error: + return error + + if employee.role != "mechanic": + return error_response("Mechanic not found", 404) + + return jsonify({ + "mechanic": employee.serialize() + }), 200 + +###---------------MECHANICS ID UPDATE---------------------- + +@api.route("/mechanics/", methods=["PUT"]) +@jwt_required() +def update_mechanic(employee_id): + + current_user = get_current_user() + + if not is_admin(current_user): + return error_response("Only admin users can update mechanics", 403) + + employee, error = get_employee_or_error(employee_id, current_user) + + if error: + return error + + if employee.role != "mechanic": + return error_response("Mechanic not found", 404) + + data = request.get_json() or {} + + if "dni" in data and data.get("dni") != employee.dni: + existing_employee = Employee.query.filter( + Employee.dni == data.get("dni"), + Employee.id != employee.id + ).first() + + if existing_employee: + return error_response("An employee with this DNI already exists", 409) + + editable_fields = [ + "first_name", + "last_name", + "dni", + "phone", + "address", + "city", + "postal_code", + "specialty" + ] + + for field in editable_fields: + if field in data: + setattr(employee, field, data[field]) + + db.session.commit() + + return jsonify({ + "message": "Mechanic updated successfully", + "mechanic": employee.serialize() + }), 200 + +###---------------MECHANICS ID DEACTIVATE---------------------- + +@api.route("/mechanics/", methods=["DELETE"]) +@jwt_required() +def delete_mechanic(employee_id): + + current_user = get_current_user() + + if not is_admin(current_user): + return error_response("Only admin users can delete mechanics", 403) + + employee, error = get_employee_or_error(employee_id, current_user) + + if error: + return error + + if employee.role != "mechanic": + return error_response("Mechanic not found", 404) + + employee.is_active = False + db.session.commit() + + return jsonify({ + "message": "Mechanic deactivated successfully" + }), 200 + + +###---------------VEHICLES GET--------------------- + +@api.route("/vehicles", methods=["GET"]) +@jwt_required() +def get_vehicles(): + + current_user = get_current_user() + + if not is_admin(current_user): + return error_response("Only admin users can see vehicles", 403) + + workshop_id = get_current_workshop_id(current_user) + + if not workshop_id: + return error_response("Workshop not found for this user", 404) + + vehicles = ( + Vehicle.query + .join(Customer) + .filter( + Vehicle.workshop_id == workshop_id, + Vehicle.is_active == True, + Customer.is_active == True + ) + .order_by(Vehicle.created_at.desc()) + .all() + ) + + return jsonify({ + "vehicles": [vehicle.serialize() for vehicle in vehicles] + }), 200 + + +#-------------VEHICLES: CREATE---------------------- + +@api.route("/vehicles", methods=["POST"]) +@jwt_required() +def create_vehicle(): + + current_user = get_current_user() + + if not is_admin(current_user): + return error_response("Only admin users can create vehicles", 403) + + workshop_id = get_current_workshop_id(current_user) + + if not workshop_id: + return error_response("Workshop not found for this user", 404) + + data = request.get_json() or {} + + customer_id = data.get("customer_id") + plate = data.get("plate") + vin = data.get("vin") + + brand = data.get("brand") + model = data.get("model") + version = data.get("version") + year = data.get("year") + + fuel_type = data.get("fuel_type") + power_hp = data.get("power_hp") + engine_cc = data.get("engine_cc") + color = data.get("color") + + mileage = data.get("mileage", 0) + first_registration_date = data.get("first_registration_date") + + if not customer_id or not plate or not brand or not model or not fuel_type: + return error_response("customer_id, plate, brand, model and fuel_type are required", 400) + + if fuel_type not in FUEL_TYPES: + return jsonify({ + "message": "Invalid fuel_type", + "error": "Invalid fuel_type", + "allowed_values": FUEL_TYPES + }), 400 + + customer, error = get_customer_or_error(customer_id, current_user) + + if error: + return error + + normalized_plate = plate.upper().strip() + + existing_vehicle = Vehicle.query.filter_by(plate=normalized_plate).first() + + if existing_vehicle: + return error_response("A vehicle with this plate already exists", 409) + + normalized_vin = vin.upper().strip() if vin else None + + if normalized_vin: + existing_vin = Vehicle.query.filter_by(vin=normalized_vin).first() + + if existing_vin: + return error_response("A vehicle with this VIN already exists", 409) + + vehicle = Vehicle( + customer_id=customer.id, + workshop_id=workshop_id, + plate=normalized_plate, + vin=normalized_vin, + brand=brand, + model=model, + version=version, + year=year, + fuel_type=fuel_type, + power_hp=power_hp, + engine_cc=engine_cc, + color=color, + mileage=mileage or 0, + first_registration_date=parse_date(first_registration_date) + ) + + db.session.add(vehicle) + db.session.commit() + + return jsonify({ + "message": "Vehicle created successfully", + "vehicle": vehicle.serialize() + }), 201 + + +###------------ VEHICLES ID GETL---------------------- + +@api.route("/vehicles/", methods=["GET"]) +@jwt_required() +def get_vehicle_detail(vehicle_id): + + current_user = get_current_user() + + if not is_admin(current_user): + return error_response("Only admin users can see vehicle details", 403) + + vehicle, error = get_vehicle_or_error(vehicle_id, current_user) + + if error: + return error + + return jsonify({ + "vehicle": vehicle.serialize() + }), 200 + +##---------------VEHICLES:UPDATE---------------------- + +@api.route("/vehicles/", methods=["PUT"]) +@jwt_required() +def update_vehicle(vehicle_id): + + current_user = get_current_user() + + if not is_admin(current_user): + return error_response("Only admin users can update vehicles", 403) + + vehicle, error = get_vehicle_or_error(vehicle_id, current_user) + + if error: + return error + + data = request.get_json() or {} + + if "fuel_type" in data and data.get("fuel_type") not in FUEL_TYPES: + return jsonify({ + "message": "Invalid fuel_type", + "error": "Invalid fuel_type", + "allowed_values": FUEL_TYPES + }), 400 + + if "plate" in data and data.get("plate"): + normalized_plate = data.get("plate").upper().strip() + + existing_vehicle_plate = Vehicle.query.filter( + Vehicle.plate == normalized_plate, + Vehicle.id != vehicle.id + ).first() + + if existing_vehicle_plate: + return error_response("A vehicle with this plate already exists", 409) + + vehicle.plate = normalized_plate + + if "vin" in data: + normalized_vin = data.get("vin").upper().strip() if data.get("vin") else None + + if normalized_vin and normalized_vin != vehicle.vin: + existing_vehicle_vin = Vehicle.query.filter( + Vehicle.vin == normalized_vin, + Vehicle.id != vehicle.id + ).first() + + if existing_vehicle_vin: + return error_response("A vehicle with this VIN already exists", 409) + + vehicle.vin = normalized_vin + + editable_fields = [ + "brand", + "model", + "version", + "year", + "fuel_type", + "power_hp", + "engine_cc", + "color", + "mileage" + ] + + for field in editable_fields: + if field in data: + setattr(vehicle, field, data[field]) + + if "first_registration_date" in data: + vehicle.first_registration_date = parse_date(data.get("first_registration_date")) + + db.session.commit() + + return jsonify({ + "message": "Vehicle updated successfully", + "vehicle": vehicle.serialize() + }), 200 + + +###---------------VEHICLES: DELETE -------------------- + +@api.route("/vehicles/", methods=["DELETE"]) +@jwt_required() +def delete_vehicle(vehicle_id): + + current_user = get_current_user() + + if not is_admin(current_user): + return error_response("Only admin users can delete vehicles", 403) + + vehicle, error = get_vehicle_or_error(vehicle_id, current_user) + + if error: + return error + + vehicle.is_active = False + db.session.commit() + + return jsonify({ + "message": "Vehicle deactivated successfully" + }), 200 + + +##---------------CUSTOMERS: SERIALIZER---------------------- + +# Customer helper +def serialize_customer_with_vehicles(customer): + + customer_data = customer.serialize() + + active_vehicles = [] + + for vehicle in customer.vehicles: + if vehicle.is_active: + active_vehicles.append(vehicle) + + customer_data["vehicles_count"] = len(active_vehicles) + + customer_data["vehicles"] = [ + { + "id": vehicle.id, + "plate": vehicle.plate, + "brand": vehicle.brand, + "model": vehicle.model, + "version": vehicle.version, + "year": vehicle.year, + "fuel_type": vehicle.fuel_type, + "mileage": vehicle.mileage, + "customer_id": vehicle.customer_id, + "workshop_id": vehicle.workshop_id, + "is_active": vehicle.is_active + } + for vehicle in active_vehicles + ] + + return customer_data + + +###---------------CUSTOMERS GET-------------------- + +@api.route("/customers", methods=["GET"]) +@jwt_required() +def get_customers(): + + current_user = get_current_user() + + if not is_admin(current_user): + return error_response("Only admin users can see customers", 403) + + workshop_id = get_current_workshop_id(current_user) + + if not workshop_id: + return error_response("Workshop not found for this user", 404) + + customers = ( + Customer.query + .filter_by( + workshop_id=workshop_id, + is_active=True + ) + .order_by(Customer.created_at.desc()) + .all() + ) + + return jsonify({ + "customers": [ + serialize_customer_with_vehicles(customer) + for customer in customers + ] + }), 200 + + + +#-------------CUSTOMERS CREATE---------------- + +@api.route("/customers", methods=["POST"]) +@jwt_required() +def create_customer(): + + current_user = get_current_user() + + if not is_admin(current_user): + return error_response("Only admin users can create customers", 403) + + data = request.get_json() or {} + + first_name = data.get("first_name") + last_name = data.get("last_name") + dni = data.get("dni") + driving_license = data.get("driving_license") + phone = data.get("phone") + email = data.get("email") + address = data.get("address") + + if not first_name or not last_name or not dni or not driving_license or not phone: + return error_response("first_name, last_name, dni, driving_license and phone are required", 400) + + existing_customer_dni = Customer.query.filter_by(dni=dni).first() + + if existing_customer_dni: + return error_response("A customer with this DNI already exists", 409) + + existing_customer_license = Customer.query.filter_by(driving_license=driving_license).first() + + if existing_customer_license: + return error_response("A customer with this driving license already exists", 409) + + workshop_id = get_current_workshop_id(current_user) + + if not workshop_id: + return error_response("Workshop not found for this user", 404) + + customer = Customer( + first_name=first_name, + last_name=last_name, + dni=dni, + driving_license=driving_license, + phone=phone, + email=email, + address=address, + workshop_id=workshop_id + ) + + db.session.add(customer) + db.session.commit() + + return jsonify({ + "message": "Customer created successfully", + "customer": serialize_customer_with_vehicles(customer) + }), 201 + + +##--------------CUSTOMERS GET---------------------- + +@api.route("/customers/", methods=["GET"]) +@jwt_required() +def get_customer_detail(customer_id): + + current_user = get_current_user() + + if not is_admin(current_user): + return error_response("Only admin users can see customer details", 403) + + customer, error = get_customer_or_error(customer_id, current_user) + + if error: + return error + + return jsonify({ + "customer": serialize_customer_with_vehicles(customer) + }), 200 + + +###---------------CUSTOMERS UPDATE---------------------- + +@api.route("/customers/", methods=["PUT"]) +@jwt_required() +def update_customer(customer_id): + + current_user = get_current_user() + + if not is_admin(current_user): + return error_response("Only admin users can update customers", 403) + + customer, error = get_customer_or_error(customer_id, current_user) + + if error: + return error + + data = request.get_json() or {} + + if "dni" in data and data.get("dni") != customer.dni: + existing_customer_dni = Customer.query.filter( + Customer.dni == data.get("dni"), + Customer.id != customer.id + ).first() + + if existing_customer_dni: + return error_response("A customer with this DNI already exists", 409) + + if "driving_license" in data and data.get("driving_license") != customer.driving_license: + existing_customer_license = Customer.query.filter( + Customer.driving_license == data.get("driving_license"), + Customer.id != customer.id + ).first() + + if existing_customer_license: + return error_response("A customer with this driving license already exists", 409) + + editable_fields = [ + "first_name", + "last_name", + "dni", + "driving_license", + "phone", + "email", + "address" + ] + + for field in editable_fields: + if field in data: + setattr(customer, field, data[field]) + + db.session.commit() + + return jsonify({ + "message": "Customer updated successfully", + "customer": serialize_customer_with_vehicles(customer) + }), 200 + + +##------------CUSTOMERS:DELETE---------------------- + +@api.route("/customers/", methods=["DELETE"]) +@jwt_required() +def delete_customer(customer_id): + + current_user = get_current_user() + + if not is_admin(current_user): + return error_response("Only admin users can delete customers", 403) + + customer, error = get_customer_or_error(customer_id, current_user) + + if error: + return error + + customer.is_active = False + db.session.commit() + + return jsonify({ + "message": "Customer deactivated successfully" + }), 200 + +###---------------SERVICES--------------------------- +###---------------SERVICES GET---------------------- + +@api.route("/services", methods=["GET"]) +@jwt_required() +def get_services(): + + current_user = get_current_user() + + if not is_admin(current_user) and not is_mechanic(current_user): + return error_response("Only admin or mechanic users can see services", 403) + + workshop_id = get_current_workshop_id(current_user) + + if not workshop_id: + return error_response("Workshop not found for this user", 404) + + query = Service.query.filter_by(workshop_id=workshop_id) + + if is_mechanic(current_user): + current_employee = get_current_employee(current_user) + + if not current_employee: + return error_response("Employee profile not found", 404) + + query = query.filter_by(employee_id=current_employee.id) + + status = request.args.get("status") + customer_id = request.args.get("customer_id") + vehicle_id = request.args.get("vehicle_id") + employee_id = request.args.get("employee_id") + + if status: + if status not in SERVICE_STATUSES: + return jsonify({ + "message": "Invalid status", + "error": "Invalid status", + "allowed_values": SERVICE_STATUSES + }), 400 + + query = query.filter_by(status=status) + + if customer_id: + query = query.filter_by(customer_id=customer_id) + + if vehicle_id: + query = query.filter_by(vehicle_id=vehicle_id) + + if employee_id and is_admin(current_user): + query = query.filter_by(employee_id=employee_id) + + services = ( + query + .order_by(Service.created_at.desc()) + .all() + ) + + return jsonify({ + "services": [service.serialize() for service in services] + }), 200 + + +###---------------SERVICES CREATE---------------------- + +@api.route("/services", methods=["POST"]) +@jwt_required() +def create_service(): + + current_user = get_current_user() + + if not is_admin(current_user): + return error_response("Only admin users can create services", 403) + + workshop_id = get_current_workshop_id(current_user) + + if not workshop_id: + return error_response("Workshop not found for this user", 404) + + data = request.get_json() or {} + + title = data.get("title") + description = data.get("description") + service_type = data.get("service_type") + status = data.get("status", "pending") + priority = data.get("priority", "normal") + + customer_id = data.get("customer_id") + vehicle_id = data.get("vehicle_id") + employee_id = data.get("employee_id") or None + + entry_mileage = data.get("entry_mileage") + start_date = data.get("start_date") + end_date = data.get("end_date") + observations = data.get("observations") + + if not title or not service_type or not customer_id or not vehicle_id: + return error_response("title, service_type, customer_id and vehicle_id are required", 400) + + if service_type not in SERVICE_TYPES: + return jsonify({ + "message": "Invalid service_type", + "error": "Invalid service_type", + "allowed_values": SERVICE_TYPES + }), 400 + + if status not in SERVICE_STATUSES: + return jsonify({ + "message": "Invalid status", + "error": "Invalid status", + "allowed_values": SERVICE_STATUSES + }), 400 + + if priority not in SERVICE_PRIORITIES: + return jsonify({ + "message": "Invalid priority", + "error": "Invalid priority", + "allowed_values": SERVICE_PRIORITIES + }), 400 + + customer, error = get_customer_or_error(customer_id, current_user) + + if error: + return error + + vehicle, error = get_vehicle_or_error(vehicle_id, current_user) + + if error: + return error + + if vehicle.customer_id != customer.id: + return error_response("This vehicle does not belong to the selected customer", 400) + + assigned_employee = None + + if employee_id: + assigned_employee, error = get_employee_or_error(employee_id, current_user) + + if error: + return error + + if assigned_employee.role != "mechanic": + return error_response("The assigned employee must be a mechanic", 400) + + service = Service( + title=title, + description=description, + service_type=service_type, + status=status, + priority=priority, + entry_mileage=entry_mileage, + start_date=parse_datetime(start_date) if start_date else datetime.now(timezone.utc), + end_date=parse_datetime(end_date) if end_date else None, + observations=observations, + workshop_id=workshop_id, + customer_id=customer.id, + vehicle_id=vehicle.id, + employee_id=assigned_employee.id if assigned_employee else None + ) + + db.session.add(service) + db.session.flush() + + current_employee = get_current_employee(current_user) + + if current_employee: + status_log = ServiceStatusLog( + service_id=service.id, + from_status=None, + to_status=service.status, + employee_id=current_employee.id, + note="Service created" + ) + + db.session.add(status_log) + + db.session.commit() + + return jsonify({ + "message": "Service created successfully", + "service": service.serialize() + }), 201 + + +###---------------SERVICES ID GET---------------------- + +@api.route("/services/", methods=["GET"]) +@jwt_required() +def get_service_detail(service_id): + + current_user = get_current_user() + + if not is_admin(current_user) and not is_mechanic(current_user): + return error_response("Only admin or mechanic users can see service details", 403) + + service, error = get_service_or_error(service_id, current_user) + + if error: + return error + + if is_mechanic(current_user): + current_employee = get_current_employee(current_user) + + if not current_employee: + return error_response("Employee profile not found", 404) + + if service.employee_id != current_employee.id: + return error_response("You do not have permission to see this service", 403) + + return jsonify({ + "service": service.serialize() + }), 200 + + +###---------------SERVICES UPDATE---------------------- + +@api.route("/services/", methods=["PUT"]) +@jwt_required() +def update_service(service_id): + + current_user = get_current_user() + + if not is_admin(current_user): + return error_response("Only admin users can update services", 403) + + service, error = get_service_or_error(service_id, current_user) + + if error: + return error + + data = request.get_json() or {} + + if "service_type" in data and data.get("service_type") not in SERVICE_TYPES: + return jsonify({ + "message": "Invalid service_type", + "error": "Invalid service_type", + "allowed_values": SERVICE_TYPES + }), 400 + + if "status" in data and data.get("status") not in SERVICE_STATUSES: + return jsonify({ + "message": "Invalid status", + "error": "Invalid status", + "allowed_values": SERVICE_STATUSES + }), 400 + + if "priority" in data and data.get("priority") not in SERVICE_PRIORITIES: + return jsonify({ + "message": "Invalid priority", + "error": "Invalid priority", + "allowed_values": SERVICE_PRIORITIES + }), 400 + + customer = service.customer + vehicle = service.vehicle + + if "customer_id" in data and data.get("customer_id"): + customer, error = get_customer_or_error(data.get("customer_id"), current_user) + + if error: + return error + + if "vehicle_id" in data and data.get("vehicle_id"): + vehicle, error = get_vehicle_or_error(data.get("vehicle_id"), current_user) + + if error: + return error + + if customer and vehicle and vehicle.customer_id != customer.id: + return error_response("This vehicle does not belong to the selected customer", 400) + + if "employee_id" in data: + employee_id = data.get("employee_id") or None + + if employee_id: + assigned_employee, error = get_employee_or_error(employee_id, current_user) + + if error: + return error + + if assigned_employee.role != "mechanic": + return error_response("The assigned employee must be a mechanic", 400) + + service.employee_id = assigned_employee.id + else: + service.employee_id = None + + if "customer_id" in data and customer: + service.customer_id = customer.id + + if "vehicle_id" in data and vehicle: + service.vehicle_id = vehicle.id + + editable_fields = [ + "title", + "description", + "service_type", + "priority", + "entry_mileage", + "observations" + ] + + for field in editable_fields: + if field in data: + setattr(service, field, data[field]) + + if "start_date" in data: + service.start_date = parse_datetime(data.get("start_date")) if data.get("start_date") else None + + if "end_date" in data: + service.end_date = parse_datetime(data.get("end_date")) if data.get("end_date") else None + + if "status" in data: + new_status = data.get("status") + + if new_status != service.status: + current_employee = get_current_employee(current_user) + + create_status_log( + service=service, + employee_id=current_employee.id if current_employee else None, + new_status=new_status, + note=data.get("status_note") + ) + + db.session.commit() + + return jsonify({ + "message": "Service updated successfully", + "service": service.serialize() + }), 200 + + +###---------------SERVICES STATUS UPDATE---------------------- + +@api.route("/services//status", methods=["PATCH"]) +@jwt_required() +def update_service_status(service_id): + + current_user = get_current_user() + + if not is_admin(current_user) and not is_mechanic(current_user): + return error_response("Only admin or mechanic users can update service status", 403) + + service, error = get_service_or_error(service_id, current_user) + + if error: + return error + + current_employee = get_current_employee(current_user) + + if not current_employee: + return error_response("Employee profile not found", 404) + + if is_mechanic(current_user) and service.employee_id != current_employee.id: + return error_response("You do not have permission to update this service", 403) + + data = request.get_json() or {} + + new_status = data.get("status") + note = data.get("note") + + if not new_status: + return error_response("status is required", 400) + + if new_status not in SERVICE_STATUSES: + return jsonify({ + "message": "Invalid status", + "error": "Invalid status", + "allowed_values": SERVICE_STATUSES + }), 400 + + if new_status == service.status: + return jsonify({ + "message": "Service already has this status", + "service": service.serialize() + }), 200 + + create_status_log( + service=service, + employee_id=current_employee.id, + new_status=new_status, + note=note + ) + + db.session.commit() + + return jsonify({ + "message": "Service status updated successfully", + "service": service.serialize() + }), 200 + + +###---------------SERVICES STATUS LOGS GET---------------------- + +@api.route("/services//status-logs", methods=["GET"]) +@jwt_required() +def get_service_status_logs(service_id): + + current_user = get_current_user() + + if not is_admin(current_user) and not is_mechanic(current_user): + return error_response("Only admin or mechanic users can see service status logs", 403) + + service, error = get_service_or_error(service_id, current_user) + + if error: + return error + + if is_mechanic(current_user): + current_employee = get_current_employee(current_user) + + if not current_employee: + return error_response("Employee profile not found", 404) + + if service.employee_id != current_employee.id: + return error_response("You do not have permission to see these logs", 403) + + logs = ( + ServiceStatusLog.query + .filter_by(service_id=service.id) + .order_by(ServiceStatusLog.changed_at.desc()) + .all() + ) + + return jsonify({ + "status_logs": [log.serialize() for log in logs] + }), 200 + + +###---------------SERVICES DELETE---------------------- + +@api.route("/services/", methods=["DELETE"]) +@jwt_required() +def delete_service(service_id): + + current_user = get_current_user() + + if not is_admin(current_user): + return error_response("Only admin users can delete services", 403) + + service, error = get_service_or_error(service_id, current_user) + + if error: + return error + + current_employee = get_current_employee(current_user) + + if service.status != "cancelled": + create_status_log( + service=service, + employee_id=current_employee.id if current_employee else None, + new_status="cancelled", + note="Service cancelled" + ) + + db.session.commit() + + return jsonify({ + "message": "Service cancelled successfully", + "service": service.serialize() + }), 200 +###----------SERVICE COMMENTS GET--------------------- + +@api.route("/services//comments", methods=["GET"]) +@jwt_required() +def get_service_comments(service_id): + + current_user = get_current_user() + service, error = get_service_or_error(service_id, current_user) + + if error: + return error + + if is_mechanic(current_user): + employee = get_current_employee(current_user) + + if not employee: + return error_response("Employee profile not found", 404) + + if service.employee_id != employee.id: + return error_response("You do not have permission to access these comments", 403) + + comments = ( + ServiceComment.query + .filter_by(service_id=service.id) + .order_by(ServiceComment.created_at.desc()) + .all() + ) + + return jsonify({ + "comments": [comment.serialize() for comment in comments] + }), 200 + +###--------------- SERVICE COMMENTS CREATE ------------- + +@api.route("/services//comments", methods=["POST"]) +@jwt_required() +def create_service_comment(service_id): + + current_user = get_current_user() + service, error = get_service_or_error(service_id, current_user) + + if error: + return error + + current_employee = get_current_employee(current_user) + + if not current_employee: + return error_response("Employee profile not found", 404) + + if is_mechanic(current_user): + if service.employee_id != current_employee.id: + return error_response("You do not have permission to comment on this service", 403) + + # This route now supports both JSON and FormData. + # JSON is useful for old comments without images. + # FormData is needed when we upload an image. + data = request.form if request.form else (request.get_json(silent=True) or {}) + + comment_text = data.get("comment", "").strip() + comment_type = data.get("comment_type", "note") + image = request.files.get("image") + + if not comment_text: + return error_response("comment is required", 400) + + if comment_type not in COMMENT_TYPES: + return jsonify({ + "message": "Invalid comment_type", + "error": "Invalid comment_type", + "allowed_values": COMMENT_TYPES + }), 400 + + image_url = None + image_public_id = None + + if image and image.filename: + if not image.mimetype.startswith("image/"): + return error_response("Only image files are allowed", 400) + + try: + upload_result = cloudinary.uploader.upload( + image, + folder=f"workshop-app/services/{service.id}/comments", + resource_type="image" + ) + + image_url = upload_result.get("secure_url") + image_public_id = upload_result.get("public_id") + + except Exception: + return error_response("Image upload failed", 500) + + comment = ServiceComment( + service_id=service.id, + employee_id=current_employee.id, + comment=comment_text, + comment_type=comment_type, + image_url=image_url, + image_public_id=image_public_id + ) + + db.session.add(comment) + db.session.commit() + + return jsonify({ + "message": "Comment created successfully", + "comment": comment.serialize() + }), 201 + +##------------COMMENTS: NOTIFY ADMIN--------------- + +@api.route("/services//notify-admin", methods=["POST"]) +@jwt_required() +def notify_admin(service_id): + + current_user = get_current_user() + service, error = get_service_or_error(service_id, current_user) + + if error: + return error + + current_employee = get_current_employee(current_user) + + if not current_employee: + return error_response("Employee profile not found", 404) + + if is_mechanic(current_user): + if service.employee_id != current_employee.id: + return error_response("You do not have permission to notify admin about this service", 403) + + data = request.get_json() or {} + message = data.get("message") + + if not message: + return error_response("message is required", 400) + + comment = ServiceComment( + service_id=service.id, + employee_id=current_employee.id, + comment=message, + comment_type="admin_alert" + ) + + db.session.add(comment) + db.session.commit() + + return jsonify({ + "message": "Admin notified successfully", + "comment": comment.serialize() + }), 201 + + +##-------------MECHANIC DASHBOARD------------------ + +@api.route("/mechanic/services", methods=["GET"]) +@jwt_required() +def get_my_mechanic_services(): + + current_user = get_current_user() + + if not is_mechanic(current_user): + return error_response("Only mechanic users can access this endpoint", 403) + + employee = get_current_employee(current_user) + + if not employee: + return error_response("Employee profile not found", 404) + + workshop_id = get_current_workshop_id(current_user) + + if not workshop_id: + return error_response("Workshop not found for this user", 404) + + services = ( + Service.query + .filter_by( + workshop_id=workshop_id, + employee_id=employee.id + ) + .order_by(Service.created_at.desc()) + .all() + ) + + assigned_count = len(services) + + in_repair_count = len([ + service for service in services + if service.status == "in_repair" + ]) + + finished_count = len([ + service for service in services + if service.status in ["ready_to_deliver", "delivered"] + ]) + + return jsonify({ + "services": [service.serialize() for service in services], + "stats": { + "assigned": assigned_count, + "in_repair": in_repair_count, + "finished": finished_count + } + }), 200 + + +##-------------ADMIN DASHBOARD---------------------- + +@api.route("/admin/dashboard", methods=["GET"]) +@jwt_required() +def get_admin_dashboard(): + + current_user = get_current_user() + + if not is_admin(current_user): + return error_response("Only admin users can access this endpoint", 403) + + workshop_id = get_current_workshop_id(current_user) + + if not workshop_id: + return error_response("Workshop not found for this user", 404) + + active_vehicles = ( + Vehicle.query + .join(Customer) + .filter( + Vehicle.workshop_id == workshop_id, + Vehicle.is_active == True, + Customer.is_active == True + ) + .count() + ) + + mechanics_count = Employee.query.filter_by( + workshop_id=workshop_id, + role="mechanic", + is_active=True + ).count() + + budget_pending = Service.query.filter_by( + workshop_id=workshop_id, + status="budget_pending" + ).count() + + services = ( + Service.query + .filter_by(workshop_id=workshop_id) + .order_by(Service.created_at.desc()) + .all() + ) + + services_by_status = {} + + for status in SERVICE_STATUSES: + services_by_status[status] = [ + service.serialize() for service in services + if service.status == status + ] + + return jsonify({ + "stats": { + "active_vehicles": active_vehicles, + "mechanics_count": mechanics_count, + "budget_pending": budget_pending + }, + "services_by_status": services_by_status + }), 200 + +####-----------------------------###-------------------------- + +###-------------- FORGOT PASSWORD---------------------- + +@api.route("/forgot-password", methods=["POST"]) +def forgot_password(): + + data = request.get_json() or {} + + email = data.get("email") + + if not email: + return error_response("email is required", 400) + user = User.query.filter_by(email=email).first() + if not user: + return jsonify({ + "message": "If the email exists, a password reset link has been generated" + }), 200 + + now = datetime.now(timezone.utc) + + previous_tokens = PasswordResetToken.query.filter_by( + user_id=user.id, + used_at=None + ).all() + + for previous_token in previous_tokens: + previous_token.used_at = now + + raw_token = secrets.token_urlsafe(32) + token_hash = hashlib.sha256(raw_token.encode("utf-8")).hexdigest() + + reset_token = PasswordResetToken( + user_id=user.id, + token_hash=token_hash, + expires_at=now + timedelta(minutes=30) + ) + + db.session.add(reset_token) + db.session.commit() + + return jsonify({ + "message": "Password reset token generated successfully", + "reset_token": raw_token + }), 200 + + +###--------------RESET PASSWORD---------------------- + +@api.route("/reset-password", methods=["POST"]) +def reset_password(): + + data = request.get_json() or {} + + raw_token = data.get("token") + password = data.get("password") + password_confirm = data.get("password_confirm") + + if not raw_token or not password or not password_confirm: + return error_response("token, password and password_confirm are required", 400) + + if password != password_confirm: + return error_response("Passwords do not match", 400) + + token_hash = hashlib.sha256(raw_token.encode("utf-8")).hexdigest() + + reset_token = PasswordResetToken.query.filter_by( + token_hash=token_hash, + used_at=None + ).first() + + if not reset_token: + return error_response("Invalid or expired reset token", 400) + + now = datetime.now(timezone.utc) + + expires_at = reset_token.expires_at + + if expires_at.tzinfo is None: + expires_at = expires_at.replace(tzinfo=timezone.utc) + + if expires_at < now: + reset_token.used_at = now + db.session.commit() + + return error_response("Invalid or expired reset token", 400) + + user = User.query.get(reset_token.user_id) + + if not user: + return error_response("User not found", 404) + + password_hash = bcrypt.generate_password_hash(password).decode("utf-8") + + user.password_hash = password_hash + reset_token.used_at = now + + db.session.commit() + + return jsonify({ + "message": "Password updated successfully" + }), 200 \ No newline at end of file diff --git a/src/app.py b/src/app.py index 1b3340c0fa..d0ddbb77c4 100644 --- a/src/app.py +++ b/src/app.py @@ -1,72 +1,55 @@ -""" -This module takes care of starting the API Server, Loading the DB and Adding the endpoints -""" import os -from flask import Flask, request, jsonify, url_for, send_from_directory +from datetime import timedelta +from flask import Flask, jsonify from flask_migrate import Migrate -from flask_swagger import swagger +from flask_cors import CORS +from flask_jwt_extended import JWTManager + from api.utils import APIException, generate_sitemap from api.models import db -from api.routes import api +from api.routes import api, bcrypt from api.admin import setup_admin -from api.commands import setup_commands -# from models import Person +from api.cloudinary_config import configure_cloudinary + -ENV = "development" if os.getenv("FLASK_DEBUG") == "1" else "production" -static_file_dir = os.path.join(os.path.dirname( - os.path.realpath(__file__)), '../dist/') app = Flask(__name__) app.url_map.strict_slashes = False -# database condiguration +configure_cloudinary() + +app.config["JWT_SECRET_KEY"] = os.getenv("JWT_SECRET_KEY", "super-secret-key") +app.config["JWT_ACCESS_TOKEN_EXPIRES"] = timedelta(hours=8) + db_url = os.getenv("DATABASE_URL") if db_url is not None: - app.config['SQLALCHEMY_DATABASE_URI'] = db_url.replace( - "postgres://", "postgresql://") + app.config["SQLALCHEMY_DATABASE_URI"] = db_url.replace("postgres://", "postgresql://") else: - app.config['SQLALCHEMY_DATABASE_URI'] = "sqlite:////tmp/test.db" + app.config["SQLALCHEMY_DATABASE_URI"] = "sqlite:///database.db" +app.config["SQLALCHEMY_TRACK_MODIFICATIONS"] = False -app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False -MIGRATE = Migrate(app, db, compare_type=True) db.init_app(app) +MIGRATE = Migrate(app, db) -# add the admin -setup_admin(app) - -# add the admin -setup_commands(app) +CORS(app) -# Add all endpoints form the API with a "api" prefix -app.register_blueprint(api, url_prefix='/api') +jwt = JWTManager(app) +bcrypt.init_app(app) -# Handle/serialize errors like a JSON object +setup_admin(app) +app.register_blueprint(api, url_prefix="/api") @app.errorhandler(APIException) def handle_invalid_usage(error): return jsonify(error.to_dict()), error.status_code -# generate sitemap with all your endpoints - -@app.route('/') +@app.route("/") def sitemap(): - if ENV == "development": - return generate_sitemap(app) - return send_from_directory(static_file_dir, 'index.html') - -# any other endpoint will try to serve it like a static file -@app.route('/', methods=['GET']) -def serve_any_other_file(path): - if not os.path.isfile(os.path.join(static_file_dir, path)): - path = 'index.html' - response = send_from_directory(static_file_dir, path) - response.cache_control.max_age = 0 # avoid cache memory - return response - - -# this only runs if `$ python src/main.py` is executed -if __name__ == '__main__': - PORT = int(os.environ.get('PORT', 3001)) - app.run(host='0.0.0.0', port=PORT, debug=True) + return generate_sitemap(app) + + +if __name__ == "__main__": + PORT = int(os.environ.get("PORT", 3001)) + app.run(host="0.0.0.0", port=PORT, debug=True) \ No newline at end of file diff --git a/src/front/assets/img/little_logo.png b/src/front/assets/img/little_logo.png new file mode 100644 index 0000000000..4aa47f2a1b Binary files /dev/null and b/src/front/assets/img/little_logo.png differ diff --git a/src/front/assets/img/logoTaller.png b/src/front/assets/img/logoTaller.png new file mode 100644 index 0000000000..6dc610b91f Binary files /dev/null and b/src/front/assets/img/logoTaller.png differ diff --git a/src/front/components/ClientSelector.jsx b/src/front/components/ClientSelector.jsx new file mode 100644 index 0000000000..2d425be2f5 --- /dev/null +++ b/src/front/components/ClientSelector.jsx @@ -0,0 +1,191 @@ +import { useState, useEffect } from "react"; +import { listarClientes } from "../services/api"; + +export const ClientSelector = ({ + value, + onChange, + nuevoCliente, + onChangeNuevoCliente, + error +}) => { + const [clientes, setClientes] = useState([]); + const [cargando, setCargando] = useState(false); + const [errorCarga, setErrorCarga] = useState(null); + const [modo, setModo] = useState("existente"); + + + useEffect(() => { + setCargando(true); + listarClientes() + .then((data) => setClientes(data)) + .catch((err) => setErrorCarga(err.message)) + .finally(() => setCargando(false)); + }, []); + + const cambiarModo = (nuevoModo) => { + setModo(nuevoModo); + + if (nuevoModo === "existente") { + onChangeNuevoCliente({}); + } else if (nuevoModo === "nuevo") { + onChange(null); + } else { + onChange(null); + onChangeNuevoCliente({}); + } + }; + + const handleNuevoChange = (campo, valor) => { + onChangeNuevoCliente({ ...nuevoCliente, [campo]: valor }); + }; + + return ( +
+
+ Propietario del vehículo +
+
+ +
+ cambiarModo("existente")} + /> + + + cambiarModo("nuevo")} + /> + + + cambiarModo("sin")} + /> + +
+ + + {modo === "existente" && ( + <> + {cargando &&

Cargando clientes...

} + {errorCarga && ( +

Error al cargar: {errorCarga}

+ )} + {!cargando && !errorCarga && ( + + )} + {error && ( +
{error}
+ )} + + )} + + + {modo === "nuevo" && ( +
+
+ + handleNuevoChange("nombre", e.target.value)} + /> +
+
+ + + handleNuevoChange("apellidos", e.target.value) + } + /> +
+
+ + + handleNuevoChange("dni", e.target.value.toUpperCase()) + } + /> +
+
+ + + handleNuevoChange("telefono", e.target.value) + } + /> +
+
+ + handleNuevoChange("email", e.target.value)} + /> +
+
+ + + handleNuevoChange("direccion", e.target.value) + } + /> +
+ {error && ( +
+
{error}
+
+ )} +
+ )} + +
+
+ ); + +}; \ No newline at end of file diff --git a/src/front/components/CustomerForm.jsx b/src/front/components/CustomerForm.jsx new file mode 100644 index 0000000000..eda3098edb --- /dev/null +++ b/src/front/components/CustomerForm.jsx @@ -0,0 +1,218 @@ +function ButtonSpinner({ text }) { + return ( + <> + + {text} + + ); +} + +export function CustomerForm({ + customers, + selectedCustomer, + customerForm, + showCustomerForm, + isBusy, + isSavingCustomer, + onSelectCustomer, + onCustomerFormChange, + onCreateCustomer, + onToggleCustomerForm, + onContinue, + getCustomerLabel +}) { + return ( +
+

Customer

+ + {customers.length === 0 && !showCustomerForm && ( +
+ No customers found. Create the first customer before creating the service ticket. +
+ )} + +
+ + + +
+ + + + {showCustomerForm && ( +
+
+
+ + + +
+ +
+ + + +
+ +
+ + + +
+ +
+ + + +
+ +
+ + + +
+ +
+ + + +
+ +
+ + + +
+ +
+ +
+
+
+ )} + +
+ +
+
+ ); +} \ No newline at end of file diff --git a/src/front/components/DashboardSidebar.jsx b/src/front/components/DashboardSidebar.jsx new file mode 100644 index 0000000000..a5ae7f6c27 --- /dev/null +++ b/src/front/components/DashboardSidebar.jsx @@ -0,0 +1,158 @@ +import React from "react"; +import { NavLink, useNavigate } from "react-router-dom"; +import {Home, LogOut, ClipboardPlus, Users, Car, Wrench } from "lucide-react"; + +const getStoredItem = (key) => { + const storedValue = localStorage.getItem(key); + + if (!storedValue) { + return null; + } + + try { + return JSON.parse(storedValue); + } catch { + localStorage.removeItem(key); + return null; + } +}; + +export const DashboardSidebar = ({ role, user, employee, onLogout }) => { + const workshop = getStoredItem("workshop"); + const storedEmployee = getStoredItem("employee"); + const currentEmployee = employee || storedEmployee; + + const navigate = useNavigate(); + + const today = new Intl.DateTimeFormat("en-GB", { + weekday: "long", + day: "numeric", + month: "short", + }).format(new Date()); + + const displayName = + currentEmployee?.first_name && currentEmployee?.last_name + ? `${currentEmployee.first_name} ${currentEmployee.last_name}` + : currentEmployee?.first_name || + user?.employee?.first_name || + user?.email || + "User"; + + const roleLabel = role === "admin" ? "admin" : "mechanic"; + const homePath = role === "admin" ? "/admin" : "/mechanic"; + + const getNavClass = ({ isActive }) => + `dashboard-sidebar-link ${isActive ? "active" : ""}`; + + return ( + + ); +}; \ No newline at end of file diff --git a/src/front/components/Footer.jsx b/src/front/components/Footer.jsx index f06302dbd2..f3b06ab27c 100644 --- a/src/front/components/Footer.jsx +++ b/src/front/components/Footer.jsx @@ -1,11 +1,13 @@ -export const Footer = () => ( - -); +import React from "react"; + +export const Footer = () => { + return ( +
+
+ + © 2026 Workshop Manager. All rights reserved. + +
+
+ ); +}; \ No newline at end of file diff --git a/src/front/components/Navbar.jsx b/src/front/components/Navbar.jsx index 30d43a2636..562e20ef58 100644 --- a/src/front/components/Navbar.jsx +++ b/src/front/components/Navbar.jsx @@ -1,19 +1,102 @@ -import { Link } from "react-router-dom"; +import React from "react"; +import { Link, NavLink, useLocation, useNavigate } from "react-router-dom"; +import logoTaller from "../assets/img/logoTaller.png"; export const Navbar = () => { + const navigate = useNavigate(); + const location = useLocation(); - return ( - - ); + const token = localStorage.getItem("token"); + + const isDashboardArea = + location.pathname === "/dashboard" || + location.pathname.startsWith("/admin") || + location.pathname.startsWith("/mechanic"); + + const handleLogout = () => { + localStorage.removeItem("token"); + localStorage.removeItem("user"); + localStorage.removeItem("employee"); + localStorage.removeItem("workshop"); + + navigate("/login"); + window.location.reload(); + }; + + return ( + + ); }; \ No newline at end of file diff --git a/src/front/components/ServiceDetailsForm.jsx b/src/front/components/ServiceDetailsForm.jsx new file mode 100644 index 0000000000..de95e83131 --- /dev/null +++ b/src/front/components/ServiceDetailsForm.jsx @@ -0,0 +1,219 @@ +function ButtonSpinner({ text }) { + return ( + <> + + {text} + + ); +} + +export function ServiceDetailsForm({ + selectedCustomer, + selectedVehicle, + mechanics, + formData, + isSavingService, + onServiceFormChange, + onSubmit, + onBack, + getCustomerLabel, + getVehicleName, + getMechanicLabel, + serviceTypes, + serviceStatuses, + servicePriorities +}) { + return ( +
+

Service ticket

+ +
+

+ Customer:{" "} + {selectedCustomer ? getCustomerLabel(selectedCustomer) : "No customer selected"} +

+ +

+ Vehicle:{" "} + {selectedVehicle ? getVehicleName(selectedVehicle) : "No vehicle selected"} +

+
+ + {mechanics.length === 0 && ( +
+ No mechanics found. You can create the ticket unassigned, or create a mechanic account later. +
+ )} + +
+
+
+ + + +
+ +
+ + + +
+ +
+ + + +
+ +
+ + + +
+ +
+ + + +
+ +
+ + + +
+ +
+ + + +
+ +
+ + + +
+
+
+
+ ); +} \ No newline at end of file diff --git a/src/front/components/ServiceDetailsModal.jsx b/src/front/components/ServiceDetailsModal.jsx new file mode 100644 index 0000000000..f9ffeb3a57 --- /dev/null +++ b/src/front/components/ServiceDetailsModal.jsx @@ -0,0 +1,462 @@ +import { useEffect, useRef, useState } from "react"; +import { apiFetch } from "../services/api"; + +const STATUS_LABELS = { + pending: "Pending", + diagnosis: "Diagnosis", + budget_pending: "Budget pending", + waiting_parts: "Waiting parts", + in_repair: "In repair", + ready_to_deliver: "Ready to deliver", + delivered: "Delivered", + cancelled: "Cancelled", +}; + +const PRIORITY_LABELS = { + low: "Low", + normal: "Normal", + high: "High", + urgent: "Urgent", +}; + +const COMMENT_TYPE_OPTIONS = [ + { value: "note", label: "Note" }, + { value: "status_update", label: "Status update" }, + { value: "admin_alert", label: "Admin alert" }, +]; + +const COMMENT_TYPE_LABELS = { + note: "Note", + status_update: "Status update", + admin_alert: "Admin alert", +}; + +function formatDate(dateValue) { + if (!dateValue) return "-"; + + return new Intl.DateTimeFormat("en-GB", { + day: "2-digit", + month: "short", + year: "numeric", + }).format(new Date(dateValue)); +} + +function formatDateTime(dateValue) { + if (!dateValue) return "-"; + + return new Intl.DateTimeFormat("en-GB", { + day: "2-digit", + month: "short", + year: "numeric", + hour: "2-digit", + minute: "2-digit", + }).format(new Date(dateValue)); +} + +function InfoRow({ label, value }) { + return ( +
+

{label}

+

{value || "-"}

+
+ ); +} + +export function ServiceDetailsModal({ + serviceId, + role = "admin", + onClose, + onServiceUpdated, +}) { + const [service, setService] = useState(null); + const [comments, setComments] = useState([]); + + const [commentText, setCommentText] = useState(""); + const [commentType, setCommentType] = useState("note"); + const [commentImage, setCommentImage] = useState(null); + const [commentImagePreview, setCommentImagePreview] = useState(""); + const fileInputRef = useRef(null); + + const [loading, setLoading] = useState(true); + const [savingComment, setSavingComment] = useState(false); + const [error, setError] = useState(""); + + const isMechanic = role === "mechanic"; + + async function loadDetails() { + try { + setLoading(true); + setError(""); + + const [serviceData, commentsData] = await Promise.all([ + apiFetch(`/services/${serviceId}`), + apiFetch(`/services/${serviceId}/comments`), + ]); + + setService(serviceData.service || serviceData); + setComments(commentsData.comments || []); + } catch (error) { + setError(error.message || "Could not load service details."); + } finally { + setLoading(false); + } + } + + useEffect(() => { + loadDetails(); + }, [serviceId]); + + useEffect(() => { + const originalOverflow = document.body.style.overflow; + document.body.style.overflow = "hidden"; + + return () => { + document.body.style.overflow = originalOverflow; + }; + }, []); + + function handleCommentImageChange(event) { + const selectedImage = event.target.files[0]; + + if (!selectedImage) { + setCommentImage(null); + setCommentImagePreview(""); + return; + } + + setCommentImage(selectedImage); + setCommentImagePreview(URL.createObjectURL(selectedImage)); + } + + async function handleSubmitComment(event) { + event.preventDefault(); + + if (!commentText.trim()) { + setError("Please write a comment before saving."); + return; + } + + try { + setSavingComment(true); + setError(""); + + const token = localStorage.getItem("token"); + + const formData = new FormData(); + formData.append("comment", commentText.trim()); + formData.append("comment_type", commentType); + + if (commentImage) { + formData.append("image", commentImage); + } + + const response = await fetch( + `${import.meta.env.VITE_BACKEND_URL}/api/services/${serviceId}/comments`, + { + method: "POST", + headers: { + Authorization: `Bearer ${token}`, + }, + body: formData, + } + ); + + const data = await response.json(); + + if (!response.ok) { + throw new Error( + data.message || data.error || "Could not save comment." + ); + } + + setCommentText(""); + setCommentType("note"); + setCommentImage(null); + setCommentImagePreview(""); + + if (fileInputRef.current) { + fileInputRef.current.value = ""; + } + + await loadDetails(); + + if (onServiceUpdated) { + onServiceUpdated(); + } + } catch (error) { + setError(error.message || "Could not save comment."); + } finally { + setSavingComment(false); + } + } + + function handleBackdropClick(event) { + if (event.target === event.currentTarget) { + onClose(); + } + } + + return ( + <> +
+
+
+
+
+

+ {isMechanic ? "My Task" : "Service Details"} +

+ +
+ {service?.title || "Service"} +
+
+ + +
+ +
+ {error && ( +
+ {error} +
+ )} + + {loading ? ( +
+
+

Loading service details...

+
+ ) : !service ? ( +
Service not found.
+ ) : ( + <> +
+
+ + {STATUS_LABELS[service.status] || service.status} + + + + {PRIORITY_LABELS[service.priority] || + service.priority || + "Normal"} + +
+ +

+ {service.description || "No description provided."} +

+
+ +
+ +
+ + + + + + + + + + + + + + + +
+ + {service.observations && ( + <> +
+ +
+
Observations
+

{service.observations}

+
+ + )} + +
+ +
+
Comments ({comments.length})
+ + {comments.length === 0 ? ( +

No comments yet.

+ ) : ( +
+ {comments.map((comment) => ( +
+
+ + {comment.author_name || "Unknown author"} + + +

+ {formatDateTime(comment.created_at)} +

+
+ +

+ {COMMENT_TYPE_LABELS[comment.comment_type] || + comment.comment_type || + "Note"} +

+ +

{comment.comment}

+ + {comment.image_url && ( + Service comment + )} +
+ ))} +
+ )} +
+ +
+
+
Add Comment
+ +
+ + + +
+ +
+ + +