diff --git a/package-lock.json b/package-lock.json index 302ff5ac7..8a4a4b174 100644 --- a/package-lock.json +++ b/package-lock.json @@ -159,7 +159,7 @@ "version": "5.1.11", "resolved": "https://registry.npmjs.org/@asamuzakjp/css-color/-/css-color-5.1.11.tgz", "integrity": "sha512-KVw6qIiCTUQhByfTd78h2yD1/00waTmm9uy/R7Ck/ctUyAPj+AEDLkQIdJW0T8+qGgj3j5bpNKK7Q3G+LedJWg==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "@asamuzakjp/generational-cache": "^1.0.1", @@ -176,7 +176,7 @@ "version": "7.1.1", "resolved": "https://registry.npmjs.org/@asamuzakjp/dom-selector/-/dom-selector-7.1.1.tgz", "integrity": "sha512-67RZDnYRc8H/8MLDgQCDE//zoqVFwajkepHZgmXrbwybzXOEwOWGPYGmALYl9J2DOLfFPPs6kKCqmbzV895hTQ==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "@asamuzakjp/generational-cache": "^1.0.1", @@ -193,7 +193,7 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/@asamuzakjp/generational-cache/-/generational-cache-1.0.1.tgz", "integrity": "sha512-wajfB8KqzMCN2KGNFdLkReeHncd0AslUSrvHVvvYWuU8ghncRJoA50kT3zP9MVL0+9g4/67H+cdvBskj9THPzg==", - "devOptional": true, + "dev": true, "license": "MIT", "engines": { "node": "^20.19.0 || ^22.12.0 || >=24.0.0" @@ -203,7 +203,7 @@ "version": "2.3.9", "resolved": "https://registry.npmjs.org/@asamuzakjp/nwsapi/-/nwsapi-2.3.9.tgz", "integrity": "sha512-n8GuYSrI9bF7FFZ/SjhwevlHc8xaVlb/7HmHelnc/PZXBD2ZR49NnN9sMMuDdEGPeeRQ5d0hqlSlEpgCX3Wl0Q==", - "devOptional": true, + "dev": true, "license": "MIT" }, "node_modules/@babel/code-frame": { @@ -289,7 +289,7 @@ "version": "2.4.2", "resolved": "https://registry.npmjs.org/@bramus/specificity/-/specificity-2.4.2.tgz", "integrity": "sha512-ctxtJ/eA+t+6q2++vj5j7FYX3nRu311q1wfYH3xjlLOsczhlhxAg2FWNUXhpGvAw3BWo1xBcvOV6/YLc2r5FJw==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "css-tree": "^3.0.0" @@ -308,7 +308,7 @@ "version": "6.0.2", "resolved": "https://registry.npmjs.org/@csstools/color-helpers/-/color-helpers-6.0.2.tgz", "integrity": "sha512-LMGQLS9EuADloEFkcTBR3BwV/CGHV7zyDxVRtVDTwdI2Ca4it0CCVTT9wCkxSgokjE5Ho41hEPgb8OEUwoXr6Q==", - "devOptional": true, + "dev": true, "funding": [ { "type": "github", @@ -328,7 +328,7 @@ "version": "3.2.1", "resolved": "https://registry.npmjs.org/@csstools/css-calc/-/css-calc-3.2.1.tgz", "integrity": "sha512-DtdHlgXh5ZkA43cwBcAm+huzgJiwx3ZTWVjBs94kwz2xKqSimDA3lBgCjphYgwgVUMWatSM0pDd8TILB1yrVVg==", - "devOptional": true, + "dev": true, "funding": [ { "type": "github", @@ -352,7 +352,7 @@ "version": "4.1.1", "resolved": "https://registry.npmjs.org/@csstools/css-color-parser/-/css-color-parser-4.1.1.tgz", "integrity": "sha512-eZ5XOtyhK+mggRafYUWzA0tvaYOFgdY8AkgQiCJF9qNAePnUo/zmsqqYubBBb3sQ8uNUaSKTY9s9klfRaAXL0g==", - "devOptional": true, + "dev": true, "funding": [ { "type": "github", @@ -380,7 +380,7 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/@csstools/css-parser-algorithms/-/css-parser-algorithms-4.0.0.tgz", "integrity": "sha512-+B87qS7fIG3L5h3qwJ/IFbjoVoOe/bpOdh9hAjXbvx0o8ImEmUsGXN0inFOnk2ChCFgqkkGFQ+TpM5rbhkKe4w==", - "devOptional": true, + "dev": true, "funding": [ { "type": "github", @@ -403,7 +403,7 @@ "version": "1.1.4", "resolved": "https://registry.npmjs.org/@csstools/css-syntax-patches-for-csstree/-/css-syntax-patches-for-csstree-1.1.4.tgz", "integrity": "sha512-wgsqt92b7C7tQhIdPNxj0n9zuUbQlvAuI1exyzeNrOKOi62SD7ren8zqszmpVREjAOqg8cD2FqYhQfAuKjk4sw==", - "devOptional": true, + "dev": true, "funding": [ { "type": "github", @@ -428,7 +428,7 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/@csstools/css-tokenizer/-/css-tokenizer-4.0.0.tgz", "integrity": "sha512-QxULHAm7cNu72w97JUNCBFODFaXpbDg+dP8b/oWFAZ2MTRppA3U00Y2L1HqaS4J6yBqxwa/Y3nMBaxVKbB/NsA==", - "devOptional": true, + "dev": true, "funding": [ { "type": "github", @@ -1155,7 +1155,7 @@ "version": "1.15.0", "resolved": "https://registry.npmjs.org/@exodus/bytes/-/bytes-1.15.0.tgz", "integrity": "sha512-UY0nlA+feH81UGSHv92sLEPLCeZFjXOuHhrIo0HQydScuQc8s0A7kL/UdgwgDq8g8ilksmuoF35YVTNphV2aBQ==", - "devOptional": true, + "dev": true, "license": "MIT", "engines": { "node": "^20.19.0 || ^22.12.0 || >=24.0.0" @@ -4127,13 +4127,6 @@ "integrity": "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==", "license": "MIT" }, - "node_modules/@types/dom-mediacapture-record": { - "version": "1.0.22", - "resolved": "https://registry.npmjs.org/@types/dom-mediacapture-record/-/dom-mediacapture-record-1.0.22.tgz", - "integrity": "sha512-mUMZLK3NvwRLcAAT9qmcK+9p7tpU2FHdDsntR3YI4+GY88XrgG4XiE7u1Q2LAN2/FZOz/tdMDC3GQCR4T8nFuw==", - "license": "MIT", - "peer": true - }, "node_modules/@types/dompurify": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/@types/dompurify/-/dompurify-3.2.0.tgz", @@ -4272,7 +4265,7 @@ "version": "25.6.2", "resolved": "https://registry.npmjs.org/@types/node/-/node-25.6.2.tgz", "integrity": "sha512-sokuT28dxf9JT5Kady1fsXOvI4HVpjZa95NKT5y9PNTIrs2AsobR4GFAA90ZG8M+nxVRLysCXsVj6eGC7Vbrlw==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "undici-types": "~7.19.0" @@ -4282,7 +4275,7 @@ "version": "7.19.2", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.19.2.tgz", "integrity": "sha512-qYVnV5OEm2AW8cJMCpdV20CDyaN3g0AjDlOGf1OW4iaDEx8MwdtChUp4zu4H0VP3nDRF/8RKWH+IPp9uW0YGZg==", - "devOptional": true, + "dev": true, "license": "MIT" }, "node_modules/@types/nprogress": { @@ -4302,6 +4295,7 @@ "version": "15.7.15", "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.15.tgz", "integrity": "sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw==", + "dev": true, "license": "MIT" }, "node_modules/@types/raf": { @@ -4315,6 +4309,7 @@ "version": "18.3.28", "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.28.tgz", "integrity": "sha512-z9VXpC7MWrhfWipitjNdgCauoMLRdIILQsAEV+ZesIzBq/oUlxk0m3ApZuMFCXdnS4U7KrI+l3WRUEGQ8K1QKw==", + "dev": true, "license": "MIT", "dependencies": { "@types/prop-types": "*", @@ -4325,7 +4320,7 @@ "version": "18.3.7", "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.3.7.tgz", "integrity": "sha512-MEe3UeoENYVFXzoXEWsvcpg6ZvlrFNlOQ7EOsvhI3CfAXwzPfO8Qwuxd40nepsYKqyyVQnTdEfv68q91yLcKrQ==", - "devOptional": true, + "dev": true, "license": "MIT", "peerDependencies": { "@types/react": "^18.0.0" @@ -5183,7 +5178,7 @@ "version": "1.0.3", "resolved": "https://registry.npmjs.org/bidi-js/-/bidi-js-1.0.3.tgz", "integrity": "sha512-RKshQI1R3YQ+n9YJz2QQ147P66ELpa1FQEg20Dk8oW9t2KgLbpDLLp9aGZ7y8WHSshDknG0bknqGw5/tyCs5tw==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "require-from-string": "^2.0.2" @@ -5973,7 +5968,7 @@ "version": "3.2.1", "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-3.2.1.tgz", "integrity": "sha512-X7sjQzceUhu1u7Y/ylrRZFU2FS6LRiFVp6rKLPg23y3x3c3DOKAwuXGDp+PAGjh6CSnCjYeAul8pcT8bAl+lSA==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "mdn-data": "2.27.1", @@ -6139,7 +6134,7 @@ "version": "7.0.0", "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-7.0.0.tgz", "integrity": "sha512-23XHcCF+coGYevirZceTVD7NdJOqVn+49IHyxgszm+JIiHLoB2TkmPtsYkNWT1pvRSGkc35L6NHs0yHkN2SumA==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "whatwg-mimetype": "^5.0.0", @@ -6231,7 +6226,7 @@ "version": "10.6.0", "resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.6.0.tgz", "integrity": "sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==", - "devOptional": true, + "dev": true, "license": "MIT" }, "node_modules/decimal.js-light": { @@ -6495,7 +6490,7 @@ "version": "8.0.0", "resolved": "https://registry.npmjs.org/entities/-/entities-8.0.0.tgz", "integrity": "sha512-zwfzJecQ/Uej6tusMqwAqU/6KL2XaB2VZ2Jg54Je6ahNBGNH6Ek6g3jjNCF0fG9EWQKGZNddNjU5F1ZQn/sBnA==", - "devOptional": true, + "dev": true, "license": "BSD-2-Clause", "engines": { "node": ">=20.19.0" @@ -7574,7 +7569,7 @@ "version": "4.14.0", "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.14.0.tgz", "integrity": "sha512-yTb+8DXzDREzgvYmh6s9vHsSVCHeC0G3PI5bEXNBHtmshPnO+S5O7qgLEOn0I5QvMy6kpZN8K1NKGyilLb93wA==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "resolve-pkg-maps": "^1.0.0" @@ -7842,7 +7837,7 @@ "version": "6.0.0", "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-6.0.0.tgz", "integrity": "sha512-CV9TW3Y3f8/wT0BRFc1/KAVQ3TUHiXmaAb6VW9vtiMFf7SLoMd1PdAc4W3KFOFETBJUb90KatHqlsZMWV+R9Gg==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "@exodus/bytes": "^1.6.0" @@ -8398,7 +8393,7 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz", "integrity": "sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==", - "devOptional": true, + "dev": true, "license": "MIT" }, "node_modules/is-regex": { @@ -8993,7 +8988,7 @@ "version": "29.1.1", "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-29.1.1.tgz", "integrity": "sha512-ECi4Fi2f7BdJtUKTflYRTiaMxIB0O6zfR1fX0GXpUrf6flp8QIYn1UT20YQqdSOfk2dfkCwS8LAFoJDEppNK5Q==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "@asamuzakjp/css-color": "^5.1.11", @@ -9034,7 +9029,7 @@ "version": "11.3.6", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.3.6.tgz", "integrity": "sha512-Gf/KoL3C/MlI7Bt0PGI9I+TeTC/I6r/csU58N4BSNc4lppLBeKsOdFYkK+dX0ABDUMJNfCHTyPpzwwO21Awd3A==", - "devOptional": true, + "dev": true, "license": "BlueOak-1.0.0", "engines": { "node": "20 || >=22" @@ -9766,7 +9761,7 @@ "version": "2.27.1", "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.27.1.tgz", "integrity": "sha512-9Yubnt3e8A0OKwxYSXyhLymGW4sCufcLG6VdiDdUGVkPhpqLxlvP5vl1983gQjJl3tqbrM731mjaZaP68AgosQ==", - "devOptional": true, + "dev": true, "license": "CC0-1.0" }, "node_modules/merge2": { @@ -10791,7 +10786,7 @@ "version": "8.0.1", "resolved": "https://registry.npmjs.org/parse5/-/parse5-8.0.1.tgz", "integrity": "sha512-z1e/HMG90obSGeidlli3hj7cbocou0/wa5HacvI3ASx34PecNjNQeaHNo5WIZpWofN9kgkqV1q5YvXe3F0FoPw==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "entities": "^8.0.0" @@ -11868,7 +11863,7 @@ "version": "2.0.2", "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", - "devOptional": true, + "dev": true, "license": "MIT", "engines": { "node": ">=0.10.0" @@ -11910,7 +11905,7 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz", "integrity": "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==", - "devOptional": true, + "dev": true, "license": "MIT", "funding": { "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1" @@ -12166,7 +12161,7 @@ "version": "6.0.0", "resolved": "https://registry.npmjs.org/saxes/-/saxes-6.0.0.tgz", "integrity": "sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==", - "devOptional": true, + "dev": true, "license": "ISC", "dependencies": { "xmlchars": "^2.2.0" @@ -12872,7 +12867,7 @@ "version": "3.2.4", "resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz", "integrity": "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==", - "devOptional": true, + "dev": true, "license": "MIT" }, "node_modules/tailwind-merge": { @@ -13065,7 +13060,7 @@ "version": "7.0.30", "resolved": "https://registry.npmjs.org/tldts/-/tldts-7.0.30.tgz", "integrity": "sha512-ELrFxuqsDdHUwoh0XxDbxuLD3Wnz49Z57IFvTtvWy1hJdcMZjXLIuonjilCiWHlT2GbE4Wlv1wKVTzDFnXH1aw==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "tldts-core": "^7.0.30" @@ -13078,7 +13073,7 @@ "version": "7.0.30", "resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-7.0.30.tgz", "integrity": "sha512-uiHN8PIB1VmWyS98eZYja4xzlYqeFZVjb4OuYlJQnZAuJhMw4PbKQOKgHKhBdJR3FE/t5mUQ1Kd80++B+qhD1Q==", - "devOptional": true, + "dev": true, "license": "MIT" }, "node_modules/to-regex-range": { @@ -13097,7 +13092,7 @@ "version": "6.0.1", "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-6.0.1.tgz", "integrity": "sha512-LktZQb3IeoUWB9lqR5EWTHgW/VTITCXg4D21M+lvybRVdylLrRMnqaIONLVb5mav8vM19m44HIcGq4qASeu2Qw==", - "devOptional": true, + "dev": true, "license": "BSD-3-Clause", "dependencies": { "tldts": "^7.0.5" @@ -13110,7 +13105,7 @@ "version": "6.0.0", "resolved": "https://registry.npmjs.org/tr46/-/tr46-6.0.0.tgz", "integrity": "sha512-bLVMLPtstlZ4iMQHpFHTR7GAGj2jxi8Dg0s2h2MafAE4uSWF98FC/3MomU51iQAMf8/qDUbKWf5GxuvvVcXEhw==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "punycode": "^2.3.1" @@ -13167,7 +13162,7 @@ "version": "4.21.0", "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.21.0.tgz", "integrity": "sha512-5C1sg4USs1lfG0GFb2RLXsdpXqBSEhAaA/0kPL01wxzpMqLILNxIxIOKiILz+cdg/pLnOUxFYOR5yhHU666wbw==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "esbuild": "~0.27.0", @@ -13190,6 +13185,7 @@ "cpu": [ "ppc64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -13206,6 +13202,7 @@ "cpu": [ "arm" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -13222,6 +13219,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -13238,6 +13236,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -13254,6 +13253,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -13270,6 +13270,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -13286,6 +13287,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -13302,6 +13304,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -13318,6 +13321,7 @@ "cpu": [ "arm" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -13334,6 +13338,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -13350,6 +13355,7 @@ "cpu": [ "ia32" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -13366,6 +13372,7 @@ "cpu": [ "loong64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -13382,6 +13389,7 @@ "cpu": [ "mips64el" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -13398,6 +13406,7 @@ "cpu": [ "ppc64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -13414,6 +13423,7 @@ "cpu": [ "riscv64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -13430,6 +13440,7 @@ "cpu": [ "s390x" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -13446,6 +13457,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -13462,6 +13474,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -13478,6 +13491,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -13494,6 +13508,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -13510,6 +13525,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -13526,6 +13542,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -13542,6 +13559,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -13558,6 +13576,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -13574,6 +13593,7 @@ "cpu": [ "ia32" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -13590,6 +13610,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -13603,7 +13624,7 @@ "version": "0.27.7", "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.7.tgz", "integrity": "sha512-IxpibTjyVnmrIQo5aqNpCgoACA/dTKLTlhMHihVHhdkxKyPO1uBBthumT0rdHmcsk9uMonIWS0m4FljWzILh3w==", - "devOptional": true, + "dev": true, "hasInstallScript": true, "license": "MIT", "bin": { @@ -13645,6 +13666,7 @@ "version": "2.3.3", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, "hasInstallScript": true, "license": "MIT", "optional": true, @@ -13754,6 +13776,7 @@ "version": "5.4.5", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.4.5.tgz", "integrity": "sha512-vcI4UpRgg81oIRUFwR0WSIHKt11nJ7SAVlYNIu+QpqeyXP+gpQJy/Z4+F0aGxSE4MqwjyXvW/TzgkLAx2AGHwQ==", + "dev": true, "license": "Apache-2.0", "bin": { "tsc": "bin/tsc", @@ -13785,7 +13808,7 @@ "version": "7.25.0", "resolved": "https://registry.npmjs.org/undici/-/undici-7.25.0.tgz", "integrity": "sha512-xXnp4kTyor2Zq+J1FfPI6Eq3ew5h6Vl0F/8d9XU5zZQf1tX9s2Su1/3PiMmUANFULpmksxkClamIZcaUqryHsQ==", - "devOptional": true, + "dev": true, "license": "MIT", "engines": { "node": ">=20.18.1" @@ -14246,7 +14269,7 @@ "version": "5.0.0", "resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-5.0.0.tgz", "integrity": "sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "xml-name-validator": "^5.0.0" @@ -14259,7 +14282,7 @@ "version": "8.0.1", "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-8.0.1.tgz", "integrity": "sha512-BMhLD/Sw+GbJC21C/UgyaZX41nPt8bUTg+jWyDeg7e7YN4xOM05YPSIXceACnXVtqyEw/LMClUQMtMZ+PGGpqQ==", - "devOptional": true, + "dev": true, "license": "BSD-2-Clause", "engines": { "node": ">=20" @@ -14282,7 +14305,7 @@ "version": "5.0.0", "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-5.0.0.tgz", "integrity": "sha512-sXcNcHOC51uPGF0P/D4NVtrkjSU2fNsm9iog4ZvZJsL3rjoDAzXZhkm2MWt1y+PUdggKAYVoMAIYcs78wJ51Cw==", - "devOptional": true, + "dev": true, "license": "MIT", "engines": { "node": ">=20" @@ -14292,7 +14315,7 @@ "version": "16.0.1", "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-16.0.1.tgz", "integrity": "sha512-1to4zXBxmXHV3IiSSEInrreIlu02vUOvrhxJJH5vcxYTBDAx51cqZiKdyTxlecdKNSjj8EcxGBxNf6Vg+945gw==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "@exodus/bytes": "^1.11.0", @@ -14565,7 +14588,7 @@ "version": "5.0.0", "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-5.0.0.tgz", "integrity": "sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==", - "devOptional": true, + "dev": true, "license": "Apache-2.0", "engines": { "node": ">=18" @@ -14575,7 +14598,7 @@ "version": "2.2.0", "resolved": "https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz", "integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==", - "devOptional": true, + "dev": true, "license": "MIT" }, "node_modules/y18n": { diff --git a/src/components/admin/DiscountApprovalQueue.tsx b/src/components/admin/DiscountApprovalQueue.tsx index a4fb90327..3e86a24aa 100644 --- a/src/components/admin/DiscountApprovalQueue.tsx +++ b/src/components/admin/DiscountApprovalQueue.tsx @@ -105,7 +105,7 @@ export function DiscountApprovalQueue() {

Cliente: {quote?.client_name || quote?.client_company || "—"} - {quote?.total !== null && <> · Total: R$ {Number(quote.total).toFixed(2)}} + {quote !== undefined && quote.total !== null && <> · Total: R$ {Number(quote.total).toFixed(2)}}

