diff --git a/package-lock.json b/package-lock.json index 446abec..e9140ab 100644 --- a/package-lock.json +++ b/package-lock.json @@ -16,10 +16,22 @@ "@testing-library/jest-dom": "^6.6.4", "@testing-library/react": "^16.3.0", "@testing-library/user-event": "^13.5.0", + "@tiptap/extension-code-block": "^3.2.0", + "@tiptap/extension-highlight": "^3.2.0", + "@tiptap/extension-image": "^3.2.0", + "@tiptap/extension-link": "^3.2.0", + "@tiptap/extension-table": "^3.2.0", + "@tiptap/extension-table-cell": "^3.2.0", + "@tiptap/extension-table-header": "^3.2.0", + "@tiptap/extension-table-row": "^3.2.0", + "@tiptap/pm": "^3.2.0", + "@tiptap/react": "^3.2.0", + "@tiptap/starter-kit": "^3.2.0", "@types/jest": "^27.5.2", "@types/node": "^16.18.126", "@types/react": "^19.1.9", "@types/react-dom": "^19.1.7", + "@types/react-syntax-highlighter": "^15.5.13", "@types/socket.io-client": "^1.4.36", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", @@ -28,9 +40,11 @@ "react-cookie": "^8.0.1", "react-dom": "^19.1.1", "react-hot-toast": "^2.5.2", + "react-icons": "^5.5.0", "react-redux": "^9.2.0", "react-router-dom": "^6.30.1", "react-scripts": "5.0.1", + "react-syntax-highlighter": "^15.6.1", "recharts": "^3.1.0", "socket.io-client": "^4.8.1", "tailwind-merge": "^3.3.1", @@ -3306,6 +3320,12 @@ "url": "https://opencollective.com/immer" } }, + "node_modules/@remirror/core-constants": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@remirror/core-constants/-/core-constants-3.0.0.tgz", + "integrity": "sha512-42aWfPrimMfDKDi4YegyS7x+/0tlzaqwPQCULLanv3DMIlu96KTJR0fM5isWX2UViOqlGnX6YFgqWepcX+XMNg==", + "license": "MIT" + }, "node_modules/@remix-run/router": { "version": "1.23.0", "resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.23.0.tgz", @@ -3854,6 +3874,517 @@ "@testing-library/dom": ">=7.21.4" } }, + "node_modules/@tiptap/core": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/@tiptap/core/-/core-3.2.0.tgz", + "integrity": "sha512-1Dk1AIwzJejcyi4FOEcKoBSLMkHMfxeDiQ0daG51eS1IZ1ZSF+Xlhg9OD5sAjbUTGQjK1HfU6kGwS9wJcR/9ZQ==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/pm": "^3.2.0" + } + }, + "node_modules/@tiptap/extension-blockquote": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/@tiptap/extension-blockquote/-/extension-blockquote-3.2.0.tgz", + "integrity": "sha512-ivOvkzkpj/+1i6cZwUr1M+T/knci2XMl8NW/Q6g70Kx2TinEoFjOGnDi34j1mVznyzhxVlY9jm3Xvv4s2+Ni/w==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/core": "^3.2.0" + } + }, + "node_modules/@tiptap/extension-bold": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/@tiptap/extension-bold/-/extension-bold-3.2.0.tgz", + "integrity": "sha512-YuwK/muBzc0r7VFV0CcnF5Otg90zfUW2D5x+KpSF0e9bUBltSvitHgRaKWFwbPAgXYwkh6+ftxEazbXeqyTv8Q==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/core": "^3.2.0" + } + }, + "node_modules/@tiptap/extension-bubble-menu": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/@tiptap/extension-bubble-menu/-/extension-bubble-menu-3.2.0.tgz", + "integrity": "sha512-ZTE/h7Bv/dWrnEdUcvff79aEsPSYnx7s311z5xFKmNaIg8OaSEypCNKav7+2ttSBt03HcSCm7cRUY0b8yY2Y+w==", + "license": "MIT", + "optional": true, + "dependencies": { + "@floating-ui/dom": "^1.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/core": "^3.2.0", + "@tiptap/pm": "^3.2.0" + } + }, + "node_modules/@tiptap/extension-bullet-list": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/@tiptap/extension-bullet-list/-/extension-bullet-list-3.2.0.tgz", + "integrity": "sha512-xfQp2tpotSQ6pU627j20Bu+Ahgvjqe3uMVmczqjJ3V9Ze9xbiKFKJ2foC6mjQ1gfEGXkUE4AL5mVRBCWcfK8oQ==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/extension-list": "^3.2.0" + } + }, + "node_modules/@tiptap/extension-code": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/@tiptap/extension-code/-/extension-code-3.2.0.tgz", + "integrity": "sha512-8eUeWzT0YNxvT9yh6p9mRziUmZjiRqHeQhHbcRYDM7gdFKb0mgsWPxiaonns0sdj7WW1vP6g0M80PC+HYCIO+g==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/core": "^3.2.0" + } + }, + "node_modules/@tiptap/extension-code-block": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/@tiptap/extension-code-block/-/extension-code-block-3.2.0.tgz", + "integrity": "sha512-rGkwir451oAegeTMZw0h0JwfLfbexrgXDWw5eUpQ32/5M3n07HU2+EamTSKt7VW76AxdlehEfsd7yo+FkuVlWA==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/core": "^3.2.0", + "@tiptap/pm": "^3.2.0" + } + }, + "node_modules/@tiptap/extension-document": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/@tiptap/extension-document/-/extension-document-3.2.0.tgz", + "integrity": "sha512-2SQoew2HtOwuORpk8l7NjKDzsCBotMsij9vRSTp4nwoa4ZFGwmlVMsNIaKq/E/GuS7oiejmUUROsiD3w1stPKw==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/core": "^3.2.0" + } + }, + "node_modules/@tiptap/extension-dropcursor": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/@tiptap/extension-dropcursor/-/extension-dropcursor-3.2.0.tgz", + "integrity": "sha512-fyTfePUfyfD/s+3BauAkhJh+3HvQ3hI/mblbNKHAi9u+QqlB+a0W04I027XWag8F76DTp2clGkZunlxHYHas4g==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/extensions": "^3.2.0" + } + }, + "node_modules/@tiptap/extension-floating-menu": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/@tiptap/extension-floating-menu/-/extension-floating-menu-3.2.0.tgz", + "integrity": "sha512-CZJztT6eC8QKNwx+89PeJGhxSsGeU04/fPS18/y2DP4bEumrfTEq7jbtholJsLqErpghOdmm0XS+byYI6Dk9pg==", + "license": "MIT", + "optional": true, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@floating-ui/dom": "^1.0.0", + "@tiptap/core": "^3.2.0", + "@tiptap/pm": "^3.2.0" + } + }, + "node_modules/@tiptap/extension-gapcursor": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/@tiptap/extension-gapcursor/-/extension-gapcursor-3.2.0.tgz", + "integrity": "sha512-LUElisCLru8kAOMtihJ6nRbJ+Fml2S8GOhiry3GBRRdMpGT+Hzy2pAd9TsuNsyIdqKH+yM1lLRyquW7maaNc5g==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/extensions": "^3.2.0" + } + }, + "node_modules/@tiptap/extension-hard-break": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/@tiptap/extension-hard-break/-/extension-hard-break-3.2.0.tgz", + "integrity": "sha512-97CFsZK5vMHoWQzAP3thKD86dFDXqagXMs7vIKVK+SErwC3eR5KXmpvS5v5hi+iNs0I8+c+es6km+ZNMaUudKA==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/core": "^3.2.0" + } + }, + "node_modules/@tiptap/extension-heading": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/@tiptap/extension-heading/-/extension-heading-3.2.0.tgz", + "integrity": "sha512-bgvRBeqrVZ9rnzOrlLVsr+2S6UR3m9SAPqkdPUr0eFeC2MnaK6FVapiOx1HoQRNyGY7Cw7QBZQVsWhYmEfJpbQ==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/core": "^3.2.0" + } + }, + "node_modules/@tiptap/extension-highlight": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/@tiptap/extension-highlight/-/extension-highlight-3.2.0.tgz", + "integrity": "sha512-1TrV+NBZH+mlcx7ynupXETXA31PqpHfBzZazinX7+wart8nr67N1QSVCZccFadYmHm7qSkpjwiTF6m0hiWiDOA==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/core": "^3.2.0" + } + }, + "node_modules/@tiptap/extension-horizontal-rule": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/@tiptap/extension-horizontal-rule/-/extension-horizontal-rule-3.2.0.tgz", + "integrity": "sha512-ttMzFO/WoRVpqfhDAZyIg1nBxgkDOXy1thE6xpfSQAEi0m7zbVEkp4OkATgt0meM69YNxJaEyzQjGztybMBgBw==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/core": "^3.2.0", + "@tiptap/pm": "^3.2.0" + } + }, + "node_modules/@tiptap/extension-image": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/@tiptap/extension-image/-/extension-image-3.2.0.tgz", + "integrity": "sha512-1jFWdrMXkWDfCeM4857wUd0pe6eCeDRmEJgtuZkfdvUALNYdyADdX6cWUjC7JBG8zGYFmp2XUryLQGHjNTB+iw==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/core": "^3.2.0" + } + }, + "node_modules/@tiptap/extension-italic": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/@tiptap/extension-italic/-/extension-italic-3.2.0.tgz", + "integrity": "sha512-5OTX5Z3h7zrKcjj0snE8y+2a8dZ5G6GdZKVI1tedeDBfyUFqMSkvTZ/ygYenJ/zRkMvC9gz337ZU872IWx+ung==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/core": "^3.2.0" + } + }, + "node_modules/@tiptap/extension-link": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/@tiptap/extension-link/-/extension-link-3.2.0.tgz", + "integrity": "sha512-ye9n73aFv9q/Adv6clFgV00hif7/rhsYf8gXpwmq5So/4teyOC7yp8Fg77CG2r8vZNhiJviZq6U6jlHVIBeTnw==", + "license": "MIT", + "dependencies": { + "linkifyjs": "^4.3.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/core": "^3.2.0", + "@tiptap/pm": "^3.2.0" + } + }, + "node_modules/@tiptap/extension-list": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/@tiptap/extension-list/-/extension-list-3.2.0.tgz", + "integrity": "sha512-/3ZL9MpgDnEOv8tPwFwW3ct/TBbxZg3r3rN9noLHOR+YgiZKwbpMt9pDSJGu8y++rzT3vIHVmRRp0xFOXBvptQ==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/core": "^3.2.0", + "@tiptap/pm": "^3.2.0" + } + }, + "node_modules/@tiptap/extension-list-item": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/@tiptap/extension-list-item/-/extension-list-item-3.2.0.tgz", + "integrity": "sha512-v4cvZEWO9jQGCYUCJughMOpu5c0+9mjieTemwdF+Dd2q16WLonFx7+4YJkymB38YDaUi6dO1HFRtMKuGQRxlsg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/extension-list": "^3.2.0" + } + }, + "node_modules/@tiptap/extension-list-keymap": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/@tiptap/extension-list-keymap/-/extension-list-keymap-3.2.0.tgz", + "integrity": "sha512-R9r6CJpuwCFrfRQ+mU+m9vyujMjnu6C10J+8engQyvVtkc4KrgnHcABoyCkDJcZe36KRgz7mL0T6wqCezPD9lQ==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/extension-list": "^3.2.0" + } + }, + "node_modules/@tiptap/extension-ordered-list": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/@tiptap/extension-ordered-list/-/extension-ordered-list-3.2.0.tgz", + "integrity": "sha512-ditrS5HVX3ugx8R/4h2cHl8KOSnF1/T3caVvLpPccgbbTJNC8WldHJK/VvX/OusDO9DmbCMQiC8vIAagHMInhg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/extension-list": "^3.2.0" + } + }, + "node_modules/@tiptap/extension-paragraph": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/@tiptap/extension-paragraph/-/extension-paragraph-3.2.0.tgz", + "integrity": "sha512-RQ6mu06nVP9xEVULE64oWH9MT92mQhHVwJpnkYQuc7XEe3KgPYVbUmaLWMzJbjBOnAhTBC4k37Wwk0JKiTUBNQ==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/core": "^3.2.0" + } + }, + "node_modules/@tiptap/extension-strike": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/@tiptap/extension-strike/-/extension-strike-3.2.0.tgz", + "integrity": "sha512-gIfaIJmvQNBxjvtJfNQ8LhvWuGEFVFDMJxLa80speY4ngsDYZ3K8Ea4+0FTctKJVIXEZ5cDoHZR96ELkQMPzWA==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/core": "^3.2.0" + } + }, + "node_modules/@tiptap/extension-table": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/@tiptap/extension-table/-/extension-table-3.2.0.tgz", + "integrity": "sha512-dmhy6WvD2FIowOPPOCfWQfj9tvv0Mp1g1jZ7/GOi6bubUKO9W0wU+9c39yr/lCx7REABSxT5oEdk/2vlPPRT0w==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/core": "^3.2.0", + "@tiptap/pm": "^3.2.0" + } + }, + "node_modules/@tiptap/extension-table-cell": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/@tiptap/extension-table-cell/-/extension-table-cell-3.2.0.tgz", + "integrity": "sha512-VsAdOfGBJa5euOGoPktn3MY4NMFLrOFnuZjqM5sgz9M6I/8jQxvucdBrNXIEsYRIIIywS1t1AlJg2gWPqaYIEg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/extension-table": "^3.2.0" + } + }, + "node_modules/@tiptap/extension-table-header": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/@tiptap/extension-table-header/-/extension-table-header-3.2.0.tgz", + "integrity": "sha512-pRRAAEYbzz8o4H6FH5TgZ6Yyxww3Bava8paQL9hHjhQ1zv17NEKPHgR77+nDf/O/6/gmtEkiM8MaGjD/GSPiUA==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/extension-table": "^3.2.0" + } + }, + "node_modules/@tiptap/extension-table-row": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/@tiptap/extension-table-row/-/extension-table-row-3.2.0.tgz", + "integrity": "sha512-/19mWC1lnT5+Wrydic3Z4Io91gCVn/H9YLs9wqA1wCepzw9IWRFmt7d2HFbCFHNXKlTZ+mJymMAsxfpo5f7mKg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/extension-table": "^3.2.0" + } + }, + "node_modules/@tiptap/extension-text": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/@tiptap/extension-text/-/extension-text-3.2.0.tgz", + "integrity": "sha512-VDQPjVr6zd3MGh8+xPIIj+fuIhnws9tpAmP7jvBep+0tL7P2RYpN2NSj0zqeStHPcGfv9iMuXEuuMkEsi5gIZg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/core": "^3.2.0" + } + }, + "node_modules/@tiptap/extension-underline": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/@tiptap/extension-underline/-/extension-underline-3.2.0.tgz", + "integrity": "sha512-0ObBGqgeOAhQpNdNRJcu23GVQodtVp5dBYNC9vSW+mcJPRQy3Sc+kv59pqyrH0qV4SwtzzeKQSHuNaQpMPOU5A==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/core": "^3.2.0" + } + }, + "node_modules/@tiptap/extensions": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/@tiptap/extensions/-/extensions-3.2.0.tgz", + "integrity": "sha512-voEvFhStH1fxw6PF5Hb7kj7PH+BMYM3d0S6d/V0bNdGB7mMsILruXkXIsxvP8Dkskv07Xmfs+iU6pdVy0hWD7Q==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/core": "^3.2.0", + "@tiptap/pm": "^3.2.0" + } + }, + "node_modules/@tiptap/pm": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/@tiptap/pm/-/pm-3.2.0.tgz", + "integrity": "sha512-2UTlVts1Q7snQaUlxUtXfwEndNgcBcEIf74f1aEzcLzJRwBRRexCS6M0n+F50IifoB0OVBZENMBv1X/YXIPGPA==", + "license": "MIT", + "dependencies": { + "prosemirror-changeset": "^2.3.0", + "prosemirror-collab": "^1.3.1", + "prosemirror-commands": "^1.6.2", + "prosemirror-dropcursor": "^1.8.1", + "prosemirror-gapcursor": "^1.3.2", + "prosemirror-history": "^1.4.1", + "prosemirror-inputrules": "^1.4.0", + "prosemirror-keymap": "^1.2.2", + "prosemirror-markdown": "^1.13.1", + "prosemirror-menu": "^1.2.4", + "prosemirror-model": "^1.24.1", + "prosemirror-schema-basic": "^1.2.3", + "prosemirror-schema-list": "^1.5.0", + "prosemirror-state": "^1.4.3", + "prosemirror-tables": "^1.6.4", + "prosemirror-trailing-node": "^3.0.0", + "prosemirror-transform": "^1.10.2", + "prosemirror-view": "^1.38.1" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + } + }, + "node_modules/@tiptap/react": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/@tiptap/react/-/react-3.2.0.tgz", + "integrity": "sha512-rsro1+1yP3vLJ+YAQpWhJv/WftBr3hCGTfK6168jeX5INSjulUgALpA57o4rzp3LoNDwKreWu9iSp1JS4rsjFA==", + "license": "MIT", + "dependencies": { + "@types/use-sync-external-store": "^0.0.6", + "fast-deep-equal": "^3.1.3", + "use-sync-external-store": "^1.4.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "optionalDependencies": { + "@tiptap/extension-bubble-menu": "^3.2.0", + "@tiptap/extension-floating-menu": "^3.2.0" + }, + "peerDependencies": { + "@tiptap/core": "^3.2.0", + "@tiptap/pm": "^3.2.0", + "react": "^17.0.0 || ^18.0.0 || ^19.0.0", + "react-dom": "^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/@tiptap/starter-kit": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/@tiptap/starter-kit/-/starter-kit-3.2.0.tgz", + "integrity": "sha512-hcA12kqxVz1ypNqXhTUStQHWmDFmd2nm8NPmQmepK5T/T2FVXhVhhGSN8KsCR8sYFr3TwOk9utszxQLkT4AwOA==", + "license": "MIT", + "dependencies": { + "@tiptap/core": "^3.2.0", + "@tiptap/extension-blockquote": "^3.2.0", + "@tiptap/extension-bold": "^3.2.0", + "@tiptap/extension-bullet-list": "^3.2.0", + "@tiptap/extension-code": "^3.2.0", + "@tiptap/extension-code-block": "^3.2.0", + "@tiptap/extension-document": "^3.2.0", + "@tiptap/extension-dropcursor": "^3.2.0", + "@tiptap/extension-gapcursor": "^3.2.0", + "@tiptap/extension-hard-break": "^3.2.0", + "@tiptap/extension-heading": "^3.2.0", + "@tiptap/extension-horizontal-rule": "^3.2.0", + "@tiptap/extension-italic": "^3.2.0", + "@tiptap/extension-link": "^3.2.0", + "@tiptap/extension-list": "^3.2.0", + "@tiptap/extension-list-item": "^3.2.0", + "@tiptap/extension-list-keymap": "^3.2.0", + "@tiptap/extension-ordered-list": "^3.2.0", + "@tiptap/extension-paragraph": "^3.2.0", + "@tiptap/extension-strike": "^3.2.0", + "@tiptap/extension-text": "^3.2.0", + "@tiptap/extension-underline": "^3.2.0", + "@tiptap/extensions": "^3.2.0", + "@tiptap/pm": "^3.2.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + } + }, "node_modules/@tootallnate/once": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-1.1.2.tgz", @@ -4091,6 +4622,15 @@ "@types/node": "*" } }, + "node_modules/@types/hast": { + "version": "2.3.10", + "resolved": "https://registry.npmjs.org/@types/hast/-/hast-2.3.10.tgz", + "integrity": "sha512-McWspRw8xx8J9HurkVBfYj0xKoE25tOFlHGdx4MJ5xORQrMGZNqJhVQWaIbm6Oyla5kYOXtDiopzKRJzEOkwJw==", + "license": "MIT", + "dependencies": { + "@types/unist": "^2" + } + }, "node_modules/@types/hoist-non-react-statics": { "version": "3.3.7", "resolved": "https://registry.npmjs.org/@types/hoist-non-react-statics/-/hoist-non-react-statics-3.3.7.tgz", @@ -4170,6 +4710,28 @@ "integrity": "sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==", "license": "MIT" }, + "node_modules/@types/linkify-it": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@types/linkify-it/-/linkify-it-5.0.0.tgz", + "integrity": "sha512-sVDA58zAw4eWAffKOaQH5/5j3XeayukzDk+ewSsnv3p4yJEZHCCzMDiZM8e0OUrRvmpGZ85jf4yDHkHsgBNr9Q==", + "license": "MIT" + }, + "node_modules/@types/markdown-it": { + "version": "14.1.2", + "resolved": "https://registry.npmjs.org/@types/markdown-it/-/markdown-it-14.1.2.tgz", + "integrity": "sha512-promo4eFwuiW+TfGxhi+0x3czqTYJkG8qB17ZUJiVF10Xm7NLVRSLUsfRTU/6h1e24VvRnXCx+hG7li58lkzog==", + "license": "MIT", + "dependencies": { + "@types/linkify-it": "^5", + "@types/mdurl": "^2" + } + }, + "node_modules/@types/mdurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@types/mdurl/-/mdurl-2.0.0.tgz", + "integrity": "sha512-RGdgjQUZba5p6QEFAVx2OGb8rQDL/cPRG7GiedRzMcJ1tYnUANBncjbSB1NRGwbvjcPeikRABz2nshyPk1bhWg==", + "license": "MIT" + }, "node_modules/@types/mime": { "version": "1.3.5", "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.5.tgz", @@ -4239,6 +4801,15 @@ "@types/react": "^19.0.0" } }, + "node_modules/@types/react-syntax-highlighter": { + "version": "15.5.13", + "resolved": "https://registry.npmjs.org/@types/react-syntax-highlighter/-/react-syntax-highlighter-15.5.13.tgz", + "integrity": "sha512-uLGJ87j6Sz8UaBAooU0T6lWJ0dBmjZgN1PZTrj05TNql2/XpC6+4HhMT5syIdFUUt+FASfCeLLv4kBygNU+8qA==", + "license": "MIT", + "dependencies": { + "@types/react": "*" + } + }, "node_modules/@types/resolve": { "version": "1.17.1", "resolved": "https://registry.npmjs.org/@types/resolve/-/resolve-1.17.1.tgz", @@ -4317,6 +4888,12 @@ "integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==", "license": "MIT" }, + "node_modules/@types/unist": { + "version": "2.0.11", + "resolved": "https://registry.npmjs.org/@types/unist/-/unist-2.0.11.tgz", + "integrity": "sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA==", + "license": "MIT" + }, "node_modules/@types/use-sync-external-store": { "version": "0.0.6", "resolved": "https://registry.npmjs.org/@types/use-sync-external-store/-/use-sync-external-store-0.0.6.tgz", @@ -5975,6 +6552,36 @@ "node": ">=10" } }, + "node_modules/character-entities": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/character-entities/-/character-entities-1.2.4.tgz", + "integrity": "sha512-iBMyeEHxfVnIakwOuDXpVkc54HijNgCyQB2w0VfGQThle6NXn50zU6V/u+LDhxHcDUPojn6Kpga3PTAD8W1bQw==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/character-entities-legacy": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/character-entities-legacy/-/character-entities-legacy-1.1.4.tgz", + "integrity": "sha512-3Xnr+7ZFS1uxeiUDvV02wQ+QDbc55o97tIV5zHScSPJpcLm/r0DFPcoY3tYRp+VZukxuMeKgXYmsXQHO05zQeA==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/character-reference-invalid": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/character-reference-invalid/-/character-reference-invalid-1.1.4.tgz", + "integrity": "sha512-mKKUkUbhPpQlCOfIuZkvSEgktjPFIsZKRRbC6KWVEMvlzblj3i3asQv5ODsrwt0N3pHAEvjP8KTQPHkp0+6jOg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/check-types": { "version": "11.2.3", "resolved": "https://registry.npmjs.org/check-types/-/check-types-11.2.3.tgz", @@ -6243,6 +6850,16 @@ "node": ">= 0.8" } }, + "node_modules/comma-separated-tokens": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/comma-separated-tokens/-/comma-separated-tokens-1.0.8.tgz", + "integrity": "sha512-GHuDRO12Sypu2cV70d1dkA2EUmXHgntrzbpvOB+Qy+49ypNfGgFQIC2fhhXbnyrJRynDCAARsT7Ou0M6hirpfw==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/commander": { "version": "8.3.0", "resolved": "https://registry.npmjs.org/commander/-/commander-8.3.0.tgz", @@ -6432,6 +7049,12 @@ "node": ">=10" } }, + "node_modules/crelt": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/crelt/-/crelt-1.0.6.tgz", + "integrity": "sha512-VQ2MBenTq1fWZUH9DJNGti7kKv6EeAuYr3cLwxUWhIu1baTaXh4Ib5W2CqHVqib4/MqbYGJqiL3Zb8GJZr3l4g==", + "license": "MIT" + }, "node_modules/cross-spawn": { "version": "7.0.6", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", @@ -8659,6 +9282,19 @@ "reusify": "^1.0.4" } }, + "node_modules/fault": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/fault/-/fault-1.0.4.tgz", + "integrity": "sha512-CJ0HCB5tL5fYTEA7ToAq5+kTwd++Borf1/bifxd9iT70QcXr4MRrO3Llf8Ifs70q+SJcGHFtnIE/Nw6giCtECA==", + "license": "MIT", + "dependencies": { + "format": "^0.2.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/faye-websocket": { "version": "0.11.4", "resolved": "https://registry.npmjs.org/faye-websocket/-/faye-websocket-0.11.4.tgz", @@ -9040,6 +9676,14 @@ "node": ">= 6" } }, + "node_modules/format": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/format/-/format-0.2.2.tgz", + "integrity": "sha512-wzsgA6WOq+09wrU1tsJ09udeR/YZRaeArL9e1wPbFg3GG2yDnC2ldKpxs4xunpFF9DgqCqOIra3bc1HWrJ37Ww==", + "engines": { + "node": ">=0.4.x" + } + }, "node_modules/forwarded": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", @@ -9523,6 +10167,33 @@ "node": ">= 0.4" } }, + "node_modules/hast-util-parse-selector": { + "version": "2.2.5", + "resolved": "https://registry.npmjs.org/hast-util-parse-selector/-/hast-util-parse-selector-2.2.5.tgz", + "integrity": "sha512-7j6mrk/qqkSehsM92wQjdIgWM2/BW61u/53G6xmC8i1OmEdKLHbk419QKQUjz6LglWsfqoiHmyMRkP1BGjecNQ==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hastscript": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/hastscript/-/hastscript-6.0.0.tgz", + "integrity": "sha512-nDM6bvd7lIqDUiYEiu5Sl/+6ReP0BMk/2f4U/Rooccxkj0P5nm+acM5PrGJ/t5I8qPGiqZSE6hVAwZEdZIvP4w==", + "license": "MIT", + "dependencies": { + "@types/hast": "^2.0.0", + "comma-separated-tokens": "^1.0.0", + "hast-util-parse-selector": "^2.0.0", + "property-information": "^5.0.0", + "space-separated-tokens": "^1.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, "node_modules/he": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz", @@ -9532,6 +10203,21 @@ "he": "bin/he" } }, + "node_modules/highlight.js": { + "version": "10.7.3", + "resolved": "https://registry.npmjs.org/highlight.js/-/highlight.js-10.7.3.tgz", + "integrity": "sha512-tzcUFauisWKNHaRkN4Wjl/ZA07gENAjFl3J/c480dprkGTg5EQstgaNFqBfUqCq54kZRIEcreTsAgF/m2quD7A==", + "license": "BSD-3-Clause", + "engines": { + "node": "*" + } + }, + "node_modules/highlightjs-vue": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/highlightjs-vue/-/highlightjs-vue-1.0.0.tgz", + "integrity": "sha512-PDEfEF102G23vHmPhLyPboFCD+BkMGu+GuJe2d9/eH4FsCwvgBpnc9n0pGE+ffKdph38s6foEZiEjdgHdzp+IA==", + "license": "CC0-1.0" + }, "node_modules/hoist-non-react-statics": { "version": "3.3.2", "resolved": "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz", @@ -9990,6 +10676,30 @@ "node": ">= 10" } }, + "node_modules/is-alphabetical": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/is-alphabetical/-/is-alphabetical-1.0.4.tgz", + "integrity": "sha512-DwzsA04LQ10FHTZuL0/grVDk4rFoVH1pjAToYwBrHSxcrBIGQuXrQMtD5U1b0U2XVgKZCTLLP8u2Qxqhy3l2Vg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/is-alphanumerical": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/is-alphanumerical/-/is-alphanumerical-1.0.4.tgz", + "integrity": "sha512-UzoZUr+XfVz3t3v4KyGEniVL9BDRoQtY7tOyrRybkVNjDFWyo1yhXNGrrBTQxp3ib9BLAWs7k2YKBQsFRkZG9A==", + "license": "MIT", + "dependencies": { + "is-alphabetical": "^1.0.0", + "is-decimal": "^1.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/is-array-buffer": { "version": "3.0.5", "resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.5.tgz", @@ -10135,6 +10845,16 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/is-decimal": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/is-decimal/-/is-decimal-1.0.4.tgz", + "integrity": "sha512-RGdriMmQQvZ2aqaQq3awNA6dCGtKpiDFcOzrTWrDAT2MiWrKQVPmxLGHl7Y2nNu6led0kEyoX0enY0qXYsv9zw==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/is-docker": { "version": "2.2.1", "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-2.2.1.tgz", @@ -10222,6 +10942,16 @@ "node": ">=0.10.0" } }, + "node_modules/is-hexadecimal": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/is-hexadecimal/-/is-hexadecimal-1.0.4.tgz", + "integrity": "sha512-gyPJuv83bHMpocVYoqof5VDiZveEoGoFL8m3BXNb2VW8Xs+rz9kqO8LOQ5DH6EsuvilT1ApazU0pyl+ytbPtlw==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/is-map": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/is-map/-/is-map-2.0.3.tgz", @@ -11843,6 +12573,21 @@ "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", "license": "MIT" }, + "node_modules/linkify-it": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/linkify-it/-/linkify-it-5.0.0.tgz", + "integrity": "sha512-5aHCbzQRADcdP+ATqnDuhhJ/MRIqDkZX5pyjFHRRysS8vZ5AbqGEoFIb6pYHPZ+L/OC2Lc+xT8uHVVR5CAK/wQ==", + "license": "MIT", + "dependencies": { + "uc.micro": "^2.0.0" + } + }, + "node_modules/linkifyjs": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/linkifyjs/-/linkifyjs-4.3.2.tgz", + "integrity": "sha512-NT1CJtq3hHIreOianA8aSXn6Cw0JzYOuDQbOrSPe7gqFnCpKP++MQe3ODgO3oh2GJFORkAAdqredOa60z63GbA==", + "license": "MIT" + }, "node_modules/loader-runner": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/loader-runner/-/loader-runner-4.3.0.tgz", @@ -11947,6 +12692,20 @@ "tslib": "^2.0.3" } }, + "node_modules/lowlight": { + "version": "1.20.0", + "resolved": "https://registry.npmjs.org/lowlight/-/lowlight-1.20.0.tgz", + "integrity": "sha512-8Ktj+prEb1RoCPkEOrPMYUN/nCggB7qAWe3a7OpMjWQkh3l2RD5wKRQ+o8Q8YuI9RG/xs95waaI/E6ym/7NsTw==", + "license": "MIT", + "dependencies": { + "fault": "^1.0.0", + "highlight.js": "~10.7.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/lru-cache": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", @@ -12016,6 +12775,41 @@ "tmpl": "1.0.5" } }, + "node_modules/markdown-it": { + "version": "14.1.0", + "resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-14.1.0.tgz", + "integrity": "sha512-a54IwgWPaeBCAAsv13YgmALOF1elABB08FxO9i+r4VFk5Vl4pKokRPeX8u5TCgSsPi6ec1otfLjdOpVcgbpshg==", + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1", + "entities": "^4.4.0", + "linkify-it": "^5.0.0", + "mdurl": "^2.0.0", + "punycode.js": "^2.3.1", + "uc.micro": "^2.1.0" + }, + "bin": { + "markdown-it": "bin/markdown-it.mjs" + } + }, + "node_modules/markdown-it/node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "license": "Python-2.0" + }, + "node_modules/markdown-it/node_modules/entities": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", + "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, "node_modules/math-intrinsics": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", @@ -12031,6 +12825,12 @@ "integrity": "sha512-iV3XNKw06j5Q7mi6h+9vbx23Tv7JkjEVgKHW4pimwyDGWm0OIQntJJ+u1C6mg6mK1EaTv42XQ7w76yuzH7M2cA==", "license": "CC0-1.0" }, + "node_modules/mdurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/mdurl/-/mdurl-2.0.0.tgz", + "integrity": "sha512-Lf+9+2r+Tdp5wXDXC4PcIBjTDtq4UKjCPMQhKIuzpJNW0b96kVqSwW0bT7FhRSfmAiFYgP+SCRvdrDozfh0U5w==", + "license": "MIT" + }, "node_modules/media-typer": { "version": "0.3.0", "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", @@ -12622,6 +13422,12 @@ "node": ">= 0.8.0" } }, + "node_modules/orderedmap": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/orderedmap/-/orderedmap-2.1.1.tgz", + "integrity": "sha512-TvAWxi0nDe1j/rtMcWcIj94+Ffe6n7zhow33h40SKxmsmozs6dz/e+EajymfoFcHd7sxNn8yHM8839uixMOV6g==", + "license": "MIT" + }, "node_modules/own-keys": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/own-keys/-/own-keys-1.0.1.tgz", @@ -12716,6 +13522,24 @@ "node": ">=6" } }, + "node_modules/parse-entities": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/parse-entities/-/parse-entities-2.0.0.tgz", + "integrity": "sha512-kkywGpCcRYhqQIchaWqZ875wzpS/bMKhz5HnN3p7wveJTkTtyAB/AlnS0f8DFSqYW1T82t6yEAkEcB+A1I3MbQ==", + "license": "MIT", + "dependencies": { + "character-entities": "^1.0.0", + "character-entities-legacy": "^1.0.0", + "character-reference-invalid": "^1.0.0", + "is-alphanumerical": "^1.0.0", + "is-decimal": "^1.0.0", + "is-hexadecimal": "^1.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/parse-json": { "version": "5.2.0", "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", @@ -14316,6 +15140,15 @@ "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, + "node_modules/prismjs": { + "version": "1.30.0", + "resolved": "https://registry.npmjs.org/prismjs/-/prismjs-1.30.0.tgz", + "integrity": "sha512-DEvV2ZF2r2/63V+tK8hQvrR2ZGn10srHbXviTlcv7Kpzw8jWiNTqbVgjO3IY8RxrrOUF8VPMQQFysYYYv0YZxw==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/process-nextick-args": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", @@ -14361,6 +15194,214 @@ "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", "license": "MIT" }, + "node_modules/property-information": { + "version": "5.6.0", + "resolved": "https://registry.npmjs.org/property-information/-/property-information-5.6.0.tgz", + "integrity": "sha512-YUHSPk+A30YPv+0Qf8i9Mbfe/C0hdPXk1s1jPVToV8pk8BQtpw10ct89Eo7OWkutrwqvT0eicAxlOg3dOAu8JA==", + "license": "MIT", + "dependencies": { + "xtend": "^4.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/prosemirror-changeset": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/prosemirror-changeset/-/prosemirror-changeset-2.3.1.tgz", + "integrity": "sha512-j0kORIBm8ayJNl3zQvD1TTPHJX3g042et6y/KQhZhnPrruO8exkTgG8X+NRpj7kIyMMEx74Xb3DyMIBtO0IKkQ==", + "license": "MIT", + "dependencies": { + "prosemirror-transform": "^1.0.0" + } + }, + "node_modules/prosemirror-collab": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/prosemirror-collab/-/prosemirror-collab-1.3.1.tgz", + "integrity": "sha512-4SnynYR9TTYaQVXd/ieUvsVV4PDMBzrq2xPUWutHivDuOshZXqQ5rGbZM84HEaXKbLdItse7weMGOUdDVcLKEQ==", + "license": "MIT", + "dependencies": { + "prosemirror-state": "^1.0.0" + } + }, + "node_modules/prosemirror-commands": { + "version": "1.7.1", + "resolved": "https://registry.npmjs.org/prosemirror-commands/-/prosemirror-commands-1.7.1.tgz", + "integrity": "sha512-rT7qZnQtx5c0/y/KlYaGvtG411S97UaL6gdp6RIZ23DLHanMYLyfGBV5DtSnZdthQql7W+lEVbpSfwtO8T+L2w==", + "license": "MIT", + "dependencies": { + "prosemirror-model": "^1.0.0", + "prosemirror-state": "^1.0.0", + "prosemirror-transform": "^1.10.2" + } + }, + "node_modules/prosemirror-dropcursor": { + "version": "1.8.2", + "resolved": "https://registry.npmjs.org/prosemirror-dropcursor/-/prosemirror-dropcursor-1.8.2.tgz", + "integrity": "sha512-CCk6Gyx9+Tt2sbYk5NK0nB1ukHi2ryaRgadV/LvyNuO3ena1payM2z6Cg0vO1ebK8cxbzo41ku2DE5Axj1Zuiw==", + "license": "MIT", + "dependencies": { + "prosemirror-state": "^1.0.0", + "prosemirror-transform": "^1.1.0", + "prosemirror-view": "^1.1.0" + } + }, + "node_modules/prosemirror-gapcursor": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/prosemirror-gapcursor/-/prosemirror-gapcursor-1.3.2.tgz", + "integrity": "sha512-wtjswVBd2vaQRrnYZaBCbyDqr232Ed4p2QPtRIUK5FuqHYKGWkEwl08oQM4Tw7DOR0FsasARV5uJFvMZWxdNxQ==", + "license": "MIT", + "dependencies": { + "prosemirror-keymap": "^1.0.0", + "prosemirror-model": "^1.0.0", + "prosemirror-state": "^1.0.0", + "prosemirror-view": "^1.0.0" + } + }, + "node_modules/prosemirror-history": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/prosemirror-history/-/prosemirror-history-1.4.1.tgz", + "integrity": "sha512-2JZD8z2JviJrboD9cPuX/Sv/1ChFng+xh2tChQ2X4bB2HeK+rra/bmJ3xGntCcjhOqIzSDG6Id7e8RJ9QPXLEQ==", + "license": "MIT", + "dependencies": { + "prosemirror-state": "^1.2.2", + "prosemirror-transform": "^1.0.0", + "prosemirror-view": "^1.31.0", + "rope-sequence": "^1.3.0" + } + }, + "node_modules/prosemirror-inputrules": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/prosemirror-inputrules/-/prosemirror-inputrules-1.5.0.tgz", + "integrity": "sha512-K0xJRCmt+uSw7xesnHmcn72yBGTbY45vm8gXI4LZXbx2Z0jwh5aF9xrGQgrVPu0WbyFVFF3E/o9VhJYz6SQWnA==", + "license": "MIT", + "dependencies": { + "prosemirror-state": "^1.0.0", + "prosemirror-transform": "^1.0.0" + } + }, + "node_modules/prosemirror-keymap": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/prosemirror-keymap/-/prosemirror-keymap-1.2.3.tgz", + "integrity": "sha512-4HucRlpiLd1IPQQXNqeo81BGtkY8Ai5smHhKW9jjPKRc2wQIxksg7Hl1tTI2IfT2B/LgX6bfYvXxEpJl7aKYKw==", + "license": "MIT", + "dependencies": { + "prosemirror-state": "^1.0.0", + "w3c-keyname": "^2.2.0" + } + }, + "node_modules/prosemirror-markdown": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/prosemirror-markdown/-/prosemirror-markdown-1.13.2.tgz", + "integrity": "sha512-FPD9rHPdA9fqzNmIIDhhnYQ6WgNoSWX9StUZ8LEKapaXU9i6XgykaHKhp6XMyXlOWetmaFgGDS/nu/w9/vUc5g==", + "license": "MIT", + "dependencies": { + "@types/markdown-it": "^14.0.0", + "markdown-it": "^14.0.0", + "prosemirror-model": "^1.25.0" + } + }, + "node_modules/prosemirror-menu": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/prosemirror-menu/-/prosemirror-menu-1.2.5.tgz", + "integrity": "sha512-qwXzynnpBIeg1D7BAtjOusR+81xCp53j7iWu/IargiRZqRjGIlQuu1f3jFi+ehrHhWMLoyOQTSRx/IWZJqOYtQ==", + "license": "MIT", + "dependencies": { + "crelt": "^1.0.0", + "prosemirror-commands": "^1.0.0", + "prosemirror-history": "^1.0.0", + "prosemirror-state": "^1.0.0" + } + }, + "node_modules/prosemirror-model": { + "version": "1.25.3", + "resolved": "https://registry.npmjs.org/prosemirror-model/-/prosemirror-model-1.25.3.tgz", + "integrity": "sha512-dY2HdaNXlARknJbrManZ1WyUtos+AP97AmvqdOQtWtrrC5g4mohVX5DTi9rXNFSk09eczLq9GuNTtq3EfMeMGA==", + "license": "MIT", + "dependencies": { + "orderedmap": "^2.0.0" + } + }, + "node_modules/prosemirror-schema-basic": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/prosemirror-schema-basic/-/prosemirror-schema-basic-1.2.4.tgz", + "integrity": "sha512-ELxP4TlX3yr2v5rM7Sb70SqStq5NvI15c0j9j/gjsrO5vaw+fnnpovCLEGIcpeGfifkuqJwl4fon6b+KdrODYQ==", + "license": "MIT", + "dependencies": { + "prosemirror-model": "^1.25.0" + } + }, + "node_modules/prosemirror-schema-list": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/prosemirror-schema-list/-/prosemirror-schema-list-1.5.1.tgz", + "integrity": "sha512-927lFx/uwyQaGwJxLWCZRkjXG0p48KpMj6ueoYiu4JX05GGuGcgzAy62dfiV8eFZftgyBUvLx76RsMe20fJl+Q==", + "license": "MIT", + "dependencies": { + "prosemirror-model": "^1.0.0", + "prosemirror-state": "^1.0.0", + "prosemirror-transform": "^1.7.3" + } + }, + "node_modules/prosemirror-state": { + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/prosemirror-state/-/prosemirror-state-1.4.3.tgz", + "integrity": "sha512-goFKORVbvPuAQaXhpbemJFRKJ2aixr+AZMGiquiqKxaucC6hlpHNZHWgz5R7dS4roHiwq9vDctE//CZ++o0W1Q==", + "license": "MIT", + "dependencies": { + "prosemirror-model": "^1.0.0", + "prosemirror-transform": "^1.0.0", + "prosemirror-view": "^1.27.0" + } + }, + "node_modules/prosemirror-tables": { + "version": "1.7.1", + "resolved": "https://registry.npmjs.org/prosemirror-tables/-/prosemirror-tables-1.7.1.tgz", + "integrity": "sha512-eRQ97Bf+i9Eby99QbyAiyov43iOKgWa7QCGly+lrDt7efZ1v8NWolhXiB43hSDGIXT1UXgbs4KJN3a06FGpr1Q==", + "license": "MIT", + "dependencies": { + "prosemirror-keymap": "^1.2.2", + "prosemirror-model": "^1.25.0", + "prosemirror-state": "^1.4.3", + "prosemirror-transform": "^1.10.3", + "prosemirror-view": "^1.39.1" + } + }, + "node_modules/prosemirror-trailing-node": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/prosemirror-trailing-node/-/prosemirror-trailing-node-3.0.0.tgz", + "integrity": "sha512-xiun5/3q0w5eRnGYfNlW1uU9W6x5MoFKWwq/0TIRgt09lv7Hcser2QYV8t4muXbEr+Fwo0geYn79Xs4GKywrRQ==", + "license": "MIT", + "dependencies": { + "@remirror/core-constants": "3.0.0", + "escape-string-regexp": "^4.0.0" + }, + "peerDependencies": { + "prosemirror-model": "^1.22.1", + "prosemirror-state": "^1.4.2", + "prosemirror-view": "^1.33.8" + } + }, + "node_modules/prosemirror-transform": { + "version": "1.10.4", + "resolved": "https://registry.npmjs.org/prosemirror-transform/-/prosemirror-transform-1.10.4.tgz", + "integrity": "sha512-pwDy22nAnGqNR1feOQKHxoFkkUtepoFAd3r2hbEDsnf4wp57kKA36hXsB3njA9FtONBEwSDnDeCiJe+ItD+ykw==", + "license": "MIT", + "dependencies": { + "prosemirror-model": "^1.21.0" + } + }, + "node_modules/prosemirror-view": { + "version": "1.40.1", + "resolved": "https://registry.npmjs.org/prosemirror-view/-/prosemirror-view-1.40.1.tgz", + "integrity": "sha512-pbwUjt3G7TlsQQHDiYSupWBhJswpLVB09xXm1YiJPdkjkh9Pe7Y51XdLh5VWIZmROLY8UpUpG03lkdhm9lzIBA==", + "license": "MIT", + "dependencies": { + "prosemirror-model": "^1.20.0", + "prosemirror-state": "^1.0.0", + "prosemirror-transform": "^1.1.0" + } + }, "node_modules/proxy-addr": { "version": "2.0.7", "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", @@ -14404,6 +15445,15 @@ "node": ">=6" } }, + "node_modules/punycode.js": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode.js/-/punycode.js-2.3.1.tgz", + "integrity": "sha512-uxFIHU0YlHYhDQtV4R9J6a52SLx28BCjT+4ieh7IGbgwVJWO+km431c4yRlREUAsAmt/uMjQUyQHNEPf0M39CA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/q": { "version": "1.5.1", "resolved": "https://registry.npmjs.org/q/-/q-1.5.1.tgz", @@ -14690,6 +15740,15 @@ "react-dom": ">=16" } }, + "node_modules/react-icons": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/react-icons/-/react-icons-5.5.0.tgz", + "integrity": "sha512-MEFcXdkP3dLo8uumGI5xN3lDFNsRtrjbOEKDLD7yv76v4wpnEq2Lt2qeHaQOr34I/wPN3s3+N08WkQ+CW37Xiw==", + "license": "MIT", + "peerDependencies": { + "react": "*" + } + }, "node_modules/react-is": { "version": "17.0.2", "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", @@ -14910,6 +15969,23 @@ "node": ">=14.0.0" } }, + "node_modules/react-syntax-highlighter": { + "version": "15.6.1", + "resolved": "https://registry.npmjs.org/react-syntax-highlighter/-/react-syntax-highlighter-15.6.1.tgz", + "integrity": "sha512-OqJ2/vL7lEeV5zTJyG7kmARppUjiB9h9udl4qHQjjgEos66z00Ia0OckwYfRxCSFrW8RJIBnsBwQsHZbVPspqg==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.3.1", + "highlight.js": "^10.4.1", + "highlightjs-vue": "^1.0.0", + "lowlight": "^1.17.0", + "prismjs": "^1.27.0", + "refractor": "^3.6.0" + }, + "peerDependencies": { + "react": ">= 0.14.0" + } + }, "node_modules/read-cache": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz", @@ -15050,6 +16126,30 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/refractor": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/refractor/-/refractor-3.6.0.tgz", + "integrity": "sha512-MY9W41IOWxxk31o+YvFCNyNzdkc9M20NoZK5vq6jkv4I/uh2zkWcfudj0Q1fovjUQJrNewS9NMzeTtqPf+n5EA==", + "license": "MIT", + "dependencies": { + "hastscript": "^6.0.0", + "parse-entities": "^2.0.0", + "prismjs": "~1.27.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/refractor/node_modules/prismjs": { + "version": "1.27.0", + "resolved": "https://registry.npmjs.org/prismjs/-/prismjs-1.27.0.tgz", + "integrity": "sha512-t13BGPUlFDR7wRB5kQDG4jjl7XeuH6jbJGt11JHPL96qwsEHNX2+68tFXqc1/k+/jALsbSWJKUOT/hcYAZ5LkA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/regenerate": { "version": "1.4.2", "resolved": "https://registry.npmjs.org/regenerate/-/regenerate-1.4.2.tgz", @@ -15404,6 +16504,12 @@ "randombytes": "^2.1.0" } }, + "node_modules/rope-sequence": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/rope-sequence/-/rope-sequence-1.3.4.tgz", + "integrity": "sha512-UT5EDe2cu2E/6O4igUr5PSFs23nvvukicWHx6GnOPlHAiiYbzNuCRQCuiUdHJQcqKalLKlrYJnjY0ySGsXNQXQ==", + "license": "MIT" + }, "node_modules/run-parallel": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", @@ -16129,6 +17235,16 @@ "deprecated": "Please use @jridgewell/sourcemap-codec instead", "license": "MIT" }, + "node_modules/space-separated-tokens": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/space-separated-tokens/-/space-separated-tokens-1.1.5.tgz", + "integrity": "sha512-q/JSVd1Lptzhf5bkYm4ob4iWPjx0KiRe3sRFBNrVqbJkFaBm5vbbowy1mymoPNLRa52+oadOhJ+K49wsSeSjTA==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/spdy": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/spdy/-/spdy-4.0.2.tgz", @@ -17412,6 +18528,12 @@ "node": ">=4.2.0" } }, + "node_modules/uc.micro": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/uc.micro/-/uc.micro-2.1.0.tgz", + "integrity": "sha512-ARDJmphmdvUk6Glw7y9DQ2bFkKBHwQHLi2lsaH6PPmz/Ka9sFOBsBluozhDltWmnv9u/cF6Rt87znRTPV+yp/A==", + "license": "MIT" + }, "node_modules/unbox-primitive": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.1.0.tgz", @@ -17704,6 +18826,12 @@ "browser-process-hrtime": "^1.0.0" } }, + "node_modules/w3c-keyname": { + "version": "2.2.8", + "resolved": "https://registry.npmjs.org/w3c-keyname/-/w3c-keyname-2.2.8.tgz", + "integrity": "sha512-dpojBhNsCNN7T82Tm7k26A6G9ML3NkhDsnw9n/eoxSRlVBB4CEtIQ/KTCLI2Fwf3ataSXRhYFkQi3SlnFwPvPQ==", + "license": "MIT" + }, "node_modules/w3c-xmlserializer": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-2.0.0.tgz", @@ -18579,6 +19707,15 @@ "node": ">=0.4.0" } }, + "node_modules/xtend": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", + "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==", + "license": "MIT", + "engines": { + "node": ">=0.4" + } + }, "node_modules/y18n": { "version": "5.0.8", "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", diff --git a/package.json b/package.json index 4a6a4ee..aee961b 100644 --- a/package.json +++ b/package.json @@ -11,10 +11,22 @@ "@testing-library/jest-dom": "^6.6.4", "@testing-library/react": "^16.3.0", "@testing-library/user-event": "^13.5.0", + "@tiptap/extension-code-block": "^3.2.0", + "@tiptap/extension-highlight": "^3.2.0", + "@tiptap/extension-image": "^3.2.0", + "@tiptap/extension-link": "^3.2.0", + "@tiptap/extension-table": "^3.2.0", + "@tiptap/extension-table-cell": "^3.2.0", + "@tiptap/extension-table-header": "^3.2.0", + "@tiptap/extension-table-row": "^3.2.0", + "@tiptap/pm": "^3.2.0", + "@tiptap/react": "^3.2.0", + "@tiptap/starter-kit": "^3.2.0", "@types/jest": "^27.5.2", "@types/node": "^16.18.126", "@types/react": "^19.1.9", "@types/react-dom": "^19.1.7", + "@types/react-syntax-highlighter": "^15.5.13", "@types/socket.io-client": "^1.4.36", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", @@ -23,9 +35,11 @@ "react-cookie": "^8.0.1", "react-dom": "^19.1.1", "react-hot-toast": "^2.5.2", + "react-icons": "^5.5.0", "react-redux": "^9.2.0", "react-router-dom": "^6.30.1", "react-scripts": "5.0.1", + "react-syntax-highlighter": "^15.6.1", "recharts": "^3.1.0", "socket.io-client": "^4.8.1", "tailwind-merge": "^3.3.1", diff --git a/src/App.tsx b/src/App.tsx index d6864eb..b82c82e 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -31,11 +31,13 @@ import ResellerLogin from './pages/reseller/Login'; import ResellerSignup from './pages/reseller/Signup'; import ResellerDashboardMain from './pages/reseller/Dashboard'; import ResellerDashboardCustomers from './pages/reseller/Customers'; +import ResellerProducts from './pages/reseller/Products'; import ResellerDashboardInstances from './pages/reseller/Instances'; import ResellerBilling from './pages/reseller/Billing'; import ResellerSupport from './pages/reseller/Support'; import ResellerReports from './pages/reseller/Reports'; import ResellerTraining from './pages/reseller/Training'; +import ResellerCertifications from './pages/reseller/Certifications'; import Receipts from './pages/reseller/Receipts'; import ResellerLayout from './components/Layout/ResellerLayout'; @@ -52,13 +54,15 @@ import AdminSettings from './pages/admin/Settings'; import AdminFeedback from './pages/admin/Feedback'; import RegisteredVendors from './pages/admin/RegisteredVendors'; import Resellers from './pages/admin/Resellers'; - +import Logs from './pages/admin/Logs'; import Unauthorized from './pages/Unauthorized'; import CookieConsent from './components/CookieConsent'; import AuthDebug from './components/AuthDebug'; -import DeveloperFeedback from './components/DeveloperFeedback'; import socketService from './services/socketService'; import './index.css'; +import VendorSalesDashboard from './components/VendorSalesDashboard'; +import KnowledgeBase from './pages/KnowledgeBase'; +import AdminKnowledgeBase from './pages/admin/KnowledgeBase'; // Component to handle role-based redirects const RoleBasedRedirect: React.FC = () => { @@ -108,9 +112,13 @@ function App() { if (savedTheme) { store.dispatch(setTheme(savedTheme as 'light' | 'dark')); } else { - // Check system preference - const systemTheme = window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'; - store.dispatch(setTheme(systemTheme)); + // Default to dark theme instead of system preference + store.dispatch(setTheme('dark')); + } + + // Ensure dark theme is applied to document element on first load + if (!savedTheme) { + document.documentElement.classList.add('dark'); } // Listen for system theme changes @@ -194,7 +202,7 @@ function App() { } /> + @@ -228,10 +236,10 @@ function App() { } /> - - + } /> @@ -259,7 +267,7 @@ function App() { - + } /> @@ -313,6 +321,13 @@ function App() { } /> + + + + + + } /> @@ -348,6 +363,13 @@ function App() { } /> + + + + + + } /> {/* Admin Routes */} } /> + + + + + + } /> + + + + + + } /> @@ -451,14 +487,14 @@ function App() { - + } /> - + } /> @@ -486,12 +522,12 @@ function App() { } /> - + } /> - + } /> {/* */} - + {/* DeveloperFeedback - Only visible to admin users */} + {/* Moved to AdminLayout component for proper auth context */} diff --git a/src/components/DualCurrencyDisplay.tsx b/src/components/DualCurrencyDisplay.tsx index 13532dd..265d477 100644 --- a/src/components/DualCurrencyDisplay.tsx +++ b/src/components/DualCurrencyDisplay.tsx @@ -2,7 +2,7 @@ import React from 'react'; import { formatCurrencyDualDisplay } from '../utils/format'; interface DualCurrencyDisplayProps { - amount: number; + amount: number | undefined | null; currency?: 'USD' | 'INR'; className?: string; showSecondary?: boolean; @@ -14,7 +14,8 @@ const DualCurrencyDisplay: React.FC = ({ className = '', showSecondary = true }) => { - const formatted = formatCurrencyDualDisplay(amount, currency); + const safeAmount = amount ?? 0; + const formatted = formatCurrencyDualDisplay(safeAmount, currency); return (
@@ -31,4 +32,4 @@ const DualCurrencyDisplay: React.FC = ({ ); }; -export default DualCurrencyDisplay; \ No newline at end of file +export default DualCurrencyDisplay; \ No newline at end of file diff --git a/src/components/Layout/AdminLayout.tsx b/src/components/Layout/AdminLayout.tsx index 72c4431..ee26592 100644 --- a/src/components/Layout/AdminLayout.tsx +++ b/src/components/Layout/AdminLayout.tsx @@ -1,6 +1,7 @@ import React from 'react'; import AdminSidebar from './AdminSidebar'; import NotificationBell from '../NotificationBell'; +import DeveloperFeedback from '../DeveloperFeedback'; interface AdminLayoutProps { children: React.ReactNode; @@ -22,16 +23,16 @@ const AdminLayout: React.FC = ({ children }) => {
-
+
-
-

+
+

Admin Panel

-

+

System Administration & Management

@@ -43,12 +44,12 @@ const AdminLayout: React.FC = ({ children }) => { {/* User Menu */}
-
+
A
-
-

Admin User

-

System Administrator

+
+

Admin User

+

System Administrator

@@ -58,9 +59,14 @@ const AdminLayout: React.FC = ({ children }) => { {/* Page Content */}
- {children} +
+ {children} +
+ + {/* Developer Feedback - Only visible on admin pages */} +

); diff --git a/src/components/Layout/AdminSidebar.tsx b/src/components/Layout/AdminSidebar.tsx index 93d79d0..6cd23d5 100644 --- a/src/components/Layout/AdminSidebar.tsx +++ b/src/components/Layout/AdminSidebar.tsx @@ -1,13 +1,14 @@ import React, { useState } from 'react'; -import { Link, useLocation } from 'react-router-dom'; +import { Link, useLocation, useNavigate } from 'react-router-dom'; import { useAppSelector, useAppDispatch } from '../../store/hooks'; import { toggleTheme } from '../../store/slices/themeSlice'; import { Home, Users, Building, Clock, Settings, Menu, X, Sun, Moon, LogOut, - Shield, BarChart3, FileText, UserCheck, UserX, TrendingUp, Activity, Package, MessageSquare + Shield, BarChart3, FileText, UserCheck, UserX, TrendingUp, Activity, Package, MessageSquare, BookOpen } from 'lucide-react'; import { logout } from '../../store/slices/authSlice'; import { cn } from '../../utils/cn'; +import toast from 'react-hot-toast'; const adminNavigation = [ { name: 'Dashboard', href: '/admin', icon: Home }, @@ -18,18 +19,30 @@ const adminNavigation = [ { name: 'Products', href: '/admin/products', icon: Package }, { name: 'Analytics', href: '/admin/analytics', icon: BarChart3 }, { name: 'Reports', href: '/admin/reports', icon: FileText }, + { name: 'Logs', href: '/admin/logs', icon: Activity }, { name: 'Feedback', href: '/admin/feedback', icon: MessageSquare }, + { name: 'Knowledge Base', href: '/admin/knowledge-base', icon: BookOpen }, { name: 'Settings', href: '/admin/settings', icon: Settings }, ]; const AdminSidebar: React.FC = () => { const [isCollapsed, setIsCollapsed] = useState(false); const location = useLocation(); + const navigate = useNavigate(); const dispatch = useAppDispatch(); const { theme } = useAppSelector((state) => state.theme); const handleLogout = () => { + // Clear Redux state dispatch(logout()); + + // Show success message + toast.success('Logged out successfully'); + + // Wait a bit for Redux state to update, then navigate + setTimeout(() => { + navigate('/login'); + }, 100); }; return ( @@ -40,21 +53,22 @@ const AdminSidebar: React.FC = () => { {/* Header */}
{!isCollapsed && ( -
-
+
+
- + Admin
)}
diff --git a/src/components/Layout/ResellerLayout.tsx b/src/components/Layout/ResellerLayout.tsx index 4f5f5ea..f15f10d 100644 --- a/src/components/Layout/ResellerLayout.tsx +++ b/src/components/Layout/ResellerLayout.tsx @@ -1,4 +1,6 @@ -import React from 'react'; +import React, { useState, useEffect } from 'react'; +import { useAppSelector } from '../../store/hooks'; +import { Bell, CheckCircle, XCircle, Clock, AlertCircle } from 'lucide-react'; import ResellerSidebar from './ResellerSidebar'; interface ResellerLayoutProps { @@ -6,81 +8,256 @@ interface ResellerLayoutProps { } const ResellerLayout: React.FC = ({ children }) => { + const { user } = useAppSelector(state => state.auth); + const [notifications, setNotifications] = useState([]); + const [showNotifications, setShowNotifications] = useState(false); + const [unreadCount, setUnreadCount] = useState(0); + + // Fetch notifications from the backend + useEffect(() => { + const fetchNotifications = async () => { + try { + const token = localStorage.getItem('accessToken'); + if (!token) return; + + const response = await fetch('http://localhost:5000/api/notifications/reseller', { + headers: { + 'Authorization': `Bearer ${token}`, + 'Content-Type': 'application/json' + } + }); + + if (response.ok) { + const data = await response.json(); + const userNotifications = data.data || []; + setNotifications(userNotifications); + setUnreadCount(userNotifications.filter((n: any) => !n.isRead).length); + } + } catch (error) { + console.error('Error fetching notifications:', error); + } + }; + + fetchNotifications(); + + // Set up polling for new notifications every 30 seconds + const interval = setInterval(fetchNotifications, 30000); + + return () => clearInterval(interval); + }, []); + + // Mark notification as read + const markAsRead = async (notificationId: number) => { + try { + const token = localStorage.getItem('accessToken'); + if (!token) return; + + const response = await fetch(`http://localhost:5000/api/notifications/${notificationId}/read`, { + method: 'PUT', + headers: { + 'Authorization': `Bearer ${token}`, + 'Content-Type': 'application/json' + } + }); + + if (response.ok) { + setNotifications(prev => + prev.map(n => + n.id === notificationId ? { ...n, isRead: true } : n + ) + ); + setUnreadCount(prev => Math.max(0, prev - 1)); + } + } catch (error) { + console.error('Error marking notification as read:', error); + } + }; + + // Close notifications when clicking outside + useEffect(() => { + const handleClickOutside = (event: MouseEvent) => { + const target = event.target as Element; + if (showNotifications && !target.closest('.notifications-dropdown')) { + setShowNotifications(false); + } + }; + + document.addEventListener('mousedown', handleClickOutside); + return () => document.removeEventListener('mousedown', handleClickOutside); + }, [showNotifications]); + + // Get notification icon based on type + const getNotificationIcon = (type: string) => { + switch (type) { + case 'SALE_APPROVED': + return ; + case 'SALE_REJECTED': + return ; + case 'SALE_PENDING': + return ; + case 'COMMISSION_EARNED': + return ; + default: + return ; + } + }; + + // Get notification color based on type + const getNotificationColor = (type: string) => { + switch (type) { + case 'SALE_APPROVED': + return 'border-l-green-500 bg-green-50 dark:bg-green-900/20'; + case 'SALE_REJECTED': + return 'border-l-red-500 bg-red-50 dark:bg-red-900/20'; + case 'SALE_PENDING': + return 'border-l-yellow-500 bg-yellow-50 dark:bg-yellow-900/20'; + case 'COMMISSION_EARNED': + return 'border-l-emerald-500 bg-emerald-50 dark:bg-emerald-900/20'; + default: + return 'border-l-blue-500 bg-blue-50 dark:bg-blue-900/20'; + } + }; + return (
-
+ {/* Top Navigation Bar - Full Width */} +
+
+ {/* Left Side - Logo and Title */} +
+
+
+
+
+

+ Reseller Portal +

+

+ Cloud Services Management +

+
+
+ + {/* Right Side - Search, Notifications, User */} +
+ {/* Notifications */} +
+ + + {/* Notifications Dropdown */} + {showNotifications && ( +
+
+

Notifications

+

+ {unreadCount > 0 ? `${unreadCount} unread` : 'All caught up!'} +

+
+ +
+ {notifications.length === 0 ? ( +
+ +

No notifications yet

+
+ ) : ( +
+ {notifications.slice(0, 10).map((notification) => ( +
markAsRead(notification.id)} + > +
+
+ {getNotificationIcon(notification.type)} +
+
+

+ {notification.title} +

+

+ {notification.message} +

+

+ {new Date(notification.createdAt).toLocaleDateString()} +

+
+ {!notification.isRead && ( +
+ )} +
+
+ ))} +
+ )} +
+ + {notifications.length > 10 && ( +
+ +
+ )} +
+ )} +
+ + {/* Divider */} +
+ + {/* User Menu */} +
+
+

+ {user?.firstName && user?.lastName ? `${user.firstName} ${user.lastName}` : user?.email || 'User'} +

+

+ {user?.company || 'Company'} +

+
+
+ + {user?.firstName && user?.lastName + ? `${user.firstName.charAt(0)}${user.lastName.charAt(0)}` + : user?.email?.charAt(0)?.toUpperCase() || 'U' + } + +
+
+
+
+
+ + {/* Main Content Area with Sidebar */} +
{/* Sidebar */}
- {/* Main Content */} -
- {/* Top Navigation Bar */} -
-
- {/* Left Side - Logo and Title */} -
-
-
-
-
-

- Reseller Portal -

-

- Cloud Services Management -

-
-
- - {/* Right Side - Search, Notifications, User */} -
- {/* Search Bar */} -
- -
- - - -
-
- - {/* Notifications */} - - - {/* Divider */} -
- - {/* User Menu */} -
-
-

John Reseller

-

Tech Solutions Inc

-
-
- JR -
-
-
-
-
- - {/* Page Content */} -
-
-
- {children} -
+ {/* Page Content */} +
+
+
+ {children}
diff --git a/src/components/Layout/ResellerSidebar.tsx b/src/components/Layout/ResellerSidebar.tsx index 6ea96ad..c5be882 100644 --- a/src/components/Layout/ResellerSidebar.tsx +++ b/src/components/Layout/ResellerSidebar.tsx @@ -5,10 +5,8 @@ import { Home, Users, Cloud, - CreditCard, Headphones, BarChart3, - Wallet, BookOpen, ShoppingBag, Award, @@ -23,7 +21,9 @@ import { Target, TrendingUp, Package, - FileText + FileText, + User, + ChevronRight } from 'lucide-react'; import { RootState } from '../../store'; import { toggleTheme } from '../../store/slices/themeSlice'; @@ -34,15 +34,13 @@ const resellerNavigation = [ { name: 'Dashboard', href: '/reseller-dashboard', icon: Home }, { name: 'Customers', href: '/reseller-dashboard/customers', icon: Users }, { name: 'Products', href: '/reseller-dashboard/products', icon: Package }, - { name: 'Billing', href: '/reseller-dashboard/billing', icon: CreditCard }, + { name: 'Receipts & Sales', href: '/reseller-dashboard/receipts', icon: FileText }, { name: 'Support', href: '/reseller-dashboard/support', icon: Headphones }, - { name: 'Reports', href: '/reseller-dashboard/reports', icon: BarChart3 }, - { name: 'Receipts', href: '/reseller-dashboard/receipts', icon: FileText }, - { name: 'Wallet', href: '/reseller-dashboard/wallet', icon: Wallet }, { name: 'Training', href: '/reseller-dashboard/training', icon: BookOpen }, - { name: 'Marketplace', href: '/reseller-dashboard/marketplace', icon: ShoppingBag }, { name: 'Certifications', href: '/reseller-dashboard/certifications', icon: Award }, { name: 'Knowledge Base', href: '/reseller-dashboard/knowledge-base', icon: HelpCircle }, + { name: 'Reports', href: '/reseller-dashboard/reports', icon: BarChart3 }, + { name: 'Marketplace', href: '/reseller-dashboard/marketplace', icon: ShoppingBag }, { name: 'Settings', href: '/reseller-dashboard/settings', icon: Settings }, ]; @@ -62,35 +60,35 @@ const ResellerSidebar: React.FC = () => { "flex flex-col h-full bg-gradient-to-b from-slate-900 via-slate-800 to-slate-900 border-r border-slate-700/50 transition-all duration-300", isCollapsed ? "w-16" : "w-64" )}> - {/* Header */} -
- {!isCollapsed && ( -
-
- -
- - Reseller - -
- )} - -
- {/* Navigation */} - {/* User Profile & Actions */} -
- {/* Theme Toggle */} - - - {/* Logout */} - + +
+ + + +
+
); diff --git a/src/components/Layout/Sidebar.tsx b/src/components/Layout/Sidebar.tsx index f171ec9..9c84153 100644 --- a/src/components/Layout/Sidebar.tsx +++ b/src/components/Layout/Sidebar.tsx @@ -1,5 +1,5 @@ import React, { useState } from 'react'; -import { Link, useLocation } from 'react-router-dom'; +import { Link, useLocation, useNavigate } from 'react-router-dom'; import { useAppSelector, useAppDispatch } from '../../store/hooks'; import { Home, @@ -35,15 +35,15 @@ import toast from 'react-hot-toast'; const navigation = [ { name: 'Dashboard', href: '/dashboard', icon: Home }, { name: 'Product Management', href: '/product-management', icon: Package }, - { name: 'Reseller Requests', href: '/resellers', icon: Users }, - { name: 'Approved Resellers', href: '/approved-resellers', icon: Handshake }, - // { name: 'Deals', href: '/deals', icon: Briefcase }, + { name: 'Sales Management', href: '/sales-management', icon: ShoppingBag }, + { name: 'Reseller Requests', href: '/resellers', icon: Users }, + { name: 'Approved Resellers', href: '/approved-resellers', icon: Handshake }, + // { name: 'Deals', href: '/deals', icon: Briefcase }, { name: 'Commissions', href: '/commissions', icon: Wallet }, { name: 'Training', href: '/training', icon: BookOpen }, { name: 'Support', href: '/support', icon: Headphones }, { name: 'Analytics', href: '/analytics', icon: BarChart3 }, { name: 'Reports', href: '/reports', icon: FileText }, - { name: 'Targets', href: '/targets', icon: Target }, { name: 'Performance', href: '/performance', icon: TrendingUp }, { name: 'Marketplace', href: '/marketplace', icon: ShoppingBag }, { name: 'Certifications', href: '/certifications', icon: Award }, @@ -54,6 +54,7 @@ const navigation = [ const Sidebar: React.FC = () => { const [isCollapsed, setIsCollapsed] = useState(false); const location = useLocation(); + const navigate = useNavigate(); const dispatch = useAppDispatch(); const { theme } = useAppSelector((state: RootState) => state.theme); const { user } = useAppSelector((state: RootState) => state.auth); @@ -61,6 +62,11 @@ const Sidebar: React.FC = () => { const handleLogout = () => { dispatch(logout()); toast.success('Logged out successfully'); + + // Wait a bit for Redux state to update, then navigate + setTimeout(() => { + navigate('/login'); + }, 100); }; return ( diff --git a/src/components/NotificationBell.tsx b/src/components/NotificationBell.tsx index 8bc673c..17f8c15 100644 --- a/src/components/NotificationBell.tsx +++ b/src/components/NotificationBell.tsx @@ -57,10 +57,14 @@ const NotificationBell: React.FC = () => { }); socket.on('SUPPORT_TICKET', (data: { title: string }) => { + // Only show support ticket notifications for admin users + const isAdmin = user?.roles?.[0]?.name === 'system_admin' || user?.role === 'system_admin'; + if (isAdmin) { toast.error(`New support ticket: ${data.title}`, { duration: 6000, }); fetchUnreadCount(); + } }); socket.on('VENDOR_NOTIFICATION', (data: { message: string; type: 'success' | 'error' | 'info' }) => { @@ -71,6 +75,35 @@ const NotificationBell: React.FC = () => { fetchUnreadCount(); }); + // Reseller-related events + socket.on('NEW_RESELLER_REQUEST', (data: { resellerName: string; company: string; vendorId: string }) => { + toast.success(`New reseller request: ${data.resellerName} from ${data.company}`, { + duration: 5000, + }); + fetchUnreadCount(); + }); + + socket.on('RESELLER_CREATED', (data: { resellerName: string; company: string; vendorId: string }) => { + toast.success(`New reseller account created: ${data.resellerName} from ${data.company}`, { + duration: 5000, + }); + fetchUnreadCount(); + }); + + socket.on('RESELLER_APPROVED', (data: { resellerName: string; company: string; vendorId: string }) => { + toast.success(`Reseller approved: ${data.resellerName} from ${data.company}`, { + duration: 5000, + }); + fetchUnreadCount(); + }); + + socket.on('RESELLER_REJECTED', (data: { resellerName: string; company: string; vendorId: string; reason?: string }) => { + toast.error(`Reseller rejected: ${data.resellerName} from ${data.company}`, { + duration: 5000, + }); + fetchUnreadCount(); + }); + return () => { socket.off('connect'); socket.off('disconnect'); @@ -79,9 +112,13 @@ const NotificationBell: React.FC = () => { socket.off('PAYMENT_RECEIVED'); socket.off('SUPPORT_TICKET'); socket.off('VENDOR_NOTIFICATION'); + socket.off('NEW_RESELLER_REQUEST'); + socket.off('RESELLER_CREATED'); + socket.off('RESELLER_APPROVED'); + socket.off('RESELLER_REJECTED'); }; } - }, [socket]); + }, [socket, user]); const fetchUnreadCount = async () => { try { @@ -95,6 +132,8 @@ const NotificationBell: React.FC = () => { endpoint = '/resellers/notifications/stats'; } + console.log('Fetching notification stats from:', endpoint); + const response = await fetch(`${process.env.REACT_APP_API_URL || 'http://localhost:5000/api'}${endpoint}`, { headers: { 'Authorization': `Bearer ${localStorage.getItem('accessToken')}`, @@ -104,10 +143,18 @@ const NotificationBell: React.FC = () => { if (response.ok) { const data = await response.json(); - setUnreadCount(data.data?.unreadNotifications || 0); + console.log('Notification stats response:', data); + setUnreadCount(data.data?.unread || data.unread || 0); + } else if (response.status === 404) { + console.log('Notification stats endpoint not found, setting count to 0'); + setUnreadCount(0); + } else { + console.error('Failed to fetch notification stats:', response.status); + setUnreadCount(0); } } catch (error) { console.error('Error fetching notification count:', error); + setUnreadCount(0); } finally { setLoading(false); } diff --git a/src/components/NotificationPanel.tsx b/src/components/NotificationPanel.tsx index 12ed98a..c53531c 100644 --- a/src/components/NotificationPanel.tsx +++ b/src/components/NotificationPanel.tsx @@ -33,6 +33,13 @@ const NotificationPanel: React.FC = ({ isOpen, onClose } const [deletingNotificationId, setDeletingNotificationId] = useState(null); const { user } = useAppSelector((state) => state.auth); + // Ensure admin users always see unread notifications + useEffect(() => { + if (user?.role === 'system_admin') { + setActiveTab('unread'); + } + }, [user?.role]); + useEffect(() => { if (isOpen) { fetchNotifications(); @@ -76,7 +83,8 @@ const NotificationPanel: React.FC = ({ isOpen, onClose } const params = new URLSearchParams({ page: '1', limit: '50', - ...(activeTab === 'unread' && { unreadOnly: 'true' }) + ...(activeTab === 'unread' && { unreadOnly: 'true' }), + ...(user?.role === 'system_admin' && { unreadOnly: 'true' }) }); const response = await fetch(`${process.env.REACT_APP_API_URL || 'http://localhost:5000/api'}${endpoint}?${params}`, { @@ -88,7 +96,12 @@ const NotificationPanel: React.FC = ({ isOpen, onClose } if (response.ok) { const data = await response.json(); - setNotifications(data.data.notifications); + // Filter out read notifications for admin users + let filteredNotifications = data.data.notifications; + if (user?.role === 'system_admin') { + filteredNotifications = filteredNotifications.filter((n: Notification) => !n.isRead); + } + setNotifications(filteredNotifications); setUnreadCount(data.data.notifications.filter((n: Notification) => !n.isRead).length); } } catch (error) { @@ -233,7 +246,7 @@ const NotificationPanel: React.FC = ({ isOpen, onClose }

- Notifications + {user?.role === 'system_admin' ? 'Unread Notifications' : 'Notifications'}

{unreadCount > 0 && ( @@ -242,7 +255,7 @@ const NotificationPanel: React.FC = ({ isOpen, onClose } )}
- {unreadCount > 0 && ( + {unreadCount > 0 && user?.role !== 'system_admin' && ( + {user?.role !== 'system_admin' && ( + + )}
@@ -292,8 +307,12 @@ const NotificationPanel: React.FC = ({ isOpen, onClose } ) : notifications.length === 0 ? (
-

No notifications

-

You're all caught up!

+

+ {user?.role === 'system_admin' ? 'No unread notifications' : 'No notifications'} +

+

+ {user?.role === 'system_admin' ? 'All notifications have been read' : "You're all caught up!"} +

) : ( notifications.map((notification) => ( @@ -342,13 +361,16 @@ const NotificationPanel: React.FC = ({ isOpen, onClose } )} - + {/* Hide delete button for admin users */} + {user?.role !== 'system_admin' && ( + + )}
@@ -360,7 +382,7 @@ const NotificationPanel: React.FC = ({ isOpen, onClose }
{/* Delete Confirmation Modal */} - {isDeleteModalOpen && ( + {isDeleteModalOpen && user?.role !== 'system_admin' && (

Confirm Delete

diff --git a/src/components/ResellerCertificates.tsx b/src/components/ResellerCertificates.tsx new file mode 100644 index 0000000..6434a04 --- /dev/null +++ b/src/components/ResellerCertificates.tsx @@ -0,0 +1,446 @@ +import React, { useState, useEffect } from 'react'; +import { Award, Search, Download, Eye, Calendar, BookOpen, Star } from 'lucide-react'; +import toast from 'react-hot-toast'; + +interface Certificate { + id: number; + certificateNumber: string; + issuedAt: string; + completionDate: string; + grade: string; + score: number; + course: { + id: number; + title: string; + description: string; + level: string; + category: string; + }; +} + +interface CertificateStats { + totalCertificates: number; + thisMonthCertificates: number; + thisYearCertificates: number; + averageScore: number; +} + +const ResellerCertificates: React.FC = () => { + const [certificates, setCertificates] = useState([]); + const [stats, setStats] = useState(null); + const [loading, setLoading] = useState(true); + const [search, setSearch] = useState(''); + const [selectedCertificate, setSelectedCertificate] = useState(null); + + useEffect(() => { + fetchCertificates(); + fetchCertificateStats(); + }, []); + + const fetchCertificates = async () => { + try { + setLoading(true); + const token = localStorage.getItem('accessToken'); + const response = await fetch(`${process.env.REACT_APP_API_URL || 'http://localhost:5000/api'}/training-new/certificates`, { + headers: { + 'Authorization': `Bearer ${token}`, + 'Content-Type': 'application/json' + } + }); + + if (response.ok) { + const data = await response.json(); + setCertificates(data.data || []); + } else { + toast.error('Failed to fetch certificates'); + } + } catch (error) { + console.error('Error fetching certificates:', error); + toast.error('Failed to fetch certificates'); + } finally { + setLoading(false); + } + }; + + const fetchCertificateStats = async () => { + try { + const token = localStorage.getItem('accessToken'); + const response = await fetch(`${process.env.REACT_APP_API_URL || 'http://localhost:5000/api'}/training-new/certificates/stats`, { + headers: { + 'Authorization': `Bearer ${token}`, + 'Content-Type': 'application/json' + } + }); + + if (response.ok) { + const data = await response.json(); + setStats(data.data); + } + } catch (error) { + console.error('Error fetching certificate stats:', error); + } + }; + + const handleSearch = (e: React.FormEvent) => { + e.preventDefault(); + }; + + const formatDate = (dateString: string) => { + return new Date(dateString).toLocaleDateString('en-US', { + year: 'numeric', + month: 'long', + day: 'numeric' + }); + }; + + const getGradeColor = (grade: string) => { + switch (grade) { + case 'Pass': return 'bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-200'; + case 'Merit': return 'bg-blue-100 text-blue-800 dark:bg-blue-900 dark:text-blue-200'; + case 'Distinction': return 'bg-purple-100 text-purple-800 dark:bg-purple-900 dark:text-purple-200'; + default: return 'bg-gray-100 text-gray-800 dark:bg-gray-900 dark:text-gray-200'; + } + }; + + const getLevelColor = (level: string) => { + switch (level) { + case 'Beginner': return 'bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-200'; + case 'Intermediate': return 'bg-yellow-100 text-yellow-800 dark:bg-yellow-900 dark:text-yellow-200'; + case 'Advanced': return 'bg-red-100 text-red-800 dark:bg-red-900 dark:text-red-200'; + default: return 'bg-gray-100 text-gray-800 dark:bg-gray-900 dark:text-gray-200'; + } + }; + + const getScoreColor = (score: number) => { + if (score >= 90) return 'text-purple-600 dark:text-purple-400'; + if (score >= 80) return 'text-blue-600 dark:text-blue-400'; + if (score >= 70) return 'text-green-600 dark:text-green-400'; + if (score >= 60) return 'text-yellow-600 dark:text-yellow-400'; + return 'text-red-600 dark:text-red-400'; + }; + + const filteredCertificates = certificates.filter(certificate => + certificate.course.title.toLowerCase().includes(search.toLowerCase()) || + certificate.course.category.toLowerCase().includes(search.toLowerCase()) || + certificate.grade.toLowerCase().includes(search.toLowerCase()) + ); + + if (loading) { + return ( +
+
+
+ ); + } + + return ( +
+ {/* Header */} +
+

+ My Certificates +

+

+ View and download your earned training certificates +

+
+ + {/* Stats Cards */} + {stats && ( +
+
+
+
+

+ Total Certificates +

+

+ {stats.totalCertificates} +

+
+
+ +
+
+
+ +
+
+
+

+ This Month +

+

+ {stats.thisMonthCertificates} +

+
+
+ +
+
+
+ +
+
+
+

+ This Year +

+

+ {stats.thisYearCertificates} +

+
+
+ +
+
+
+ +
+
+
+

+ Average Score +

+

+ {stats.averageScore}% +

+
+
+ +
+
+
+
+ )} + + {/* Search */} +
+
+
+ + setSearch(e.target.value)} + className="w-full pl-10 pr-4 py-2 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-700 text-gray-900 dark:text-white focus:outline-none focus:ring-2 focus:ring-primary-500" + /> +
+
+
+ + {/* Certificates Grid */} +
+ {filteredCertificates.map((certificate) => ( +
+
+ {/* Certificate Header */} +
+
+ +
+
+
Certificate #
+
+ {certificate.certificateNumber} +
+
+
+ + {/* Course Info */} +
+

+ {certificate.course.title} +

+
+ + {certificate.course.level} + + + {certificate.course.category} + +
+
+ + {/* Grade and Score */} +
+ + {certificate.grade} + +
+ + + {certificate.score}% + +
+
+ + {/* Dates */} +
+
+ Completed: + + {formatDate(certificate.completionDate)} + +
+
+ Issued: + + {formatDate(certificate.issuedAt)} + +
+
+ + {/* Actions */} +
+ + +
+
+
+ ))} +
+ + {filteredCertificates.length === 0 && !loading && ( +
+
+ +
+

+ {search ? 'No certificates found' : 'No certificates earned yet'} +

+

+ {search ? 'Try adjusting your search terms' : 'Complete training courses to earn your first certificate'} +

+
+ )} + + {/* Certificate Detail Modal */} + {selectedCertificate && ( +
+
+
+
+
+
+ +
+
+

+ Certificate Details +

+

+ {selectedCertificate.certificateNumber} +

+
+
+ +
+ +
+ {/* Course Information */} +
+

+ Course Information +

+
+
+ Course Title: +

+ {selectedCertificate.course.title} +

+
+
+ Level: + + {selectedCertificate.course.level} + +
+
+ Category: +

+ {selectedCertificate.course.category} +

+
+
+ Description: +

+ {selectedCertificate.course.description} +

+
+
+
+ + {/* Achievement Details */} +
+

+ Achievement Details +

+
+
+ Grade: + + {selectedCertificate.grade} + +
+
+ Score: +

+ {selectedCertificate.score}% +

+
+
+ Completion Date: +

+ {formatDate(selectedCertificate.completionDate)} +

+
+
+ Issued Date: +

+ {formatDate(selectedCertificate.issuedAt)} +

+
+
+
+ + {/* Actions */} +
+ + +
+
+
+
+
+ )} +
+ ); +}; + +export default ResellerCertificates; \ No newline at end of file diff --git a/src/components/ResellerTargetsDisplay.tsx b/src/components/ResellerTargetsDisplay.tsx new file mode 100644 index 0000000..04ca4fd --- /dev/null +++ b/src/components/ResellerTargetsDisplay.tsx @@ -0,0 +1,264 @@ +import React from 'react'; +import { Target, TrendingUp, Calendar, DollarSign, CheckCircle, Clock, AlertCircle } from 'lucide-react'; + +interface ResellerTarget { + id: number; + targetType: string; + targetPeriod: string; + startDate: string; + endDate: string; + salesTarget: number; + quantityTarget?: number; + baseCommissionRate: number; + bonusCommissionRate?: number; + currentSales: number; + currentQuantity: number; + achievementPercentage: number; + status: string; + isTargetMet: boolean; + reseller: { + id: number; + firstName: string; + lastName: string; + email: string; + company: string; + }; +} + +interface ResellerTargetsDisplayProps { + targets: ResellerTarget[]; + loading?: boolean; +} + +const ResellerTargetsDisplay: React.FC = ({ targets, loading = false }) => { + // Immediate debugging + console.log('ResellerTargetsDisplay rendering with props:', { + targets, + targetsType: typeof targets, + targetsIsArray: Array.isArray(targets), + loading, + hasTargets: !!targets + }); + + // Ensure targets is always an array + const safeTargets = Array.isArray(targets) ? targets : []; + + console.log('ResellerTargetsDisplay: Safe targets created:', { + safeTargets, + safeTargetsType: typeof safeTargets, + safeTargetsIsArray: Array.isArray(safeTargets), + safeTargetsLength: safeTargets.length + }); + + const getTargetStatusColor = (target: ResellerTarget) => { + if (target.isTargetMet) return 'bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-300'; + if (target.achievementPercentage >= 80) return 'bg-yellow-100 text-yellow-800 dark:bg-yellow-900 dark:text-yellow-300'; + if (target.achievementPercentage >= 50) return 'bg-orange-100 text-orange-800 dark:bg-orange-900 dark:text-orange-300'; + return 'bg-red-100 text-red-800 dark:bg-red-900 dark:text-red-300'; + }; + + const getTargetStatusIcon = (target: ResellerTarget) => { + if (target.isTargetMet) return ; + if (target.achievementPercentage >= 80) return ; + if (target.achievementPercentage >= 50) return ; + return ; + }; + + const getProgressColor = (percentage: number) => { + if (percentage >= 100) return 'bg-green-500'; + if (percentage >= 80) return 'bg-yellow-500'; + if (percentage >= 50) return 'bg-orange-500'; + return 'bg-red-500'; + }; + + if (loading) { + return ( +
+
+
+
+ {[1, 2, 3].map((i) => ( +
+ ))} +
+
+
+ ); + } + + if (!safeTargets || safeTargets.length === 0) { + return ( +
+
+ +

Set Your First Reseller Target

+

+ Create performance targets for your resellers to track their progress and calculate commissions based on achievement. +

+
+

Why Set Targets?

+
    +
  • Performance Tracking - Monitor reseller progress
  • +
  • Commission Calculation - Pay based on achievement
  • +
  • Motivation - Clear goals drive better results
  • +
  • Analytics - Data-driven business decisions
  • +
+
+
+
+ ); + } + + return ( +
+
+

+ Reseller Targets & Performance +

+
+ + + {safeTargets.length} active target{safeTargets.length !== 1 ? 's' : ''} + +
+
+ +
+ {safeTargets.map((target) => ( +
+ {/* Target Header */} +
+
+
+ +
+
+

+ {target.reseller.firstName} {target.reseller.lastName} +

+

{target.reseller.company}

+
+
+
+ {getTargetStatusIcon(target)} + {target.isTargetMet ? 'Target Met' : `${target.achievementPercentage.toFixed(1)}%`} +
+
+ + {/* Target Details */} +
+
+

Target Period

+

{target.targetPeriod}

+

{target.targetType}

+
+
+

Sales Target

+

+ ${target.salesTarget.toLocaleString()} +

+

+ {target.quantityTarget ? `${target.quantityTarget} units` : 'Amount only'} +

+
+
+

Current Progress

+

+ ${target.currentSales.toLocaleString()} +

+

+ {target.currentQuantity} units +

+
+
+

Commission Rate

+

+ {target.baseCommissionRate}% +

+ {target.bonusCommissionRate && ( +

+ +{target.bonusCommissionRate}% bonus +

+ )} +
+
+ + {/* Progress Bar */} +
+
+ Progress + {target.achievementPercentage.toFixed(1)}% +
+
+
+
+
+ + {/* Target Dates */} +
+
+ + Start: {new Date(target.startDate).toLocaleDateString()} +
+
+ + End: {new Date(target.endDate).toLocaleDateString()} +
+
+ + {/* Commission Info */} +
+
+ Commission Status: +
+ {target.isTargetMet ? ( +
+ + Eligible for {target.baseCommissionRate}% +
+ ) : ( +
+ + + Need ${(target.salesTarget - target.currentSales).toLocaleString()} more + +
+ )} +
+
+
+
+ ))} +
+ + {/* Summary Stats */} +
+
+
+
+ {safeTargets.filter(t => t && t.isTargetMet).length} +
+
Targets Met
+
+
+
+ {safeTargets.filter(t => t && t.achievementPercentage >= 80).length} +
+
On Track
+
+
+
+ {safeTargets.filter(t => t && t.achievementPercentage < 50).length} +
+
Needs Attention
+
+
+
+
+ ); +}; + +export default ResellerTargetsDisplay; \ No newline at end of file diff --git a/src/components/RichTextEditor.tsx b/src/components/RichTextEditor.tsx new file mode 100644 index 0000000..227aafa --- /dev/null +++ b/src/components/RichTextEditor.tsx @@ -0,0 +1,292 @@ +import React, { useEffect } from 'react'; +import { useEditor, EditorContent } from '@tiptap/react'; +import StarterKit from '@tiptap/starter-kit'; +import CodeBlock from '@tiptap/extension-code-block'; +import Highlight from '@tiptap/extension-highlight'; +import Link from '@tiptap/extension-link'; +import Image from '@tiptap/extension-image'; +import { Table } from '@tiptap/extension-table'; +import { TableRow } from '@tiptap/extension-table-row'; +import { TableCell } from '@tiptap/extension-table-cell'; +import { TableHeader } from '@tiptap/extension-table-header'; +import { + Bold, + Italic, + Underline, + Strikethrough, + Code, + Quote, + List, + ListOrdered, + Heading1, + Heading2, + Heading3, + Link as LinkIcon, + Image as ImageIcon, + Table as TableIcon, + Undo, + Redo, + Highlighter, +} from 'lucide-react'; + +interface RichTextEditorProps { + content: string; + onChange: (content: string) => void; + placeholder?: string; +} + +const RichTextEditor: React.FC = ({ + content, + onChange, + placeholder = 'Start writing your article...' +}) => { + const editor = useEditor({ + extensions: [ + StarterKit, + CodeBlock, + Highlight, + Link.configure({ + openOnClick: false, + HTMLAttributes: { + class: 'text-blue-600 underline cursor-pointer', + }, + }), + Image, + Table.configure({ + resizable: true, + }), + TableRow, + TableCell, + TableHeader, + ], + content, + onUpdate: ({ editor }) => { + onChange(editor.getHTML()); + }, + editorProps: { + attributes: { + class: 'prose prose-slate dark:prose-invert max-w-none focus:outline-none min-h-[400px] p-4', + }, + }, + }); + + useEffect(() => { + if (editor && content !== editor.getHTML()) { + editor.commands.setContent(content); + } + }, [content, editor]); + + if (!editor) { + return null; + } + + const addLink = () => { + const url = window.prompt('Enter URL'); + if (url) { + editor.chain().focus().setLink({ href: url }).run(); + } + }; + + const addImage = () => { + const url = window.prompt('Enter image URL'); + if (url) { + editor.chain().focus().setImage({ src: url }).run(); + } + }; + + const addTable = () => { + editor.chain().focus().insertTable({ rows: 3, cols: 3, withHeaderRow: true }).run(); + }; + + const MenuBar = () => ( +
+
+ {/* Text Formatting */} +
+ + + + +
+ + {/* Headings */} +
+ + + +
+ + {/* Lists */} +
+ + +
+ + + + {/* Special Elements */} +
+ + + +
+ + {/* Insert Elements */} +
+ + + +
+ + {/* History */} +
+ + +
+
+
+ ); + + return ( +
+ + + {!content && ( +
+ {placeholder} +
+ )} +
+ ); +}; + +export default RichTextEditor; \ No newline at end of file diff --git a/src/components/SalesWorkflowVisual.tsx b/src/components/SalesWorkflowVisual.tsx new file mode 100644 index 0000000..42b6220 --- /dev/null +++ b/src/components/SalesWorkflowVisual.tsx @@ -0,0 +1,159 @@ +import React from 'react'; +import { CheckCircle, Clock, XCircle, FileText, Target, TrendingUp, DollarSign } from 'lucide-react'; + +interface SalesWorkflowVisualProps { + currentStep: 'created' | 'pending_verification' | 'verified' | 'rejected' | 'completed'; + saleData?: { + id: number; + productName: string; + customerName: string; + quantity: number; + totalAmount: number; + createdAt: string; + verificationNotes?: string; + }; +} + +const SalesWorkflowVisual: React.FC = ({ currentStep, saleData }) => { + const steps = [ + { + id: 'created', + title: 'Sale Created', + description: 'Reseller creates sale request', + icon: FileText, + status: 'completed' + }, + { + id: 'pending_verification', + title: 'Pending Verification', + description: 'Vendor reviews sale details', + icon: Clock, + status: currentStep === 'pending_verification' ? 'current' : + ['verified', 'rejected', 'completed'].includes(currentStep) ? 'completed' : 'pending' + }, + { + id: 'verified', + title: 'Vendor Approved', + description: 'Sale verified, stock updated, receipt generated', + icon: CheckCircle, + status: currentStep === 'verified' ? 'current' : + currentStep === 'completed' ? 'completed' : 'pending' + }, + { + id: 'completed', + title: 'Sale Completed', + description: 'Payment processed, commission calculated', + icon: DollarSign, + status: currentStep === 'completed' ? 'current' : 'pending' + } + ]; + + const getStepStatus = (step: any) => { + if (step.status === 'completed') return 'bg-green-500 text-white'; + if (step.status === 'current') return 'bg-blue-500 text-white'; + return 'bg-gray-300 text-gray-600'; + }; + + const getStepIcon = (step: any) => { + const Icon = step.icon; + if (step.status === 'completed') return ; + if (step.status === 'current') return ; + return ; + }; + + return ( +
+

+ Sales Workflow Status +

+ + {/* Workflow Steps */} +
+ {/* Progress Line */} +
+ +
+ {steps.map((step, index) => ( +
+ {/* Step Icon */} +
+ {getStepIcon(step)} +
+ + {/* Step Title */} +

+ {step.title} +

+ + {/* Step Description */} +

+ {step.description} +

+
+ ))} +
+
+ + {/* Current Status Details */} + {saleData && ( +
+

+ Sale Details +

+
+
+ Product: + {saleData.productName} +
+
+ Customer: + {saleData.customerName} +
+
+ Quantity: + {saleData.quantity} +
+
+ Amount: + ${saleData.totalAmount} +
+
+ Created: + + {new Date(saleData.createdAt).toLocaleDateString()} + +
+
+ Status: + + {currentStep.replace('_', ' ')} + +
+
+ + {saleData.verificationNotes && ( +
+ Notes: +

{saleData.verificationNotes}

+
+ )} +
+ )} + + {/* Workflow Information */} +
+

+ How the Workflow Works +

+
+

Stock is NOT reduced until vendor approves the sale

+

• Vendor can approve, reject, or request changes

+

• Receipt is automatically generated upon approval

+

• Commissions are calculated based on target achievement

+
+
+
+ ); +}; + +export default SalesWorkflowVisual; \ No newline at end of file diff --git a/src/components/VendorCertificates.tsx b/src/components/VendorCertificates.tsx new file mode 100644 index 0000000..aedbaa2 --- /dev/null +++ b/src/components/VendorCertificates.tsx @@ -0,0 +1,436 @@ +import React, { useState, useEffect } from 'react'; +import { Award, Search, Filter, Download, Eye, Calendar, User, BookOpen } from 'lucide-react'; +import toast from 'react-hot-toast'; + +interface Certificate { + id: number; + certificateNumber: string; + issuedAt: string; + completionDate: string; + grade: string; + score: number; + course: { + id: number; + title: string; + description: string; + level: string; + category: string; + }; + user: { + id: number; + firstName: string; + lastName: string; + email: string; + avatar?: string; + }; +} + +interface CertificateStats { + totalCertificates: number; + thisMonthCertificates: number; + thisYearCertificates: number; + averageScore: number; +} + +const VendorCertificates: React.FC = () => { + const [certificates, setCertificates] = useState([]); + const [stats, setStats] = useState(null); + const [loading, setLoading] = useState(true); + const [search, setSearch] = useState(''); + const [selectedCourse, setSelectedCourse] = useState('all'); + const [currentPage, setCurrentPage] = useState(1); + const [totalPages, setTotalPages] = useState(1); + const [courses, setCourses] = useState>([]); + + useEffect(() => { + fetchCertificates(); + fetchCertificateStats(); + fetchCourses(); + }, [currentPage, search, selectedCourse]); + + const fetchCertificates = async () => { + try { + setLoading(true); + const token = localStorage.getItem('accessToken'); + const params = new URLSearchParams({ + page: currentPage.toString(), + limit: '10' + }); + + if (search) params.append('search', search); + if (selectedCourse !== 'all') params.append('courseId', selectedCourse); + + const response = await fetch(`${process.env.REACT_APP_API_URL || 'http://localhost:5000/api'}/training-new/vendor/certificates?${params}`, { + headers: { + 'Authorization': `Bearer ${token}`, + 'Content-Type': 'application/json' + } + }); + + if (response.ok) { + const data = await response.json(); + setCertificates(data.data.certificates || []); + setTotalPages(data.data.totalPages || 1); + } else { + toast.error('Failed to fetch certificates'); + } + } catch (error) { + console.error('Error fetching certificates:', error); + toast.error('Failed to fetch certificates'); + } finally { + setLoading(false); + } + }; + + const fetchCertificateStats = async () => { + try { + const token = localStorage.getItem('accessToken'); + const response = await fetch(`${process.env.REACT_APP_API_URL || 'http://localhost:5000/api'}/training-new/vendor/certificates/stats`, { + headers: { + 'Authorization': `Bearer ${token}`, + 'Content-Type': 'application/json' + } + }); + + if (response.ok) { + const data = await response.json(); + setStats(data.data); + } + } catch (error) { + console.error('Error fetching certificate stats:', error); + } + }; + + const fetchCourses = async () => { + try { + const token = localStorage.getItem('accessToken'); + const response = await fetch(`${process.env.REACT_APP_API_URL || 'http://localhost:5000/api'}/training-new/courses`, { + headers: { + 'Authorization': `Bearer ${token}`, + 'Content-Type': 'application/json' + } + }); + + if (response.ok) { + const data = await response.json(); + setCourses(data.data.courses || []); + } + } catch (error) { + console.error('Error fetching courses:', error); + } + }; + + const handlePageChange = (page: number) => { + setCurrentPage(page); + }; + + const handleSearch = (e: React.FormEvent) => { + e.preventDefault(); + setCurrentPage(1); + }; + + const handleCourseFilter = (courseId: string) => { + setSelectedCourse(courseId); + setCurrentPage(1); + }; + + const formatDate = (dateString: string) => { + return new Date(dateString).toLocaleDateString('en-US', { + year: 'numeric', + month: 'short', + day: 'numeric' + }); + }; + + const getGradeColor = (grade: string) => { + switch (grade) { + case 'Pass': return 'bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-200'; + case 'Merit': return 'bg-blue-100 text-blue-800 dark:bg-blue-900 dark:text-blue-200'; + case 'Distinction': return 'bg-purple-100 text-purple-800 dark:bg-purple-900 dark:text-purple-200'; + default: return 'bg-gray-100 text-gray-800 dark:bg-gray-900 dark:text-gray-200'; + } + }; + + const getLevelColor = (level: string) => { + switch (level) { + case 'Beginner': return 'bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-200'; + case 'Intermediate': return 'bg-yellow-100 text-yellow-800 dark:bg-yellow-900 dark:text-yellow-200'; + case 'Advanced': return 'bg-red-100 text-red-800 dark:bg-red-900 dark:text-red-200'; + default: return 'bg-gray-100 text-gray-800 dark:bg-gray-900 dark:text-gray-200'; + } + }; + + if (loading && certificates.length === 0) { + return ( +
+
+
+ ); + } + + return ( +
+ {/* Header */} +
+

+ Certificates Issued +

+

+ Track all certificates issued to your resellers +

+
+ + {/* Stats Cards */} + {stats && ( +
+
+
+
+

+ Total Certificates +

+

+ {stats.totalCertificates} +

+
+
+ +
+
+
+ +
+
+
+

+ This Month +

+

+ {stats.thisMonthCertificates} +

+
+
+ +
+
+
+ +
+
+
+

+ This Year +

+

+ {stats.thisYearCertificates} +

+
+
+ +
+
+
+ +
+
+
+

+ Average Score +

+

+ {stats.averageScore}% +

+
+
+ +
+
+
+
+ )} + + {/* Filters and Search */} +
+
+
+
+
+ + setSearch(e.target.value)} + className="w-full pl-10 pr-4 py-2 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-700 text-gray-900 dark:text-white focus:outline-none focus:ring-2 focus:ring-primary-500" + /> +
+
+ +
+ + +
+
+
+
+ + {/* Certificates Table */} +
+
+ + + + + + + + + + + + + {certificates.map((certificate) => ( + + + + + + + + + ))} + +
+ Reseller + + Course + + Certificate + + Grade & Score + + Issued Date + + Actions +
+
+
+ {certificate.user.avatar ? ( + {`${certificate.user.firstName} + ) : ( + + {certificate.user.firstName.charAt(0)}{certificate.user.lastName.charAt(0)} + + )} +
+
+
+ {certificate.user.firstName} {certificate.user.lastName} +
+
+ {certificate.user.email} +
+
+
+
+
+
+ {certificate.course.title} +
+
+ + {certificate.course.level} + + + {certificate.course.category} + +
+
+
+
+ {certificate.certificateNumber} +
+
+
+ + {certificate.grade} + + + {certificate.score}% + +
+
+ {formatDate(certificate.issuedAt)} + +
+ + +
+
+
+ + {certificates.length === 0 && !loading && ( +
+
+ +
+

+ No certificates issued yet +

+

+ Certificates will appear here once resellers complete your courses +

+
+ )} +
+ + {/* Pagination */} + {totalPages > 1 && ( +
+
+ Page {currentPage} of {totalPages} +
+
+ + +
+
+ )} +
+ ); +}; + +export default VendorCertificates; \ No newline at end of file diff --git a/src/components/VendorSalesDashboard.tsx b/src/components/VendorSalesDashboard.tsx new file mode 100644 index 0000000..3673517 --- /dev/null +++ b/src/components/VendorSalesDashboard.tsx @@ -0,0 +1,2388 @@ +import React, { useState, useEffect } from 'react'; +import { + TrendingUp, + Users, + Target, + FileText, + CheckCircle, + XCircle, + Clock, + DollarSign, + Eye, + Plus, + Edit, + Trash2, + AlertTriangle, + ShoppingCart, + X, + Info, + Upload, + Zap +} from 'lucide-react'; +import SalesWorkflowVisual from './SalesWorkflowVisual'; +import ResellerTargetsDisplay from './ResellerTargetsDisplay'; +import toast from 'react-hot-toast'; + +interface ProductSale { + id: number; + productId: number; + resellerId: number; + vendorId: number; + customerId: number; + orderId: number | null; + quantity: number; + unitPrice: number; + totalAmount: number; + currency: string; + commissionRate: number; + commissionAmount: number; + status: string; + verificationStatus: string; + verifiedBy: number | null; + verifiedAt: string | null; + verificationNotes: string | null; + paymentStatus: string; + paymentMethod: string | null; + paymentTransactionId: string | null; + customerName: string; + customerEmail: string; + customerPhone: string; + shippingAddress: string; + billingAddress: string; + notes: string; + expectedDeliveryDate: string | null; + actualDeliveryDate: string | null; + metadata: any; + createdAt: string; + updatedAt: string; + product: { + id: number; + name: string; + sku: string; + category: string; + price: number; + }; + reseller: { + id: number; + firstName: string; + lastName: string; + email: string; + company: string; + }; + customer: { + id: number; + firstName: string; + lastName: string; + email: string; + company: string; + }; +} + +interface ResellerTarget { + id: number; + targetType: string; + targetPeriod: string; + startDate: string; + endDate: string; + salesTarget: number; + quantityTarget?: number; + baseCommissionRate: number; + bonusCommissionRate?: number; + currentSales: number; + currentQuantity: number; + achievementPercentage: number; + status: string; + isTargetMet: boolean; + reseller: { + id: number; + firstName: string; + lastName: string; + email: string; + company: string; + }; +} + +interface Receipt { + id: number; + amount: number; + currency: string; + status: string; + description: string; + createdAt: string; + reseller: { + id: number; + firstName: string; + lastName: string; + email: string; + company: string; + }; + customer: { + id: number; + firstName: string; + lastName: string; + email: string; + company: string; + }; +} + +interface FileReceipt { + id: number; + productSaleId: number; + resellerId: number; + customerId: number; + fileName: string; + filePath: string; + fileSize: number; + fileType: string; + uploadDate: string; + status: string; +} + +const VendorSalesDashboard: React.FC = () => { + const [activeTab, setActiveTab] = useState('overview'); + const [sales, setSales] = useState([]); + const [targets, setTargets] = useState([]); + const [receipts, setReceipts] = useState([]); + const [saleReceipts, setSaleReceipts] = useState([]); + const [overview, setOverview] = useState(null); + const [loading, setLoading] = useState(true); + const [selectedSale, setSelectedSale] = useState(null); + const [isVerificationModalOpen, setIsVerificationModalOpen] = useState(false); + const [verificationAction, setVerificationAction] = useState<'approve' | 'reject'>('approve'); + const [verificationNotes, setVerificationNotes] = useState(''); + const [isTargetModalOpen, setIsTargetModalOpen] = useState(false); + const [targetForm, setTargetForm] = useState({ + resellerId: '', + targetType: 'monthly', + targetPeriod: '', + startDate: '', + endDate: '', + salesTarget: '', + quantityTarget: '', + baseCommissionRate: '10', + bonusCommissionRate: '', + description: '', + notes: '' + }); + + const [addSaleForm, setAddSaleForm] = useState({ + productId: '', + resellerId: '', + customerId: '', + quantity: '', + totalAmount: '', + status: 'verified', + verificationStatus: 'verified', + notes: '' + }); + + const [receiptUploadForm, setReceiptUploadForm] = useState({ + saleId: '', + amount: '', + currency: 'USD', + description: '', + receiptFile: null as File | null + }); + const [isAddSaleModalOpen, setIsAddSaleModalOpen] = useState(false); + const [isReceiptUploadModalOpen, setIsReceiptUploadModalOpen] = useState(false); + const [salesFilter, setSalesFilter] = useState('all'); + const [receiptFilter, setReceiptFilter] = useState('all'); + + // Immediate debugging after state declarations + console.log('VendorSalesDashboard rendering with targets:', { + targets, + type: typeof targets, + isArray: Array.isArray(targets), + hasTargets: !!targets + }); + + const baseUrl = process.env.REACT_APP_API_URL || 'http://localhost:5000/api'; + + // Debug function to log targets state + const debugTargets = (data: any, source: string) => { + console.log(`[${source}] Targets debug:`, { + data, + type: typeof data, + isArray: Array.isArray(data), + hasData: data && typeof data === 'object', + dataType: data?.data ? typeof data.data : 'no data property', + dataIsArray: data?.data ? Array.isArray(data.data) : 'no data property' + }); + }; + + // Detect route and set default tab + useEffect(() => { + const path = window.location.pathname; + console.log('Route detection:', { + path, + isSalesManagement: path === '/sales-management', + willSetTab: path === '/sales-management' ? 'sales' : 'overview' + }); + + if (path === '/sales-management') { + console.log('Setting active tab to sales'); + setActiveTab('sales'); + } else { + console.log('Setting active tab to overview'); + setActiveTab('overview'); + } + }, []); + + // Fetch receipts when receipts tab is activated + useEffect(() => { + if (activeTab === 'receipts') { + fetchAllSaleReceipts(); + } + }, [activeTab]); + + const getHeaders = () => { + const token = localStorage.getItem('accessToken'); + return { + 'Authorization': `Bearer ${token}`, + 'Content-Type': 'application/json' + }; + }; + + const getFileUploadHeaders = () => { + const token = localStorage.getItem('accessToken'); + return { + 'Authorization': `Bearer ${token}` + // No Content-Type for file uploads + }; + }; + + // Fetch receipts for a specific sale + const fetchSaleReceipts = async (saleId: number) => { + try { + const response = await fetch(`${baseUrl}/receipts/sale/${saleId}`, { + headers: getHeaders() + }); + + if (response.ok) { + const data = await response.json(); + if (data.success && data.data) { + setSaleReceipts(data.data); + } else { + setSaleReceipts([]); + } + } else { + console.error('Failed to fetch sale receipts:', response.status); + setSaleReceipts([]); + } + } catch (error) { + console.error('Error fetching sale receipts:', error); + setSaleReceipts([]); + } + }; + + // Fetch receipts for all sales + const fetchAllSaleReceipts = async () => { + try { + const response = await fetch(`${baseUrl}/receipts/all`, { + headers: getHeaders() + }); + + if (response.ok) { + const data = await response.json(); + if (data.success && data.data) { + setSaleReceipts(data.data); + } else { + setSaleReceipts([]); + } + } else { + console.error('Failed to fetch all sale receipts:', response.status); + setSaleReceipts([]); + } + } catch (error) { + console.error('Error fetching all sale receipts:', error); + setSaleReceipts([]); + } + }; + + // Handle receipt verification (approve/reject) + const handleReceiptVerification = async (receiptId: number, status: 'verified' | 'rejected') => { + try { + const response = await fetch(`${baseUrl}/receipts/${receiptId}/verify`, { + method: 'PUT', + headers: getHeaders(), + body: JSON.stringify({ status }) + }); + + if (response.ok) { + const data = await response.json(); + if (data.success) { + // Update local state + setSaleReceipts(prev => prev.map(receipt => + receipt.id === receiptId + ? { ...receipt, status } + : receipt + )); + + // Show success message + console.log(`Receipt ${status} successfully`); + + // Refresh receipts data + fetchAllSaleReceipts(); + } + } else { + console.error('Failed to verify receipt:', response.status); + } + } catch (error) { + console.error('Error verifying receipt:', error); + } + }; + + const fetchData = async () => { + try { + setLoading(true); + + // Fetch sales data + const salesResponse = await fetch(`${baseUrl}/vendors/reseller-sales`, { + headers: getHeaders() + }); + if (salesResponse.ok) { + const salesData = await salesResponse.json(); + console.log('Sales data received:', salesData); + console.log('Sales data type:', typeof salesData); + console.log('Sales data.data type:', typeof salesData.data); + console.log('Sales data.data is array:', Array.isArray(salesData.data)); + + // Enhanced debugging for sales + console.log('[Sales API Response] Sales debug:', { + data: salesData, + type: typeof salesData, + isArray: Array.isArray(salesData), + hasData: salesData && typeof salesData === 'object', + dataType: salesData?.data ? typeof salesData.data : 'no data property', + dataIsArray: salesData?.data ? Array.isArray(salesData.data) : 'no data property', + sales: salesData?.sales, + salesType: typeof salesData?.sales, + salesIsArray: Array.isArray(salesData?.sales) + }); + + // Handle different API response structures + let safeSales = []; + if (salesData && salesData.data && salesData.data.sales && Array.isArray(salesData.data.sales)) { + // API returns { success: true, data: { sales: [...], pagination: {...} } } + safeSales = salesData.data.sales; + console.log('Using salesData.data.sales:', safeSales); + } else if (salesData && salesData.sales && Array.isArray(salesData.sales)) { + // API returns { sales: [...], pagination: {...} } + safeSales = salesData.sales; + console.log('Using salesData.sales:', safeSales); + } else if (salesData && salesData.data && Array.isArray(salesData.data)) { + // API returns { data: [...] } + safeSales = salesData.data; + console.log('Using salesData.data:', safeSales); + } else if (Array.isArray(salesData)) { + // API returns array directly + safeSales = salesData; + console.log('Using salesData directly:', safeSales); + } else { + console.warn('Sales data is not in expected format, setting empty array'); + console.warn('salesData structure:', salesData); + safeSales = []; + } + + console.log('Setting sales to:', safeSales); + console.log('Sales state will be set to:', safeSales); + console.log('Sales state type will be:', typeof safeSales); + console.log('Sales state is array will be:', Array.isArray(safeSales)); + setSales(safeSales); + } else { + console.error('Sales API failed:', salesResponse.status, salesResponse.statusText); + setSales([]); + } + + // Fetch targets data + const targetsResponse = await fetch(`${baseUrl}/vendors/reseller-targets`, { + headers: getHeaders() + }); + if (targetsResponse.ok) { + const targetsData = await targetsResponse.json(); + console.log('Targets data received:', targetsData); + console.log('Targets data type:', typeof targetsData); + console.log('Targets data.data type:', typeof targetsData.data); + console.log('Targets data.data is array:', Array.isArray(targetsData.data)); + + // Enhanced debugging + debugTargets(targetsData, 'API Response'); + + // Ensure we always set an array + let safeTargets = []; + if (targetsData && targetsData.data && Array.isArray(targetsData.data)) { + safeTargets = targetsData.data; + } else if (Array.isArray(targetsData)) { + safeTargets = targetsData; + } else { + console.warn('Targets data is not in expected format, setting empty array'); + safeTargets = []; + } + + console.log('Setting targets to:', safeTargets); + setTargets(safeTargets); + } else { + console.error('Targets API failed:', targetsResponse.status, targetsResponse.statusText); + setTargets([]); + } + + // Fetch receipts data + const receiptsResponse = await fetch(`${baseUrl}/vendors/receipts`, { + headers: getHeaders() + }); + if (receiptsResponse.ok) { + const receiptsData = await receiptsResponse.json(); + console.log('Receipts data received:', receiptsData); + console.log('Receipts data type:', typeof receiptsData); + console.log('Receipts data.data type:', typeof receiptsData.data); + console.log('Receipts data.data is array:', Array.isArray(receiptsData.data)); + + // Enhanced debugging for receipts + console.log('[Receipts API Response] Receipts debug:', { + data: receiptsData, + type: typeof receiptsData, + isArray: Array.isArray(receiptsData), + hasData: receiptsData && typeof receiptsData === 'object', + dataType: receiptsData?.data ? typeof receiptsData.data : 'no data property', + dataIsArray: receiptsData?.data ? Array.isArray(receiptsData.data) : 'no data property' + }); + + // Ensure we always set an array + let safeReceipts = []; + if (receiptsData && receiptsData.data && Array.isArray(receiptsData.data)) { + safeReceipts = receiptsData.data; + } else if (Array.isArray(receiptsData)) { + safeReceipts = receiptsData; + } else { + console.warn('Receipts data is not in expected format, setting empty array'); + safeReceipts = []; + } + + console.log('Setting receipts to:', safeReceipts); + setReceipts(safeReceipts); + } else { + console.error('Receipts API failed:', receiptsResponse.status, receiptsResponse.statusText); + setReceipts([]); + } + + // Fetch overview data + const overviewResponse = await fetch(`${baseUrl}/vendors/reseller-sales/overview`, { + headers: getHeaders() + }); + if (overviewResponse.ok) { + const overviewData = await overviewResponse.json(); + console.log('Overview data received:', overviewData); + setOverview(overviewData.data || {}); + } + } catch (error) { + console.error('Error fetching data:', error); + } finally { + setLoading(false); + } + }; + + useEffect(() => { + fetchData(); + }, []); + + // Monitor targets state changes + useEffect(() => { + console.log('Targets state changed:', { + targets, + type: typeof targets, + isArray: Array.isArray(targets), + length: Array.isArray(targets) ? targets.length : 'not array' + }); + debugTargets(targets, 'State Change'); + }, [targets]); + + // Monitor sales state changes + useEffect(() => { + console.log('Sales state changed:', { + sales, + type: typeof sales, + isArray: Array.isArray(sales), + length: Array.isArray(sales) ? sales.length : 'not array' + }); + }, [sales]); + + // Monitor activeTab changes + useEffect(() => { + console.log('Active tab changed to:', activeTab); + }, [activeTab]); + + const handleVerifySale = async () => { + if (!selectedSale) return; + + try { + const response = await fetch(`${baseUrl}/vendors/reseller-sales/${selectedSale.id}/verify`, { + method: 'PUT', + headers: getHeaders(), + body: JSON.stringify({ + action: verificationAction, + notes: verificationNotes + }) + }); + + if (!response.ok) { + const errorData = await response.json(); + throw new Error(errorData.message || 'Failed to verify sale'); + } + + // Show success message + toast.success(`Sale ${verificationAction === 'approve' ? 'approved' : 'rejected'} successfully`); + + // Refresh data + fetchData(); + setIsVerificationModalOpen(false); + setSelectedSale(null); + setVerificationNotes(''); + } catch (error: any) { + console.error('Error verifying sale:', error); + toast.error(error.message || 'Failed to verify sale'); + } + }; + + const handleCreateTarget = async () => { + try { + const response = await fetch(`${baseUrl}/vendors/reseller-targets`, { + method: 'POST', + headers: getHeaders(), + body: JSON.stringify(targetForm) + }); + + if (!response.ok) throw new Error('Failed to create target'); + + // Refresh targets + fetchData(); + setIsTargetModalOpen(false); + setTargetForm({ + resellerId: '', + targetType: 'monthly', + targetPeriod: '', + startDate: '', + endDate: '', + salesTarget: '', + quantityTarget: '', + baseCommissionRate: '10', + bonusCommissionRate: '', + description: '', + notes: '' + }); + } catch (error) { + console.error('Error creating target:', error); + } + }; + + const handleAddSale = async () => { + try { + const response = await fetch(`${baseUrl}/vendors/reseller-sales`, { + method: 'POST', + headers: getHeaders(), + body: JSON.stringify(addSaleForm) + }); + + if (!response.ok) throw new Error('Failed to add sale'); + + // Refresh sales + fetchData(); + setIsAddSaleModalOpen(false); + setAddSaleForm({ + productId: '', + resellerId: '', + customerId: '', + quantity: '', + totalAmount: '', + status: 'verified', + verificationStatus: 'verified', + notes: '' + }); + } catch (error) { + console.error('Error adding sale:', error); + } + }; + + const handleReceiptUpload = async () => { + if (!receiptUploadForm.saleId || !receiptUploadForm.amount || !receiptUploadForm.receiptFile) { + alert('Please select a sale, enter amount, and upload a receipt file.'); + return; + } + + const formData = new FormData(); + formData.append('saleId', receiptUploadForm.saleId); + formData.append('amount', receiptUploadForm.amount); + formData.append('currency', receiptUploadForm.currency); + formData.append('description', receiptUploadForm.description); + formData.append('receiptFile', receiptUploadForm.receiptFile); + + try { + const response = await fetch(`${baseUrl}/vendors/receipts`, { + method: 'POST', + headers: getFileUploadHeaders(), + body: formData + }); + + if (!response.ok) throw new Error('Failed to upload receipt'); + + // Refresh receipts + fetchData(); + setIsReceiptUploadModalOpen(false); + setReceiptUploadForm({ + saleId: '', + amount: '', + currency: 'USD', + description: '', + receiptFile: null + }); + } catch (error) { + console.error('Error uploading receipt:', error); + } + }; + + const getStatusColor = (status: string) => { + switch (status) { + case 'pending_verification': return 'bg-yellow-100 text-yellow-800 dark:bg-yellow-900 dark:text-yellow-300'; + case 'verified': return 'bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-300'; + case 'cancelled': return 'bg-red-100 text-red-800 dark:bg-red-900 dark:text-red-300'; + default: return 'bg-gray-100 text-gray-800 dark:bg-gray-900 dark:text-gray-300'; + } + }; + + const getVerificationStatusColor = (status: string) => { + switch (status) { + case 'pending': return 'bg-yellow-100 text-yellow-800 dark:bg-yellow-900 dark:text-yellow-300'; + case 'verified': return 'bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-300'; + case 'rejected': return 'bg-red-100 text-red-800 dark:bg-red-900 dark:text-red-300'; + default: return 'bg-gray-100 text-gray-800 dark:bg-gray-900 dark:text-gray-300'; + } + }; + + if (loading) { + console.log('VendorSalesDashboard: Loading state, targets:', targets); + return ( +
+
+
+ ); + } + + // Additional safety check before render + console.log('VendorSalesDashboard: About to render, targets:', { + targets, + type: typeof targets, + isArray: Array.isArray(targets), + safeTargets: Array.isArray(targets) ? targets : [] + }); + + // Ensure targets is always an array before any rendering + const safeTargets = Array.isArray(targets) ? targets : []; + console.log('VendorSalesDashboard: Safe targets created:', safeTargets); + + // Ensure sales is always an array before any rendering + const safeSales = Array.isArray(sales) ? sales : []; + console.log('VendorSalesDashboard: Safe sales created:', safeSales); + console.log('VendorSalesDashboard: Raw sales state:', sales); + console.log('VendorSalesDashboard: Sales state type:', typeof sales); + console.log('VendorSalesDashboard: Sales is array:', Array.isArray(sales)); + console.log('VendorSalesDashboard: Sales length:', Array.isArray(sales) ? sales.length : 'not array'); + + // Filter sales based on selected filter + const filteredSales = safeSales.filter(sale => { + if (salesFilter === 'all') return true; + if (salesFilter === 'pending_verification') return sale.status === 'pending_verification'; + if (salesFilter === 'verified') return sale.status === 'verified'; + if (salesFilter === 'rejected') return sale.status === 'rejected'; + return true; + }); + + console.log('VendorSalesDashboard: Filtered sales:', filteredSales); + console.log('VendorSalesDashboard: Filter applied:', salesFilter); + + // Ensure receipts is always an array before any rendering + const safeReceipts = Array.isArray(receipts) ? receipts : []; + console.log('VendorSalesDashboard: Safe receipts created:', safeReceipts); + + return ( +
+ {/* Header */} +
+

+ Sales Management +

+
+ +
+
+ + {/* Tabs */} +
+
+ Debug: Active Tab = {activeTab} | Total Tabs = 4 +
+ +
+ + {/* Tab Content */} +
+ {activeTab === 'overview' && ( +
+ {overview ? ( + <> + {/* Stats Grid */} +
+
+
+
+ +
+
+

Pending Verifications

+

{overview.pendingVerifications || 0}

+
+
+
+ +
+
+
+ +
+
+

Total Resellers

+

{overview.totalResellers || 0}

+
+
+
+ +
+
+
+ +
+
+

Active Targets

+

{overview.activeTargets || 0}

+
+
+
+ +
+
+
+ +
+
+

Period

+

{overview.period || 'Current'}

+
+
+
+
+ + {/* Pending Verifications Alert */} + {(overview.pendingVerifications || 0) > 0 && ( +
+
+
+ +
+
+

+ Action Required: {overview.pendingVerifications || 0} Sale{(overview.pendingVerifications || 0) !== 1 ? 's' : ''} Pending Verification +

+

+ These sales are waiting for your review and approval. Stock will only be reduced after you verify the sales. +

+
+
+ +
+ )} + + {/* Sales Workflow Visual */} + + + {/* Approval Workflow Note */} +
+
+
+ +
+
+

+ Sales Approval Workflow +

+

+ When resellers create sales, they are marked as "Pending Verification". + You must review and approve these sales before they count towards analytics and commissions. + Only approved sales will reduce inventory and generate receipts. +

+
+
+
+ + ) : ( +
+
+

Loading overview data...

+
+ )} +
+ )} + + {activeTab === 'sales' && ( +
+ {/* Header with Add Sale Button */} +
+

Reseller Sales

+
+ + + +
+
+ + {/* Sales Summary Cards */} +
+
+
+
+ +
+
+

Pending Verification

+

+ {filteredSales.filter(sale => sale.status === 'pending_verification').length} +

+
+
+
+ +
+
+
+ +
+
+

Verified

+

+ {filteredSales.filter(sale => sale.status === 'verified').length} +

+
+
+
+ +
+
+
+ +
+
+

Rejected

+

+ {filteredSales.filter(sale => sale.status === 'rejected').length} +

+
+
+
+ +
+
+
+ +
+
+

Total Sales

+

+ {filteredSales.length} +

+
+
+
+
+ + {/* Sales Table */} +
+
+
+

Sales Records

+
+ Showing {filteredSales.length} of {safeSales.length} sales + {salesFilter !== 'all' && ` (${salesFilter.replace('_', ' ')} only)`} +
+
+
+
+ + + + + + + + + + + + + + {filteredSales.length > 0 ? ( + filteredSales.map((sale) => ( + + + + + + + + + + )) + ) : ( + + + + )} + +
+ Product + + Reseller + + Customer + + Amount + + Status + + Date + + Actions +
+
+
{sale.product?.name || 'N/A'}
+
SKU: {sale.product?.sku || 'N/A'}
+
+
+
+
+ {sale.reseller?.firstName || 'N/A'} {sale.reseller?.lastName || 'N/A'} +
+
{sale.reseller?.company || 'N/A'}
+
+
+
+
+ {sale.customer?.firstName || 'N/A'} {sale.customer?.lastName || 'N/A'} +
+
{sale.customer?.email || 'N/A'}
+
+
+
+ ${(Number(sale.totalAmount) || 0).toFixed(2)} +
+
+ Qty: {sale.quantity || 0} +
+
+
+
+ + {sale.status?.replace('_', ' ') || 'N/A'} + + {sale.status === 'pending_verification' && ( + + + Pending Review + + )} +
+
+ Verification: {sale.verificationStatus || 'N/A'} +
+
+
+ {new Date(sale.createdAt).toLocaleDateString()} + +
+ {sale.status === 'pending_verification' && ( + <> + + + + )} + +
+
+
+ +

No sales records found

+

+ {salesFilter === 'pending_verification' + ? 'No sales are currently pending verification' + : salesFilter === 'verified' + ? 'No verified sales found' + : salesFilter === 'rejected' + ? 'No rejected sales found' + : 'Add a sale or wait for reseller submissions' + } +

+
+
+
+
+
+ )} + + {activeTab === 'targets' && ( +
+ {/* Header with Create Target Button */} +
+

Reseller Targets

+ +
+ + {/* Targets Display */} +
+ {(() => { + try { + return ( + + ); + } catch (error) { + console.error('Error rendering ResellerTargetsDisplay:', error); + return ( +
+
+

+ Error Loading Targets +

+

+ There was an error loading the targets. Please refresh the page or contact support. +

+ +
+
+ ); + } + })()} +
+ + {/* Call to Action when no targets */} + {(!safeTargets || safeTargets.length === 0) && !loading && ( +
+
+

+ Ready to Set Your First Target? +

+

+ Start by creating a performance target for one of your resellers. This will help you track their progress and calculate commissions. +

+ +
+
+ )} +
+ )} + + {activeTab === 'receipts' && ( +
+

Receipts & Sales Management

+

+ Complete overview of all reseller sales with receipt status and verification workflow +

+ + {/* Quick Actions */} +
+

+ + Quick Actions +

+
+ + + + +
+
+ + {/* Summary Statistics */} +
+
+
+
+ +
+
+

Total Sales

+

{sales.length}

+
+
+
+ +
+
+
+ +
+
+

Pending Verification

+

+ {sales.filter(sale => sale.verificationStatus === 'pending').length} +

+
+
+
+ +
+
+
+ +
+
+

Verified Sales

+

+ {sales.filter(sale => sale.verificationStatus === 'verified').length} +

+
+
+
+ +
+
+
+ +
+
+

Sales Without Receipts

+

+ {sales.filter(sale => { + const hasReceipts = saleReceipts.some(receipt => receipt.productSaleId === sale.id); + return !hasReceipts && sale.verificationStatus === 'pending'; + }).length} +

+
+
+
+
+ + {/* All Sales Overview Section */} +
+
+
+
+

All Reseller Sales Overview

+

+ Complete list of all sales with receipt status and verification workflow +

+
+
+ +
+
+
+ + {/* Filtered Sales Table */} + {(() => { + // Apply filters + let filteredSales = sales; + + if (salesFilter === 'pending_verification') { + filteredSales = sales.filter(sale => sale.verificationStatus === 'pending'); + } else if (salesFilter === 'verified') { + filteredSales = sales.filter(sale => sale.verificationStatus === 'verified'); + } else if (salesFilter === 'rejected') { + filteredSales = sales.filter(sale => sale.verificationStatus === 'rejected'); + } else if (salesFilter === 'no_receipts') { + filteredSales = sales.filter(sale => { + const hasReceipts = saleReceipts.some(receipt => receipt.productSaleId === sale.id); + return !hasReceipts && sale.verificationStatus === 'pending'; + }); + } + + return ( +
+ + + + + + + + + + + + + + + + {filteredSales.map((sale) => { + // Check if sale has receipts + const hasReceipts = saleReceipts.some(receipt => receipt.productSaleId === sale.id); + + return ( + + + + + + + + + + + + ); + })} + +
+ Sale # + + Product + + Reseller + + Customer + + Amount + + Receipt Status + + Verification + + Date + + Actions +
+ #{sale.id} + +
+
+ {sale.product?.name || 'N/A'} +
+
+ Qty: {sale.quantity} +
+
+
+
+
+ {sale.reseller?.firstName} {sale.reseller?.lastName} +
+
+ {sale.reseller?.company || 'N/A'} +
+
+
+
+
+ {sale.customerName} +
+
+ {sale.customerEmail} +
+
+
+ ${sale.totalAmount} + + + {hasReceipts ? ( + <> + + Receipts Uploaded + + ) : ( + <> + + No Receipts + + )} + + + + {sale.verificationStatus === 'pending' ? 'Pending' : + sale.verificationStatus === 'verified' ? 'Verified' : + sale.verificationStatus === 'rejected' ? 'Rejected' : 'Unknown'} + + + {new Date(sale.createdAt).toLocaleDateString()} + +
+ {sale.verificationStatus === 'pending' && ( + <> + + + + )} + + {!hasReceipts && sale.verificationStatus === 'pending' && ( + + )} +
+
+
+ ); + })()} + + {(() => { + // Apply filters for empty state check + let filteredSales = sales; + + if (salesFilter === 'pending_verification') { + filteredSales = sales.filter(sale => sale.verificationStatus === 'pending'); + } else if (salesFilter === 'verified') { + filteredSales = sales.filter(sale => sale.verificationStatus === 'verified'); + } else if (salesFilter === 'rejected') { + filteredSales = sales.filter(sale => sale.verificationStatus === 'rejected'); + } else if (salesFilter === 'no_receipts') { + filteredSales = sales.filter(sale => { + const hasReceipts = saleReceipts.some(receipt => receipt.productSaleId === sale.id); + return !hasReceipts && sale.verificationStatus === 'pending'; + }); + } + + if (filteredSales.length === 0) { + return ( +
+ +

+ {sales.length === 0 ? 'No sales records found' : `No sales match the "${salesFilter}" filter`} +

+
+ ); + } + return null; + })()} +
+ + {/* Comprehensive Receipt Management Section */} +
+
+
+
+

Receipt Management

+

+ All uploaded receipts with sale details and management actions +

+
+
+ +
+
+
+ +
+ + + + + + + + + + + + + + + + {(() => { + // Apply receipt filters + let filteredReceipts = saleReceipts; + + if (receiptFilter === 'pending_verification') { + filteredReceipts = saleReceipts.filter(receipt => receipt.status === 'pending_verification'); + } else if (receiptFilter === 'verified') { + filteredReceipts = saleReceipts.filter(receipt => receipt.status === 'verified'); + } else if (receiptFilter === 'rejected') { + filteredReceipts = saleReceipts.filter(receipt => receipt.status === 'rejected'); + } + + return filteredReceipts.map((receipt) => { + // Find associated sale + const associatedSale = sales.find(sale => sale.id === receipt.productSaleId); + + return ( + + + + + + + + + + + + ); + }); + })()} + +
+ Receipt # + + Sale # + + Product + + Reseller + + Customer + + File Details + + Upload Date + + Status + + Actions +
+ #{receipt.id} + + {associatedSale ? `#${associatedSale.id}` : 'N/A'} + +
+
+ {associatedSale?.product?.name || 'N/A'} +
+
+ Qty: {associatedSale?.quantity || 'N/A'} +
+
+
+
+
+ {associatedSale?.reseller?.firstName} {associatedSale?.reseller?.lastName} +
+
+ {associatedSale?.reseller?.company || 'N/A'} +
+
+
+
+
+ {associatedSale?.customerName || 'N/A'} +
+
+ {associatedSale?.customerEmail || 'N/A'} +
+
+
+
+
+ {receipt.fileName} +
+
+ {(receipt.fileSize / 1024 / 1024).toFixed(2)} MB • {receipt.fileType} +
+
+
+ {new Date(receipt.uploadDate).toLocaleDateString()} + + + {receipt.status === 'pending_verification' ? 'Pending' : + receipt.status === 'verified' ? 'Verified' : + receipt.status === 'rejected' ? 'Rejected' : 'Unknown'} + + +
+ + + {receipt.status === 'pending_verification' && ( + <> + + + + )} +
+
+
+ + {saleReceipts.length === 0 && ( +
+ +

No receipts found

+
+ )} +
+
+ )} +
+ + {/* Sale Verification Modal */} + {isVerificationModalOpen && selectedSale && ( +
+
+

+ {verificationAction === 'approve' ? 'Approve' : 'Reject'} Sale #{selectedSale.id} +

+ + {/* Sale Details */} +
+

Sale Details

+
+
+ Product: + {selectedSale.product?.name} +
+
+ SKU: + {selectedSale.product?.sku} +
+
+ Quantity: + {selectedSale.quantity} +
+
+ Total Amount: + ${selectedSale.totalAmount} +
+
+ Commission Rate: + {selectedSale.commissionRate}% +
+
+ Commission Amount: + ${selectedSale.commissionAmount} +
+
+
+ + {/* Customer Information */} +
+

Customer Information

+
+
+ Name: + {selectedSale.customer?.firstName} {selectedSale.customer?.lastName} +
+
+ Email: + {selectedSale.customer?.email} +
+
+ Phone: + {selectedSale.customerPhone} +
+
+ Company: + {selectedSale.customer?.company} +
+
+
+ + {/* Reseller Information */} +
+

Reseller Information

+
+
+ Name: + {selectedSale.reseller?.firstName} {selectedSale.reseller?.lastName} +
+
+ Email: + {selectedSale.reseller?.email} +
+
+ Company: + {selectedSale.reseller?.company} +
+
+ Created: + {new Date(selectedSale.createdAt).toLocaleString()} +
+
+
+ + {/* Receipts & Documents */} +
+

+ + Receipts & Documents + + {saleReceipts.length} file(s) + +

+ + {saleReceipts.length > 0 ? ( +
+ {saleReceipts.map((receipt) => ( +
+
+ +
+

{receipt.fileName}

+

{(receipt.fileSize / 1024 / 1024).toFixed(2)} MB • {receipt.fileType}

+

Uploaded: {new Date(receipt.uploadDate).toLocaleString()}

+
+
+
+ + +
+
+ ))} +
+ ) : ( +
+ +

+ No receipts or documents uploaded for this sale +

+

+ Reseller should upload payment receipts before sale verification +

+
+ )} +
+ +
+
+ + +
+ +
+ + + + -
-
- - -
-
- - -
-
+ {/* Video Preview Modal */} + {isVideoPreviewModalOpen && selectedVideo && ( +
+
+
+
+

+ {selectedVideo.title} +

-
- + + {/* Video Player */} +
+ {selectedVideo.videoType === 'youtube' && selectedVideo.youtubeUrl ? ( +
+