{hasMarkup && (
diff --git a/src/components/admin/MockupPromptManager.tsx b/src/components/admin/MockupPromptManager.tsx index c8bee6b13..3561b4278 100644 --- a/src/components/admin/MockupPromptManager.tsx +++ b/src/components/admin/MockupPromptManager.tsx @@ -2,26 +2,45 @@ * MockupPromptManager — Orchestrator (refactored) * Sub-components in ./mockup-prompts/ */ -import { useState, useEffect } from "react"; -import { supabase } from "@/integrations/supabase/client"; -import { toast } from "sonner"; -import { useAuth } from "@/contexts/AuthContext"; -import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"; -import { Button } from "@/components/ui/button"; -import { Badge } from "@/components/ui/badge"; -import { Accordion, AccordionContent, AccordionItem, AccordionTrigger } from "@/components/ui/accordion"; -import { Loader2, Plus, Brain, Sparkles } from "lucide-react"; - -import { PromptEditor, type PromptConfig } from "./mockup-prompts/PromptEditor"; -import { HistoryDialog, TestDialog, AddTechniqueDialog, type PromptHistory, type Technique } from "./mockup-prompts/PromptDialogs"; +import { useState, useEffect } from 'react'; +import { supabase } from '@/integrations/supabase/client'; +// eslint-disable-next-line @typescript-eslint/no-explicit-any +const anySupabase = supabase as any; +import { toast } from 'sonner'; +import { useAuth } from '@/contexts/AuthContext'; +import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'; +import { Button } from '@/components/ui/button'; +import { Badge } from '@/components/ui/badge'; +import { + Accordion, + AccordionContent, + AccordionItem, + AccordionTrigger, +} from '@/components/ui/accordion'; +import { Loader2, Plus, Brain, Sparkles } from 'lucide-react'; + +import { PromptEditor, type PromptConfig } from './mockup-prompts/PromptEditor'; +import { + HistoryDialog, + TestDialog, + AddTechniqueDialog, + type PromptHistory, + type Technique, +} from './mockup-prompts/PromptDialogs'; const VARIABLE_REFERENCE = [ - ["{{productName}}", "Nome do produto"], ["{{techniquePrompt}}", "Descrição da técnica"], - ["{{positionX}}", "Posição X (%)"], ["{{positionY}}", "Posição Y (%)"], - ["{{horizontalPos}}", "Descrição horizontal"], ["{{verticalPos}}", "Descrição vertical"], - ["{{positionDesc}}", "Descrição posição completa"], ["{{sizeDesc}}", "Tamanho (small/medium/large)"], - ["{{logoWidthCm}}", "Largura do logo (cm)"], ["{{logoHeightCm}}", "Altura do logo (cm)"], - ["{{scaleInstruction}}", "Instrução de escala"], ["{{rotationInstruction}}", "Instrução de rotação"], + ['{{productName}}', 'Nome do produto'], + ['{{techniquePrompt}}', 'Descrição da técnica'], + ['{{positionX}}', 'Posição X (%)'], + ['{{positionY}}', 'Posição Y (%)'], + ['{{horizontalPos}}', 'Descrição horizontal'], + ['{{verticalPos}}', 'Descrição vertical'], + ['{{positionDesc}}', 'Descrição posição completa'], + ['{{sizeDesc}}', 'Tamanho (small/medium/large)'], + ['{{logoWidthCm}}', 'Largura do logo (cm)'], + ['{{logoHeightCm}}', 'Altura do logo (cm)'], + ['{{scaleInstruction}}', 'Instrução de escala'], + ['{{rotationInstruction}}', 'Instrução de rotação'], ]; export function MockupPromptManager() { @@ -30,132 +49,289 @@ export function MockupPromptManager() { const [techniques, setTechniques] = useState([]); const [isLoading, setIsLoading] = useState(true); const [savingId, setSavingId] = useState(null); - const [editedPrompts, setEditedPrompts] = useState>({}); + const [editedPrompts, setEditedPrompts] = useState< + Record + >({}); const [changeNotes, setChangeNotes] = useState>({}); - const [historyDialog, setHistoryDialog] = useState<{ configId: string; label: string } | null>(null); + const [historyDialog, setHistoryDialog] = useState<{ configId: string; label: string } | null>( + null, + ); const [history, setHistory] = useState([]); const [historyLoading, setHistoryLoading] = useState(false); const [addTechniqueDialog, setAddTechniqueDialog] = useState(false); - const [selectedTechnique, setSelectedTechnique] = useState(""); + const [selectedTechnique, setSelectedTechnique] = useState(''); const [testDialog, setTestDialog] = useState<{ configId: string; label: string } | null>(null); const [testResult, setTestResult] = useState(null); const [isTesting, setIsTesting] = useState(false); - useEffect(() => { fetchAll(); }, []); + useEffect(() => { + fetchAll(); + }, []); const fetchAll = async () => { setIsLoading(true); try { const [cr, tr] = await Promise.all([ - supabase.from("mockup_prompt_configs").select("*").order("config_key"), - supabase.from("personalization_techniques").select("id, name, code").eq("is_active", true), + supabase.from('mockup_prompt_configs').select('*').order('config_key'), + anySupabase.from('personalization_techniques').select('id, name, code').eq('is_active', true), ]); - if (cr.error) throw cr.error; if (tr.error) throw tr.error; - setConfigs((cr.data || []) as PromptConfig[]); setTechniques(tr.data || []); - } catch (err: unknown) { toast.error("Erro ao carregar configurações", { description: err instanceof Error ? err.message : undefined }); } - finally { setIsLoading(false); } + if (cr.error) throw cr.error; + if (tr.error) throw tr.error; + setConfigs((cr.data || []) as PromptConfig[]); + setTechniques((tr.data || []) as unknown as Technique[]); + } catch (err: unknown) { + toast.error('Erro ao carregar configurações', { + description: err instanceof Error ? err.message : undefined, + }); + } finally { + setIsLoading(false); + } }; - const getEdited = (c: PromptConfig) => editedPrompts[c.id] || { prompt_text: c.prompt_text, ai_model: c.ai_model }; - const hasChanges = (c: PromptConfig) => { const e = editedPrompts[c.id]; return !!e && (e.prompt_text !== c.prompt_text || e.ai_model !== c.ai_model); }; + const getEdited = (c: PromptConfig) => + editedPrompts[c.id] || { prompt_text: c.prompt_text, ai_model: c.ai_model }; + const hasChanges = (c: PromptConfig) => { + const e = editedPrompts[c.id]; + return !!e && (e.prompt_text !== c.prompt_text || e.ai_model !== c.ai_model); + }; const handleSave = async (config: PromptConfig) => { - const edited = getEdited(config); if (!hasChanges(config)) return; + const edited = getEdited(config); + if (!hasChanges(config)) return; setSavingId(config.id); try { - await supabase.from("mockup_prompt_history").insert({ config_id: config.id, version: config.version, prompt_text: config.prompt_text, ai_model: config.ai_model, changed_by: user?.id, change_notes: changeNotes[config.id] || null }); - const { error } = await supabase.from("mockup_prompt_configs").update({ prompt_text: edited.prompt_text, ai_model: edited.ai_model, version: config.version + 1, updated_by: user?.id }).eq("id", config.id); + await supabase + .from('mockup_prompt_history') + .insert({ + config_id: config.id, + config_key: config.config_key, + version: config.version, + new_prompt: edited.prompt_text, + old_prompt: config.prompt_text, + ai_model: config.ai_model, + changed_by: user?.id, + change_notes: changeNotes[config.id] || null, + }); + const { error } = await supabase + .from('mockup_prompt_configs') + .update({ + prompt_text: edited.prompt_text, + ai_model: edited.ai_model, + version: config.version + 1, + }) + .eq('id', config.id); if (error) throw error; toast.success(`Prompt "${config.label}" salvo (v${config.version + 1})`); - setEditedPrompts(p => { const n = { ...p }; delete n[config.id]; return n; }); - setChangeNotes(p => { const n = { ...p }; delete n[config.id]; return n; }); + setEditedPrompts((p) => { + const n = { ...p }; + delete n[config.id]; + return n; + }); + setChangeNotes((p) => { + const n = { ...p }; + delete n[config.id]; + return n; + }); fetchAll(); - } catch (err: unknown) { toast.error("Erro ao salvar", { description: err instanceof Error ? err.message : undefined }); } - finally { setSavingId(null); } + } catch (err: unknown) { + toast.error('Erro ao salvar', { + description: err instanceof Error ? err.message : undefined, + }); + } finally { + setSavingId(null); + } }; const openHistory = async (configId: string, label: string) => { - setHistoryDialog({ configId, label }); setHistoryLoading(true); + setHistoryDialog({ configId, label }); + setHistoryLoading(true); try { - const { data, error } = await supabase.from("mockup_prompt_history").select("*").eq("config_id", configId).order("version", { ascending: false }); - if (error) throw error; setHistory((data || []) as PromptHistory[]); - } catch { toast.error("Erro ao carregar histórico"); } - finally { setHistoryLoading(false); } + const { data, error } = await supabase + .from('mockup_prompt_history') + .select('*') + .eq('config_id', configId) + .order('version', { ascending: false }); + if (error) throw error; + setHistory((data || []) as unknown as PromptHistory[]); + } catch { + toast.error('Erro ao carregar histórico'); + } finally { + setHistoryLoading(false); + } }; const restoreVersion = (entry: PromptHistory) => { if (!historyDialog) return; - setEditedPrompts(p => ({ ...p, [historyDialog.configId]: { prompt_text: entry.prompt_text, ai_model: entry.ai_model } })); - setChangeNotes(p => ({ ...p, [historyDialog.configId]: `Restaurado da versão ${entry.version}` })); - setHistoryDialog(null); toast.info(`Versão ${entry.version} carregada. Clique em Salvar para confirmar.`); + setEditedPrompts((p) => ({ + ...p, + [historyDialog.configId]: { prompt_text: entry.prompt_text, ai_model: entry.ai_model }, + })); + setChangeNotes((p) => ({ + ...p, + [historyDialog.configId]: `Restaurado da versão ${entry.version}`, + })); + setHistoryDialog(null); + toast.info(`Versão ${entry.version} carregada. Clique em Salvar para confirmar.`); }; const handleAddTechnique = async () => { - const tech = techniques.find(t => t.id === selectedTechnique); if (!tech) return; - if (configs.find(c => c.config_key === `technique_${tech.id}`)) { toast.error("Já existe um prompt para essa técnica"); return; } + const tech = techniques.find((t) => t.id === selectedTechnique); + if (!tech) return; + if (configs.find((c) => c.config_key === `technique_${tech.id}`)) { + toast.error('Já existe um prompt para essa técnica'); + return; + } try { - const { error } = await supabase.from("mockup_prompt_configs").insert({ config_key: `technique_${tech.id}`, label: `Prompt: ${tech.name}`, prompt_text: `Apply the logo using ${tech.name} technique. The result should look realistic with proper ${tech.name.toLowerCase()} texture and finish on the product surface.`, ai_model: "google/gemini-2.5-flash-image-preview", technique_id: tech.id, created_by: user?.id }); - if (error) throw error; toast.success(`Prompt para "${tech.name}" criado`); setAddTechniqueDialog(false); setSelectedTechnique(""); fetchAll(); - } catch (err: unknown) { toast.error("Erro ao criar prompt", { description: err instanceof Error ? err.message : undefined }); } + const { error } = await supabase + .from('mockup_prompt_configs') + .insert({ + config_key: `technique_${tech.id}`, + label: `Prompt: ${tech.name}`, + prompt_text: `Apply the logo using ${tech.name} technique. The result should look realistic with proper ${tech.name.toLowerCase()} texture and finish on the product surface.`, + ai_model: 'google/gemini-2.5-flash-image-preview', + technique_id: tech.id, + }); + if (error) throw error; + toast.success(`Prompt para "${tech.name}" criado`); + setAddTechniqueDialog(false); + setSelectedTechnique(''); + fetchAll(); + } catch (err: unknown) { + toast.error('Erro ao criar prompt', { + description: err instanceof Error ? err.message : undefined, + }); + } }; const handleTest = (config: PromptConfig) => { const edited = getEdited(config); - setTestDialog({ configId: config.id, label: config.label }); setIsTesting(true); - const result = edited.prompt_text.replace(/\{\{productName\}\}/g, "Caneca Cerâmica Branca").replace(/\{\{techniquePrompt\}\}/g, "applied as sublimation print") - .replace(/\{\{positionX\}\}/g, "50").replace(/\{\{positionY\}\}/g, "50").replace(/\{\{horizontalPos\}\}/g, "horizontally centered") - .replace(/\{\{verticalPos\}\}/g, "vertically centered").replace(/\{\{positionDesc\}\}/g, "vertically centered, horizontally centered") - .replace(/\{\{sizeDesc\}\}/g, "medium-sized").replace(/\{\{logoWidthCm\}\}/g, "5").replace(/\{\{logoHeightCm\}\}/g, "5") - .replace(/\{\{scaleInstruction\}\}/g, "").replace(/\{\{rotationInstruction\}\}/g, ""); - setTestResult(result); setIsTesting(false); + setTestDialog({ configId: config.id, label: config.label }); + setIsTesting(true); + const result = edited.prompt_text + .replace(/\{\{productName\}\}/g, 'Caneca Cerâmica Branca') + .replace(/\{\{techniquePrompt\}\}/g, 'applied as sublimation print') + .replace(/\{\{positionX\}\}/g, '50') + .replace(/\{\{positionY\}\}/g, '50') + .replace(/\{\{horizontalPos\}\}/g, 'horizontally centered') + .replace(/\{\{verticalPos\}\}/g, 'vertically centered') + .replace(/\{\{positionDesc\}\}/g, 'vertically centered, horizontally centered') + .replace(/\{\{sizeDesc\}\}/g, 'medium-sized') + .replace(/\{\{logoWidthCm\}\}/g, '5') + .replace(/\{\{logoHeightCm\}\}/g, '5') + .replace(/\{\{scaleInstruction\}\}/g, '') + .replace(/\{\{rotationInstruction\}\}/g, ''); + setTestResult(result); + setIsTesting(false); }; - const setPromptField = (id: string, config: PromptConfig, field: 'prompt_text' | 'ai_model', val: string) => { - setEditedPrompts(p => ({ ...p, [id]: { ...getEdited(config), [field]: val } })); + const setPromptField = ( + id: string, + config: PromptConfig, + field: 'prompt_text' | 'ai_model', + val: string, + ) => { + setEditedPrompts((p) => ({ ...p, [id]: { ...getEdited(config), [field]: val } })); }; - const mainPrompt = configs.find(c => c.config_key === "main_prompt"); - const techniquePrompts = configs.filter(c => c.config_key.startsWith("technique_")); - const techniquesWithPrompt = new Set(techniquePrompts.map(c => c.technique_id)); - const techniquesWithoutPrompt = techniques.filter(t => !techniquesWithPrompt.has(t.id)); + const mainPrompt = configs.find((c) => c.config_key === 'main_prompt'); + const techniquePrompts = configs.filter((c) => c.config_key.startsWith('technique_')); + const techniquesWithPrompt = new Set(techniquePrompts.map((c) => c.technique_id)); + const techniquesWithoutPrompt = techniques.filter((t) => !techniquesWithPrompt.has(t.id)); - if (isLoading) return
; + if (isLoading) + return ( +
+ +
+ ); return (
-
Gestão de Prompts - Gerador de MockupsEdite os prompts enviados para a IA, selecione o modelo e acompanhe versões
- +
+ +
+ Gestão de Prompts - Gerador de Mockups + + Edite os prompts enviados para a IA, selecione o modelo e acompanhe versões + +
+
+
{mainPrompt && ( - setPromptField(mainPrompt.id, mainPrompt, 'prompt_text', v)} - onChangeModel={v => setPromptField(mainPrompt.id, mainPrompt, 'ai_model', v)} onChangeNote={v => setChangeNotes(p => ({ ...p, [mainPrompt.id]: v }))} - onSave={() => handleSave(mainPrompt)} onHistory={() => openHistory(mainPrompt.id, mainPrompt.label)} onTest={() => handleTest(mainPrompt)} isMain /> + setPromptField(mainPrompt.id, mainPrompt, 'prompt_text', v)} + onChangeModel={(v) => setPromptField(mainPrompt.id, mainPrompt, 'ai_model', v)} + onChangeNote={(v) => setChangeNotes((p) => ({ ...p, [mainPrompt.id]: v }))} + onSave={() => handleSave(mainPrompt)} + onHistory={() => openHistory(mainPrompt.id, mainPrompt.label)} + onTest={() => handleTest(mainPrompt)} + isMain + /> )} {techniquePrompts.length > 0 && ( - Prompts por Técnica - Prompts específicos que complementam o prompt principal para cada técnica + + + + Prompts por Técnica + + + Prompts específicos que complementam o prompt principal para cada técnica + + - {techniquePrompts.map(config => ( - + {techniquePrompts.map((config) => ( + -
{config.label}v{config.version} - {hasChanges(config) && Alterado}
+
+ {config.label} + + v{config.version} + + {hasChanges(config) && ( + + Alterado + + )} +
- setPromptField(config.id, config, 'prompt_text', v)} - onChangeModel={v => setPromptField(config.id, config, 'ai_model', v)} onChangeNote={v => setChangeNotes(p => ({ ...p, [config.id]: v }))} - onSave={() => handleSave(config)} onHistory={() => openHistory(config.id, config.label)} onTest={() => handleTest(config)} isMain={false} /> + setPromptField(config.id, config, 'prompt_text', v)} + onChangeModel={(v) => setPromptField(config.id, config, 'ai_model', v)} + onChangeNote={(v) => setChangeNotes((p) => ({ ...p, [config.id]: v }))} + onSave={() => handleSave(config)} + onHistory={() => openHistory(config.id, config.label)} + onTest={() => handleTest(config)} + isMain={false} + />
))} @@ -165,21 +341,44 @@ export function MockupPromptManager() { )} - Variáveis Disponíveis + + Variáveis Disponíveis + -
+
{VARIABLE_REFERENCE.map(([key, desc]) => ( -
{key}{desc}
+
+ {key} + {desc} +
))}
- setAddTechniqueDialog(false)} /> - setHistoryDialog(null)} onRestore={restoreVersion} /> - setTestDialog(null)} /> + setAddTechniqueDialog(false)} + /> + setHistoryDialog(null)} + onRestore={restoreVersion} + /> + setTestDialog(null)} + />
); } diff --git a/src/components/admin/SellerDiscountLimitsPanel.tsx b/src/components/admin/SellerDiscountLimitsPanel.tsx index 7b25997bb..6a756c4e1 100644 --- a/src/components/admin/SellerDiscountLimitsPanel.tsx +++ b/src/components/admin/SellerDiscountLimitsPanel.tsx @@ -55,8 +55,9 @@ export function SellerDiscountLimitsPanel() { mutationFn: async ({ userId, percent }: { userId: string; percent: number }) => { const { data: u } = await supabase.auth.getUser(); if (!u.user) throw new Error("Não autenticado"); - const { error } = await supabase - .from("seller_discount_limits" as never) + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const { error } = await (supabase as any) + .from("seller_discount_limits") .upsert({ user_id: userId, max_discount_percent: percent, set_by: u.user.id }, { onConflict: "user_id" }); if (error) throw error; }, diff --git a/src/components/admin/TechniquesManager.tsx b/src/components/admin/TechniquesManager.tsx index c9e88ff8c..71f845678 100644 --- a/src/components/admin/TechniquesManager.tsx +++ b/src/components/admin/TechniquesManager.tsx @@ -34,7 +34,7 @@ export function TechniquesManager() { isLoading={isLoading} isRemoving={isRemoving} onToggleStatus={toggleStatus} - onUpdate={update} + onUpdate={update as (params: Record) => void} onRemove={remove} />
diff --git a/src/components/admin/connections/Bitrix24Tab.tsx b/src/components/admin/connections/Bitrix24Tab.tsx index 01cbfd679..ce00cf1cd 100644 --- a/src/components/admin/connections/Bitrix24Tab.tsx +++ b/src/components/admin/connections/Bitrix24Tab.tsx @@ -92,7 +92,7 @@ export function Bitrix24Tab() { {isTesting ? "Testando…" : "Testar conexão (crm.contact.fields)"} - + void list()} />
(null); const [extLoading, setExtLoading] = useState(false); const [extError, setExtError] = useState(null); diff --git a/src/components/admin/connections/KeysValidationTab.tsx b/src/components/admin/connections/KeysValidationTab.tsx index 34c48296f..81efa7f49 100644 --- a/src/components/admin/connections/KeysValidationTab.tsx +++ b/src/components/admin/connections/KeysValidationTab.tsx @@ -205,7 +205,7 @@ const STATUS_META: Record< }; export function KeysValidationTab() { - const { secrets, list, loading } = useSecretsManager(); + const { secrets, list, isLoading: loading } = useSecretsManager(); const [filter, setFilter] = useState(''); const [onlyIssues, setOnlyIssues] = useState(false); const [collapsed, setCollapsed] = useState>(new Set()); diff --git a/src/components/admin/connections/N8nTab.tsx b/src/components/admin/connections/N8nTab.tsx index 79a2c7f34..b404dc55f 100644 --- a/src/components/admin/connections/N8nTab.tsx +++ b/src/components/admin/connections/N8nTab.tsx @@ -80,7 +80,7 @@ export function N8nTab() { {isTesting ? "Testando…" : "Testar /healthz"} - + void list()} />
{ diff --git a/src/components/admin/products/ProductFormStepContent.tsx b/src/components/admin/products/ProductFormStepContent.tsx index 410c3a03f..3641e0c3c 100644 --- a/src/components/admin/products/ProductFormStepContent.tsx +++ b/src/components/admin/products/ProductFormStepContent.tsx @@ -10,7 +10,7 @@ import { Switch } from '@/components/ui/switch'; import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip'; import { Loader2, Package, Layers, Info, Wand2 } from 'lucide-react'; import { cn } from '@/lib/utils'; -import { SectionCard } from './ProductFormHelpers'; +import { SectionCard, type FormSectionProps } from './ProductFormHelpers'; import { CategoryCascadeSelector } from './CategoryCascadeSelector'; import { ProductSupplierSection } from './sections/ProductSupplierSection'; import { ProductInfoSection } from './sections/ProductInfoSection'; @@ -53,7 +53,7 @@ interface FormProps { setValue: UseFormSetValue; watch: UseFormWatch; errors: FieldErrors; - numericProps: (name: keyof ProductFormData) => object; + numericProps: (name: keyof ProductFormData) => Record; } interface StepContentProps { @@ -127,13 +127,13 @@ export function ProductFormStepContent({ primarySupplierName={formValues.brand || ''} /> [0])} + skuStatus={skuStatus as 'idle' | 'valid' | 'duplicate' | 'checking'} + duplicateName={duplicateName ?? ''} skuManuallyEdited={skuManuallyEdited} onSkuManualEdit={onSkuManualEdit} /> - + ); case 'commercial': @@ -155,12 +155,12 @@ export function ProductFormStepContent({ ); case 'packaging': - return ; + return ; case 'fiscal': return ( <> - + ); case 'content': @@ -191,7 +191,7 @@ export function ProductFormStepContent({ {isSeoGenerating ? 'Gerando...' : 'Preencher com IA'}
- + ); diff --git a/src/components/admin/products/hooks/useProductFormDraft.ts b/src/components/admin/products/hooks/useProductFormDraft.ts index 1f0a3ed6f..035d7b8f0 100644 --- a/src/components/admin/products/hooks/useProductFormDraft.ts +++ b/src/components/admin/products/hooks/useProductFormDraft.ts @@ -4,7 +4,7 @@ import { useCallback, useEffect, useRef, useState } from 'react'; import { type UseFormSetValue } from 'react-hook-form'; import { toast } from 'sonner'; -import type { ProductFormData } from './ProductFormSchema'; +import type { ProductFormData } from '../ProductFormSchema'; export function useProductFormDraft( productId: string | undefined, diff --git a/src/components/admin/products/hooks/useSkuValidation.ts b/src/components/admin/products/hooks/useSkuValidation.ts index ba1801296..a049395cd 100644 --- a/src/components/admin/products/hooks/useSkuValidation.ts +++ b/src/components/admin/products/hooks/useSkuValidation.ts @@ -22,16 +22,16 @@ export function useSkuValidation(currentSku: string, isEdit: boolean, originalSk try { const { fetchPromobrindProducts } = await import('@/lib/external-db'); const existing = await fetchPromobrindProducts({ search: currentSku, limit: 5 }); - const products = Array.isArray(existing) + const products = (Array.isArray(existing) ? existing - : (existing as Record).products || []; + : (existing as Record).products || []) as Array>; const dup = products.find( (p: Record) => (p.sku as string | undefined)?.toLowerCase() === currentSku.toLowerCase(), ); if (dup) { setStatus('duplicate'); - setDuplicateName(dup.name || ''); + setDuplicateName(String(dup.name || '')); } else { setStatus('valid'); setDuplicateName(''); diff --git a/src/components/admin/products/new-supplier/tabs/BasicDataTab.tsx b/src/components/admin/products/new-supplier/tabs/BasicDataTab.tsx index 9d894d8a4..a06282d82 100644 --- a/src/components/admin/products/new-supplier/tabs/BasicDataTab.tsx +++ b/src/components/admin/products/new-supplier/tabs/BasicDataTab.tsx @@ -2,12 +2,18 @@ import { Input } from '@/components/ui/input'; import { Label } from '@/components/ui/label'; import { Button } from '@/components/ui/button'; import { Switch } from '@/components/ui/switch'; -import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from '@/components/ui/select'; import { Loader2, ImagePlus, X, Search } from 'lucide-react'; import { maskCnpj, maskPhone, ESTADOS_BR } from '@/utils/masks'; import type { NewSupplierForm } from '../useNewSupplierForm'; -const fieldClass = "mt-1.5 h-9"; +const fieldClass = 'mt-1.5 h-9'; interface BasicDataTabProps { form: NewSupplierForm; @@ -20,34 +26,82 @@ export function BasicDataTab({ form }: BasicDataTabProps) {
{form.logoUrl ? ( -
- -Logo -
) : ( - )} - +
- ) => form.setTradingName(e.target.value)} placeholder="Ex: Asia Import" className={fieldClass} autoFocus /> + ) => + form.setTradingName(e.target.value) + } + placeholder="Ex: Asia Import" + className={fieldClass} + autoFocus + />
- - ) => form.setCode(e.target.value)} placeholder="Auto-gerado" className={`${fieldClass} font-mono uppercase`} /> + + ) => form.setCode(e.target.value)} + placeholder="Auto-gerado" + className={`${fieldClass} font-mono uppercase`} + />
{/* Razão Social */}
- - ) => form.setName(e.target.value)} placeholder="Ex: Asia Import Comércio LTDA" className={fieldClass} /> + + ) => form.setName(e.target.value)} + placeholder="Ex: Asia Import Comércio LTDA" + className={fieldClass} + />
{/* CNPJ + Inscrição Estadual */} @@ -55,16 +109,45 @@ export function BasicDataTab({ form }: BasicDataTabProps) {
- ) => { form.setCnpj(maskCnpj(e.target.value)); form.setCnpjError(''); }} placeholder="00.000.000/0000-00" className={`${fieldClass} font-mono flex-1 ${form.cnpjError ? 'border-destructive' : ''}`} maxLength={18} /> -
- {form.cnpjError &&

{form.cnpjError}

} + {form.cnpjError && ( +

{form.cnpjError}

+ )}
- ) => form.setInscricaoEstadual(e.target.value)} placeholder="Ex: 123.456.789.000" className={fieldClass} /> + ) => + form.setInscricaoEstadual(e.target.value) + } + placeholder="Ex: 123.456.789.000" + className={fieldClass} + />
@@ -72,11 +155,27 @@ export function BasicDataTab({ form }: BasicDataTabProps) {
- ) => form.setFoneFixo1(maskPhone(e.target.value))} placeholder="(00) 0000-0000" className={fieldClass} maxLength={15} /> + ) => + form.setFoneFixo1(maskPhone(e.target.value)) + } + placeholder="(00) 0000-0000" + className={fieldClass} + maxLength={15} + />
- ) => form.setFoneFixo2(maskPhone(e.target.value))} placeholder="(00) 0000-0000" className={fieldClass} maxLength={15} /> + ) => + form.setFoneFixo2(maskPhone(e.target.value)) + } + placeholder="(00) 0000-0000" + className={fieldClass} + maxLength={15} + />
@@ -85,7 +184,9 @@ export function BasicDataTab({ form }: BasicDataTabProps) {
- + + + - {ESTADOS_BR.map(uf => {uf})} + {ESTADOS_BR.map((uf) => ( + + {uf} + + ))}
diff --git a/src/components/admin/products/sections/ProductClassificationSection.tsx b/src/components/admin/products/sections/ProductClassificationSection.tsx index f25a610ce..df3422c9a 100644 --- a/src/components/admin/products/sections/ProductClassificationSection.tsx +++ b/src/components/admin/products/sections/ProductClassificationSection.tsx @@ -153,7 +153,7 @@ export default function ProductClassificationSection({ subtitle="Paleta de cores disponíveis" icon={Palette} iconColor="bg-primary/10 text-primary" - defaultOpen={showFullContent} + defaultOpen={!!showFullContent} disabled={!showFullContent} > {savedProductId ? ( diff --git a/src/components/admin/products/sections/ProductEngravingSection.tsx b/src/components/admin/products/sections/ProductEngravingSection.tsx index 63f71424d..c7aae94dc 100644 --- a/src/components/admin/products/sections/ProductEngravingSection.tsx +++ b/src/components/admin/products/sections/ProductEngravingSection.tsx @@ -151,7 +151,7 @@ export default function ProductEngravingSection({ productId, isEdit }: Props) { ); const renderDetailsStep = () => { - const maxCores = w.selectedTechnique?.max_cores !== null ? Number(w.selectedTechnique.max_cores) : null; + const maxCores = w.selectedTechnique !== null && w.selectedTechnique !== undefined && w.selectedTechnique.max_cores !== null && w.selectedTechnique.max_cores !== undefined ? Number(w.selectedTechnique.max_cores) : null; const custoSetup = w.selectedTechnique?.custo_setup ?? null; return (
diff --git a/src/components/admin/products/sections/ProductSupplierSection.tsx b/src/components/admin/products/sections/ProductSupplierSection.tsx index 430b5c1d8..c26c93fde 100644 --- a/src/components/admin/products/sections/ProductSupplierSection.tsx +++ b/src/components/admin/products/sections/ProductSupplierSection.tsx @@ -47,7 +47,7 @@ const emptyForm = { export function ProductSupplierSection({ supplierId, onSupplierChange, setValue, errors, - productId, _isEdit, _primarySupplierName, + productId, isEdit: _isEdit, primarySupplierName: _primarySupplierName, }: Props) { const { sources, isLoading, addSource, removeSource, setPreferred } = useProductSupplierSources(productId); const [pendingSources, setPendingSources] = useState>([]); diff --git a/src/components/admin/products/sections/engraving/useEngravingWizard.ts b/src/components/admin/products/sections/engraving/useEngravingWizard.ts index 8a403ad84..c13179ca8 100644 --- a/src/components/admin/products/sections/engraving/useEngravingWizard.ts +++ b/src/components/admin/products/sections/engraving/useEngravingWizard.ts @@ -228,7 +228,7 @@ function enrichArea(area: PrintAreaTechnique, techById: Map { - const imageUrl = getProductImageUrl(p); + const formatted = productsData.map((p) => { + const imageUrl = getProductImageUrl(p as unknown as PromobrindProduct); + const pRec = p as unknown as Record; return { id: p.id, sku: p.sku, name: p.name, description: p.description ?? p.short_description ?? null, short_description: p.short_description ?? null, meta_description: p.meta_description ?? null, brand: p.brand ?? null, - price: getProductPrice(p), + price: getProductPrice(p as unknown as PromobrindProduct), cost_price: p.cost_price ?? null, - stock: getProductStock(p), - category_id: p.category_id ?? p.main_category_id ?? null, + stock: getProductStock(p as unknown as PromobrindProduct), + category_id: p.category_id ?? (pRec.main_category_id as string | null | undefined) ?? null, supplier_id: p.supplier_id ?? null, supplier_reference: p.supplier_reference ?? null, - is_active: p.is_active ?? p.active ?? true, + is_active: p.is_active ?? (pRec.active as boolean | undefined) ?? true, images: imageUrl ? [imageUrl] : (Array.isArray(p.images) ? p.images : []), colors: Array.isArray(p.colors) ? p.colors : [], materials: p.materials ? (typeof p.materials === 'string' ? [p.materials] : p.materials) : [], min_quantity: p.min_quantity ?? 1, is_featured: p.is_featured ?? false, - is_bestseller: (p as ExternalProduct).is_bestseller ?? false, + is_bestseller: (pRec.is_bestseller as boolean | undefined) ?? false, is_new: p.is_new ?? false, is_on_sale: p.is_on_sale ?? false, - is_kit: (p as ExternalProduct).is_kit ?? false, + is_kit: (pRec.is_kit as boolean | undefined) ?? false, has_commercial_packaging: p.has_commercial_packaging ?? false, is_imported: p.is_imported ?? false, is_textil: p.is_textil ?? false, @@ -165,12 +166,12 @@ export function useProductsManager() { meta_title: p.meta_title ?? null, meta_keywords: Array.isArray(p.meta_keywords) ? p.meta_keywords : null, slug: p.slug ?? null, canonical_url: p.canonical_url ?? null, - video_url: (p as ExternalProduct).videos?.[0] ?? null, + video_url: ((pRec.videos as unknown[] | undefined)?.[0] as string | undefined) ?? null, key_benefits: p.key_benefits ?? null, use_cases: p.use_cases ?? null, created_at: p.created_at ?? '', updated_at: p.updated_at ?? '', }; }); - setProducts(formatted); + setProducts(formatted as unknown as AdminProduct[]); } catch (error) { console.error("Error fetching products:", error); toast.error("Erro ao carregar produtos"); diff --git a/src/components/admin/products/video-gallery/VideoMetaEditor.tsx b/src/components/admin/products/video-gallery/VideoMetaEditor.tsx index c8eaff4d1..09611a96e 100644 --- a/src/components/admin/products/video-gallery/VideoMetaEditor.tsx +++ b/src/components/admin/products/video-gallery/VideoMetaEditor.tsx @@ -63,8 +63,8 @@ export function VideoMetaEditor({ video, onSave, onCancel }: Props) { className="h-6 w-6 text-white hover:bg-white/20" onClick={() => onSave({ - title: title.trim() || null, - description: description.trim() || null, + title: title.trim() || undefined, + description: description.trim() || undefined, video_type: videoType, }) } diff --git a/src/components/admin/security/SecureUploadManager.tsx b/src/components/admin/security/SecureUploadManager.tsx index db55bc586..37344be99 100644 --- a/src/components/admin/security/SecureUploadManager.tsx +++ b/src/components/admin/security/SecureUploadManager.tsx @@ -1,45 +1,55 @@ -import React, { useState, useEffect } from "react"; -import { Card, CardContent, CardHeader, CardTitle, CardDescription } from "@/components/ui/card"; -import { Badge } from "@/components/ui/badge"; -import { Button } from "@/components/ui/button"; -import { Input } from "@/components/ui/input"; -import { supabase } from "@/integrations/supabase/client"; -import { - Upload, - CheckCircle2, - XCircle, - History, +import React, { useState, useEffect } from 'react'; +import { Card, CardContent, CardHeader, CardTitle, CardDescription } from '@/components/ui/card'; +import { Badge } from '@/components/ui/badge'; +import { Button } from '@/components/ui/button'; +import { Input } from '@/components/ui/input'; +import { supabase } from '@/integrations/supabase/client'; +import { + Upload, + CheckCircle2, + XCircle, + History, Search, RefreshCw, Clock, ShieldAlert, FileSearch, - Lock -} from "lucide-react"; -import { toast } from "sonner"; -import { format } from "date-fns"; -import { ptBR } from "date-fns/locale"; + Lock, +} from 'lucide-react'; +import { toast } from 'sonner'; +import { format } from 'date-fns'; +import { ptBR } from 'date-fns/locale'; + +interface FileScanLog { + id: string; + created_at: string; + path: string; + hash: string; + bucket: string; + status_code: number; + scan_result?: { malicious?: number } | null; +} export function SecureUploadManager() { - const [logs, setLogs] = useState[]>([]); + const [logs, setLogs] = useState([]); const [isLoading, setIsLoading] = useState(true); const [isUploading, setIsUploading] = useState(false); - const [searchTerm, setSearchTerm] = useState(""); + const [searchTerm, setSearchTerm] = useState(''); const fetchLogs = async () => { setIsLoading(true); try { const { data, error } = await supabase - .from("file_scan_logs") - .select("*") - .order("created_at", { ascending: false }) + .from('file_scan_logs') + .select('*') + .order('created_at', { ascending: false }) .limit(20); if (error) throw error; - setLogs(data || []); + setLogs((data || []) as unknown as FileScanLog[]); } catch (error) { - console.error("Error fetching logs:", error); - toast.error("Erro ao carregar logs de auditoria"); + console.error('Error fetching logs:', error); + toast.error('Erro ao carregar logs de auditoria'); } finally { setIsLoading(false); } @@ -55,37 +65,38 @@ export function SecureUploadManager() { setIsUploading(true); const formData = new FormData(); - formData.append("file", file); - formData.append("folder", "dev-test"); + formData.append('file', file); + formData.append('folder', 'dev-test'); try { - const { error } = await supabase.functions.invoke("secure-upload", { + const { error } = await supabase.functions.invoke('secure-upload', { body: formData, }); if (error) { if (error.status === 403) { - toast.error("Upload Bloqueado: Ameaça detectada ou falha na verificação!"); + toast.error('Upload Bloqueado: Ameaça detectada ou falha na verificação!'); } else { toast.error(`Erro no upload: ${error.message}`); } return; } - toast.success("Upload realizado com sucesso e verificado!"); + toast.success('Upload realizado com sucesso e verificado!'); fetchLogs(); } catch (error) { - console.error("Test upload error:", error); - toast.error("Erro ao realizar upload de teste"); + console.error('Test upload error:', error); + toast.error('Erro ao realizar upload de teste'); } finally { setIsUploading(false); - e.target.value = ""; + e.target.value = ''; } }; - const filteredLogs = logs.filter(log => - log.path.toLowerCase().includes(searchTerm.toLowerCase()) || - log.hash.toLowerCase().includes(searchTerm.toLowerCase()) + const filteredLogs = logs.filter( + (log) => + log.path.toLowerCase().includes(searchTerm.toLowerCase()) || + log.hash.toLowerCase().includes(searchTerm.toLowerCase()), ); return ( @@ -94,7 +105,7 @@ export function SecureUploadManager() { {/* Test Card */} - + Teste de Upload Seguro @@ -103,9 +114,9 @@ export function SecureUploadManager() { -
- -

+

+ +

Arraste um arquivo ou clique para selecionar

{isUploading ? ( <> - + Processando... ) : ( <> - + Selecionar Arquivo )}
-
- +
+

Aviso de Segurança:

-

Arquivos suspeitos serão automaticamente movidos para o bucket de quarantine e o acesso público será bloqueado.

+

+ Arquivos suspeitos serão automaticamente movidos para o bucket de{' '} + quarantine e o acesso público será bloqueado. +

@@ -144,28 +158,32 @@ export function SecureUploadManager() { {/* Info Card */} - + Estado da Infraestrutura
-
- Bucket: personalization-images +
+ + Bucket: personalization-images + Privado
-
+
Bucket: quarantine Restrito (Admin Only)
-
+
Edge Function: secure-upload - Ativa + Ativa
Verificação Antimalware - VirusTotal V3 + + VirusTotal V3 +
@@ -176,14 +194,16 @@ export function SecureUploadManager() {
- + Audit Log (Últimos 20 scans) - Monitoramento em tempo real das varreduras de arquivos. + + Monitoramento em tempo real das varreduras de arquivos. +
@@ -198,7 +218,7 @@ export function SecureUploadManager() { />
-
+
@@ -211,19 +231,30 @@ export function SecureUploadManager() { {isLoading ? ( - + + + ) : filteredLogs.length === 0 ? ( - + + + ) : ( filteredLogs.map((log) => ( - -
Carregando auditoria...
+ Carregando auditoria... +
Nenhum log encontrado.
+ Nenhum log encontrado. +
+
- {format(new Date(log.created_at), "dd/MM/yyyy HH:mm", { locale: ptBR })} + {format(new Date(log.created_at), 'dd/MM/yyyy HH:mm', { locale: ptBR })}
+ {log.path.split('/').pop()} @@ -233,18 +264,21 @@ export function SecureUploadManager() { {log.status_code === 200 ? ( - + OK ) : ( - {log.status_code === 403 ? 'Bloqueado' : 'Erro'} + {' '} + {log.status_code === 403 ? 'Bloqueado' : 'Erro'} )} - {log.scan_result?.malicious > 0 ? ( - {log.scan_result.malicious}! + {(log.scan_result?.malicious ?? 0) > 0 ? ( + + {log.scan_result?.malicious}! + ) : ( 0 )} diff --git a/src/components/admin/security/keys/audit/McpAuditRow.tsx b/src/components/admin/security/keys/audit/McpAuditRow.tsx index 0fab03091..3f61b6f04 100644 --- a/src/components/admin/security/keys/audit/McpAuditRow.tsx +++ b/src/components/admin/security/keys/audit/McpAuditRow.tsx @@ -99,7 +99,7 @@ export function McpAuditRow({ row }: Props) { )} - {(d.target_repo || d.target_tool || d.expires_at || (row.is_full && d.justification)) && ( + {!!(d.target_repo || d.target_tool || d.expires_at || (row.is_full && d.justification)) && (
{d.target_repo ? (
diff --git a/src/components/admin/security/keys/audit/useMcpAuditFeed.ts b/src/components/admin/security/keys/audit/useMcpAuditFeed.ts index 153c746c2..0867ec0e8 100644 --- a/src/components/admin/security/keys/audit/useMcpAuditFeed.ts +++ b/src/components/admin/security/keys/audit/useMcpAuditFeed.ts @@ -109,7 +109,7 @@ export function useMcpAuditFeed() { const enriched = base.map((r) => { const d = (r.details ?? {}) as Record; - const scopes = (d.scopes ?? d.after?.['scopes'] ?? []) as string[]; + const scopes = (d.scopes ?? (d.after as Record)?.['scopes'] ?? []) as string[]; const isFull = (Array.isArray(scopes) && scopes.includes('*')) || d.is_full_access === true; const escalated = d.escalated_to_full === true || r.action === 'mcp_key.scope_escalated'; const prof = r.user_id ? profiles[r.user_id] : undefined; diff --git a/src/components/admin/suppliers-manager/useSuppliersManager.ts b/src/components/admin/suppliers-manager/useSuppliersManager.ts index e9377e1c5..ca1256237 100644 --- a/src/components/admin/suppliers-manager/useSuppliersManager.ts +++ b/src/components/admin/suppliers-manager/useSuppliersManager.ts @@ -172,18 +172,20 @@ export function useSuppliersManager() { }; const handleEdit = (supplier: Supplier) => { - const s = { ...supplier } as Record; + const s = { ...supplier } as unknown as Record; try { - const addr = (supplier as Record).address_details - ? JSON.parse((supplier as Record).address_details as string) + const supplierRecord = supplier as unknown as Record; + const addr = supplierRecord.address_details + ? JSON.parse(supplierRecord.address_details as string) : null; if (addr && typeof addr === 'object') Object.assign(s, addr); } catch { /* ignore */ } try { - const social = (supplier as Record).social_details - ? JSON.parse((supplier as Record).social_details as string) + const supplierRecord = supplier as unknown as Record; + const social = supplierRecord.social_details + ? JSON.parse(supplierRecord.social_details as string) : null; if (social && typeof social === 'object') Object.assign(s, social); } catch { diff --git a/src/components/admin/telemetry/AppHealthDashboard.tsx b/src/components/admin/telemetry/AppHealthDashboard.tsx index cda00aaa5..93dcc1645 100644 --- a/src/components/admin/telemetry/AppHealthDashboard.tsx +++ b/src/components/admin/telemetry/AppHealthDashboard.tsx @@ -34,7 +34,7 @@ const WINDOW_OPTIONS: { value: HealthWindow; label: string }[] = [ ]; function fmtMs(n: number | null | undefined) { - if (n === null) return '—'; + if (n === null || n === undefined) return '—'; if (n >= 1000) return `${(n / 1000).toFixed(2)}s`; return `${n}ms`; } diff --git a/src/components/admin/telemetry/ColdVsWarmCrmCard.tsx b/src/components/admin/telemetry/ColdVsWarmCrmCard.tsx index 7e25634a3..baccf5f1a 100644 --- a/src/components/admin/telemetry/ColdVsWarmCrmCard.tsx +++ b/src/components/admin/telemetry/ColdVsWarmCrmCard.tsx @@ -45,7 +45,7 @@ const FN_URL = SUPABASE_URL ? `${SUPABASE_URL}/functions/v1/crm-db-bridge?op=dia const POLL_MS = 30_000; function fmtMs(v: number | null | undefined): string { - if (v === null) return '—'; + if (v === null || v === undefined) return '—'; if (v < 1000) return `${Math.round(v)} ms`; return `${(v / 1000).toFixed(2)} s`; } @@ -58,7 +58,7 @@ function fmtAge(ms: number): string { } function tone(ms: number | null | undefined, warn: number, bad: number): string { - if (ms === null) return 'text-muted-foreground'; + if (ms === null || ms === undefined) return 'text-muted-foreground'; if (ms >= bad) return 'text-destructive'; if (ms >= warn) return 'text-warning'; return 'text-foreground'; diff --git a/src/components/auth/KnownDevicesManager.tsx b/src/components/auth/KnownDevicesManager.tsx index 81951102c..581cea70e 100644 --- a/src/components/auth/KnownDevicesManager.tsx +++ b/src/components/auth/KnownDevicesManager.tsx @@ -50,7 +50,7 @@ export function KnownDevicesManager({ targetUserId }: KnownDevicesManagerProps) const loadDevices = async () => { setIsLoading(true); const data = await getKnownDevices(); - setDevices(data as KnownDevice[]); + setDevices(data as unknown as KnownDevice[]); setIsLoading(false); }; diff --git a/src/components/cart/CartCompanyPicker.tsx b/src/components/cart/CartCompanyPicker.tsx index 813d6a0aa..28b73acf3 100644 --- a/src/components/cart/CartCompanyPicker.tsx +++ b/src/components/cart/CartCompanyPicker.tsx @@ -12,7 +12,8 @@ import { ScrollArea } from "@/components/ui/scroll-area"; import { cn } from "@/lib/utils"; import { selectCrm, searchCrm } from "@/lib/crm-db"; import { getCompanyDisplayName, type CrmCompany } from "@/types/crm"; -import { useSellerCartContext, type CreateCartInput } from "@/contexts/SellerCartContext"; +import { useSellerCartContext } from "@/contexts/SellerCartContext"; +import type { CreateCartInput } from "@/hooks/products"; import { useSearchHistory } from "@/hooks/common"; interface CompanyItem { diff --git a/src/components/cart/CartCompanyPickerDialog.tsx b/src/components/cart/CartCompanyPickerDialog.tsx index 2e993a7d3..ab23a2413 100644 --- a/src/components/cart/CartCompanyPickerDialog.tsx +++ b/src/components/cart/CartCompanyPickerDialog.tsx @@ -21,7 +21,8 @@ import { Skeleton } from '@/components/ui/skeleton'; import { cn } from '@/lib/utils'; import { selectCrm, searchCrm } from '@/lib/crm-db'; import { getCompanyDisplayName, type CrmCompany } from '@/types/crm'; -import { useSellerCartContext, type CreateCartInput } from '@/contexts/SellerCartContext'; +import { useSellerCartContext } from '@/contexts/SellerCartContext'; +import type { CreateCartInput } from '@/hooks/products'; interface CompanyItem { id: string; diff --git a/src/components/cart/SortableCartItem.tsx b/src/components/cart/SortableCartItem.tsx index 3c891344e..e51e4be7b 100644 --- a/src/components/cart/SortableCartItem.tsx +++ b/src/components/cart/SortableCartItem.tsx @@ -4,6 +4,7 @@ import { useState, useRef, memo } from "react"; import { Card } from "@/components/ui/card"; +import { Badge } from "@/components/ui/badge"; import { Button } from "@/components/ui/button"; import { Textarea } from "@/components/ui/textarea"; import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "@/components/ui/collapsible"; diff --git a/src/components/cart/__tests__/SortableCartItemExcellence.test.tsx b/src/components/cart/__tests__/SortableCartItemExcellence.test.tsx index bca49bd5e..2d288c01f 100644 --- a/src/components/cart/__tests__/SortableCartItemExcellence.test.tsx +++ b/src/components/cart/__tests__/SortableCartItemExcellence.test.tsx @@ -45,6 +45,9 @@ const mockItem: SellerCartItem = { created_at: new Date().toISOString(), updated_at: new Date().toISOString(), sort_order: 0, + color_name: null, + color_hex: null, + notes: null, }; const renderComponent = (item = mockItem) => { diff --git a/src/components/catalog/CatalogActiveFilters.tsx b/src/components/catalog/CatalogActiveFilters.tsx index a39f30ebe..daabca8d4 100644 --- a/src/components/catalog/CatalogActiveFilters.tsx +++ b/src/components/catalog/CatalogActiveFilters.tsx @@ -1,6 +1,6 @@ import { Badge } from "@/components/ui/badge"; import type { FilterState } from "@/components/filters/FilterPanel"; -import { getCategoryIcon, useCategoryIcons, useExternalCategoriesQuery, useSupplierNames } from "@/hooks/products"; +import { getCategoryIcon, useCategoryIcons, useExternalCategoriesQuery, useSupplierNames, type CategoryIcon } from "@/hooks/products"; import { toTitleCase } from "@/lib/textUtils"; import { X } from "lucide-react"; @@ -12,7 +12,8 @@ interface CatalogActiveFiltersProps { export function CatalogActiveFilters({ filters, setFilters, activeFiltersCount }: CatalogActiveFiltersProps) { const { data: categories = [] } = useExternalCategoriesQuery(); - const { data: icons = [] } = useCategoryIcons(); + const { data: iconsRaw } = useCategoryIcons(); + const icons = (iconsRaw ?? []) as CategoryIcon[]; const { data: supplierNamesMap } = useSupplierNames(filters.suppliers); if (activeFiltersCount === 0) return null; diff --git a/src/components/collections/CollectionTableView.tsx b/src/components/collections/CollectionTableView.tsx index 978839ae0..1ebbf7937 100644 --- a/src/components/collections/CollectionTableView.tsx +++ b/src/components/collections/CollectionTableView.tsx @@ -107,9 +107,9 @@ function CollectionTableRow({ updatedAgo, index, }: CollectionTableRowProps) { - const previewImage = products[0]?.images?.[0]; + const previewImage = products[0]?.image_url; const iconChar = collection.icon || '📁'; - const iconColor = collection.iconColor || '#6366f1'; + const iconColor = (collection as unknown as { iconColor?: string }).iconColor || collection.color || '#6366f1'; return (
e.stopPropagation()}> - +
@@ -308,7 +308,7 @@ export function CollectionTableView({ onToggleSelect(collection.id)} diff --git a/src/components/expert/chat/ChatInputBar.tsx b/src/components/expert/chat/ChatInputBar.tsx index 78c532c50..1ea11d5ee 100644 --- a/src/components/expert/chat/ChatInputBar.tsx +++ b/src/components/expert/chat/ChatInputBar.tsx @@ -28,8 +28,9 @@ export function ChatInputBar({ recognition.interimResults = false; recognition.maxAlternatives = 1; toast.info("🎙️ Ouvindo… fale agora", { duration: 3000 }); - recognition.onresult = (event: SpeechRecognitionEvent) => { - const transcript = event.results[0][0].transcript; + recognition.onresult = (event: Event) => { + const speechEvent = event as unknown as { results: ArrayLike> }; + const transcript = speechEvent.results[0][0].transcript; if (transcript) { setInput(transcript); isFromVoiceRef.current = true; diff --git a/src/components/filters/filter-panel/sections/MaterialsFilter.tsx b/src/components/filters/filter-panel/sections/MaterialsFilter.tsx index 21ff7471e..a37079d97 100644 --- a/src/components/filters/filter-panel/sections/MaterialsFilter.tsx +++ b/src/components/filters/filter-panel/sections/MaterialsFilter.tsx @@ -8,18 +8,19 @@ import { Skeleton } from '@/components/ui/skeleton'; import { ScrollArea } from '@/components/ui/scroll-area'; import { MaterialBadge } from '@/components/materials/MaterialBadge'; import { cn } from '@/lib/utils'; +import type { MaterialGroup, MaterialComplete } from '@/services/materialService'; interface MaterialsFilterProps { materialSearch: string; setMaterialSearch: (v: string) => void; - materialGroups: Record[]; - allMaterials: Record[]; + materialGroups: MaterialGroup[]; + allMaterials: MaterialComplete[]; materialsLoading: boolean; materialFilterState: { selectedGroups: string[]; selectedTypes: string[] }; toggleMaterialGroup: (slug: string) => void; toggleMaterialType: (slug: string) => void; isMaterialGroupSelected: (slug: string) => boolean; - getTypesForGroup: (slug: string) => unknown[]; + getTypesForGroup: (slug: string) => MaterialComplete[]; openSections: string[]; toggleSection: (id: string) => void; } diff --git a/src/components/filters/filter-panel/sections/RamosFilter.tsx b/src/components/filters/filter-panel/sections/RamosFilter.tsx index 09f5ee1f5..db8162c76 100644 --- a/src/components/filters/filter-panel/sections/RamosFilter.tsx +++ b/src/components/filters/filter-panel/sections/RamosFilter.tsx @@ -6,18 +6,19 @@ import { ScrollArea } from '@/components/ui/scroll-area'; import { RamoAtividadeBadge } from '@/components/ramo-atividade/RamoAtividadeBadge'; import { RamoAtividadeGroupAccordion } from '@/components/ramo-atividade/RamoAtividadeGroupAccordion'; import type { FilterState } from '../types'; +import type { RamoAtividadeGroup, SegmentoComplete } from '@/types/ramo-atividade'; interface RamosFilterProps { filters: FilterState; onFilterChange: (filters: FilterState) => void; ramoSearch: string; setRamoSearch: (v: string) => void; - ramoGroups: Record[]; - allSegmentos: Record[]; + ramoGroups: RamoAtividadeGroup[]; + allSegmentos: SegmentoComplete[]; ramosLoading: boolean; totalRamoGroups: number; totalRamoSegmentos: number; - getSegmentosForRamo: (slug: string) => unknown[]; + getSegmentosForRamo: (slug: string) => SegmentoComplete[]; productCountsByRamo: { ramoCounts: Map; segmentoCounts: Map }; } diff --git a/src/components/intelligence/MarketIntelligenceChart.tsx b/src/components/intelligence/MarketIntelligenceChart.tsx index 90aa774aa..fcf1fb888 100644 --- a/src/components/intelligence/MarketIntelligenceChart.tsx +++ b/src/components/intelligence/MarketIntelligenceChart.tsx @@ -4,7 +4,7 @@ * mas com dados agregados de todos os produtos. * Inclui mock data quando não há dados reais. */ -import { useMemo, useState } from "react"; +import { useMemo, useState } from 'react'; import { ResponsiveContainer, Area, @@ -15,10 +15,10 @@ import { Bar, ComposedChart, Legend, -} from "recharts"; -import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"; -import { Badge } from "@/components/ui/badge"; -import { Tabs, TabsList, TabsTrigger } from "@/components/ui/tabs"; +} from 'recharts'; +import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'; +import { Badge } from '@/components/ui/badge'; +import { Tabs, TabsList, TabsTrigger } from '@/components/ui/tabs'; import { Target, ShoppingCart, @@ -29,13 +29,18 @@ import { Package, Loader2, AlertCircle, -} from "lucide-react"; -import { cn } from "@/lib/utils"; -import { KpiCard } from "@/components/ui/kpi-card"; -import { useMarketIntelligenceMacro, type MacroSupplierMetrics, type MacroMarketPoint, type MacroMarketKpis } from "@/hooks/intelligence"; -import { useSupplierNames } from "@/hooks/products"; -import { safeParseDateForChart } from "@/lib/stock-chart-utils"; -import { SupplierChartFilter } from "@/components/products/SupplierChartFilter"; +} from 'lucide-react'; +import { cn } from '@/lib/utils'; +import { KpiCard } from '@/components/ui/kpi-card'; +import { + useMarketIntelligenceMacro, + type MacroSupplierMetrics, + type MacroMarketPoint, + type MacroMarketKpis, +} from '@/hooks/intelligence'; +import { useSupplierNames } from '@/hooks/products'; +import { safeParseDateForChart } from '@/lib/stock-chart-utils'; +import { SupplierChartFilter } from '@/components/products/SupplierChartFilter'; interface Props { days?: number; @@ -69,15 +74,50 @@ function generateMockMarketData(days: number) { const totalDepleted7d = d7.reduce((s, p) => s + p.depleted, 0); const totalDepleted30d = daily.reduce((s, p) => s + p.depleted, 0); const totalRestocked30d = daily.reduce((s, p) => s + p.restocked, 0); - const activeDays = daily.filter(d => d.depleted > 0).length; + const activeDays = daily.filter((d) => d.depleted > 0).length; - const topDay = daily.reduce((best, d) => d.depleted > (best?.depleted ?? 0) ? d : best, daily[0]); + const topDay = daily.reduce( + (best, d) => (d.depleted > (best?.depleted ?? 0) ? d : best), + daily[0], + ); const mockSuppliers: MacroSupplierMetrics[] = [ - { supplierId: 'mock-supplier-1', avgDailyDepletion7d: 10.2, currentStock: 659, totalDepleted: 420, totalRestocked: 200, velocityTrend: 1.26, daysToStockout: 64 }, - { supplierId: 'mock-supplier-2', avgDailyDepletion7d: 9.1, currentStock: 413, totalDepleted: 350, totalRestocked: 150, velocityTrend: 1.15, daysToStockout: 45 }, - { supplierId: 'mock-supplier-3', avgDailyDepletion7d: 8.7, currentStock: 362, totalDepleted: 310, totalRestocked: 100, velocityTrend: 0.52, daysToStockout: 41 }, - { supplierId: 'mock-supplier-4', avgDailyDepletion7d: 3.5, currentStock: 424, totalDepleted: 140, totalRestocked: 80, velocityTrend: 0.95, daysToStockout: 121 }, + { + supplierId: 'mock-supplier-1', + avgDailyDepletion7d: 10.2, + currentStock: 659, + totalDepleted: 420, + totalRestocked: 200, + velocityTrend: 1.26, + daysToStockout: 64, + }, + { + supplierId: 'mock-supplier-2', + avgDailyDepletion7d: 9.1, + currentStock: 413, + totalDepleted: 350, + totalRestocked: 150, + velocityTrend: 1.15, + daysToStockout: 45, + }, + { + supplierId: 'mock-supplier-3', + avgDailyDepletion7d: 8.7, + currentStock: 362, + totalDepleted: 310, + totalRestocked: 100, + velocityTrend: 0.52, + daysToStockout: 41, + }, + { + supplierId: 'mock-supplier-4', + avgDailyDepletion7d: 3.5, + currentStock: 424, + totalDepleted: 140, + totalRestocked: 80, + velocityTrend: 0.95, + daysToStockout: 121, + }, ]; const kpis: MacroMarketKpis = { @@ -97,10 +137,20 @@ function generateMockMarketData(days: number) { ['mock-supplier-4', 'Master Promo'], ]); - return { daily, kpis, suppliers: mockSuppliers, supplierIds: mockSuppliers.map(s => s.supplierId), supplierNames: mockSupplierNames }; + return { + daily, + kpis, + suppliers: mockSuppliers, + supplierIds: mockSuppliers.map((s) => s.supplierId), + supplierNames: mockSupplierNames, + }; } -export function MarketIntelligenceChart({ days: defaultDays = 30, supplierId, productId }: Props) { +export function MarketIntelligenceChart({ + days: defaultDays = 30, + supplierId, + productId: _productId, +}: Props) { const [period, setPeriod] = useState(String(defaultDays)); const [selectedSupplier, setSelectedSupplier] = useState('all'); const days = Number(period); @@ -109,20 +159,22 @@ export function MarketIntelligenceChart({ days: defaultDays = 30, supplierId, pr // Mock data fallback const mock = useMemo(() => generateMockMarketData(days), [days]); - const hasRealData = !!(realData?.daily?.length); + const hasRealData = !!realData?.daily?.length; const isDemo = !hasRealData && !error; const effectiveData = hasRealData ? realData : mock; const effectiveSupplierNames = hasRealData ? null : mock.supplierNames; // Supplier names (only fetch for real data) - const { data: realSupplierNamesMap } = useSupplierNames(hasRealData ? (realData?.supplierIds ?? []) : []); + const { data: realSupplierNamesMap } = useSupplierNames( + hasRealData ? (realData?.supplierIds ?? []) : [], + ); const supplierNamesMap = hasRealData ? realSupplierNamesMap : effectiveSupplierNames; const supplierOptions = useMemo(() => { const ids = effectiveData?.supplierIds ?? []; if (ids.length <= 1) return []; - return ids.map(id => ({ + return ids.map((id) => ({ id, name: supplierNamesMap?.get(id) ?? `Fornecedor ${id.slice(0, 6)}`, })); @@ -130,7 +182,9 @@ export function MarketIntelligenceChart({ days: defaultDays = 30, supplierId, pr const chartData = useMemo(() => { if (!effectiveData?.daily?.length) return []; - return effectiveData.daily.reduce>((acc, d) => { + return effectiveData.daily.reduce< + Array<(typeof effectiveData.daily)[0] & { dateFormatted: string; fullDate: string }> + >((acc, d) => { const parsed = safeParseDateForChart(d.date); if (parsed) acc.push({ ...d, ...parsed }); return acc; @@ -156,7 +210,7 @@ export function MarketIntelligenceChart({ days: defaultDays = 30, supplierId, pr if (error && !hasRealData) { return ( - +

Erro ao carregar dados de mercado

Verifique a conexão e tente novamente

@@ -170,33 +224,55 @@ export function MarketIntelligenceChart({ days: defaultDays = 30, supplierId, pr // Market demand level const avgDepletion = kpis?.avgDailyDepletion ?? 0; - const demandLevel = avgDepletion >= 50 ? 'Muito Alta' : avgDepletion >= 20 ? 'Alta' : avgDepletion >= 5 ? 'Moderada' : 'Baixa'; - const demandColor = avgDepletion >= 50 ? 'text-destructive' : avgDepletion >= 20 ? 'text-warning' : avgDepletion >= 5 ? 'text-primary' : 'text-muted-foreground'; + const demandLevel = + avgDepletion >= 50 + ? 'Muito Alta' + : avgDepletion >= 20 + ? 'Alta' + : avgDepletion >= 5 + ? 'Moderada' + : 'Baixa'; + const demandColor = + avgDepletion >= 50 + ? 'text-destructive' + : avgDepletion >= 20 + ? 'text-warning' + : avgDepletion >= 5 + ? 'text-primary' + : 'text-muted-foreground'; // Trend: compare 7d vs 30d depletion rate const trend7d = (kpis?.totalDepleted7d ?? 0) / 7; const trend30d = (kpis?.totalDepleted30d ?? 0) / Math.max(days, 1); const trendRatio = trend30d > 0 ? trend7d / trend30d : 1; const trendPercent = Math.round((trendRatio - 1) * 100); - const trendLabel = trendRatio > 1.2 ? '↑ acelerando' : trendRatio < 0.8 ? '↓ desacelerando' : '→ estável'; + const trendLabel = + trendRatio > 1.2 ? '↑ acelerando' : trendRatio < 0.8 ? '↓ desacelerando' : '→ estável'; return ( -
+
- + Como o mercado está comprando · visão macro · {days} dias - {isDemo && dados ilustrativos} + {isDemo && ( + + dados ilustrativos + + )}
{trendRatio > 1.3 && ( - + 🚀 Mercado Aquecido )} @@ -206,7 +282,11 @@ export function MarketIntelligenceChart({ days: defaultDays = 30, supplierId, pr {/* KPI Cards */} -
+
1.2 ? TrendingUp : trendRatio < 0.8 ? TrendingDown : BarChart3} label="Tendência" value={`${trendPercent >= 0 ? '+' : ''}${trendPercent}%`} - sub={trendRatio > 1 ? 'demanda crescente' : trendRatio < 0.8 ? 'demanda caindo' : 'demanda estável'} + sub={ + trendRatio > 1 + ? 'demanda crescente' + : trendRatio < 0.8 + ? 'demanda caindo' + : 'demanda estável' + } highlight={trendRatio > 1.3} /> {/* Period selector + supplier filter */} -
+
- {['15', '30', '60', '90', '120', '180', '360'].map(p => ( - {p}d + {['15', '30', '60', '90', '120', '180', '360'].map((p) => ( + + {p}d + ))} @@ -256,26 +344,69 @@ export function MarketIntelligenceChart({ days: defaultDays = 30, supplierId, pr {/* Chart */} {!hasData ? ( -
+
Sem dados de mercado disponíveis para este período
) : ( -
+
- - + + - } /> + ( + + )} + /> {value}} + formatter={(value: string) => ( + {value} + )} + /> + + + - - -
@@ -283,14 +414,24 @@ export function MarketIntelligenceChart({ days: defaultDays = 30, supplierId, pr {/* Supplier Comparison Cards */} {effectiveData?.suppliers && effectiveData.suppliers.length > 1 && supplierNamesMap && ( - + )} {/* Insight */} {kpis?.topDepletionDay && (

- 📊 Pico de saídas: {kpis.topDepletionDay.value.toLocaleString('pt-BR')} un em{' '} - {new Date(kpis.topDepletionDay.date + 'T00:00:00').toLocaleDateString('pt-BR', { day: '2-digit', month: '2-digit' })} + 📊 Pico de saídas:{' '} + + {kpis.topDepletionDay.value.toLocaleString('pt-BR')} un + {' '} + em{' '} + {new Date(kpis.topDepletionDay.date + 'T00:00:00').toLocaleDateString('pt-BR', { + day: '2-digit', + month: '2-digit', + })} {isDemo && ' (demo)'}

)} @@ -301,7 +442,13 @@ export function MarketIntelligenceChart({ days: defaultDays = 30, supplierId, pr // ---------- Supplier Comparison (macro version) ---------- -function MacroSupplierComparison({ suppliers, supplierNames }: { suppliers: MacroSupplierMetrics[]; supplierNames: Map }) { +function MacroSupplierComparison({ + suppliers, + supplierNames, +}: { + suppliers: MacroSupplierMetrics[]; + supplierNames: Map; +}) { const COLORS = [ 'border-l-primary', 'border-l-destructive', @@ -313,10 +460,10 @@ function MacroSupplierComparison({ suppliers, supplierNames }: { suppliers: Macr return (
-

+

Comparativo por Fornecedor

-
+
{suppliers.map((s, idx) => { const name = supplierNames.get(s.supplierId) ?? `Fornecedor ${s.supplierId.slice(0, 6)}`; const isBest = idx === 0 && suppliers.length > 1; @@ -325,16 +472,19 @@ function MacroSupplierComparison({ suppliers, supplierNames }: { suppliers: Macr
- + {name} {isBest && ( - + Maior saída )} @@ -347,27 +497,41 @@ function MacroSupplierComparison({ suppliers, supplierNames }: { suppliers: Macr

Estoque

-

{s.currentStock.toLocaleString('pt-BR')}

+

+ {s.currentStock.toLocaleString('pt-BR')} +

Tendência

-

1 ? 'text-primary' : s.velocityTrend < 0.8 ? 'text-destructive' : 'text-muted-foreground' - )}> - {s.velocityTrend > 1 ? : - s.velocityTrend < 0.8 ? : - } +

1 + ? 'text-primary' + : s.velocityTrend < 0.8 + ? 'text-destructive' + : 'text-muted-foreground', + )} + > + {s.velocityTrend > 1 ? ( + + ) : s.velocityTrend < 0.8 ? ( + + ) : ( + + )} {((s.velocityTrend - 1) * 100).toFixed(0)}%

{s.daysToStockout !== null && s.daysToStockout < 30 && ( -

+

{s.daysToStockout < 7 ? '⚠️' : '⏳'} Esgota em ~{s.daysToStockout}d

@@ -382,13 +546,26 @@ function MacroSupplierComparison({ suppliers, supplierNames }: { suppliers: Macr // ---------- Tooltip ---------- -function MarketMacroTooltip({ active, payload }: { active?: boolean; payload?: { payload: Record }[] }) { +interface MarketDataPoint { + fullDate: string; + stockClose: number; + depleted: number; + restocked: number; +} + +function MarketMacroTooltip({ + active, + payload, +}: { + active?: boolean; + payload?: { payload: MarketDataPoint }[]; +}) { if (!active || !payload?.length) return null; const data = payload[0]?.payload; if (!data) return null; return ( -
+

{data.fullDate}

{data.stockClose > 0 && ( @@ -400,17 +577,21 @@ function MarketMacroTooltip({ active, payload }: { active?: boolean; payload?: { {data.depleted > 0 && (
Compras mercado: - {data.depleted.toLocaleString('pt-BR')} un + + {data.depleted.toLocaleString('pt-BR')} un +
)} {data.restocked > 0 && (
Reposição: - {data.restocked.toLocaleString('pt-BR')} un + + {data.restocked.toLocaleString('pt-BR')} un +
)} {data.depleted === 0 && data.restocked === 0 && ( -

Sem movimentação

+

Sem movimentação

)}
diff --git a/src/components/intelligence/SalesOverviewChart.tsx b/src/components/intelligence/SalesOverviewChart.tsx index 47d7fa432..13fb897f0 100644 --- a/src/components/intelligence/SalesOverviewChart.tsx +++ b/src/components/intelligence/SalesOverviewChart.tsx @@ -173,7 +173,14 @@ export function SalesOverviewChart({ days = 30 }: Props) { width={45} /> - } /> + ( + + )} + /> }[]; + payload?: { payload: SalesDataPoint }[]; }) { if (!active || !payload?.length) return null; const data = payload[0]?.payload; diff --git a/src/components/inventory/risk/RiskTooltip.tsx b/src/components/inventory/risk/RiskTooltip.tsx index 5311efdc6..9f5923c91 100644 --- a/src/components/inventory/risk/RiskTooltip.tsx +++ b/src/components/inventory/risk/RiskTooltip.tsx @@ -1,10 +1,21 @@ -import { forwardRef } from "react"; -import { Badge } from "@/components/ui/badge"; -import { safeNumber } from "@/lib/stock-chart-utils"; +import { forwardRef } from 'react'; +import { Badge } from '@/components/ui/badge'; +import { safeNumber } from '@/lib/stock-chart-utils'; + +interface RiskChartDataPoint { + fullDate?: string; + stockClose?: number | null; + depleted?: number | null; + restocked?: number | null; + restockDetected?: boolean; +} // #11 fix: shows fallback when zero-activity day // forwardRef required because Recharts passes refs to custom tooltip components -export const RiskTooltip = forwardRef }[] }>(function RiskTooltip({ active, payload }, ref) { +export const RiskTooltip = forwardRef< + HTMLDivElement, + { active?: boolean; payload?: { payload: RiskChartDataPoint }[] } +>(function RiskTooltip({ active, payload }, ref) { if (!active || !payload?.length) return null; const data = payload[0]?.payload; if (!data) return null; @@ -14,15 +25,21 @@ export const RiskTooltip = forwardRef 0) || (restocked !== null && restocked > 0); return ( -
+

{data.fullDate}

Estoque: - {data.stockClose !== null ? data.stockClose.toLocaleString('pt-BR') : '—'} + + {/* eslint-disable-next-line eqeqeq */} + {data.stockClose != null ? data.stockClose.toLocaleString('pt-BR') : '—'} +
{!hasActivity && ( -

Sem movimentação

+

Sem movimentação

)} {depleted !== null && depleted > 0 && (
@@ -37,7 +54,10 @@ export const RiskTooltip = forwardRef )} {data.restockDetected && ( - + 🔄 Reabastecimento )} diff --git a/src/components/kit-builder/KitSmartSuggestions.tsx b/src/components/kit-builder/KitSmartSuggestions.tsx index ba0f3b8fe..76637a764 100644 --- a/src/components/kit-builder/KitSmartSuggestions.tsx +++ b/src/components/kit-builder/KitSmartSuggestions.tsx @@ -45,7 +45,8 @@ export function KitSmartSuggestions({ selectedItems, onAddItem }: KitSmartSugges const hasSelectedItem = kitItemIds.some((id: string) => selectedIds.has(id)); if (hasSelectedItem) { - for (const item of items) { + for (const _item of items) { + const item = _item as { id: string; name: string; imageUrl?: string }; if (!selectedIds.has(item.id)) { const existing = coOccurrence.get(item.id); if (existing) { diff --git a/src/components/kit-builder/PersonalizationConfig.tsx b/src/components/kit-builder/PersonalizationConfig.tsx index aedb95eb9..fbef35f77 100644 --- a/src/components/kit-builder/PersonalizationConfig.tsx +++ b/src/components/kit-builder/PersonalizationConfig.tsx @@ -316,21 +316,21 @@ function ItemPersonalizationCard({
Preço unitário - {formatCurrency(priceData.preco_unitario)} + {formatCurrency(priceData.preco_unitario ?? 0)}
Gravação ({kitQuantity}un) - {formatCurrency(priceData.valor_gravacao)} + {formatCurrency(priceData.valor_gravacao ?? 0)}
- {priceData.setup_total > 0 && ( + {(priceData.setup_total ?? 0) > 0 && (
Setup - {formatCurrency(priceData.setup_total)} + {formatCurrency(priceData.setup_total ?? 0)}
)}
Total gravação - {formatCurrency(priceData.total_cobrado)} + {formatCurrency(priceData.total_cobrado ?? 0)}
)} diff --git a/src/components/magic-up/AdImageResult.tsx b/src/components/magic-up/AdImageResult.tsx index eb17ca80a..c5eb09606 100644 --- a/src/components/magic-up/AdImageResult.tsx +++ b/src/components/magic-up/AdImageResult.tsx @@ -22,7 +22,7 @@ import { import { cn } from '@/lib/utils'; import { type MagicUpCopyPack, - type MagicUpCurationStatus, + type MagicUpCurationStatus as MagicUpCurationStatusValue, type MagicUpQualityDiagnosis, type MagicUpQualityScore, buildQualityDiagnosis, @@ -70,8 +70,8 @@ interface AdImageResultProps { onToggleHistoryFavorite?: (id: string, current: boolean) => void; qualityScore?: MagicUpQualityScore; qualityDiagnosis?: MagicUpQualityDiagnosis; - curationStatus?: MagicUpCurationStatus; - onSetCurationStatus?: (status: MagicUpCurationStatus) => void; + curationStatus?: MagicUpCurationStatusValue; + onSetCurationStatus?: (status: MagicUpCurationStatusValue) => void; onRunQualityScore?: () => void; copyPack?: MagicUpCopyPack; aspectRatio?: string; diff --git a/src/components/notifications/NotificationsBadgeStatsPanel.tsx b/src/components/notifications/NotificationsBadgeStatsPanel.tsx index 90c47fdf7..c02b3eb61 100644 --- a/src/components/notifications/NotificationsBadgeStatsPanel.tsx +++ b/src/components/notifications/NotificationsBadgeStatsPanel.tsx @@ -1,15 +1,15 @@ -import { Activity, Download, ArrowDown } from "lucide-react"; -import { useAuth } from "@/contexts/AuthContext"; -import { useNotificationsMetricsPanel } from "./badge-stats/useNotificationsMetricsPanel"; -import { buildSparkPath, downloadJson } from "./badge-stats/utils"; -import { SuspiciousWarning } from "./badge-stats/SuspiciousWarning"; -import { EfficiencyGrid } from "./badge-stats/EfficiencyGrid"; -import { useMemo, useRef } from "react"; -import { notificationsMetrics } from "@/lib/notifications-metrics"; +import { Activity, Download, ArrowDown } from 'lucide-react'; +import { useAuth } from '@/contexts/AuthContext'; +import { useNotificationsMetricsPanel } from './badge-stats/useNotificationsMetricsPanel'; +import { buildSparkPath, downloadJson } from './badge-stats/utils'; +import { SuspiciousWarning } from './badge-stats/SuspiciousWarning'; +import { EfficiencyGrid } from './badge-stats/EfficiencyGrid'; +import { useMemo, useRef } from 'react'; +import { notificationsMetrics } from '@/lib/notifications-metrics'; export function NotificationsBadgeStatsPanel() { const { isAdmin } = useAuth(); - const isDev = Boolean((import.meta as Record).env?.DEV); + const isDev = Boolean((import.meta as unknown as { env?: { DEV?: boolean } }).env?.DEV); const visible = isDev || isAdmin; const { @@ -22,27 +22,30 @@ export function NotificationsBadgeStatsPanel() { streakWindowStats, streakTrend, SUSPICIOUS_RATIO_THRESHOLD, - SPARK_WINDOW_SECONDS + SPARK_WINDOW_SECONDS, } = useNotificationsMetricsPanel(visible); const topContributorsRef = useRef(null); - + const handleJumpToContributors = () => { - topContributorsRef.current?.scrollIntoView({ behavior: "smooth", block: "center" }); + topContributorsRef.current?.scrollIntoView({ behavior: 'smooth', block: 'center' }); const el = topContributorsRef.current; if (!el) return; - el.setAttribute("data-jump-flash", "1"); - window.setTimeout(() => el.removeAttribute("data-jump-flash"), 1500); + el.setAttribute('data-jump-flash', '1'); + window.setTimeout(() => el.removeAttribute('data-jump-flash'), 1500); }; const SPARK_W = 160; const SPARK_H = 24; - const sparkPoints = useMemo(() => buildSparkPath(samples, SPARK_W, SPARK_H, SPARK_WINDOW_SECONDS), [samples]); + const sparkPoints = useMemo( + () => buildSparkPath(samples, SPARK_W, SPARK_H, SPARK_WINDOW_SECONDS), + [samples], + ); if (!visible) return null; const handleExportDebugJson = () => { - downloadJson("notifications-metrics", { + downloadJson('notifications-metrics', { exportedAt: new Date().toISOString(), debugMode: debugOn, sparklineSamples: samples, @@ -52,7 +55,7 @@ export function NotificationsBadgeStatsPanel() { const handleExportSuspiciousStreakJson = () => { if (!isSuspicious) return; - downloadJson("notifications-suspicious-streak", { + downloadJson('notifications-suspicious-streak', { exportedAt: new Date().toISOString(), streakWindowStats, trend: streakTrend, @@ -62,26 +65,26 @@ export function NotificationsBadgeStatsPanel() { return (
-
- +
+
- - + {isSuspicious && ( <> )} @@ -343,38 +358,28 @@ export function ConfigurationPanelV6({ className="flex-1" onClick={handleEdit} > - + Editar )} {isConfirmed && editing && ( <> - - diff --git a/src/components/products/kit-composition/KitComponentCard.tsx b/src/components/products/kit-composition/KitComponentCard.tsx index 17df74e32..197a8bfad 100644 --- a/src/components/products/kit-composition/KitComponentCard.tsx +++ b/src/components/products/kit-composition/KitComponentCard.tsx @@ -1,14 +1,29 @@ -import React, { useState, useCallback } from "react"; +import React, { useState, useCallback } from 'react'; import { - Package, Palette, Settings2, Weight, Eye, Layers, - ChevronUp, ChevronDown, Tag, FileText, Hash, - Copy, CheckCheck, Box, Utensils, ArrowUpDown, ArrowLeftRight, MoveHorizontal, -} from "lucide-react"; -import { Button } from "@/components/ui/button"; -import { Badge } from "@/components/ui/badge"; -import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip"; -import { cn } from "@/lib/utils"; -import type { KitComponent } from "@/types/product-catalog"; + Package, + Palette, + Settings2, + Weight, + Eye, + Layers, + ChevronUp, + ChevronDown, + Tag, + FileText, + Hash, + Copy, + CheckCheck, + Box, + Utensils, + ArrowUpDown, + ArrowLeftRight, + MoveHorizontal, +} from 'lucide-react'; +import { Button } from '@/components/ui/button'; +import { Badge } from '@/components/ui/badge'; +import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip'; +import { cn } from '@/lib/utils'; +import type { KitComponent } from '@/types/product-catalog'; /* ── Copy Button ── */ export function CopyButton({ text }: { text: string }) { @@ -22,35 +37,60 @@ export function CopyButton({ text }: { text: string }) { setTimeout(() => setCopied(false), 1500); }); }, - [text] + [text], ); return ( - - {copied ? "Copiado!" : "Copiar"} + + {copied ? 'Copiado!' : 'Copiar'} + ); } /* ── Smart Badge ── */ -export function SmartBadge({ children, tooltip, className, icon: Icon }: { children: React.ReactNode; tooltip: string; className?: string; icon?: React.ElementType }) { +export function SmartBadge({ + children, + tooltip, + className, + icon: Icon, +}: { + children: React.ReactNode; + tooltip: string; + className?: string; + icon?: React.ElementType; +}) { return ( - + {Icon && } {children} - {tooltip} + + {tooltip} + ); @@ -60,72 +100,129 @@ export function SmartBadge({ children, tooltip, className, icon: Icon }: { child export interface KitComponentCardProps { item: KitComponent; index: number; - variant: "packaging" | "item"; + variant: 'packaging' | 'item'; onViewProduct?: (productId: string) => void; onZoomImage?: (url: string) => void; } -export function KitComponentCard({ item, index, variant, onViewProduct, onZoomImage }: KitComponentCardProps) { +export function KitComponentCard({ + item, + index, + variant, + onViewProduct, + onZoomImage, +}: KitComponentCardProps) { const [expanded, setExpanded] = useState(false); - const hasDimensions = (item.heightMm !== null && item.heightMm > 0) || (item.widthMm !== null && item.widthMm > 0) || (item.lengthMm !== null && item.lengthMm > 0); + const hasDimensions = + (item.heightMm ?? 0) > 0 || (item.widthMm ?? 0) > 0 || (item.lengthMm ?? 0) > 0; const hasExpandableInfo = item.description || item.personalizationNotes; - const hasSpecs = hasDimensions || (item.weightG !== null && item.weightG > 0); - const isPackaging = variant === "packaging"; - const borderColor = isPackaging ? "border-warning/25" : "border-border"; + const hasSpecs = hasDimensions || (item.weightG ?? 0) > 0; + const isPackaging = variant === 'packaging'; + const borderColor = isPackaging ? 'border-warning/25' : 'border-border'; - const formatWeight = (g: number) => g >= 1000 ? `${(g / 1000).toFixed(1)} kg` : `${g} g`; + const formatWeight = (g: number) => (g >= 1000 ? `${(g / 1000).toFixed(1)} kg` : `${g} g`); return ( -
+
{/* Header Row */}
hasExpandableInfo && setExpanded(!expanded)} > -
{ if (item.imageUrl && onZoomImage) { e.stopPropagation(); onZoomImage(item.imageUrl); } }} + className={cn( + 'flex h-16 w-16 items-center justify-center overflow-hidden rounded-lg border', + isPackaging ? 'border-warning/20 bg-warning/5' : 'border-border/50 bg-muted/60', + item.imageUrl && 'cursor-zoom-in transition-all hover:ring-2 hover:ring-primary/40', + )} + onClick={(e) => { + if (item.imageUrl && onZoomImage) { + e.stopPropagation(); + onZoomImage(item.imageUrl); + } + }} > {item.imageUrl ? ( - -{item.productName} + {item.productName} ) : isPackaging ? ( ) : ( )}
-
- {index} +
+ + {index} +
-
+
-
- {item.quantity}x -

{item.productName}

+
+ + {item.quantity}x + +

+ {item.productName} +

-
+
{hasExpandableInfo && ( - )} {onViewProduct && ( - -

Ver produto

+ +

Ver produto

+
)} @@ -134,27 +231,74 @@ export function KitComponentCard({ item, index, variant, onViewProduct, onZoomIm
{item.sku && ( - - SKU: {item.sku} + + SKU: {item.sku} + )} {item.supplierComponentCode && ( - - {item.supplierComponentCode} + + + {item.supplierComponentCode} + )} {item.componentTypeCode && ( - {item.componentTypeCode} + + + {item.componentTypeCode} + )}
-
- {item.isPackaging && Embalagem} - {item.isOptional && Opcional} - {item.isReplaceable && Substituível} - {item.allowsPersonalization && Personalizável} - {item.color && Cor: {item.color}} - {item.material || "—"} +
+ {item.isPackaging && ( + + Embalagem + + )} + {item.isOptional && ( + + Opcional + + )} + {item.isReplaceable && ( + + Substituível + + )} + {item.allowsPersonalization && ( + + Personalizável + + )} + {item.color && ( + + Cor: {item.color} + + )} + + {item.material || '—'} +
@@ -162,40 +306,54 @@ export function KitComponentCard({ item, index, variant, onViewProduct, onZoomIm {/* Specs */} {hasSpecs && (
-
+
{(item.heightMm ?? 0) > 0 && ( -
- +
+ Altura - {item.heightMm}mm + + {item.heightMm} + mm +
)} {(item.widthMm ?? 0) > 0 && ( -
- +
+ Largura - {item.widthMm}mm + + {item.widthMm} + mm +
)} {(item.lengthMm ?? 0) > 0 && ( -
- +
+ Prof. - {item.lengthMm}mm + + {item.lengthMm} + mm +
)} - {item.weightG !== null && item.weightG > 0 && ( -
- + {(item.weightG ?? 0) > 0 && ( +
+ Peso - {formatWeight(item.weightG)} + + {formatWeight(item.weightG ?? 0)} +
)} - {item.volumeMl !== null && item.volumeMl > 0 && ( -
- + {(item.volumeMl ?? 0) > 0 && ( +
+ Vol. - {item.volumeMl}ml + + {item.volumeMl} + ml +
)}
@@ -204,17 +362,27 @@ export function KitComponentCard({ item, index, variant, onViewProduct, onZoomIm {/* Expandable Details */} {expanded && hasExpandableInfo && ( -
+
{item.description && (
-
Descrição
-

{item.description}

+
+ + Descrição +
+

+ {item.description} +

)} {item.personalizationNotes && ( -
-
Notas de Personalização
-

{item.personalizationNotes}

+
+
+ + Notas de Personalização +
+

+ {item.personalizationNotes} +

)}
diff --git a/src/components/products/useStockChartData.ts b/src/components/products/useStockChartData.ts index 0c48a226b..c670a954b 100644 --- a/src/components/products/useStockChartData.ts +++ b/src/components/products/useStockChartData.ts @@ -12,6 +12,7 @@ import { getActiveFlags, type IntelligenceFlag, type StockVelocity, + type ProductIntelligenceData, } from '@/hooks/intelligence'; import { useSupplierNames } from '@/hooks/products'; import { @@ -41,15 +42,17 @@ export function useStockChartData(productId: string) { refetch: refetchSummary, } = useStockDailySummary(productId, days); const { - data: velocity, + data: _velocity, error: velocityError, refetch: refetchVelocity, } = useStockVelocity(productId); + const velocity = _velocity as StockVelocity[] | undefined; const { - data: intelligence, + data: _intelligence, error: intelligenceError, refetch: refetchIntelligence, } = useProductIntelligenceData(productId); + const intelligence = _intelligence as ProductIntelligenceData | null | undefined; const hasData = !!summaries?.length; const hasError = !!(summaryError || velocityError || intelligenceError); @@ -175,7 +178,8 @@ export function useStockChartData(productId: string) { return name ? `em ${name}` : 'fornecedor selecionado'; } const count = effectiveIntelligence?.supplier_count; - if (count === null || count === 0) return 'no fornecedor'; + // eslint-disable-next-line eqeqeq + if (count == null || count === 0) return 'no fornecedor'; return `em ${count} fornecedor${count > 1 ? 'es' : ''}`; }, [effectiveIntelligence, selectedSupplier, supplierNamesMap]); diff --git a/src/components/security/useSecurityData.ts b/src/components/security/useSecurityData.ts index 7b8e681dd..2c0a5ec3f 100644 --- a/src/components/security/useSecurityData.ts +++ b/src/components/security/useSecurityData.ts @@ -2,7 +2,11 @@ * useSecurityData — Hook que carrega métricas, logins e alertas de segurança */ import { useState, useEffect, useCallback, useRef } from 'react'; +import { type createClient } from '@supabase/supabase-js'; import { supabase } from '@/integrations/supabase/client'; + +// 'notifications' table not yet in generated schema — bypass type checking via raw client cast +const db = supabase as unknown as ReturnType; import { use2FA } from '@/hooks/auth'; import { useAllowedIPs } from '@/hooks/admin'; @@ -42,12 +46,23 @@ export interface UserProfile { } const defaultMetrics: SecurityMetrics = { - score: 0, mfaEnabled: false, ipRestrictionsActive: false, - knownDevicesCount: 0, recentLoginAttempts: 0, failedLoginAttempts: 0, securityAlerts: 0, + score: 0, + mfaEnabled: false, + ipRestrictionsActive: false, + knownDevicesCount: 0, + recentLoginAttempts: 0, + failedLoginAttempts: 0, + securityAlerts: 0, }; -export function useSecurityData(effectiveUserId: string | undefined, isManagingOther: boolean, selectedUserId: string | null) { - const { is2FAEnabled, isLoading: is2FALoading } = use2FA(isManagingOther ? selectedUserId! : undefined); +export function useSecurityData( + effectiveUserId: string | undefined, + isManagingOther: boolean, + selectedUserId: string | null, +) { + const { is2FAEnabled, isLoading: is2FALoading } = use2FA( + isManagingOther ? selectedUserId! : undefined, + ); const { allowedIPs } = useAllowedIPs(); const [metrics, setMetrics] = useState(defaultMetrics); const [loginAttempts, setLoginAttempts] = useState([]); @@ -63,27 +78,34 @@ export function useSecurityData(effectiveUserId: string | undefined, isManagingO if (mountedRef.current) setIsLoading(true); try { const { data: attempts } = await supabase - .from('login_attempts').select('*') + .from('login_attempts') + .select('*') .eq('user_id', effectiveUserId) - .order('created_at', { ascending: false }).limit(20); + .order('created_at', { ascending: false }) + .limit(20); if (!mountedRef.current) return; setLoginAttempts((attempts as LoginAttempt[]) || []); const { count: devicesCount } = await supabase - .from('user_known_devices').select('*', { count: 'exact', head: true }) + .from('user_known_devices') + .select('*', { count: 'exact', head: true }) .eq('user_id', effectiveUserId); - const { data: notifs } = await supabase - .from('notifications').select('*') - .eq('user_id', effectiveUserId).eq('type', 'security') - .order('created_at', { ascending: false }).limit(10); + const { data: notifs } = await db + .from('notifications') + .select('*') + .eq('user_id', effectiveUserId) + .eq('type', 'security') + .order('created_at', { ascending: false }) + .limit(10); if (!mountedRef.current) return; - setNotifications((notifs as SecurityNotification[]) || []); + const typedNotifs = notifs as SecurityNotification[] | null; + setNotifications(typedNotifs || []); - const failedAttempts = attempts?.filter(a => !a.success).length || 0; - const unreadAlerts = notifs?.filter(n => !n.is_read).length || 0; + const failedAttempts = attempts?.filter((a) => !a.success).length || 0; + const unreadAlerts = typedNotifs?.filter((n) => !n.is_read).length || 0; let score = 40; if (is2FAEnabled) score += 30; @@ -116,7 +138,15 @@ export function useSecurityData(effectiveUserId: string | undefined, isManagingO }; }, [effectiveUserId, loadSecurityData]); - return { metrics, loginAttempts, notifications, isLoading, is2FAEnabled, is2FALoading, allowedIPs }; + return { + metrics, + loginAttempts, + notifications, + isLoading, + is2FAEnabled, + is2FALoading, + allowedIPs, + }; } // Score helpers diff --git a/src/hooks/__tests__/useIPValidation.test.ts b/src/hooks/__tests__/useIPValidation.test.ts index 8fd81209e..11eec65d9 100644 --- a/src/hooks/__tests__/useIPValidation.test.ts +++ b/src/hooks/__tests__/useIPValidation.test.ts @@ -1,8 +1,13 @@ import { renderHook, act } from '@testing-library/react'; import { describe, it, expect, vi, beforeEach } from 'vitest'; -import { useIPValidation } from "@/hooks/admin/useIPValidation"; +import { useIPValidation } from '@/hooks/admin/useIPValidation'; import { supabase } from '@/integrations/supabase/client'; +type ValidationResult = Awaited< + ReturnType['validateIPForAuthenticatedUser']> +>; +type InvokeResult = { data: unknown; error: null | { message: string } }; + // Mock Supabase vi.mock('@/integrations/supabase/client', () => ({ supabase: { @@ -38,7 +43,10 @@ describe('useIPValidation', () => { }); it('falls back to ipify when get-visitor-info fails', async () => { - vi.mocked(supabase.functions.invoke).mockResolvedValueOnce({ data: null, error: { message: 'Failed' } }); + vi.mocked(supabase.functions.invoke).mockResolvedValueOnce({ + data: null, + error: { message: 'Failed' }, + }); mockFetch.mockResolvedValueOnce({ json: async () => ({ ip: '5.6.7.8' }), }); @@ -51,7 +59,10 @@ describe('useIPValidation', () => { }); it('returns null when both methods fail', async () => { - vi.mocked(supabase.functions.invoke).mockResolvedValueOnce({ data: null, error: { message: 'Failed' } }); + vi.mocked(supabase.functions.invoke).mockResolvedValueOnce({ + data: null, + error: { message: 'Failed' }, + }); mockFetch.mockRejectedValueOnce(new Error('Network error')); const { result } = renderHook(() => useIPValidation()); @@ -65,13 +76,14 @@ describe('useIPValidation', () => { it('returns isAllowed true when IP is whitelisted', async () => { vi.mocked(supabase.functions.invoke).mockImplementation(async (fnName) => { if (fnName === 'get-visitor-info') return { data: { ip: '1.2.3.4' }, error: null }; - if (fnName === 'validate-access') return { data: { allowed: true, reason: 'whitelisted' }, error: null }; + if (fnName === 'validate-access') + return { data: { allowed: true, reason: 'whitelisted' }, error: null }; return { data: null, error: null }; }); const { result } = renderHook(() => useIPValidation()); - let validationResult; + let validationResult: ValidationResult | undefined; await act(async () => { validationResult = await result.current.validateIPForAuthenticatedUser('user-123'); }); @@ -86,13 +98,14 @@ describe('useIPValidation', () => { it('returns isAllowed false when IP is blocked', async () => { vi.mocked(supabase.functions.invoke).mockImplementation(async (fnName) => { if (fnName === 'get-visitor-info') return { data: { ip: '1.2.3.4' }, error: null }; - if (fnName === 'validate-access') return { data: { allowed: false, reason: 'ip_not_whitelisted' }, error: null }; + if (fnName === 'validate-access') + return { data: { allowed: false, reason: 'ip_not_whitelisted' }, error: null }; return { data: null, error: null }; }); const { result } = renderHook(() => useIPValidation()); - let validationResult; + let validationResult: ValidationResult | undefined; await act(async () => { validationResult = await result.current.validateIPForAuthenticatedUser('user-123'); }); @@ -108,13 +121,14 @@ describe('useIPValidation', () => { it('fails open when edge function errors', async () => { vi.mocked(supabase.functions.invoke).mockImplementation(async (fnName) => { if (fnName === 'get-visitor-info') return { data: { ip: '1.2.3.4' }, error: null }; - if (fnName === 'validate-access') return { data: null, error: { message: 'Function error' } }; + if (fnName === 'validate-access') + return { data: null, error: { message: 'Function error' } }; return { data: null, error: null }; }); const { result } = renderHook(() => useIPValidation()); - let validationResult; + let validationResult: ValidationResult | undefined; await act(async () => { validationResult = await result.current.validateIPForAuthenticatedUser('user-123'); }); @@ -124,12 +138,15 @@ describe('useIPValidation', () => { }); it('returns error when current IP cannot be identified', async () => { - vi.mocked(supabase.functions.invoke).mockResolvedValue({ data: null, error: { message: 'Network' } }); + vi.mocked(supabase.functions.invoke).mockResolvedValue({ + data: null, + error: { message: 'Network' }, + }); mockFetch.mockRejectedValueOnce(new Error('Fetch failed')); const { result } = renderHook(() => useIPValidation()); - let validationResult; + let validationResult: ValidationResult | undefined; await act(async () => { validationResult = await result.current.validateIPForAuthenticatedUser('user-123'); }); @@ -144,20 +161,21 @@ describe('useIPValidation', () => { it('handles city_not_whitelisted reason correctly', async () => { vi.mocked(supabase.functions.invoke).mockImplementation(async (fnName) => { if (fnName === 'get-visitor-info') return { data: { ip: '1.2.3.4' }, error: null }; - if (fnName === 'validate-access') return { - data: { - allowed: false, - reason: 'city_not_whitelisted', - details: { detected_city: 'São Paulo' }, - }, - error: null, - }; + if (fnName === 'validate-access') + return { + data: { + allowed: false, + reason: 'city_not_whitelisted', + details: { detected_city: 'São Paulo' }, + }, + error: null, + }; return { data: null, error: null }; }); const { result } = renderHook(() => useIPValidation()); - let validationResult; + let validationResult: ValidationResult | undefined; await act(async () => { validationResult = await result.current.validateIPForAuthenticatedUser('user-123'); }); @@ -169,20 +187,21 @@ describe('useIPValidation', () => { it('handles too_many_attempts reason correctly', async () => { vi.mocked(supabase.functions.invoke).mockImplementation(async (fnName) => { if (fnName === 'get-visitor-info') return { data: { ip: '1.2.3.4' }, error: null }; - if (fnName === 'validate-access') return { - data: { - allowed: false, - reason: 'too_many_attempts', - details: { lockout_minutes: 30 }, - }, - error: null, - }; + if (fnName === 'validate-access') + return { + data: { + allowed: false, + reason: 'too_many_attempts', + details: { lockout_minutes: 30 }, + }, + error: null, + }; return { data: null, error: null }; }); const { result } = renderHook(() => useIPValidation()); - let validationResult; + let validationResult: ValidationResult | undefined; await act(async () => { validationResult = await result.current.validateIPForAuthenticatedUser('user-123'); }); @@ -192,8 +211,8 @@ describe('useIPValidation', () => { }); it('manages isValidating state correctly', async () => { - let resolveInvoke: (value: any) => void; - const invokePromise = new Promise((resolve) => { + let resolveInvoke: (value: InvokeResult) => void; + const invokePromise = new Promise((resolve) => { resolveInvoke = resolve; }); @@ -206,7 +225,7 @@ describe('useIPValidation', () => { expect(result.current.isValidating).toBe(false); - let validationPromise; + let validationPromise: Promise | undefined; await act(async () => { validationPromise = result.current.validateIPForAuthenticatedUser('user-123'); }); diff --git a/src/hooks/admin/useAllowedIPs.ts b/src/hooks/admin/useAllowedIPs.ts index 9ffa2f4d6..e35b03153 100644 --- a/src/hooks/admin/useAllowedIPs.ts +++ b/src/hooks/admin/useAllowedIPs.ts @@ -1,7 +1,11 @@ import { useState, useEffect, useCallback } from 'react'; +import { type createClient } from '@supabase/supabase-js'; import { supabase } from '@/integrations/supabase/client'; import { useAuth } from '@/contexts/AuthContext'; +// Tables not yet in generated schema — bypass type checking via raw client cast +const db = supabase as unknown as ReturnType; + interface AllowedIP { id: string; user_id: string; @@ -23,7 +27,7 @@ export function useAllowedIPs(targetUserId?: string) { const fetchCurrentIP = useCallback(async () => { try { const response = await fetch('https://api.ipify.org?format=json'); - const data = await response.json(); + const data = (await response.json()) as { ip: string }; setCurrentIP(data.ip); } catch (error) { console.error('Error fetching current IP:', error); @@ -38,14 +42,14 @@ export function useAllowedIPs(targetUserId?: string) { } try { - const { data, error } = await supabase + const { data, error } = await db .from('user_allowed_ips') .select('*') .eq('user_id', userId) .order('created_at', { ascending: false }); if (error) throw error; - setAllowedIPs(data || []); + setAllowedIPs((data || []) as AllowedIP[]); } catch (error) { console.error('Error fetching allowed IPs:', error); } finally { @@ -58,86 +62,93 @@ export function useAllowedIPs(targetUserId?: string) { fetchAllowedIPs(); }, [fetchCurrentIP, fetchAllowedIPs]); - const addIP = useCallback(async ( - ipAddress: string, - label?: string - ): Promise<{ success: boolean; error?: string }> => { - if (!userId || !user) { - return { success: false, error: 'Usuário não autenticado' }; - } + const addIP = useCallback( + async (ipAddress: string, label?: string): Promise<{ success: boolean; error?: string }> => { + if (!userId || !user) { + return { success: false, error: 'Usuário não autenticado' }; + } - try { - const { error } = await supabase - .from('user_allowed_ips') - .insert({ + try { + const { error } = await db.from('user_allowed_ips').insert({ user_id: userId, ip_address: ipAddress, label: label || null, created_by: user.id, - }); - - if (error) { - if (error.code === '23505') { - return { success: false, error: 'Este IP já está cadastrado' }; + } as never); + + if (error) { + const e = error as { code?: string }; + if (e.code === '23505') { + return { success: false, error: 'Este IP já está cadastrado' }; + } + throw error; } - throw error; - } - await fetchAllowedIPs(); - return { success: true }; - } catch (error: unknown) { - return { success: false, error: error instanceof Error ? error.message : 'Erro desconhecido' }; - } - }, [userId, user, fetchAllowedIPs]); - - const removeIP = useCallback(async ( - ipId: string - ): Promise<{ success: boolean; error?: string }> => { - try { - const { error } = await supabase - .from('user_allowed_ips') - .delete() - .eq('id', ipId); - - if (error) throw error; - - await fetchAllowedIPs(); - return { success: true }; - } catch (error: unknown) { - return { success: false, error: error instanceof Error ? error.message : 'Erro desconhecido' }; - } - }, [fetchAllowedIPs]); - - const toggleIP = useCallback(async ( - ipId: string, - isActive: boolean - ): Promise<{ success: boolean; error?: string }> => { - try { - const { error } = await supabase - .from('user_allowed_ips') - .update({ is_active: isActive }) - .eq('id', ipId); - - if (error) throw error; - - await fetchAllowedIPs(); - return { success: true }; - } catch (error: unknown) { - return { success: false, error: error instanceof Error ? error.message : 'Erro desconhecido' }; - } - }, [fetchAllowedIPs]); - - const isIPAllowed = useCallback((ip: string): boolean => { - // Se não há IPs configurados, permitir todos - if (allowedIPs.length === 0) return true; - - // Verificar se o IP está na lista de permitidos ativos - return allowedIPs.some( - allowedIP => allowedIP.is_active && allowedIP.ip_address === ip - ); - }, [allowedIPs]); - - const hasIPRestriction = allowedIPs.filter(ip => ip.is_active).length > 0; + await fetchAllowedIPs(); + return { success: true }; + } catch (error: unknown) { + return { + success: false, + error: error instanceof Error ? error.message : 'Erro desconhecido', + }; + } + }, + [userId, user, fetchAllowedIPs], + ); + + const removeIP = useCallback( + async (ipId: string): Promise<{ success: boolean; error?: string }> => { + try { + const { error } = await db.from('user_allowed_ips').delete().eq('id', ipId); + + if (error) throw error; + + await fetchAllowedIPs(); + return { success: true }; + } catch (error: unknown) { + return { + success: false, + error: error instanceof Error ? error.message : 'Erro desconhecido', + }; + } + }, + [fetchAllowedIPs], + ); + + const toggleIP = useCallback( + async (ipId: string, isActive: boolean): Promise<{ success: boolean; error?: string }> => { + try { + const { error } = await db + .from('user_allowed_ips') + .update({ is_active: isActive }) + .eq('id', ipId); + + if (error) throw error; + + await fetchAllowedIPs(); + return { success: true }; + } catch (error: unknown) { + return { + success: false, + error: error instanceof Error ? error.message : 'Erro desconhecido', + }; + } + }, + [fetchAllowedIPs], + ); + + const isIPAllowed = useCallback( + (ip: string): boolean => { + // Se não há IPs configurados, permitir todos + if (allowedIPs.length === 0) return true; + + // Verificar se o IP está na lista de permitidos ativos + return allowedIPs.some((allowedIP) => allowedIP.is_active && allowedIP.ip_address === ip); + }, + [allowedIPs], + ); + + const hasIPRestriction = allowedIPs.filter((ip) => ip.is_active).length > 0; return { allowedIPs, diff --git a/src/hooks/admin/useAuditLog.ts b/src/hooks/admin/useAuditLog.ts index ee52f4ca2..a198d089b 100644 --- a/src/hooks/admin/useAuditLog.ts +++ b/src/hooks/admin/useAuditLog.ts @@ -1,18 +1,22 @@ -import { useAuth } from "@/contexts/AuthContext"; -import { supabase } from "@/integrations/supabase/client"; +import { type createClient } from '@supabase/supabase-js'; +import { useAuth } from '@/contexts/AuthContext'; +import { supabase } from '@/integrations/supabase/client'; + +// 'audit_log' table not yet in generated schema — bypass type checking via raw client cast +const db = supabase as unknown as ReturnType; export type AuditAction = 'INSERT' | 'UPDATE' | 'DELETE'; -export type AuditEntityType = - | 'products' - | 'product_variants' +export type AuditEntityType = + | 'products' + | 'product_variants' | 'product_images' | 'product_videos' - | 'quotes' + | 'quotes' | 'quote_items' - | 'orders' + | 'orders' | 'order_items' - | 'suppliers' + | 'suppliers' | 'categories' | 'material_types' | 'color_variations' @@ -59,10 +63,10 @@ export function useAuditLog() { entityType, entityId, oldValues = null, - newValues = null + newValues = null, }: AuditLogParams): Promise<{ success: boolean; error?: Error }> => { try { - const { error } = await supabase.from('audit_log').insert({ + const { error } = await db.from('audit_log').insert({ user_id: user?.id || null, action, entity_type: entityType, @@ -70,8 +74,8 @@ export function useAuditLog() { old_values: oldValues, new_values: newValues, ip_address: null, // Pode ser capturado via API externa se necessário - user_agent: typeof navigator !== 'undefined' ? navigator.userAgent : null - }); + user_agent: typeof navigator !== 'undefined' ? navigator.userAgent : null, + } as never); if (error) { console.error('Erro ao registrar audit log:', error); @@ -90,18 +94,18 @@ export function useAuditLog() { */ const getChangedFields = ( oldRecord: Record, - newRecord: Record + newRecord: Record, ): { oldFields: Record; newFields: Record } => { const oldFields: Record = {}; const newFields: Record = {}; - Object.keys(newRecord).forEach(key => { + Object.keys(newRecord).forEach((key) => { // Ignorar campos de timestamp que mudam automaticamente if (['updated_at', 'created_at'].includes(key)) return; - + const oldValue = oldRecord[key]; const newValue = newRecord[key]; - + // Comparar valores (convertendo para JSON para comparar objetos/arrays) if (JSON.stringify(oldValue) !== JSON.stringify(newValue)) { oldFields[key] = oldValue; @@ -118,17 +122,17 @@ export function useAuditLog() { const auditedInsert = async >( table: AuditEntityType, data: T, - insertFn: () => Promise<{ data: (T & { id: string }) | null; error: Error | null }> + insertFn: () => Promise<{ data: (T & { id: string }) | null; error: Error | null }>, ): Promise<{ data: (T & { id: string }) | null; error: Error | null }> => { const result = await insertFn(); - + if (result.data && !result.error) { await logAction({ action: 'INSERT', entityType: table, entityId: result.data.id, oldValues: null, - newValues: data + newValues: data, }); } @@ -143,13 +147,13 @@ export function useAuditLog() { entityId: string, oldRecord: T, updates: Partial, - updateFn: () => Promise<{ data: T | null; error: Error | null }> + updateFn: () => Promise<{ data: T | null; error: Error | null }>, ): Promise<{ data: T | null; error: Error | null }> => { const result = await updateFn(); - + if (result.data && !result.error) { const { oldFields, newFields } = getChangedFields(oldRecord, updates); - + // Só registra se houve mudanças reais if (Object.keys(newFields).length > 0) { await logAction({ @@ -157,7 +161,7 @@ export function useAuditLog() { entityType: table, entityId, oldValues: oldFields, - newValues: newFields + newValues: newFields, }); } } @@ -172,17 +176,17 @@ export function useAuditLog() { table: AuditEntityType, entityId: string, oldRecord: T, - deleteFn: () => Promise<{ error: Error | null }> + deleteFn: () => Promise<{ error: Error | null }>, ): Promise<{ error: Error | null }> => { const result = await deleteFn(); - + if (!result.error) { await logAction({ action: 'DELETE', entityType: table, entityId, oldValues: oldRecord, - newValues: null + newValues: null, }); } @@ -194,7 +198,7 @@ export function useAuditLog() { getChangedFields, auditedInsert, auditedUpdate, - auditedDelete + auditedDelete, }; } @@ -203,17 +207,19 @@ export function useAuditLog() { */ export async function fetchAuditHistory( entityType: AuditEntityType, - entityId: string + entityId: string, ): Promise { - const { data, error } = await supabase + const { data, error } = await db .from('audit_log') - .select(` + .select( + ` *, profiles:user_id ( full_name, email ) - `) + `, + ) .eq('entity_type', entityType) .eq('entity_id', entityId) .order('created_at', { ascending: false }); @@ -237,17 +243,19 @@ export async function fetchAllAuditLogs( startDate?: Date; endDate?: Date; }, - limit = 100 + limit = 100, ): Promise { - let query = supabase + let query = db .from('audit_log') - .select(` + .select( + ` *, profiles:user_id ( full_name, email ) - `) + `, + ) .order('created_at', { ascending: false }) .limit(limit); diff --git a/src/hooks/admin/useGeoBlocking.ts b/src/hooks/admin/useGeoBlocking.ts index 7bd1af66f..d81566ad2 100644 --- a/src/hooks/admin/useGeoBlocking.ts +++ b/src/hooks/admin/useGeoBlocking.ts @@ -1,7 +1,11 @@ import { useState, useEffect, useCallback } from 'react'; +import { type createClient } from '@supabase/supabase-js'; import { supabase } from '@/integrations/supabase/client'; import { useAuth } from '@/contexts/AuthContext'; +// 'security_settings' table not yet in generated schema — bypass type checking via raw client cast +const db = supabase as unknown as ReturnType; + interface AllowedCountry { id: string; country_code: string; @@ -18,7 +22,10 @@ interface GeoBlockingSettings { export function useGeoBlocking() { const { user } = useAuth(); const [countries, setCountries] = useState([]); - const [settings, setSettings] = useState({ enabled: false, mode: 'whitelist' }); + const [settings, setSettings] = useState({ + enabled: false, + mode: 'whitelist', + }); const [isLoading, setIsLoading] = useState(true); const [currentCountry, setCurrentCountry] = useState<{ code: string; name: string } | null>(null); @@ -38,23 +45,18 @@ export function useGeoBlocking() { const fetchData = useCallback(async () => { try { const [countriesRes, settingsRes] = await Promise.all([ - supabase - .from('geo_allowed_countries') - .select('*') - .order('country_name'), - supabase - .from('security_settings') - .select('*') - .eq('setting_key', 'geo_blocking') - .single(), + supabase.from('geo_allowed_countries').select('*').order('country_name'), + db.from('security_settings').select('*').eq('setting_key', 'geo_blocking').single(), ]); if (countriesRes.error) throw countriesRes.error; setCountries(countriesRes.data || []); - if (settingsRes.data) { - const value = settingsRes.data.setting_value as GeoBlockingSettings; - setSettings(value); + const settingsResult = settingsRes as unknown as { + data: { setting_value: GeoBlockingSettings } | null; + }; + if (settingsResult.data) { + setSettings(settingsResult.data.setting_value); } } catch (error) { console.error('Error fetching geo blocking data:', error); @@ -68,98 +70,119 @@ export function useGeoBlocking() { fetchData(); }, [fetchCurrentCountry, fetchData]); - const toggleEnabled = useCallback(async (enabled: boolean): Promise<{ success: boolean; error?: string }> => { - try { - const newSettings = { ...settings, enabled }; - const { error } = await supabase - .from('security_settings') - .update({ - setting_value: newSettings, - updated_at: new Date().toISOString(), - updated_by: user?.id - }) - .eq('setting_key', 'geo_blocking'); - - if (error) throw error; - setSettings(newSettings); - return { success: true }; - } catch (error: unknown) { - return { success: false, error: error instanceof Error ? error.message : 'Erro desconhecido' }; - } - }, [settings, user]); - - const addCountry = useCallback(async ( - countryCode: string, - countryName: string - ): Promise<{ success: boolean; error?: string }> => { - if (!user) { - return { success: false, error: 'Usuário não autenticado' }; - } + const toggleEnabled = useCallback( + async (enabled: boolean): Promise<{ success: boolean; error?: string }> => { + try { + const newSettings = { ...settings, enabled }; + const { error } = await db + .from('security_settings') + .update({ + setting_value: newSettings, + updated_at: new Date().toISOString(), + updated_by: user?.id, + }) + .eq('setting_key', 'geo_blocking'); + + if (error) throw error; + setSettings(newSettings); + return { success: true }; + } catch (error: unknown) { + return { + success: false, + error: error instanceof Error ? error.message : 'Erro desconhecido', + }; + } + }, + [settings, user], + ); + + const addCountry = useCallback( + async ( + countryCode: string, + countryName: string, + ): Promise<{ success: boolean; error?: string }> => { + if (!user) { + return { success: false, error: 'Usuário não autenticado' }; + } - try { - const { error } = await supabase - .from('geo_allowed_countries') - .insert({ + try { + const { error } = await supabase.from('geo_allowed_countries').insert({ country_code: countryCode.toUpperCase(), country_name: countryName, created_by: user.id, }); - if (error) { - if (error.code === '23505') { - return { success: false, error: 'Este país já está cadastrado' }; + if (error) { + if (error.code === '23505') { + return { success: false, error: 'Este país já está cadastrado' }; + } + throw error; } - throw error; - } - await fetchData(); - return { success: true }; - } catch (error: unknown) { - return { success: false, error: error instanceof Error ? error.message : 'Erro desconhecido' }; - } - }, [user, fetchData]); - - const removeCountry = useCallback(async (id: string): Promise<{ success: boolean; error?: string }> => { - try { - const { error } = await supabase - .from('geo_allowed_countries') - .delete() - .eq('id', id); - - if (error) throw error; - await fetchData(); - return { success: true }; - } catch (error: unknown) { - return { success: false, error: error instanceof Error ? error.message : 'Erro desconhecido' }; - } - }, [fetchData]); - - const toggleCountry = useCallback(async ( - id: string, - isActive: boolean - ): Promise<{ success: boolean; error?: string }> => { - try { - const { error } = await supabase - .from('geo_allowed_countries') - .update({ is_active: isActive }) - .eq('id', id); - - if (error) throw error; - await fetchData(); - return { success: true }; - } catch (error: unknown) { - return { success: false, error: error instanceof Error ? error.message : 'Erro desconhecido' }; - } - }, [fetchData]); - - const isCountryAllowed = useCallback((countryCode: string): boolean => { - if (!settings.enabled) return true; - - const activeCountries = countries.filter(c => c.is_active); - if (activeCountries.length === 0) return true; - - return activeCountries.some(c => c.country_code.toUpperCase() === countryCode.toUpperCase()); - }, [settings.enabled, countries]); + await fetchData(); + return { success: true }; + } catch (error: unknown) { + return { + success: false, + error: error instanceof Error ? error.message : 'Erro desconhecido', + }; + } + }, + [user, fetchData], + ); + + const removeCountry = useCallback( + async (id: string): Promise<{ success: boolean; error?: string }> => { + try { + const { error } = await supabase.from('geo_allowed_countries').delete().eq('id', id); + + if (error) throw error; + await fetchData(); + return { success: true }; + } catch (error: unknown) { + return { + success: false, + error: error instanceof Error ? error.message : 'Erro desconhecido', + }; + } + }, + [fetchData], + ); + + const toggleCountry = useCallback( + async (id: string, isActive: boolean): Promise<{ success: boolean; error?: string }> => { + try { + const { error } = await supabase + .from('geo_allowed_countries') + .update({ is_active: isActive }) + .eq('id', id); + + if (error) throw error; + await fetchData(); + return { success: true }; + } catch (error: unknown) { + return { + success: false, + error: error instanceof Error ? error.message : 'Erro desconhecido', + }; + } + }, + [fetchData], + ); + + const isCountryAllowed = useCallback( + (countryCode: string): boolean => { + if (!settings.enabled) return true; + + const activeCountries = countries.filter((c) => c.is_active); + if (activeCountries.length === 0) return true; + + return activeCountries.some( + (c) => c.country_code.toUpperCase() === countryCode.toUpperCase(), + ); + }, + [settings.enabled, countries], + ); return { countries, diff --git a/src/hooks/auth/use2FA.ts b/src/hooks/auth/use2FA.ts index f768290d6..7770e8e11 100644 --- a/src/hooks/auth/use2FA.ts +++ b/src/hooks/auth/use2FA.ts @@ -1,8 +1,12 @@ import { useState, useEffect, useCallback } from 'react'; +import { type createClient } from '@supabase/supabase-js'; import { supabase } from '@/integrations/supabase/client'; import { useAuth } from '@/contexts/AuthContext'; import * as OTPAuth from 'otpauth'; +// Tables not yet in generated schema — bypass type checking via raw client cast +const db = supabase as unknown as ReturnType; + interface TwoFactorSettings { id: string; user_id: string; @@ -11,6 +15,15 @@ interface TwoFactorSettings { created_at: string; } +interface User2FARow { + id: string; + user_id: string; + is_enabled: boolean; + enabled_at: string | null; + created_at: string; + totp_secret?: string | null; +} + export function use2FA(targetUserId?: string) { const { user } = useAuth(); const effectiveUserId = targetUserId || user?.id; @@ -26,14 +39,14 @@ export function use2FA(targetUserId?: string) { } try { - const { data, error } = await supabase + const { data, error } = await db .from('user_2fa_settings') .select('id, user_id, is_enabled, enabled_at, created_at') .eq('user_id', effectiveUserId) .maybeSingle(); if (error) throw error; - setSettings(data); + setSettings(data as TwoFactorSettings | null); } catch (error) { console.error('Error fetching 2FA settings:', error); } finally { @@ -47,7 +60,7 @@ export function use2FA(targetUserId?: string) { const generateSecret = useCallback((email: string): { secret: string; uri: string } => { const totp = new OTPAuth.TOTP({ - issuer: 'Promo Gifts', + issuer: 'Promo Gifts', label: email, algorithm: 'SHA1', digits: 6, @@ -57,7 +70,7 @@ export function use2FA(targetUserId?: string) { const secret = totp.secret.base32; const uri = totp.toString(); - + setPendingSecret(secret); return { secret, uri }; }, []); @@ -72,7 +85,7 @@ export function use2FA(targetUserId?: string) { period: 30, secret: OTPAuth.Secret.fromBase32(secret), }); - + const delta = totp.validate({ token, window: 1 }); return delta !== null; } catch { @@ -80,25 +93,24 @@ export function use2FA(targetUserId?: string) { } }, []); - const enable2FA = useCallback(async (token: string): Promise<{ success: boolean; error?: string }> => { - if (!effectiveUserId || !pendingSecret) { - return { success: false, error: 'Nenhum secret pendente' }; - } + const enable2FA = useCallback( + async (token: string): Promise<{ success: boolean; error?: string }> => { + if (!effectiveUserId || !pendingSecret) { + return { success: false, error: 'Nenhum secret pendente' }; + } - // Verificar token antes de salvar - if (!verifyToken(pendingSecret, token)) { - return { success: false, error: 'Código inválido' }; - } + // Verificar token antes de salvar + if (!verifyToken(pendingSecret, token)) { + return { success: false, error: 'Código inválido' }; + } - try { - // Gerar códigos de backup - const backupCodes = Array.from({ length: 8 }, () => - Math.random().toString(36).substring(2, 10).toUpperCase() - ); + try { + // Gerar códigos de backup + const backupCodes = Array.from({ length: 8 }, () => + Math.random().toString(36).substring(2, 10).toUpperCase(), + ); - const { error } = await supabase - .from('user_2fa_settings') - .upsert({ + const { error } = await db.from('user_2fa_settings').upsert({ user_id: effectiveUserId, totp_secret: pendingSecret, is_enabled: true, @@ -106,61 +118,73 @@ export function use2FA(targetUserId?: string) { enabled_at: new Date().toISOString(), }); - if (error) throw error; + if (error) throw error; - setPendingSecret(null); - await fetchSettings(); - - return { success: true }; - } catch (error: unknown) { - return { success: false, error: error instanceof Error ? error.message : 'Erro desconhecido' }; - } - }, [effectiveUserId, pendingSecret, verifyToken, fetchSettings]); - - const disable2FA = useCallback(async (token?: string): Promise<{ success: boolean; error?: string }> => { - if (!effectiveUserId) { - return { success: false, error: 'Usuário não autenticado' }; - } - - try { - // Buscar secret atual - const { data: currentSettings } = await supabase - .from('user_2fa_settings') - .select('totp_secret') - .eq('user_id', effectiveUserId) - .single(); + setPendingSecret(null); + await fetchSettings(); - if (!currentSettings?.totp_secret) { - return { success: false, error: '2FA não está habilitado' }; + return { success: true }; + } catch (error: unknown) { + return { + success: false, + error: error instanceof Error ? error.message : 'Erro desconhecido', + }; } - - // Se token fornecido, verificar. Admin pode desativar sem token se targetUserId diferente - if (token) { - if (!verifyToken(currentSettings.totp_secret, token)) { - return { success: false, error: 'Código inválido' }; - } - } else if (!targetUserId) { - return { success: false, error: 'Código necessário' }; + }, + [effectiveUserId, pendingSecret, verifyToken, fetchSettings], + ); + + const disable2FA = useCallback( + async (token?: string): Promise<{ success: boolean; error?: string }> => { + if (!effectiveUserId) { + return { success: false, error: 'Usuário não autenticado' }; } - const { error } = await supabase - .from('user_2fa_settings') - .update({ - is_enabled: false, - totp_secret: null, - backup_codes: null, - enabled_at: null, - }) - .eq('user_id', effectiveUserId); + try { + // Buscar secret atual + const { data: currentSettings } = await db + .from('user_2fa_settings') + .select('totp_secret') + .eq('user_id', effectiveUserId) + .single(); + + const row = currentSettings as User2FARow | null; + if (!row?.totp_secret) { + return { success: false, error: '2FA não está habilitado' }; + } - if (error) throw error; + // Se token fornecido, verificar. Admin pode desativar sem token se targetUserId diferente + if (token) { + if (!verifyToken(row.totp_secret, token)) { + return { success: false, error: 'Código inválido' }; + } + } else if (!targetUserId) { + return { success: false, error: 'Código necessário' }; + } - await fetchSettings(); - return { success: true }; - } catch (error: unknown) { - return { success: false, error: error instanceof Error ? error.message : 'Erro desconhecido' }; - } - }, [effectiveUserId, targetUserId, verifyToken, fetchSettings]); + const { error } = await db + .from('user_2fa_settings') + .update({ + is_enabled: false, + totp_secret: null, + backup_codes: null, + enabled_at: null, + }) + .eq('user_id', effectiveUserId); + + if (error) throw error; + + await fetchSettings(); + return { success: true }; + } catch (error: unknown) { + return { + success: false, + error: error instanceof Error ? error.message : 'Erro desconhecido', + }; + } + }, + [effectiveUserId, targetUserId, verifyToken, fetchSettings], + ); return { settings, diff --git a/src/hooks/auth/useAccessSecurity.ts b/src/hooks/auth/useAccessSecurity.ts index 6abf2076e..0fd015459 100644 --- a/src/hooks/auth/useAccessSecurity.ts +++ b/src/hooks/auth/useAccessSecurity.ts @@ -1,6 +1,10 @@ -import { useState, useEffect, useCallback } from "react"; -import { supabase } from "@/integrations/supabase/client"; -import { toast } from "sonner"; +import { useState, useEffect, useCallback } from 'react'; +import { type createClient } from '@supabase/supabase-js'; +import { supabase } from '@/integrations/supabase/client'; +import { toast } from 'sonner'; + +// Tables not yet in generated schema — bypass type checking via raw client cast +const db = supabase as unknown as ReturnType; export interface IpWhitelistEntry { id: string; @@ -51,95 +55,114 @@ export function useAccessSecurity() { setIsLoading(true); try { const [settingsRes, ipsRes, citiesRes, logsRes] = await Promise.all([ - supabase.from("access_security_settings").select("*").limit(1).single(), - supabase.from("ip_whitelist").select("*").order("created_at", { ascending: false }), - supabase.from("city_whitelist").select("*").order("created_at", { ascending: false }), - supabase.from("access_blocked_log").select("*").order("created_at", { ascending: false }).limit(50), + db.from('access_security_settings').select('*').limit(1).single(), + db.from('ip_whitelist').select('*').order('created_at', { ascending: false }), + db.from('city_whitelist').select('*').order('created_at', { ascending: false }), + db + .from('access_blocked_log') + .select('*') + .order('created_at', { ascending: false }) + .limit(50), ]); - if (settingsRes.data) setSettings(settingsRes.data as unknown as AccessSecuritySettings); - if (ipsRes.data) setIps(ipsRes.data as unknown as IpWhitelistEntry[]); - if (citiesRes.data) setCities(citiesRes.data as unknown as CityWhitelistEntry[]); - if (logsRes.data) setBlockedLogs(logsRes.data as unknown as AccessBlockedLog[]); + const settingsData = (settingsRes as unknown as { data: AccessSecuritySettings | null }).data; + if (settingsData) setSettings(settingsData); + if (ipsRes.data) setIps(ipsRes.data as IpWhitelistEntry[]); + if (citiesRes.data) setCities(citiesRes.data as CityWhitelistEntry[]); + if (logsRes.data) setBlockedLogs(logsRes.data as AccessBlockedLog[]); } catch (error) { - console.error("Erro ao carregar configurações de acesso:", error); + console.error('Erro ao carregar configurações de acesso:', error); } finally { setIsLoading(false); } }, []); - useEffect(() => { fetchAll(); }, [fetchAll]); + useEffect(() => { + fetchAll(); + }, [fetchAll]); const updateSettings = async (updates: Partial) => { if (!settings) return; - const { error } = await supabase - .from("access_security_settings") + const { error } = await db + .from('access_security_settings') .update(updates as Record) - .eq("id", settings.id); + .eq('id', settings.id); if (error) { - toast.error("Erro ao atualizar configurações"); + toast.error('Erro ao atualizar configurações'); return; } setSettings({ ...settings, ...updates }); - toast.success("Configurações atualizadas"); + toast.success('Configurações atualizadas'); }; const addIp = async (ip_address: string, label?: string) => { - const { data, error } = await supabase - .from("ip_whitelist") - .insert({ ip_address, label: label || null } as Record) + const { data, error } = await db + .from('ip_whitelist') + .insert({ ip_address, label: label || null } as never) .select() .single(); if (error) { - if (error.code === "23505") toast.error("IP já cadastrado"); - else toast.error("Erro ao adicionar IP"); + if ((error as { code?: string }).code === '23505') toast.error('IP já cadastrado'); + else toast.error('Erro ao adicionar IP'); return false; } - setIps(prev => [data as unknown as IpWhitelistEntry, ...prev]); - toast.success("IP adicionado à whitelist"); + setIps((prev) => [data as IpWhitelistEntry, ...prev]); + toast.success('IP adicionado à whitelist'); return true; }; const removeIp = async (id: string) => { - const { error } = await supabase.from("ip_whitelist").delete().eq("id", id); - if (error) { toast.error("Erro ao remover IP"); return; } - setIps(prev => prev.filter(ip => ip.id !== id)); - toast.success("IP removido"); + const { error } = await db.from('ip_whitelist').delete().eq('id', id); + if (error) { + toast.error('Erro ao remover IP'); + return; + } + setIps((prev) => prev.filter((ip) => ip.id !== id)); + toast.success('IP removido'); }; const toggleIp = async (id: string, is_active: boolean) => { - const { error } = await supabase.from("ip_whitelist").update({ is_active } as Record).eq("id", id); - if (error) { toast.error("Erro ao atualizar IP"); return; } - setIps(prev => prev.map(ip => ip.id === id ? { ...ip, is_active } : ip)); + const { error } = await db.from('ip_whitelist').update({ is_active }).eq('id', id); + if (error) { + toast.error('Erro ao atualizar IP'); + return; + } + setIps((prev) => prev.map((ip) => (ip.id === id ? { ...ip, is_active } : ip))); }; - const addCity = async (city_name: string, state?: string, country_code = "BR") => { - const { data, error } = await supabase - .from("city_whitelist") - .insert({ city_name, state: state || null, country_code } as Record) + const addCity = async (city_name: string, state?: string, country_code = 'BR') => { + const { data, error } = await db + .from('city_whitelist') + .insert({ city_name, state: state || null, country_code } as never) .select() .single(); if (error) { - if (error.code === "23505") toast.error("Cidade já cadastrada"); - else toast.error("Erro ao adicionar cidade"); + if ((error as { code?: string }).code === '23505') toast.error('Cidade já cadastrada'); + else toast.error('Erro ao adicionar cidade'); return false; } - setCities(prev => [data as unknown as CityWhitelistEntry, ...prev]); - toast.success("Cidade adicionada à whitelist"); + setCities((prev) => [data as CityWhitelistEntry, ...prev]); + toast.success('Cidade adicionada à whitelist'); return true; }; const removeCity = async (id: string) => { - const { error } = await supabase.from("city_whitelist").delete().eq("id", id); - if (error) { toast.error("Erro ao remover cidade"); return; } - setCities(prev => prev.filter(c => c.id !== id)); - toast.success("Cidade removida"); + const { error } = await db.from('city_whitelist').delete().eq('id', id); + if (error) { + toast.error('Erro ao remover cidade'); + return; + } + setCities((prev) => prev.filter((c) => c.id !== id)); + toast.success('Cidade removida'); }; const toggleCity = async (id: string, is_active: boolean) => { - const { error } = await supabase.from("city_whitelist").update({ is_active } as Record).eq("id", id); - if (error) { toast.error("Erro ao atualizar cidade"); return; } - setCities(prev => prev.map(c => c.id === id ? { ...c, is_active } : c)); + const { error } = await db.from('city_whitelist').update({ is_active }).eq('id', id); + if (error) { + toast.error('Erro ao atualizar cidade'); + return; + } + setCities((prev) => prev.map((c) => (c.id === id ? { ...c, is_active } : c))); }; return { diff --git a/src/hooks/favorites/useFavoritesPageState.ts b/src/hooks/favorites/useFavoritesPageState.ts index 5aeaef129..25f0e430f 100644 --- a/src/hooks/favorites/useFavoritesPageState.ts +++ b/src/hooks/favorites/useFavoritesPageState.ts @@ -9,6 +9,7 @@ import { } from '@/hooks/favorites'; import { useProductsContext } from '@/contexts/ProductsContext'; import { useCatalogSelection } from '@/components/catalog/useCatalogSelection'; +import type { Product } from '@/hooks/products'; import { useUndoStack } from '@/hooks/common'; import { getDefaultColumns, type ColumnCount } from '@/components/products/ColumnSelector'; import type { FavoritesSort } from '@/components/favorites/FavoritesSortBar'; @@ -77,7 +78,6 @@ export function useFavoritesPageState() { deleteList, generateShareToken, revokeShareToken, - moveItem, } = useFavoriteLists(); const { items: trashItems } = useFavoriteTrash(); const { getProductsByIds, products: _cacheSignal } = useProductsContext(); @@ -113,6 +113,7 @@ export function useFavoritesPageState() { enriched, rawItems, removeItem, + moveItem, updateItem: _updateItem, } = useEnrichedFavoriteItems(selectedListId); @@ -178,7 +179,7 @@ export function useFavoritesPageState() { } const legacyProducts = getProductsByIds(favorites.map((f) => f.productId)); return legacyProducts.map((product) => { - const variant = variantMap.get(product.id); + const variant = variantMap.get(product.id) as { thumbnail?: string } | undefined; if (variant?.thumbnail) { return { ...product, images: [variant.thumbnail, ...(product.images || [])] }; } @@ -194,7 +195,7 @@ export function useFavoritesPageState() { (p) => p.name.toLowerCase().includes(q) || p.sku?.toLowerCase().includes(q) || - p.brand?.toLowerCase().includes(q), + (p as { brand?: string }).brand?.toLowerCase().includes(q), ); } // Sorting and drops logic... (Simplified for brevity as per instructions) @@ -203,7 +204,7 @@ export function useFavoritesPageState() { }, [productsWithVariant, searchQuery, sort, onlyPriceDrops, isRemoteListView]); // Bulk selection - const selection = useCatalogSelection(filteredProducts, selectionMode); + const selection = useCatalogSelection(filteredProducts as Product[], selectionMode); // Handlers const handleClearAll = () => { diff --git a/src/hooks/intelligence/useExternalDatabase.ts b/src/hooks/intelligence/useExternalDatabase.ts index cebce6c7d..80d482ada 100644 --- a/src/hooks/intelligence/useExternalDatabase.ts +++ b/src/hooks/intelligence/useExternalDatabase.ts @@ -1,11 +1,11 @@ /** * External Database Hook - Ponto de entrada principal. - * + * * Modularizado em: * - src/lib/external-db/types.ts → Interfaces de tipos * - src/lib/external-db/tables.ts → Constantes de tabelas/views * - src/lib/external-db/invoke.ts → Retry logic e error handling - * + * * Este arquivo contém o hook principal e re-exporta tudo para compatibilidade. */ import { useState, useCallback } from 'react'; @@ -18,7 +18,7 @@ export { extractFunctionErrorMessage } from '@/lib/external-db/invoke'; import type { ExternalTable } from '@/lib/external-db/tables'; import { invokeWithRetry, extractFunctionErrorMessage } from '@/lib/external-db/invoke'; -import { logger } from "@/lib/logger"; +import { logger } from '@/lib/logger'; import type { ExternalProduct, ExternalProductImage, @@ -89,113 +89,135 @@ export function useExternalDatabase>(tableName: Exte error: null, }); - const invoke = useCallback(async ( - operation: Operation, - options?: QueryOptions & { data?: Partial } - ): Promise | null> => { - setState(prev => ({ ...prev, isLoading: true, error: null })); - - try { - const { data, error } = await invokeWithRetry({ - table: tableName, - operation, - data: options?.data, - filters: options?.filters, - id: options?.id, - select: options?.select, - orderBy: options?.orderBy, - limit: options?.limit, - offset: options?.offset, - }); - - if (error) { - const message = await extractFunctionErrorMessage(error); - throw new Error(message); + const invoke = useCallback( + async ( + operation: Operation, + options?: QueryOptions & { data?: Partial }, + ): Promise | null> => { + setState((prev) => ({ ...prev, isLoading: true, error: null })); + + try { + const { data, error } = await invokeWithRetry({ + table: tableName, + operation, + data: options?.data, + filters: options?.filters, + id: options?.id, + select: options?.select, + orderBy: options?.orderBy, + limit: options?.limit, + offset: options?.offset, + }); + + if (error) { + const message = await extractFunctionErrorMessage(error); + throw new Error(message); + } + + const bridgeData = data as { success?: boolean; error?: string; data?: unknown } | null; + + if (!bridgeData?.success) { + throw new Error(bridgeData?.error || 'Erro desconhecido'); + } + + if (operation === 'select') { + const result = (bridgeData?.data ?? null) as QueryResult; + setState((prev) => ({ + ...prev, + data: result.records, + count: result.count, + isLoading: false, + })); + return result; + } + setState((prev) => ({ ...prev, isLoading: false })); + return (bridgeData?.data ?? null) as T; + } catch (err) { + const errorMessage = await extractFunctionErrorMessage(err); + setState((prev) => ({ ...prev, error: errorMessage, isLoading: false })); + toast.error(errorMessage); + return null; } - - if (!data?.success) { - throw new Error(data?.error || 'Erro desconhecido'); + }, + [tableName], + ); + + const fetchAll = useCallback( + async (options?: Omit) => { + return invoke('select', options) as Promise | null>; + }, + [invoke], + ); + + const fetchOne = useCallback( + async (id: string, select?: string) => { + const result = await invoke('select', { id, select, limit: 1 }); + if (result && typeof result === 'object' && 'records' in result) { + return (result as QueryResult).records[0] || null; } - - if (operation === 'select') { - const result = data.data as QueryResult; - setState(prev => ({ - ...prev, - data: result.records, - count: result.count, - isLoading: false - })); - return result; - } - setState(prev => ({ ...prev, isLoading: false })); - return data.data as T; - - } catch (err) { - const errorMessage = await extractFunctionErrorMessage(err); - setState(prev => ({ ...prev, error: errorMessage, isLoading: false })); - toast.error(errorMessage); return null; - } - }, [tableName]); - - const fetchAll = useCallback(async (options?: Omit) => { - return invoke('select', options) as Promise | null>; - }, [invoke]); - - const fetchOne = useCallback(async (id: string, select?: string) => { - const result = await invoke('select', { id, select, limit: 1 }); - if (result && 'records' in result) { - return result.records[0] || null; - } - return null; - }, [invoke]); - - const create = useCallback(async (data: Partial) => { - const result = await invoke('insert', { data }); - if (!result) return null; - - if ('records' in result) { - const created = result.records[0] || null; - if (created) { - toast.success('Registro criado com sucesso!'); - return created as T; + }, + [invoke], + ); + + const create = useCallback( + async (data: Partial) => { + const result = await invoke('insert', { data }); + if (!result) return null; + + if (typeof result === 'object' && 'records' in result) { + const created = (result as QueryResult).records[0] || null; + if (created) { + toast.success('Registro criado com sucesso!'); + return created as T; + } + return null; } - return null; - } - toast.success('Registro criado com sucesso!'); - return result as T; - }, [invoke]); - - const update = useCallback(async (id: string, data: Partial) => { - const result = await invoke('update', { id, data }); - if (!result) return null; + toast.success('Registro criado com sucesso!'); + return result as T; + }, + [invoke], + ); + + const update = useCallback( + async (id: string, data: Partial) => { + const result = await invoke('update', { id, data }); + if (!result) return null; + + if (typeof result === 'object' && 'records' in result) { + const updated = (result as QueryResult).records[0] || null; + if (updated) { + toast.success('Registro atualizado com sucesso!'); + return updated as T; + } + return null; + } - if ('records' in result) { - const updated = result.records[0] || null; - if (updated) { - toast.success('Registro atualizado com sucesso!'); - return updated as T; + toast.success('Registro atualizado com sucesso!'); + return result as T; + }, + [invoke], + ); + + const remove = useCallback( + async (id: string) => { + const result = await invoke('delete', { id }); + if (result) { + toast.success('Registro excluído com sucesso!'); + return true; } - return null; - } - - toast.success('Registro atualizado com sucesso!'); - return result as T; - }, [invoke]); - - const remove = useCallback(async (id: string) => { - const result = await invoke('delete', { id }); - if (result) { - toast.success('Registro excluído com sucesso!'); - return true; - } - return false; - }, [invoke]); - - const refetch = useCallback(async (options?: Omit) => { - return fetchAll(options); - }, [fetchAll]); + return false; + }, + [invoke], + ); + + const refetch = useCallback( + async (options?: Omit) => { + return fetchAll(options); + }, + [fetchAll], + ); return { ...state, diff --git a/src/hooks/intelligence/useMagicUpState.ts b/src/hooks/intelligence/useMagicUpState.ts index 3303cdf6d..a02a26523 100644 --- a/src/hooks/intelligence/useMagicUpState.ts +++ b/src/hooks/intelligence/useMagicUpState.ts @@ -13,10 +13,10 @@ import { useAriaLive } from '@/components/a11y'; import { useProductCustomizationOptionsForMockup } from '@/hooks/mockup'; import { searchCrm } from '@/lib/crm-db'; import { getCompanyDisplayName, type CrmCompany } from '@/types/crm'; -import type { PrintAreaWithTechniques } from '@/types/gravacao'; +import type { PrintAreaWithTechniques, AreaShape } from '@/types/gravacao'; import type { ScenePrompt } from '@/components/magic-up/PromptBank'; import type { GenerationHistoryItem } from '@/components/magic-up/AdImageResult'; -import { useMagicUpGeneration } from "@/hooks/intelligence/useMagicUpGeneration"; +import { useMagicUpGeneration } from '@/hooks/intelligence/useMagicUpGeneration'; import { DEFAULT_BRAND_KIT, DEFAULT_BRIEF, @@ -204,7 +204,7 @@ export function useMagicUpState() { .order('updated_at', { ascending: false }) .limit(30); if (error) throw error; - return (data || []).map((row: Tables<'magic_up_campaigns'>) => ({ + return ((data || []) as Tables<'magic_up_campaigns'>[]).map((row) => ({ id: row.id, title: row.title, status: row.status as MagicUpCampaignStatus, @@ -278,7 +278,7 @@ export function useMagicUpState() { images: p.images || [], primary_image_url: p.primary_image_url || p.image_url || null, og_image_url: p.og_image_url || null, - })), + })) as unknown as MagicUpProduct[], ); } catch { toast.error('Erro ao carregar produtos'); @@ -326,16 +326,17 @@ export function useMagicUpState() { isPrimary: img.is_primary, isOgImage: img.is_og_image || false, })) - .filter((img: ProductImage) => img.url); + .filter((img) => !!(img as ProductImage).url) as ProductImage[]; setProductImages(images); const uniqueColors = new Map(); (variantsResult.records || []).forEach((v: Record) => { - if (!v.color_name || uniqueColors.has(v.color_name)) return; - uniqueColors.set(v.color_name, { - hex: v.color_hex || '#CCCCCC', - name: v.color_name, - code: v.color_code || '', - stock: v.stock_quantity ?? 0, + const colorName = v.color_name as string | undefined; + if (!colorName || uniqueColors.has(colorName)) return; + uniqueColors.set(colorName, { + hex: (v.color_hex as string) || '#CCCCCC', + name: colorName, + code: (v.color_code as string) || '', + stock: (v.stock_quantity as number) ?? 0, }); }); setColors(Array.from(uniqueColors.values())); @@ -360,7 +361,7 @@ export function useMagicUpState() { max_width: Math.max(...loc.options.map((o) => o.efetiva_largura_max || 0), 0), max_height: Math.max(...loc.options.map((o) => o.efetiva_altura_max || 0), 0), unit: 'cm', - shape: loc.options[0]?.shape || 'rectangle', + shape: (loc.options[0]?.shape || 'rectangle') as AreaShape, is_curved: loc.options.some((o) => o.is_curved), is_primary: idx === 0, display_order: loc.location_order, @@ -928,8 +929,6 @@ CENÁRIO: ${effectivePrompt}`; handleRunBatchQueue, handleClearBatchQueue, qualityScore, - qualityDiagnosis: generation.qualityDiagnosis, - curationStatus: generation.curationStatus, copyPack, effectivePrompt, fullPromptPreview, diff --git a/src/hooks/kit-builder/useKitBuilderPageState.ts b/src/hooks/kit-builder/useKitBuilderPageState.ts index 671e0c8eb..d137851ec 100644 --- a/src/hooks/kit-builder/useKitBuilderPageState.ts +++ b/src/hooks/kit-builder/useKitBuilderPageState.ts @@ -3,9 +3,9 @@ import confetti from 'canvas-confetti'; import { useSearchParams } from 'react-router-dom'; import { transformToKitItem, useCustomKitPersistence, useDuplicateKitDetector, useKitAutoSave, useKitBuilder, useKitUndoRedo, useTemplateSnapshot } from "@/hooks/kit-builder"; import { useKitBuilderQuote } from '@/pages/kit-builder/useKitBuilderQuote'; -import { invokeExternalDb, type PromobrindProduct } from '@/lib/external-db'; +import { invokeExternalDb } from '@/lib/external-db'; +import { calculateTotalKitPrice, type ExternalProductForKit } from '@/lib/kit-builder'; import { logger } from '@/lib/logger'; -import { calculateTotalKitPrice } from '@/lib/kit-builder'; export function useKitBuilderPageState() { const [searchParams] = useSearchParams(); @@ -25,6 +25,8 @@ export function useKitBuilderPageState() { isLoadingItems, boxFilters, itemFilters, + setBoxFilters, + setItemFilters, setKitName, selectBox, clearBox, @@ -59,7 +61,7 @@ export function useKitBuilderPageState() { if (productIdParam && !kitIdParam) { (async () => { try { - const result = await invokeExternalDb({ + const result = await invokeExternalDb({ table: 'products', operation: 'select', filters: { id: productIdParam }, @@ -103,6 +105,8 @@ export function useKitBuilderPageState() { isLoadingItems, boxFilters, itemFilters, + setBoxFilters, + setItemFilters, occasion, setOccasion, }, diff --git a/src/hooks/mockup/useMockupDraft.ts b/src/hooks/mockup/useMockupDraft.ts index b383ce4a7..4446b789e 100644 --- a/src/hooks/mockup/useMockupDraft.ts +++ b/src/hooks/mockup/useMockupDraft.ts @@ -2,6 +2,7 @@ import { useState, useEffect, useCallback, useRef } from 'react'; import { supabase } from '@/integrations/supabase/client'; import { useAuth } from '@/contexts/AuthContext'; import { type PersonalizationArea } from '@/components/mockup/MultiAreaManager'; +import type { Json } from '@/integrations/supabase/types'; const LOCAL_STORAGE_KEY = 'mockup_draft_v1'; const AUTO_SAVE_DELAY = 2000; // 2 segundos de debounce @@ -126,7 +127,7 @@ export function useMockupDraft(options: UseMockupDraftOptions = {}) { technique_name: data.techniqueName, client_id: safeClientId, client_name: data.clientName, - personalization_areas: areasWithoutLogos as unknown as Record[], + personalization_areas: areasWithoutLogos as unknown as Json, logo_data: safeLogoData, updated_at: new Date().toISOString(), }; @@ -139,16 +140,17 @@ export function useMockupDraft(options: UseMockupDraftOptions = {}) { if (upsertError) { // If FK violation or conflict, try update-only as fallback if (upsertError.code === '23503' || upsertError.code === '409') { - const { - product_id: _pid, - technique_id: _tid, - client_id: _cid, - ...safePayload - } = payload as Record; const { error: updateError } = await supabase .from('mockup_drafts') .update({ - ...safePayload, + user_id: payload.user_id, + draft_key: payload.draft_key, + product_name: payload.product_name, + technique_name: payload.technique_name, + client_name: payload.client_name, + personalization_areas: payload.personalization_areas, + logo_data: payload.logo_data, + updated_at: payload.updated_at, product_id: null, technique_id: null, client_id: null, @@ -194,17 +196,20 @@ export function useMockupDraft(options: UseMockupDraftOptions = {}) { if (data) { const areas = Array.isArray(data.personalization_areas) - ? (data.personalization_areas as unknown[]).map((a) => ({ - id: a.id || crypto.randomUUID(), - name: a.name || 'Frente', - positionX: a.positionX ?? 50, - positionY: a.positionY ?? 50, - logoWidth: a.logoWidth ?? 5, - logoHeight: a.logoHeight ?? 3, - logoRotation: a.logoRotation ?? 0, - logoScale: a.logoScale ?? 100, - logoPreview: a.logoPreview || null, - })) + ? (data.personalization_areas as unknown[]).map((item) => { + const a = item as Record; + return { + id: (a.id as string | undefined) || crypto.randomUUID(), + name: (a.name as string | undefined) || 'Frente', + positionX: (a.positionX as number | undefined) ?? 50, + positionY: (a.positionY as number | undefined) ?? 50, + logoWidth: (a.logoWidth as number | undefined) ?? 5, + logoHeight: (a.logoHeight as number | undefined) ?? 3, + logoRotation: (a.logoRotation as number | undefined) ?? 0, + logoScale: (a.logoScale as number | undefined) ?? 100, + logoPreview: (a.logoPreview as string | undefined) || null, + }; + }) : []; // Restaurar logo do campo logo_data se não estiver nas áreas diff --git a/src/hooks/products/useCatalogState.ts b/src/hooks/products/useCatalogState.ts index e25c05ad2..49e803413 100644 --- a/src/hooks/products/useCatalogState.ts +++ b/src/hooks/products/useCatalogState.ts @@ -2,7 +2,17 @@ * useCatalogState — all catalog page state & logic extracted from Index.tsx */ import React, { useState, useMemo, useEffect, useRef, useCallback, useDeferredValue } from 'react'; -import { useCatalogRealStats, useColorEnrichment, useExternalCategoriesQuery, useProductFuzzySearch, useProductsByCategory, useProductsByMaterial, useProductsCatalog, useSupplierSalesRanking, type Product } from "@/hooks/products"; +import { + useCatalogRealStats, + useColorEnrichment, + useExternalCategoriesQuery, + useProductFuzzySearch, + useProductsByCategory, + useProductsByMaterial, + useProductsCatalog, + useSupplierSalesRanking, + type Product, +} from '@/hooks/products'; import { useNavigate, useSearchParams } from 'react-router-dom'; import { Package, Heart, Users, Palette, FolderTree } from 'lucide-react'; @@ -13,13 +23,13 @@ import { type ColumnCount, } from '@/components/products/ColumnSelector'; import { useProductsContext } from '@/contexts/ProductsContext'; -import { useDebounce, useSearch } from "@/hooks/common"; +import { useDebounce, useSearch } from '@/hooks/common'; import { useFavoritesStore } from '@/stores/useFavoritesStore'; import { useFavoriteQuickAdd } from '@/hooks/favorites'; import { useComparisonStore } from '@/stores/useComparisonStore'; import { useToast } from '@/hooks/ui'; import { usePromoSalesRanking } from '@/hooks/intelligence'; -import { useCatalogFiltering } from "@/hooks/products/useCatalogFiltering"; +import { useCatalogFiltering } from '@/hooks/products/useCatalogFiltering'; export type ViewMode = 'grid' | 'list' | 'table'; export type SortOption = @@ -111,9 +121,9 @@ export function useCatalogState() { const handleResize = () => { const w = window.innerWidth; if (w < 640 && gridColumns > 1) { - setGridColumnsState(1); + setGridColumnsState(3 as ColumnCount); } else if (w >= 640 && w < 768 && gridColumns > 2) { - setGridColumnsState(2); + setGridColumnsState(3 as ColumnCount); } }; handleResize(); @@ -242,7 +252,7 @@ export function useCatalogState() { categoryFilteredProductIds, isLoadingCategoryFilter, promoSalesMap, - supplierSalesMap, + supplierSalesMap: supplierSalesMap as unknown as Map | undefined, }); const [lastNonTransitionedProducts, setLastNonTransitionedProducts] = useState([]); @@ -315,10 +325,10 @@ export function useCatalogState() { filters.colorVariations, ]); + const hasActiveCatalogConstraints = activeFiltersCount > 0 || searchQuery.trim().length > 0; const shouldShowCatalogSkeleton = isInitialCatalogLoad || (isLoading && paginatedProducts.length === 0 && !hasActiveCatalogConstraints); - const hasActiveCatalogConstraints = activeFiltersCount > 0 || searchQuery.trim().length > 0; const shouldShowEmptyState = !shouldShowCatalogSkeleton && paginatedProducts.length === 0 && !isFetchingNextPage; @@ -400,8 +410,7 @@ export function useCatalogState() { const productCount = hasActiveFilters ? deduped.length : totalEstimate || deduped.length; const localVariants = deduped.reduce((sum, p) => { - const colorCount = - p.colors?.filter((c: Record) => c.name?.trim()).length || 0; + const colorCount = p.colors?.filter((c) => (c as { name?: string }).name?.trim()).length || 0; const variationCount = !colorCount && p.variations?.length ? p.variations.length : 0; return sum + colorCount + variationCount; }, 0); @@ -496,11 +505,11 @@ export function useCatalogState() { const handleFavoriteProduct = useCallback( (product: Product, e?: React.MouseEvent) => { - const result = favQuickAdd.handleFavoriteClick(product, { shiftKey: e?.shiftKey }); + const result = favQuickAdd.handleFavoriteClick(product as never, { shiftKey: e?.shiftKey }); if (!result.resolved && result.reason === 'picker-needed') { const target = favQuickAdd.defaultList; if (target) { - void favQuickAdd.addToList(target.id, product); + void favQuickAdd.addToList(target.id, product as never); toast({ title: 'Adicionado aos Favoritos', description: `Salvo em "${target.name}". Use Shift+clique para confirmar a lista padrão sem confirmação.`, @@ -636,5 +645,11 @@ export function useCatalogState() { quickSuggestions, searchHistory: history, clearHistory, + // Navigation & pagination + navigate, + isTransitioning: deferredIsTransitioning, + hasMoreProducts, + ITEMS_PER_PAGE, + loadMore, }; } diff --git a/src/hooks/products/useVariantSupplierSources.ts b/src/hooks/products/useVariantSupplierSources.ts index 51d0eacce..fe50ddeb6 100644 --- a/src/hooks/products/useVariantSupplierSources.ts +++ b/src/hooks/products/useVariantSupplierSources.ts @@ -15,12 +15,17 @@ export interface VariantWithStock { } export interface StockEntry { + id: string; variantId: string; colorName: string; colorHex: string | null; expectedDate: string; expectedQuantity: number; thumbnail: string | null; + supplierSku?: string; + currentStock?: number; + reservedStock?: number; + entryIndex?: number; } export interface ColorSummary { @@ -53,13 +58,14 @@ export function useProductVariantsWithStock(productId: string | undefined) { }>({ table: 'product_variants', operation: 'select', - select: 'id, product_id, sku, color_code, color_name, color_hex, stock_quantity, selected_thumbnail', + select: + 'id, product_id, sku, color_code, color_name, color_hex, stock_quantity, selected_thumbnail', filters: { product_id: productId, is_active: true }, limit: 200, }); // Map to VariantWithStock (next_entry fields come from variant_supplier_sources if available) - return result.records.map(v => ({ + return result.records.map((v) => ({ ...v, next_entry_date: null, next_entry_quantity: null, @@ -76,15 +82,23 @@ export function useProductVariantsWithStock(productId: string | undefined) { export function processStockEntries(variants: VariantWithStock[]): StockEntry[] { const entries: StockEntry[] = []; + const entryCountByVariant = new Map(); for (const v of variants) { if (v.next_entry_date && v.next_entry_quantity && v.next_entry_quantity > 0) { + const idx = (entryCountByVariant.get(v.id) ?? 0) + 1; + entryCountByVariant.set(v.id, idx); entries.push({ + id: `${v.id}-${idx}`, variantId: v.id, colorName: v.color_name || 'Sem cor', colorHex: v.color_hex, expectedDate: v.next_entry_date, expectedQuantity: v.next_entry_quantity, thumbnail: v.selected_thumbnail, + supplierSku: v.sku, + currentStock: v.stock_quantity ?? 0, + reservedStock: 0, + entryIndex: idx, }); } } @@ -97,7 +111,7 @@ export function processStockEntries(variants: VariantWithStock[]): StockEntry[] */ export function calculateColorSummary( variants: VariantWithStock[], - stockEntries: StockEntry[] + stockEntries: StockEntry[], ): ColorSummary[] { const colorMap = new Map(); diff --git a/src/hooks/simulation/useSimulation.ts b/src/hooks/simulation/useSimulation.ts index b510e9119..bad9e8bb8 100644 --- a/src/hooks/simulation/useSimulation.ts +++ b/src/hooks/simulation/useSimulation.ts @@ -14,10 +14,13 @@ import { import { useAuth } from '@/contexts/AuthContext'; import { toast } from 'sonner'; import { logger } from '@/lib/logger'; -import { useMultipleTechniquePricing } from "@/hooks/simulation/useTechniquePricingOptions"; -import { useSimulatorPreferences } from "@/hooks/simulation/useSimulatorPreferences"; -import { fetchAllOptions } from "@/hooks/simulation/simulationPriceFetcher"; -import { copyOptionToClipboard, copyAllOptionsToClipboard } from "@/hooks/simulation/simulationClipboard"; +import { useMultipleTechniquePricing } from '@/hooks/simulation/useTechniquePricingOptions'; +import { useSimulatorPreferences } from '@/hooks/simulation/useSimulatorPreferences'; +import { fetchAllOptions } from '@/hooks/simulation/simulationPriceFetcher'; +import { + copyOptionToClipboard, + copyAllOptionsToClipboard, +} from '@/hooks/simulation/simulationClipboard'; import type { Product, Client, @@ -171,11 +174,14 @@ export function useSimulation() { orderBy: { column: 'name', ascending: true }, limit: 100, }); - return result.records.map((t) => ({ - ...t, - setup_cost: (t as ExternalTechnique).setup_price ?? t.setup_cost, - unit_cost: (t as ExternalTechnique).handling_price ?? t.unit_cost, - })); + return result.records.map((t) => { + const ext = t as unknown as ExternalTechnique; + return { + ...t, + setup_cost: ext.setup_price ?? t.setup_cost, + unit_cost: ext.handling_price ?? t.unit_cost, + }; + }); }, }); @@ -185,7 +191,7 @@ export function useSimulation() { ); const { isLoading: pricingLoading, getPricingInfo } = useMultipleTechniquePricing(techniqueCodes); - const { data: savedSimulations, isLoading: savedSimulationsLoading } = useQuery({ + const { data: _savedSimulations, isLoading: savedSimulationsLoading } = useQuery({ queryKey: ['saved-simulations'], queryFn: async () => { const { data, error } = await supabase @@ -197,9 +203,10 @@ export function useSimulation() { return (data || []).map((item) => ({ ...item, simulation_data: item.simulation_data as unknown as SimulationOption[], - })) as SavedSimulation[]; + })); }, }); + const savedSimulations = (_savedSimulations ?? []) as SavedSimulation[]; // ─── Derived ────────────────────────────────────────────── const selectedProduct = useMemo( @@ -453,7 +460,7 @@ export function useSimulation() { }, []); // ─── Mutations ──────────────────────────────────────────── - const saveSimulationMutation = useMutation({ + const saveSimulationMutation = useMutation({ mutationFn: async () => { if (!user || !selectedProduct || simulationOptions.length === 0) throw new Error('Dados incompletos'); @@ -484,7 +491,7 @@ export function useSimulation() { }, }); - const deleteSimulationMutation = useMutation({ + const deleteSimulationMutation = useMutation({ mutationFn: async (id: string) => { const { error } = await supabase.from('personalization_simulations').delete().eq('id', id); if (error) throw error; @@ -597,4 +604,4 @@ export function useSimulation() { // Re-export for backward compatibility with legacy simulator imports. // Keep the source of truth in the shared formatter module to avoid runtime // module-export errors during Vite ESM loading. -export { formatCurrency } from "@/lib/format"; +export { formatCurrency } from '@/lib/format'; diff --git a/src/hooks/simulation/useTecnicasUnificadas.ts b/src/hooks/simulation/useTecnicasUnificadas.ts index e0345c13e..6f5915b0a 100644 --- a/src/hooks/simulation/useTecnicasUnificadas.ts +++ b/src/hooks/simulation/useTecnicasUnificadas.ts @@ -99,7 +99,11 @@ export function useCustomizationPricing() { const calc = usePrecoCalculation(); return { - priceTables: [], // Legado - não mais usado + priceTables: [] as Array<{ + table_code: string; + customization_type_name: string; + price_by_color?: boolean | null; + }>, // Legado - não mais usado techniques: calc.techniques, standardQuantities: calc.standardQuantities, isLoading: calc.isLoading, diff --git a/src/lib/external-db/tables.ts b/src/lib/external-db/tables.ts index aff7f8adf..aee174705 100644 --- a/src/lib/external-db/tables.ts +++ b/src/lib/external-db/tables.ts @@ -1,6 +1,6 @@ /** * Constantes de tabelas e views do banco externo Promobrind. - * + * * SINCRONIZADO 2026-03-26 — Validado contra whitelist real do external-db-bridge. * Esta lista DEVE espelhar exatamente o que o bridge aceita. */ @@ -20,13 +20,13 @@ export const PRODUCT_TABLES = [ 'product_variants', 'product_materials', 'product_tags', - 'product_categories', // alias legacy → product_category_assignments - 'product_category_assignments', // vínculo N:N produto-categoria - 'product_suppliers', // fontes de fornecimento por produto - 'product_print_areas', // áreas de impressão por produto + 'product_categories', // alias legacy → product_category_assignments + 'product_category_assignments', // vínculo N:N produto-categoria + 'product_suppliers', // fontes de fornecimento por produto + 'product_print_areas', // áreas de impressão por produto 'product_kit_components', - 'product_attributes', // alias legacy → product_properties - 'product_properties', // atributos/propriedades de produto (nome real) + 'product_attributes', // alias legacy → product_properties + 'product_properties', // atributos/propriedades de produto (nome real) // Cores 'color_groups', 'color_nuances', @@ -40,8 +40,8 @@ export const PRODUCT_TABLES = [ 'supplier_materials', // Atributos e definições 'supplier_attribute_definitions', - 'supplier_product_attributes', // alias legacy → supplier_property_mappings - 'supplier_property_mappings', // mapeamentos de propriedades (nome real) + 'supplier_product_attributes', // alias legacy → supplier_property_mappings + 'supplier_property_mappings', // mapeamentos de propriedades (nome real) 'category_attributes', // Preços e variações 'price_lists', @@ -64,14 +64,14 @@ export const PRODUCT_TABLES = [ // NOTA: business_sectors removida (PGRST205 — não exposta no PostgREST externo) // NOTA: mockup_drafts e generated_mockups são tabelas LOCAIS (Lovable Cloud), não do BD externo // Técnicas de Gravação — tabelas REAIS - 'tecnicas_gravacao', // catálogo de técnicas (16 técnicas-mãe) - 'print_area_techniques', // 2654 áreas de gravação vinculadas a produtos (SSOT) + 'tecnicas_gravacao', // catálogo de técnicas (16 técnicas-mãe) + 'print_area_techniques', // 2654 áreas de gravação vinculadas a produtos (SSOT) // Sistema de Preços v2 - 'tabela_preco_gravacao_oficial', // 54 variantes de preço com configurações - 'tabela_preco_gravacao_oficial_faixa', // 301 faixas de preço - 'organization_markup_customization', // 59 configurações de markup (v5.1) - 'category_area_techniques', // vínculos área×técnica com variante_id - 'tabela_preco_fornecedores_gravacao', // preços de gravação por fornecedor + 'tabela_preco_gravacao_oficial', // 54 variantes de preço com configurações + 'tabela_preco_gravacao_oficial_faixa', // 301 faixas de preço + 'organization_markup_customization', // 59 configurações de markup (v5.1) + 'category_area_techniques', // vínculos área×técnica com variante_id + 'tabela_preco_fornecedores_gravacao', // preços de gravação por fornecedor // Histórico de preços 'price_history', // Histórico de estoque @@ -89,10 +89,10 @@ export const PRODUCT_TABLES = [ // Mantidos para que o TypeScript aceite código legado. // ============================================ export const BRIDGE_ALIASES = [ - 'tecnica_gravacao', // → tabela_preco_gravacao_oficial - 'personalization_techniques', // → tecnicas_gravacao (via bridge alias) - 'customization_price_tables', // → tabela_preco_fornecedores_gravacao (via bridge alias) - 'customization_price_tiers', // → tabela_preco_gravacao_oficial_faixa + 'tecnica_gravacao', // → tabela_preco_gravacao_oficial + 'personalization_techniques', // → tecnicas_gravacao (via bridge alias) + 'customization_price_tables', // → tabela_preco_fornecedores_gravacao (via bridge alias) + 'customization_price_tiers', // → tabela_preco_gravacao_oficial_faixa ] as const; // Views e Materialized Views (somente leitura) — do bridge @@ -135,13 +135,10 @@ export const PRODUCT_VIEWS = [ ] as const; // Tabelas de EMPRESAS/CLIENTES — acessadas via crm-db-bridge (não external-db-bridge) -export const COMPANY_TABLES = [ - 'client_contacts', - 'organizations', -] as const; +export const COMPANY_TABLES = ['client_contacts', 'organizations', 'companies'] as const; -export type ProductTable = typeof PRODUCT_TABLES[number]; -export type BridgeAlias = typeof BRIDGE_ALIASES[number]; -export type ProductView = typeof PRODUCT_VIEWS[number]; -export type CompanyTable = typeof COMPANY_TABLES[number]; +export type ProductTable = (typeof PRODUCT_TABLES)[number]; +export type BridgeAlias = (typeof BRIDGE_ALIASES)[number]; +export type ProductView = (typeof PRODUCT_VIEWS)[number]; +export type CompanyTable = (typeof COMPANY_TABLES)[number]; export type ExternalTable = ProductTable | BridgeAlias | ProductView | CompanyTable; diff --git a/src/lib/pdf/whitelabel-comparison.ts b/src/lib/pdf/whitelabel-comparison.ts index 2d744eaee..f8bdb2613 100644 --- a/src/lib/pdf/whitelabel-comparison.ts +++ b/src/lib/pdf/whitelabel-comparison.ts @@ -2,7 +2,9 @@ * whitelabel-comparison (C6 #3) — Gera PDF white-label com branding do cliente vinculado. * Busca companies.brand_logo_url / brand_color quando há clientId; cai para genérico se ausente. */ -import { supabase } from "@/integrations/supabase/client"; +import { supabase } from '@/integrations/supabase/client'; +import { type createClient } from '@supabase/supabase-js'; +const db = supabase as unknown as ReturnType; export interface ClientBranding { name: string | null; @@ -13,19 +15,21 @@ export interface ClientBranding { export async function fetchClientBranding(clientId: string | null): Promise { if (!clientId) return null; try { - const { data, error } = await supabase - .from("companies") - .select("name, brand_logo_url, brand_color") - .eq("id", clientId) + const { data, error } = await db + .from('companies') + .select('name, brand_logo_url, brand_color') + .eq('id', clientId) .maybeSingle(); if (error || !data) return null; const d = data as Record; return { - name: d.name ?? null, - logoUrl: d.brand_logo_url ?? null, - brandColor: d.brand_color ?? null, + name: (d.name as string | null) ?? null, + logoUrl: (d.brand_logo_url as string | null) ?? null, + brandColor: (d.brand_color as string | null) ?? null, }; - } catch { return null; } + } catch { + return null; + } } export async function exportWhitelabelComparisonPDF(opts: { @@ -35,75 +39,85 @@ export async function exportWhitelabelComparisonPDF(opts: { }): Promise { const { targetSelector, clientId } = opts; const [{ default: jsPDF }, html2canvasMod] = await Promise.all([ - import("jspdf"), - import("html2canvas"), + import('jspdf'), + import('html2canvas'), ]); const html2canvas = html2canvasMod.default; const el = document.querySelector(targetSelector) as HTMLElement | null; - if (!el) throw new Error("Área de exportação não encontrada"); + if (!el) throw new Error('Área de exportação não encontrada'); const branding = await fetchClientBranding(clientId); - const canvas = await html2canvas(el, { backgroundColor: "#ffffff", scale: 2, useCORS: true }); - const imgData = canvas.toDataURL("image/png"); + const canvas = await html2canvas(el, { backgroundColor: '#ffffff', scale: 2, useCORS: true }); + const imgData = canvas.toDataURL('image/png'); - const pdf = new jsPDF({ orientation: "landscape", unit: "mm", format: "a4" }); + const pdf = new jsPDF({ orientation: 'landscape', unit: 'mm', format: 'a4' }); const pageWidth = pdf.internal.pageSize.getWidth(); const pageHeight = pdf.internal.pageSize.getHeight(); // Header bar - const accent = branding?.brandColor ?? "#0f172a"; + const accent = branding?.brandColor ?? '#0f172a'; const [r, g, b] = hexToRgb(accent); pdf.setFillColor(r, g, b); - pdf.rect(0, 0, pageWidth, 14, "F"); + pdf.rect(0, 0, pageWidth, 14, 'F'); pdf.setTextColor(255, 255, 255); pdf.setFontSize(13); pdf.text( - branding?.name ? `Comparação de Produtos — ${branding.name}` : "Comparação de Produtos — Promo Gifts", - 10, 9 + branding?.name + ? `Comparação de Produtos — ${branding.name}` + : 'Comparação de Produtos — Promo Gifts', + 10, + 9, ); pdf.setFontSize(8); - pdf.text(new Date().toLocaleDateString("pt-BR"), pageWidth - 30, 9); + pdf.text(new Date().toLocaleDateString('pt-BR'), pageWidth - 30, 9); // Logo do cliente (se houver) if (branding?.logoUrl) { try { const logoData = await fetchImageAsDataUrl(branding.logoUrl); - if (logoData) pdf.addImage(logoData, "PNG", pageWidth - 50, 2, 18, 10); - } catch { /* silencioso */ } + if (logoData) pdf.addImage(logoData, 'PNG', pageWidth - 50, 2, 18, 10); + } catch { + /* silencioso */ + } } pdf.setTextColor(0, 0, 0); const imgWidth = pageWidth - 20; const imgHeight = (canvas.height * imgWidth) / canvas.width; if (imgHeight < pageHeight - 25) { - pdf.addImage(imgData, "PNG", 10, 20, imgWidth, imgHeight); + pdf.addImage(imgData, 'PNG', 10, 20, imgWidth, imgHeight); } else { let position = 20; let remaining = imgHeight; const sliceHeight = pageHeight - 25; while (remaining > 0) { - pdf.addImage(imgData, "PNG", 10, position - (imgHeight - remaining), imgWidth, imgHeight); + pdf.addImage(imgData, 'PNG', 10, position - (imgHeight - remaining), imgWidth, imgHeight); remaining -= sliceHeight; - if (remaining > 0) { pdf.addPage(); position = 10; } + if (remaining > 0) { + pdf.addPage(); + position = 10; + } } } pdf.save(opts.fileName ?? `comparacao-${new Date().toISOString().slice(0, 10)}.pdf`); } function hexToRgb(hex: string): [number, number, number] { - const m = hex.replace("#", "").match(/.{1,2}/g); + const m = hex.replace('#', '').match(/.{1,2}/g); if (!m || m.length < 3) return [15, 23, 42]; return [parseInt(m[0], 16), parseInt(m[1], 16), parseInt(m[2], 16)]; } async function fetchImageAsDataUrl(url: string): Promise { try { - const res = await fetch(url, { mode: "cors" }); + const res = await fetch(url, { mode: 'cors' }); const blob = await res.blob(); - return await new Promise(resolve => { + return await new Promise((resolve) => { const r = new FileReader(); r.onloadend = () => resolve(r.result as string); r.readAsDataURL(blob); }); - } catch { return null; } + } catch { + return null; + } } diff --git a/src/lib/personalization/adapters/price-response.adapter.ts b/src/lib/personalization/adapters/price-response.adapter.ts index 5516bc303..fc6ced250 100644 --- a/src/lib/personalization/adapters/price-response.adapter.ts +++ b/src/lib/personalization/adapters/price-response.adapter.ts @@ -13,7 +13,11 @@ */ import type { CustomizationPriceFlat } from '@/hooks/simulation'; -import { detectPriceSchema, warnUnknownSchemaOnce, type PriceSchemaVersion } from './schema-detection'; +import { + detectPriceSchema, + warnUnknownSchemaOnce, + type PriceSchemaVersion, +} from './schema-detection'; import { validateRpcPayload } from '@/lib/personalization/rpc-validator'; import { PRICE_CONTRACT } from '@/lib/personalization/rpc-contracts'; @@ -50,202 +54,130 @@ function normalizeV7Aliases(resp: Record): Record | undefined; if (markup && typeof markup === 'object') { - if ('unit_cost' in markup && !('custo_unitario' in markup)) markup.custo_unitario = markup.unit_cost; - if ('setup_cost_table' in markup && !('custo_setup_tabela' in markup)) markup.custo_setup_tabela = markup.setup_cost_table; - if ('markup_percent' in markup && !('markup_pct' in markup)) markup.markup_pct = markup.markup_percent; + if ('unit_cost' in markup && !('custo_unitario' in markup)) + markup.custo_unitario = markup.unit_cost; + if ('setup_cost_table' in markup && !('custo_setup_tabela' in markup)) + markup.custo_setup_tabela = markup.setup_cost_table; + if ('markup_percent' in markup && !('markup_pct' in markup)) + markup.markup_pct = markup.markup_percent; } // Detalhes nested const detalhes = out.detalhes as Record | undefined; if (detalhes && typeof detalhes === 'object') { - if ('charges_per_color' in detalhes && !('cobra_por_cor' in detalhes)) detalhes.cobra_por_cor = detalhes.charges_per_color; - if ('max_colors' in detalhes && !('max_cores' in detalhes)) detalhes.max_cores = detalhes.max_colors; + if ('charges_per_color' in detalhes && !('cobra_por_cor' in detalhes)) + detalhes.cobra_por_cor = detalhes.charges_per_color; + if ('max_colors' in detalhes && !('max_cores' in detalhes)) + detalhes.max_cores = detalhes.max_colors; } // Faixa nested const faixa = out.faixa as Record | undefined; if (faixa && typeof faixa === 'object') { if ('min_qty' in faixa && !('qtd_min' in faixa)) faixa.qtd_min = faixa.min_qty; if ('max_qty' in faixa && !('qtd_max' in faixa)) faixa.qtd_max = faixa.max_qty; - if ('production_days' in faixa && !('prazo_dias' in faixa)) faixa.prazo_dias = faixa.production_days; + if ('production_days' in faixa && !('prazo_dias' in faixa)) + faixa.prazo_dias = faixa.production_days; } return out; } // ============================================ -// FORMATOS DE PAYLOAD (tipagem de fronteira) +// PARSERS POR FORMATO // ============================================ -// -// Os parsers recebem objetos vindos de JSON (`Record`). -// Em vez de espalhar `as any` por cada acesso aninhado, descrevemos aqui a -// forma estrutural esperada de cada formato. Todos os campos são opcionais -// porque o payload é, por contrato, parcialmente confiável — os defaults -// (`?? ''`, `?? 0`, `?? false`, `?? null`) é que garantem o tipo canônico. - -/** Sub-objetos do formato v5.9-nested. */ -interface NestedArea { - id?: string; - code?: string; - name?: string; -} -interface NestedTabela { - id?: string; - codigo_tabela?: string; - nome?: string; - grupo_tecnica?: string; - cobra_por_cor?: boolean; - max_cores?: number; -} -interface NestedParametros { - quantidade?: number; - num_cores?: number; -} -interface NestedPrecos { - preco_unitario_final?: number; - subtotal_pecas?: number; - faturamento_minimo_gravacao?: number; - aplica_minimo?: boolean; - total_final?: number; - markup_percent?: number; -} -interface NestedCustos { - custo_base_unitario?: number; - custo_unitario_total?: number; - custo_setup_base?: number; -} -interface NestedFaixa { - ordem?: number; - quantidade_minima?: number; - quantidade_maxima?: number; - prazo_dias?: number | null; -} -interface NestedPriceResponse { - success?: boolean; - area?: NestedArea; - tabela?: NestedTabela; - parametros?: NestedParametros; - precos?: NestedPrecos; - custos?: NestedCustos; - faixa?: NestedFaixa; - codigo_orcamento?: string; - redirected_from?: string; - redirected_to?: string; -} -/** Sub-objetos do formato v6.x-flat (e v7 após normalização de aliases). */ -interface FlatMarkup { - custo_unitario?: number; - custo_setup_tabela?: number; - markup_pct?: number; -} -interface FlatDetalhes { - cobra_por_cor?: boolean; - max_cores?: number; -} -interface FlatFaixa { - faixa_id?: string | number; - qtd_min?: number; - qtd_max?: number; - prazo_dias?: number | null; -} -interface FlatPriceResponse { - success?: boolean; - area_id?: string; - area_code?: string; - nome_tabela?: string; - tabela_id?: string; - /** Código da tabela (string) — no flat o campo chama-se `tabela`. */ - tabela?: string; - grupo_tecnica?: string; - codigo_orcamento?: string; - quantidade?: number; - num_cores?: number; - preco_unitario?: number; - preco_por_unidade?: number; - valor_gravacao?: number; - setup_total?: number; - total_cobrado?: number; - prazo_dias?: number | null; - markup?: FlatMarkup; - detalhes?: FlatDetalhes; - faixa?: FlatFaixa; - redirected_from?: string; - redirected_to?: string; -} +type AnyRec = Record; -// ============================================ -// PARSERS POR FORMATO -// ============================================ +const isDefined = (v: unknown): v is NonNullable => v !== null && v !== undefined; +const str = (v: unknown): string => (isDefined(v) ? String(v) : ''); +const num = (v: unknown): number => (typeof v === 'number' ? v : Number(v) || 0); +const bool = (v: unknown): boolean => Boolean(v); +const numOrNull = (v: unknown): number | null => + isDefined(v) ? (typeof v === 'number' ? v : Number(v) || 0) : null; +const asObj = (v: unknown): AnyRec | undefined => + isDefined(v) && typeof v === 'object' && !Array.isArray(v) ? (v as AnyRec) : undefined; -function parseNested(resp: NestedPriceResponse): CustomizationPriceFlat { +function parseNested(resp: AnyRec): CustomizationPriceFlat { + const area = asObj(resp.area); + const tabela = asObj(resp.tabela); + const parametros = asObj(resp.parametros); + const precos = asObj(resp.precos); + const custos = asObj(resp.custos); + const faixa = asObj(resp.faixa); + const tabelaCodigo = str(tabela?.codigo_tabela); return { success: !!resp.success, - area_id: resp.area?.id ?? '', - area_code: resp.area?.code ?? '', - area_name: resp.area?.name ?? '', - tabela_id: resp.tabela?.id ?? '', - tabela_codigo: resp.tabela?.codigo_tabela ?? '', - tabela_codigo_curto: - (resp.tabela?.codigo_tabela ?? '').split('-')[0] || resp.tabela?.codigo_tabela || '', - technique: resp.tabela?.nome ?? '', - grupo_tecnica: resp.tabela?.grupo_tecnica ?? '', - codigo_orcamento: resp.codigo_orcamento ?? '', - quantity: resp.parametros?.quantidade ?? 0, - num_cores: resp.parametros?.num_cores ?? 1, - unit_price: resp.precos?.preco_unitario_final ?? 0, - subtotal_pecas: resp.precos?.subtotal_pecas ?? 0, - faturamento_minimo_gravacao: resp.precos?.faturamento_minimo_gravacao ?? 0, - minimum_applied: resp.precos?.aplica_minimo ?? false, - total_price: resp.precos?.total_final ?? 0, - cost_base_unit: resp.custos?.custo_base_unitario ?? 0, - cost_unit_total: resp.custos?.custo_unitario_total ?? 0, - cost_setup: resp.custos?.custo_setup_base ?? 0, - markup_percent: resp.precos?.markup_percent ?? 0, - margin_percent: resp.precos?.markup_percent ?? 0, - price_by_color: resp.tabela?.cobra_por_cor ?? false, - max_cores: resp.tabela?.max_cores ?? 1, - production_days: resp.faixa?.prazo_dias ?? null, - tier_used: resp.faixa?.ordem ?? 0, - tier_min_qty: resp.faixa?.quantidade_minima ?? 0, - tier_max_qty: resp.faixa?.quantidade_maxima ?? 0, - redirected_from: resp.redirected_from, - redirected_to: resp.redirected_to, + area_id: str(area?.id), + area_code: str(area?.code), + area_name: str(area?.name), + tabela_id: str(tabela?.id), + tabela_codigo: tabelaCodigo, + tabela_codigo_curto: tabelaCodigo.split('-')[0] || tabelaCodigo, + technique: str(tabela?.nome), + grupo_tecnica: str(tabela?.grupo_tecnica), + codigo_orcamento: str(resp.codigo_orcamento), + quantity: num(parametros?.quantidade), + num_cores: num(parametros?.num_cores) || 1, + unit_price: num(precos?.preco_unitario_final), + subtotal_pecas: num(precos?.subtotal_pecas), + faturamento_minimo_gravacao: num(precos?.faturamento_minimo_gravacao), + minimum_applied: bool(precos?.aplica_minimo), + total_price: num(precos?.total_final), + cost_base_unit: num(custos?.custo_base_unitario), + cost_unit_total: num(custos?.custo_unitario_total), + cost_setup: num(custos?.custo_setup_base), + markup_percent: num(precos?.markup_percent), + margin_percent: num(precos?.markup_percent), + price_by_color: bool(tabela?.cobra_por_cor), + max_cores: num(tabela?.max_cores) || 1, + production_days: numOrNull(faixa?.prazo_dias), + tier_used: num(faixa?.ordem), + tier_min_qty: num(faixa?.quantidade_minima), + tier_max_qty: num(faixa?.quantidade_maxima), + redirected_from: resp.redirected_from as string | undefined, + redirected_to: resp.redirected_to as string | undefined, }; } -function parseFlat(resp: FlatPriceResponse): CustomizationPriceFlat { - const tabelaCode = resp.tabela ?? ''; - const unitPrice = resp.preco_unitario ?? resp.preco_por_unidade ?? 0; - const valorGravacao = resp.valor_gravacao ?? unitPrice * (resp.quantidade ?? 0); +function parseFlat(resp: AnyRec): CustomizationPriceFlat { + const markup = asObj(resp.markup); + const detalhes = asObj(resp.detalhes); + const faixa = asObj(resp.faixa); + const tabelaCode = str(resp.tabela); + const unitPrice = num(resp.preco_unitario ?? resp.preco_por_unidade); + const quantidade = num(resp.quantidade); + const rawValorGravacao = isDefined(resp.valor_gravacao) ? num(resp.valor_gravacao) : 0; + const valorGravacao = isDefined(resp.valor_gravacao) ? rawValorGravacao : unitPrice * quantidade; + const setupTotal = num(resp.setup_total ?? markup?.custo_setup_tabela); return { - success: resp.success ?? true, - area_id: resp.area_id ?? '', - area_code: resp.area_code ?? tabelaCode, - area_name: resp.nome_tabela ?? '', - tabela_id: resp.tabela_id ?? '', + success: isDefined(resp.success) ? bool(resp.success) : true, + area_id: str(resp.area_id), + area_code: str(resp.area_code) || tabelaCode, + area_name: str(resp.nome_tabela), + tabela_id: str(resp.tabela_id), tabela_codigo: tabelaCode, tabela_codigo_curto: tabelaCode.split('-')[0] || tabelaCode, - technique: resp.nome_tabela ?? '', - grupo_tecnica: resp.grupo_tecnica ?? '', - codigo_orcamento: resp.codigo_orcamento ?? `${tabelaCode}-${resp.quantidade ?? 0}`, - quantity: resp.quantidade ?? 0, - num_cores: resp.num_cores ?? 1, + technique: str(resp.nome_tabela), + grupo_tecnica: str(resp.grupo_tecnica), + codigo_orcamento: str(resp.codigo_orcamento) || `${tabelaCode}-${quantidade}`, + quantity: quantidade, + num_cores: num(resp.num_cores) || 1, unit_price: unitPrice, subtotal_pecas: valorGravacao, - faturamento_minimo_gravacao: resp.setup_total ?? resp.markup?.custo_setup_tabela ?? 0, - minimum_applied: (resp.setup_total ?? 0) > (resp.valor_gravacao ?? 0), - total_price: resp.total_cobrado ?? resp.valor_gravacao ?? 0, - cost_base_unit: resp.markup?.custo_unitario ?? 0, - cost_unit_total: resp.markup?.custo_unitario ?? 0, - cost_setup: resp.markup?.custo_setup_tabela ?? 0, - markup_percent: resp.markup?.markup_pct ?? 0, - margin_percent: resp.markup?.markup_pct ?? 0, - price_by_color: resp.detalhes?.cobra_por_cor ?? false, - max_cores: resp.detalhes?.max_cores ?? 1, - production_days: resp.faixa?.prazo_dias ?? resp.prazo_dias ?? null, - tier_used: resp.faixa?.faixa_id ? 1 : 0, - tier_min_qty: resp.faixa?.qtd_min ?? 0, - tier_max_qty: resp.faixa?.qtd_max ?? 0, - redirected_from: resp.redirected_from, - redirected_to: resp.redirected_to, + faturamento_minimo_gravacao: num(resp.setup_total ?? markup?.custo_setup_tabela), + minimum_applied: setupTotal > rawValorGravacao, + total_price: num(resp.total_cobrado ?? resp.valor_gravacao), + cost_base_unit: num(markup?.custo_unitario), + cost_unit_total: num(markup?.custo_unitario), + cost_setup: num(markup?.custo_setup_tabela), + markup_percent: num(markup?.markup_pct), + margin_percent: num(markup?.markup_pct), + price_by_color: bool(detalhes?.cobra_por_cor), + max_cores: num(detalhes?.max_cores) || 1, + production_days: numOrNull(faixa?.prazo_dias ?? resp.prazo_dias), + tier_used: isDefined(faixa?.faixa_id) ? 1 : 0, + tier_min_qty: num(faixa?.qtd_min), + tier_max_qty: num(faixa?.qtd_max), + redirected_from: resp.redirected_from as string | undefined, + redirected_to: resp.redirected_to as string | undefined, }; } @@ -275,14 +207,14 @@ export function adaptPriceResponseWithMeta( const version = detectPriceSchema(resp); switch (version) { case 'v5.9-nested': - return { flat: parseNested(resp as unknown as NestedPriceResponse), schemaVersion: version }; + return { flat: parseNested(resp as AnyRec), schemaVersion: version }; case 'v6.x-flat': - return { flat: parseFlat(resp as unknown as FlatPriceResponse), schemaVersion: version }; + return { flat: parseFlat(resp as AnyRec), schemaVersion: version }; case 'v7-new': - return { flat: parseFlat(normalizeV7Aliases(resp) as unknown as FlatPriceResponse), schemaVersion: version }; + return { flat: parseFlat(normalizeV7Aliases(resp) as AnyRec), schemaVersion: version }; default: { warnUnknownSchemaOnce('price-response', resp); - return { flat: parseFlat(resp as unknown as FlatPriceResponse), schemaVersion: 'unknown' }; + return { flat: parseFlat(resp as AnyRec), schemaVersion: 'unknown' }; } } } diff --git a/src/lib/security/rls-denial-logger.ts b/src/lib/security/rls-denial-logger.ts index 54d8902cb..217ec76eb 100644 --- a/src/lib/security/rls-denial-logger.ts +++ b/src/lib/security/rls-denial-logger.ts @@ -7,10 +7,10 @@ * const { error } = await supabase.from("quotes").update(...).eq("id", id); * if (error) await logRlsDenialIfApplicable(error, { table: "quotes", op: "UPDATE", endpoint: "useQuotes.update", targetId: id }); */ -import { supabase } from "@/integrations/supabase/client"; -import type { PostgrestError } from "@supabase/supabase-js"; +import { supabase } from '@/integrations/supabase/client'; +import type { PostgrestError } from '@supabase/supabase-js'; -export type RlsOperation = "SELECT" | "INSERT" | "UPDATE" | "DELETE"; +export type RlsOperation = 'SELECT' | 'INSERT' | 'UPDATE' | 'DELETE'; export interface LogRlsDenialContext { table: string; @@ -31,8 +31,8 @@ export interface LogRlsDenialContext { */ export function isRlsDenialError(error: PostgrestError | null | undefined): boolean { if (!error) return false; - if (error.code === "42501") return true; - const msg = (error.message || "").toLowerCase(); + if (error.code === '42501') return true; + const msg = (error.message || '').toLowerCase(); return /row[- ]level security|violates row-level|new row violates/.test(msg); } @@ -46,17 +46,18 @@ export async function logRlsDenial( ): Promise { if (!error || !isRlsDenialError(error)) return; try { - await supabase.rpc("log_rls_denial", { + await supabase.rpc('log_rls_denial', { p_table_name: ctx.table, p_operation: ctx.op, - p_endpoint: ctx.endpoint ?? (typeof window !== "undefined" ? window.location.pathname : null), - p_query_summary: ctx.querySummary ?? null, - p_target_id: ctx.targetId ?? null, - p_target_seller_id: ctx.targetSellerId ?? null, - p_policy_hint: ctx.policyHint ?? null, - p_error_code: error.code ?? null, - p_error_message: error.message ?? null, - p_user_agent: typeof navigator !== "undefined" ? navigator.userAgent : null, + p_endpoint: + ctx.endpoint ?? (typeof window !== 'undefined' ? window.location.pathname : undefined), + p_query_summary: ctx.querySummary ?? undefined, + p_target_id: ctx.targetId ?? undefined, + p_target_seller_id: ctx.targetSellerId ?? undefined, + p_policy_hint: ctx.policyHint ?? undefined, + p_error_code: error.code ?? undefined, + p_error_message: error.message ?? undefined, + p_user_agent: typeof navigator !== 'undefined' ? navigator.userAgent : undefined, }); } catch { // Logging nunca deve quebrar o fluxo do usuário. diff --git a/src/pages/Index.tsx b/src/pages/Index.tsx index c465255b8..789aa3766 100644 --- a/src/pages/Index.tsx +++ b/src/pages/Index.tsx @@ -1,143 +1,159 @@ // Catálogo de Produtos - Index Page (v3 - refactored) -import { useState, useRef, useMemo } from "react"; +import { useState, useRef, useMemo } from 'react'; -import { PageSEO } from "@/components/seo/PageSEO"; -import { FloatingCompareBar } from "@/components/compare/FloatingCompareBar"; -import { SharePreviewDialog } from "@/components/products/share/SharePreviewDialog"; -import { VariantPickerDialog } from "@/components/products/VariantPickerDialog"; -import { CatalogHeader } from "@/components/catalog/CatalogHeader"; -import { CatalogToolbar } from "@/components/catalog/CatalogToolbar"; -import { CatalogActiveFilters } from "@/components/catalog/CatalogActiveFilters"; -import { CatalogContent } from "@/components/catalog/CatalogContent"; -import { useCatalogState, type ExternalVariantStock } from "@/hooks/products"; +import { PageSEO } from '@/components/seo/PageSEO'; +import { FloatingCompareBar } from '@/components/compare/FloatingCompareBar'; +import { SharePreviewDialog } from '@/components/products/share/SharePreviewDialog'; +import { VariantPickerDialog } from '@/components/products/VariantPickerDialog'; +import { CatalogHeader } from '@/components/catalog/CatalogHeader'; +import { CatalogToolbar } from '@/components/catalog/CatalogToolbar'; +import { CatalogActiveFilters } from '@/components/catalog/CatalogActiveFilters'; +import { CatalogContent } from '@/components/catalog/CatalogContent'; +import { useCatalogState, type ExternalVariantStock } from '@/hooks/products'; export default function Index() { const catalog = useCatalogState(); - const [variantForShare, setVariantForShare] = useState(undefined); + const [variantForShare, setVariantForShare] = useState( + undefined, + ); const variantSelectedRef = useRef(false); // Dynamic JSON-LD based on current state - const structuredData = useMemo(() => ({ - "@context": "https://schema.org", - "@type": "CollectionPage", - "name": catalog.searchQuery ? `Resultados para "${catalog.searchQuery}" - Catálogo` : "Catálogo de Brindes Promocionais", - "description": catalog.searchQuery - ? `Encontramos ${catalog.filteredProducts.length} brindes promocionais para sua busca "${catalog.searchQuery}".` - : "Explore nosso catálogo com mais de 15.000 brindes personalizáveis. Filtre por categoria, material, cor e preço.", - "url": window.location.href, - "numberOfItems": catalog.totalEstimate || catalog.filteredProducts.length, - "mainEntity": { - "@type": "ItemList", - "itemListElement": catalog.paginatedProducts.slice(0, 10).map((p, i) => ({ - "@type": "ListItem", - "position": i + 1, - "url": `${window.location.origin}/produto/${p.id}`, - "name": p.name - })) - } - }), [catalog.searchQuery, catalog.filteredProducts.length, catalog.totalEstimate, catalog.paginatedProducts]); + const structuredData = useMemo( + () => ({ + '@context': 'https://schema.org', + '@type': 'CollectionPage', + name: catalog.searchQuery + ? `Resultados para "${catalog.searchQuery}" - Catálogo` + : 'Catálogo de Brindes Promocionais', + description: catalog.searchQuery + ? `Encontramos ${catalog.filteredProducts.length} brindes promocionais para sua busca "${catalog.searchQuery}".` + : 'Explore nosso catálogo com mais de 15.000 brindes personalizáveis. Filtre por categoria, material, cor e preço.', + url: window.location.href, + numberOfItems: catalog.totalEstimate || catalog.filteredProducts.length, + mainEntity: { + '@type': 'ItemList', + itemListElement: catalog.paginatedProducts.slice(0, 10).map((p, i) => ({ + '@type': 'ListItem', + position: i + 1, + url: `${window.location.origin}/produto/${p.id}`, + name: p.name, + })), + }, + }), + [ + catalog.searchQuery, + catalog.filteredProducts.length, + catalog.totalEstimate, + catalog.paginatedProducts, + ], + ); return ( <> -
- {/* Header: Title + Search */} - { - if (result.type === "product") { - catalog.navigate(`/produto/${result.id}`); - } else if (result.type === "category") { - catalog.setFilters({ ...catalog.filters, categories: [parseInt(result.id)] }); - } else if (result.type === "supplier") { - catalog.setFilters({ ...catalog.filters, suppliers: [result.id] }); - } else { - catalog.handleSearch(result.label); - } - }} - /> +
+ {/* Header: Title + Search */} + { + if (result.type === 'product') { + catalog.navigate(`/produto/${result.id}`); + } else if (result.type === 'category') { + catalog.setFilters({ ...catalog.filters, categories: [result.id] }); + } else if (result.type === 'supplier') { + catalog.setFilters({ ...catalog.filters, suppliers: [result.id] }); + } else { + catalog.handleSearch(result.label); + } + }} + /> - {/* Toolbar: Filters + Sort + Stats + Layout — sticky abaixo do Header global. + {/* Toolbar: Filters + Sort + Stats + Layout — sticky abaixo do Header global. Usa --header-h + --breadcrumb-h (definidos por Header/MainLayout) para acompanhar a altura dinâmica em qualquer rota. */} -
- -
+
+ +
- {/* Active filter badges */} - + {/* Active filter badges */} + - {/* Product grid/list content */} - catalog.navigate(path)} - handleViewProduct={catalog.handleViewProduct} - handleShareProduct={catalog.handleShareProduct} - handleFavoriteProduct={catalog.handleFavoriteProduct} - isFavorite={catalog.isFavorite} - toggleFavorite={catalog.toggleFavorite} - isInCompare={catalog.isInCompare} - onToggleCompare={catalog.toggleCompare} - canAddToCompare={catalog.canAddMore} - onLoadMore={catalog.loadMore} - onResetFilters={catalog.resetFilters} - selectionMode={catalog.selectionMode} - onSelectedCountChange={catalog.setSelectedCount} - activeColorFilter={ - (catalog.filters.colorGroups?.length > 0 || catalog.filters.colorVariations?.length > 0) - ? { groups: catalog.filters.colorGroups || [], variations: catalog.filters.colorVariations || [] } - : null - } - /> + {/* Product grid/list content */} + catalog.navigate(path)} + handleViewProduct={catalog.handleViewProduct} + handleShareProduct={catalog.handleShareProduct} + handleFavoriteProduct={catalog.handleFavoriteProduct} + isFavorite={catalog.isFavorite} + toggleFavorite={catalog.toggleFavorite} + isInCompare={catalog.isInCompare} + onToggleCompare={catalog.toggleCompare} + canAddToCompare={catalog.canAddMore} + onLoadMore={catalog.loadMore} + onResetFilters={catalog.resetFilters} + selectionMode={catalog.selectionMode} + onSelectedCountChange={catalog.setSelectedCount} + activeColorFilter={ + catalog.filters.colorGroups?.length > 0 || catalog.filters.colorVariations?.length > 0 + ? { + groups: catalog.filters.colorGroups || [], + variations: catalog.filters.colorVariations || [], + } + : null + } + />
@@ -174,11 +190,15 @@ export default function Index() { } }} product={catalog.shareProduct} - selectedVariant={variantForShare ? { - variantName: variantForShare.color_name, - colorHex: variantForShare.color_hex, - thumbnailUrl: variantForShare.selected_thumbnail, - } : null} + selectedVariant={ + variantForShare + ? { + variantName: variantForShare.color_name, + colorHex: variantForShare.color_hex, + thumbnailUrl: variantForShare.selected_thumbnail, + } + : null + } /> )} diff --git a/src/pages/admin/PermissionsPage.tsx b/src/pages/admin/PermissionsPage.tsx index 12de8789f..2b7b586d7 100644 --- a/src/pages/admin/PermissionsPage.tsx +++ b/src/pages/admin/PermissionsPage.tsx @@ -1,18 +1,39 @@ import { useState, useEffect } from 'react'; import { supabase } from '@/integrations/supabase/client'; +import { type createClient } from '@supabase/supabase-js'; +const db = supabase as unknown as ReturnType; import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'; import { Button } from '@/components/ui/button'; import { Badge } from '@/components/ui/badge'; import { Input } from '@/components/ui/input'; import { Label } from '@/components/ui/label'; import { Textarea } from '@/components/ui/textarea'; -import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'; -import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogTrigger } from '@/components/ui/dialog'; -import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table'; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from '@/components/ui/select'; +import { + Dialog, + DialogContent, + DialogHeader, + DialogTitle, + DialogTrigger, +} from '@/components/ui/dialog'; +import { + Table, + TableBody, + TableCell, + TableHead, + TableHeader, + TableRow, +} from '@/components/ui/table'; import { useToast } from '@/hooks/ui'; import { Key, Plus, Edit, Trash2 } from 'lucide-react'; import { BackButton } from '@/components/common/BackButton'; -import { PageSEO } from "@/components/seo/PageSEO"; +import { PageSEO } from '@/components/seo/PageSEO'; interface Permission { id: string; @@ -23,14 +44,27 @@ interface Permission { created_at: string; } -const CATEGORIES = ['geral', 'produtos', 'orcamentos', 'pedidos', 'clientes', 'admin', 'relatorios']; +const CATEGORIES = [ + 'geral', + 'produtos', + 'orcamentos', + 'pedidos', + 'clientes', + 'admin', + 'relatorios', +]; export default function PermissionsPage() { const [permissions, setPermissions] = useState([]); const [isLoading, setIsLoading] = useState(true); const [isDialogOpen, setIsDialogOpen] = useState(false); const [editingPermission, setEditingPermission] = useState(null); - const [formData, setFormData] = useState({ code: '', name: '', description: '', category: 'geral' }); + const [formData, setFormData] = useState({ + code: '', + name: '', + description: '', + category: 'geral', + }); const { toast } = useToast(); useEffect(() => { @@ -44,7 +78,7 @@ export default function PermissionsPage() { const fetchPermissions = async (isCancelled: () => boolean = () => false) => { try { - const { data, error } = await supabase + const { data, error } = await db .from('permissions') .select('*') .order('category', { ascending: true }); @@ -54,7 +88,11 @@ export default function PermissionsPage() { setPermissions(data || []); } catch (error: unknown) { if (isCancelled()) return; - toast({ title: 'Erro', description: error instanceof Error ? error.message : String(error), variant: 'destructive' }); + toast({ + title: 'Erro', + description: error instanceof Error ? error.message : String(error), + variant: 'destructive', + }); } finally { if (!isCancelled()) setIsLoading(false); } @@ -63,14 +101,14 @@ export default function PermissionsPage() { const handleSubmit = async () => { try { if (editingPermission) { - const { error } = await supabase + const { error } = await db .from('permissions') .update(formData) .eq('id', editingPermission.id); if (error) throw error; toast({ title: 'Permissão atualizada com sucesso' }); } else { - const { error } = await supabase.from('permissions').insert(formData); + const { error } = await db.from('permissions').insert(formData); if (error) throw error; toast({ title: 'Permissão criada com sucesso' }); } @@ -79,7 +117,11 @@ export default function PermissionsPage() { setFormData({ code: '', name: '', description: '', category: 'geral' }); fetchPermissions(); } catch (error: unknown) { - toast({ title: 'Erro', description: error instanceof Error ? error.message : String(error), variant: 'destructive' }); + toast({ + title: 'Erro', + description: error instanceof Error ? error.message : String(error), + variant: 'destructive', + }); } }; @@ -96,149 +138,182 @@ export default function PermissionsPage() { const handleDelete = async (id: string) => { try { - const { error } = await supabase.from('permissions').delete().eq('id', id); + const { error } = await db.from('permissions').delete().eq('id', id); if (error) throw error; toast({ title: 'Permissão excluída com sucesso' }); fetchPermissions(); } catch (error: unknown) { - toast({ title: 'Erro', description: error instanceof Error ? error.message : String(error), variant: 'destructive' }); + toast({ + title: 'Erro', + description: error instanceof Error ? error.message : String(error), + variant: 'destructive', + }); } }; - const groupedPermissions = permissions.reduce((acc, perm) => { - if (!acc[perm.category]) acc[perm.category] = []; - acc[perm.category].push(perm); - return acc; - }, {} as Record); + const groupedPermissions = permissions.reduce( + (acc, perm) => { + if (!acc[perm.category]) acc[perm.category] = []; + acc[perm.category].push(perm); + return acc; + }, + {} as Record, + ); return (
- -
+ +

Gestão de Permissões

-
-
- - -
- - - Permissões do Sistema - - Gerencie as permissões de acesso -
- - - - - - - {editingPermission ? 'Editar Permissão' : 'Nova Permissão'} - -
-
- - setFormData({ ...formData, code: e.target.value })} - placeholder="ex: view_products" - /> -
-
- - setFormData({ ...formData, name: e.target.value })} - placeholder="ex: Visualizar Produtos" - /> -
-
- - -
-
- -