diff --git a/package.json b/package.json index cfaf502..0baab57 100644 --- a/package.json +++ b/package.json @@ -48,6 +48,7 @@ "embla-carousel-react": "^8.3.0", "html5-qrcode": "^2.3.8", "input-otp": "^1.2.4", + "jspdf": "^3.0.2", "lucide-react": "^0.462.0", "next-themes": "^0.3.0", "pocketbase": "^0.26.2", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 8b1cc45..c97f730 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -119,6 +119,9 @@ importers: input-otp: specifier: ^1.2.4 version: 1.4.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + jspdf: + specifier: ^3.0.2 + version: 3.0.2 lucide-react: specifier: ^0.462.0 version: 0.462.0(react@18.3.1) @@ -1374,9 +1377,15 @@ packages: '@types/node@22.14.1': resolution: {integrity: sha512-u0HuPQwe/dHrItgHHpmw3N2fYCR6x4ivMNbPHRkBVP4CvN+kiRrKHWk3i8tXiO/joPwXLMYvF9TTF0eqgHIuOw==} + '@types/pako@2.0.4': + resolution: {integrity: sha512-VWDCbrLeVXJM9fihYodcLiIv0ku+AlOa/TQ1SvYOaBuyrSKgEcro95LJyIsJ4vSo6BXIxOKxiJAat04CmST9Fw==} + '@types/prop-types@15.7.14': resolution: {integrity: sha512-gNMvNH49DJ7OJYv+KAKn0Xp45p8PLl6zo2YnvDIbTd4J6MER2BmWN49TG7n9LvkyihINxeKW8+3bfS2yDC9dzQ==} + '@types/raf@3.4.3': + resolution: {integrity: sha512-c4YAvMedbPZ5tEyxzQdMoOhhJ4RD3rngZIdwC2/qDN3d7JpEhB6fiBRKVY1lg5B7Wk+uPBjn5f39j1/2MY1oOw==} + '@types/react-dom@18.3.6': resolution: {integrity: sha512-nf22//wEbKXusP6E9pfOCDwFdHAX4u172eaJI4YkDRQEZiorm6KfYnSC2SWLDMVWUOWPERmJnN0ujeAfTBLvrw==} peerDependencies: @@ -1385,6 +1394,9 @@ packages: '@types/react@18.3.20': resolution: {integrity: sha512-IPaCZN7PShZK/3t6Q87pfTkRm6oLTd4vztyoj+cbHUF1g3FfVb2tFIL79uCRKEfv16AhqDMBywP2VW3KIZUvcg==} + '@types/trusted-types@2.0.7': + resolution: {integrity: sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==} + '@typescript-eslint/eslint-plugin@8.30.1': resolution: {integrity: sha512-v+VWphxMjn+1t48/jO4t950D6KR8JaJuNXzi33Ve6P8sEmPr5k6CEXjdGwT6+LodVnEa91EQCtwjWNUCPweo+Q==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} @@ -1493,6 +1505,10 @@ packages: balanced-match@1.0.2: resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} + base64-arraybuffer@1.0.2: + resolution: {integrity: sha512-I3yl4r9QB5ZRY3XuJVEPfc2XhZO6YweFPI+UovAzn+8/hb3oJ6lnysaFcjVpkCPfVWFUDvoZ8kmVDP7WyRtYtQ==} + engines: {node: '>= 0.6.0'} + binary-extensions@2.3.0: resolution: {integrity: sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==} engines: {node: '>=8'} @@ -1523,6 +1539,10 @@ packages: caniuse-lite@1.0.30001714: resolution: {integrity: sha512-mtgapdwDLSSBnCI3JokHM7oEQBLxiJKVRtg10AxM1AyeiKcM96f0Mkbqeq+1AbiCtvMcHRulAAEMu693JrSWqg==} + canvg@3.0.11: + resolution: {integrity: sha512-5ON+q7jCTgMp9cjpu4Jo6XbvfYwSB2Ow3kzHKfIyJfaCAOHLbdKPQqGKgfED/R5B+3TFFfe8pegYA+b423SRyA==} + engines: {node: '>=10.0.0'} + chalk@4.1.2: resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==} engines: {node: '>=10'} @@ -1558,10 +1578,16 @@ packages: concat-map@0.0.1: resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==} + core-js@3.45.1: + resolution: {integrity: sha512-L4NPsJlCfZsPeXukyzHFlg/i7IIVwHSItR0wg0FLNqYClJ4MQYTYLbC7EkjKYRLZF2iof2MUgN0EGy7MdQFChg==} + cross-spawn@7.0.6: resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} engines: {node: '>= 8'} + css-line-break@2.1.0: + resolution: {integrity: sha512-FHcKFCZcAha3LwfVBhCQbW2nCNbkZXn7KVUJcsT5/P8YmfsVja0FMPJr0B903j/E69HUphKiV9iQArX8SDYA4w==} + cssesc@3.0.0: resolution: {integrity: sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==} engines: {node: '>=4'} @@ -1644,6 +1670,9 @@ packages: dom-helpers@5.2.1: resolution: {integrity: sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA==} + dompurify@3.2.6: + resolution: {integrity: sha512-/2GogDQlohXPZe6D6NOgQvXLPSYBqIWMnZ8zzOhn09REE4eyAzb+Hed3jhoM9OkuaJ8P6ZGTTVWQKAi8ieIzfQ==} + eastasianwidth@0.2.0: resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==} @@ -1758,6 +1787,9 @@ packages: fast-levenshtein@2.0.6: resolution: {integrity: sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==} + fast-png@6.4.0: + resolution: {integrity: sha512-kAqZq1TlgBjZcLr5mcN6NP5Rv4V2f22z00c3g8vRrwkcqjerx7BEhPbOnWCPqaHUl2XWQBJQvOT/FQhdMT7X/Q==} + fastq@1.19.1: resolution: {integrity: sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ==} @@ -1769,6 +1801,9 @@ packages: picomatch: optional: true + fflate@0.8.2: + resolution: {integrity: sha512-cPJU47OaAoCbg0pBvzsgpTPhmhqI5eJjh/JIu8tPj5q+T7iLvW/JAYUqmE7KOB4R1ZyEhzBaIQpQpardBF5z8A==} + file-entry-cache@8.0.0: resolution: {integrity: sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==} engines: {node: '>=16.0.0'} @@ -1843,6 +1878,10 @@ packages: resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==} engines: {node: '>= 0.4'} + html2canvas@1.4.1: + resolution: {integrity: sha512-fPU6BHNpsyIhr8yyMpTLLxAbkaK8ArIBcmZIRiBLiDhjeqvXolaEmDGmELFuX9I4xDcaKKcJl+TKZLqruBbmWA==} + engines: {node: '>=8.0.0'} + html5-qrcode@2.3.8: resolution: {integrity: sha512-jsr4vafJhwoLVEDW3n1KvPnCCXWaQfRng0/EEYk1vNcQGcG/htAdhJX0be8YyqMoSz7+hZvOZSTAepsabiuhiQ==} @@ -1868,6 +1907,9 @@ packages: resolution: {integrity: sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==} engines: {node: '>=12'} + iobuffer@5.4.0: + resolution: {integrity: sha512-DRebOWuqDvxunfkNJAlc3IzWIPD5xVxwUNbHr7xKB8E6aLJxIPfNX3CoMJghcFjpv6RWQsrcJbghtEwSPoJqMA==} + is-binary-path@2.1.0: resolution: {integrity: sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==} engines: {node: '>=8'} @@ -1921,6 +1963,9 @@ packages: json-stable-stringify-without-jsonify@1.0.1: resolution: {integrity: sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==} + jspdf@3.0.2: + resolution: {integrity: sha512-G0fQDJ5fAm6UW78HG6lNXyq09l0PrA1rpNY5i+ly17Zb1fMMFSmS+3lw4cnrAPGyouv2Y0ylujbY2Ieq3DSlKA==} + keyv@4.5.4: resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==} @@ -2039,6 +2084,9 @@ packages: package-json-from-dist@1.0.1: resolution: {integrity: sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==} + pako@2.1.0: + resolution: {integrity: sha512-w+eufiZ1WuJYgPXbV/PO3NCMEc3xqylkKHzp8bxp1uW4qaSNQUkwmLLEc3kKsfz8lpV1F8Ht3U1Cm+9Srog2ug==} + parent-module@1.0.1: resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==} engines: {node: '>=6'} @@ -2058,6 +2106,9 @@ packages: resolution: {integrity: sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==} engines: {node: '>=16 || 14 >=14.18'} + performance-now@2.1.0: + resolution: {integrity: sha512-7EAHlyLHI56VEIdK57uwHdHKIaAGbnXPiw0yWbarQZOKaKpvUIgW0jWRVLiatnM+XXlSwsanIBH/hzGMJulMow==} + picocolors@1.1.1: resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} @@ -2144,6 +2195,9 @@ packages: queue-microtask@1.2.3: resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} + raf@3.4.1: + resolution: {integrity: sha512-Sq4CW4QhwOHE8ucn6J34MqtZCeWFP2aQSmrlroYgqAV1PjStIhJXxYuTgUIfkEk7zTLjmIjLmU5q+fbD1NnOJA==} + react-barcode@1.6.1: resolution: {integrity: sha512-pc4ftnO5syHa/UjCruEeRsomlhoxKSugIgTA8T4dH0fvc89UMHL+/1Sp25IAphqG44pJkE5hMXhv89iS09jQyw==} peerDependencies: @@ -2261,6 +2315,9 @@ packages: react: ^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 react-dom: ^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + regenerator-runtime@0.13.11: + resolution: {integrity: sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg==} + regenerator-runtime@0.14.1: resolution: {integrity: sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==} @@ -2277,6 +2334,10 @@ packages: resolution: {integrity: sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==} engines: {iojs: '>=1.0.0', node: '>=0.10.0'} + rgbcolor@1.0.1: + resolution: {integrity: sha512-9aZLIrhRaD97sgVhtJOW6ckOEh6/GnvQtdVNfdZ6s67+3/XwLS9lBcQYzEEhYVeUowN7pRzMLsyGhK2i/xvWbw==} + engines: {node: '>= 0.8.15'} + rollup@4.40.0: resolution: {integrity: sha512-Noe455xmA96nnqH5piFtLobsGbCij7Tu+tb3c1vYjNbTkfzGqXqQXG3wJaYXkRZuQ0vEYN4bhwg7QnIrqB5B+w==} engines: {node: '>=18.0.0', npm: '>=8.0.0'} @@ -2315,6 +2376,10 @@ packages: resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} engines: {node: '>=0.10.0'} + stackblur-canvas@2.7.0: + resolution: {integrity: sha512-yf7OENo23AGJhBriGx0QivY5JP6Y1HbrrDI6WLt6C5auYZXlQrheoY8hD4ibekFKz1HOfE48Ww8kMWMnJD/zcQ==} + engines: {node: '>=0.1.14'} + string-width@4.2.3: resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==} engines: {node: '>=8'} @@ -2348,6 +2413,10 @@ packages: resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==} engines: {node: '>= 0.4'} + svg-pathdata@6.0.3: + resolution: {integrity: sha512-qsjeeq5YjBZ5eMdFuUa4ZosMLxgr5RZ+F+Y1OrDhuOCEInRMA3x74XdBtggJcj9kOeInz0WE+LgCPDkZFlBYJw==} + engines: {node: '>=12.0.0'} + tailwind-merge@2.6.0: resolution: {integrity: sha512-P+Vu1qXfzediirmHOC3xKGAYeZtPcV9g76X+xg2FD4tYgR71ewMA35Y3sCz3zhiN/dwefRpJX0yBcgwi1fXNQA==} @@ -2361,6 +2430,9 @@ packages: engines: {node: '>=14.0.0'} hasBin: true + text-segmentation@1.0.3: + resolution: {integrity: sha512-iOiPUo/BGnZ6+54OsWxZidGCsdU8YbE4PSpdPinp7DeMtUJNJBoJ/ouUSTJjHkh1KntHaltHl/gDs2FC4i5+Nw==} + thenify-all@1.6.0: resolution: {integrity: sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==} engines: {node: '>=0.8'} @@ -2442,6 +2514,9 @@ packages: util-deprecate@1.0.2: resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} + utrie@1.0.2: + resolution: {integrity: sha512-1MLa5ouZiOmQzUbjbu9VmjLzn1QLXBhwpUa7kdLUQK+KQ5KA9I1vk5U4YHe/X2Ch7PYnJfWuWT+VbuxbGwljhw==} + vaul@0.9.9: resolution: {integrity: sha512-7afKg48srluhZwIkaU+lgGtFCUsYBSGOl8vcc8N/M3YQlZFlynHD15AE+pwrYdc826o7nrIND4lL9Y6b9WWZZQ==} peerDependencies: @@ -3572,8 +3647,13 @@ snapshots: dependencies: undici-types: 6.21.0 + '@types/pako@2.0.4': {} + '@types/prop-types@15.7.14': {} + '@types/raf@3.4.3': + optional: true + '@types/react-dom@18.3.6(@types/react@18.3.20)': dependencies: '@types/react': 18.3.20 @@ -3583,6 +3663,9 @@ snapshots: '@types/prop-types': 15.7.14 csstype: 3.1.3 + '@types/trusted-types@2.0.7': + optional: true + '@typescript-eslint/eslint-plugin@8.30.1(@typescript-eslint/parser@8.30.1(eslint@9.24.0(jiti@1.21.7))(typescript@5.8.3))(eslint@9.24.0(jiti@1.21.7))(typescript@5.8.3)': dependencies: '@eslint-community/regexpp': 4.12.1 @@ -3717,6 +3800,9 @@ snapshots: balanced-match@1.0.2: {} + base64-arraybuffer@1.0.2: + optional: true + binary-extensions@2.3.0: {} brace-expansion@1.1.11: @@ -3745,6 +3831,18 @@ snapshots: caniuse-lite@1.0.30001714: {} + canvg@3.0.11: + dependencies: + '@babel/runtime': 7.27.0 + '@types/raf': 3.4.3 + core-js: 3.45.1 + raf: 3.4.1 + regenerator-runtime: 0.13.11 + rgbcolor: 1.0.1 + stackblur-canvas: 2.7.0 + svg-pathdata: 6.0.3 + optional: true + chalk@4.1.2: dependencies: ansi-styles: 4.3.0 @@ -3790,12 +3888,20 @@ snapshots: concat-map@0.0.1: {} + core-js@3.45.1: + optional: true + cross-spawn@7.0.6: dependencies: path-key: 3.1.1 shebang-command: 2.0.0 which: 2.0.2 + css-line-break@2.1.0: + dependencies: + utrie: 1.0.2 + optional: true + cssesc@3.0.0: {} csstype@3.1.3: {} @@ -3859,6 +3965,11 @@ snapshots: '@babel/runtime': 7.27.0 csstype: 3.1.3 + dompurify@3.2.6: + optionalDependencies: + '@types/trusted-types': 2.0.7 + optional: true + eastasianwidth@0.2.0: {} electron-to-chromium@1.5.138: {} @@ -4008,6 +4119,12 @@ snapshots: fast-levenshtein@2.0.6: {} + fast-png@6.4.0: + dependencies: + '@types/pako': 2.0.4 + iobuffer: 5.4.0 + pako: 2.1.0 + fastq@1.19.1: dependencies: reusify: 1.1.0 @@ -4016,6 +4133,8 @@ snapshots: optionalDependencies: picomatch: 4.0.2 + fflate@0.8.2: {} + file-entry-cache@8.0.0: dependencies: flat-cache: 4.0.1 @@ -4083,6 +4202,12 @@ snapshots: dependencies: function-bind: 1.1.2 + html2canvas@1.4.1: + dependencies: + css-line-break: 2.1.0 + text-segmentation: 1.0.3 + optional: true + html5-qrcode@2.3.8: {} ignore@5.3.2: {} @@ -4101,6 +4226,8 @@ snapshots: internmap@2.0.3: {} + iobuffer@5.4.0: {} + is-binary-path@2.1.0: dependencies: binary-extensions: 2.3.0 @@ -4143,6 +4270,17 @@ snapshots: json-stable-stringify-without-jsonify@1.0.1: {} + jspdf@3.0.2: + dependencies: + '@babel/runtime': 7.27.0 + fast-png: 6.4.0 + fflate: 0.8.2 + optionalDependencies: + canvg: 3.0.11 + core-js: 3.45.1 + dompurify: 3.2.6 + html2canvas: 1.4.1 + keyv@4.5.4: dependencies: json-buffer: 3.0.1 @@ -4245,6 +4383,8 @@ snapshots: package-json-from-dist@1.0.1: {} + pako@2.1.0: {} + parent-module@1.0.1: dependencies: callsites: 3.1.0 @@ -4260,6 +4400,9 @@ snapshots: lru-cache: 10.4.3 minipass: 7.1.2 + performance-now@2.1.0: + optional: true + picocolors@1.1.1: {} picomatch@2.3.1: {} @@ -4330,6 +4473,11 @@ snapshots: queue-microtask@1.2.3: {} + raf@3.4.1: + dependencies: + performance-now: 2.1.0 + optional: true + react-barcode@1.6.1(react@18.3.1): dependencies: jsbarcode: 3.12.1 @@ -4452,6 +4600,9 @@ snapshots: tiny-invariant: 1.3.3 victory-vendor: 36.9.2 + regenerator-runtime@0.13.11: + optional: true + regenerator-runtime@0.14.1: {} resolve-from@4.0.0: {} @@ -4464,6 +4615,9 @@ snapshots: reusify@1.1.0: {} + rgbcolor@1.0.1: + optional: true + rollup@4.40.0: dependencies: '@types/estree': 1.0.7 @@ -4515,6 +4669,9 @@ snapshots: source-map-js@1.2.1: {} + stackblur-canvas@2.7.0: + optional: true + string-width@4.2.3: dependencies: emoji-regex: 8.0.0 @@ -4553,6 +4710,9 @@ snapshots: supports-preserve-symlinks-flag@1.0.0: {} + svg-pathdata@6.0.3: + optional: true + tailwind-merge@2.6.0: {} tailwindcss-animate@1.0.7(tailwindcss@3.4.17): @@ -4586,6 +4746,11 @@ snapshots: transitivePeerDependencies: - ts-node + text-segmentation@1.0.3: + dependencies: + utrie: 1.0.2 + optional: true + thenify-all@1.6.0: dependencies: thenify: 3.3.1 @@ -4658,6 +4823,11 @@ snapshots: util-deprecate@1.0.2: {} + utrie@1.0.2: + dependencies: + base64-arraybuffer: 1.0.2 + optional: true + vaul@0.9.9(@types/react-dom@18.3.6(@types/react@18.3.20))(@types/react@18.3.20)(react-dom@18.3.1(react@18.3.1))(react@18.3.1): dependencies: '@radix-ui/react-dialog': 1.1.7(@types/react-dom@18.3.6(@types/react@18.3.20))(@types/react@18.3.20)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) diff --git a/src/components/purchase-orders/PurchaseOrderDataTable.tsx b/src/components/purchase-orders/PurchaseOrderDataTable.tsx index d09ee13..e2654a8 100644 --- a/src/components/purchase-orders/PurchaseOrderDataTable.tsx +++ b/src/components/purchase-orders/PurchaseOrderDataTable.tsx @@ -14,23 +14,15 @@ import { useReactTable, } from "@tanstack/react-table" import { Columns } from "lucide-react" - -import { - Table, - TableBody, - TableCell, - TableHead, - TableHeader, - TableRow, -} from "@/components/ui/table" import { Button } from "@/components/ui/button" -import { Input } from "@/components/ui/input" import { DropdownMenu, DropdownMenuCheckboxItem, DropdownMenuContent, DropdownMenuTrigger, } from "@/components/ui/dropdown-menu" +import { jsPDF } from "jspdf" +import { ReceiveStockDialog } from "./ReceiveStockDialog" export type PurchaseOrder = { id: string @@ -38,6 +30,8 @@ export type PurchaseOrder = { orderDate: string status: string itemCount: number + items: Array<{ stockItemId: string; itemName: string; quantity: number }> + quantities: number[] } interface PurchaseOrderDataTableProps { @@ -45,6 +39,7 @@ interface PurchaseOrderDataTableProps { data: TData[] filterValue: string onFilterChange: (value: string) => void + refreshData: () => void } export function PurchaseOrderDataTable({ @@ -52,6 +47,7 @@ export function PurchaseOrderDataTable({ data, filterValue, onFilterChange, + refreshData, }: PurchaseOrderDataTableProps) { const [sorting, setSorting] = React.useState([]) const [columnFilters, setColumnFilters] = React.useState([]) @@ -66,6 +62,55 @@ export function PurchaseOrderDataTable({ return {}; }); const [rowSelection, setRowSelection] = React.useState({}) + const [receiveDialogOpen, setReceiveDialogOpen] = React.useState(false) + const [selectedOrder, setSelectedOrder] = React.useState(null) + + const openReceiveDialog = (order: PurchaseOrder) => { + setSelectedOrder(order) + setReceiveDialogOpen(true) + } + + const closeReceiveDialog = () => { + setSelectedOrder(null) + setReceiveDialogOpen(false) + } + + const generatePDF = (order: PurchaseOrder) => { + const doc = new jsPDF() + doc.setFontSize(16) + doc.text("Purchase Order", 14, 20) + doc.setFontSize(12) + doc.text(`Supplier: ${order.supplierName}`, 14, 30) + doc.text(`Order Date: ${new Date(order.orderDate).toLocaleDateString()}`, 14, 40) + doc.text(`Status: ${order.status}`, 14, 50) + doc.text("Items:", 14, 60) + + let y = 70 + order.items.forEach((item, index) => { + doc.text(`${index + 1}. ${item.itemName} - Qty: ${item.quantity}`, 14, y) + y += 10 + }) + + doc.save(`purchase_order_${order.id}.pdf`) + } + + const approveOrder = async (order: PurchaseOrder) => { + try { + await pb.collection('purchase_orders').update(order.id, { status: "Approved" }) + refreshData() + } catch (error) { + console.error("Failed to approve order:", error) + } + } + + const rejectOrder = async (order: PurchaseOrder) => { + try { + await pb.collection('purchase_orders').update(order.id, { status: "Rejected" }) + refreshData() + } catch (error) { + console.error("Failed to reject order:", error) + } + } const table = useReactTable({ data, @@ -92,110 +137,116 @@ export function PurchaseOrderDataTable({ }, [filterValue, table]) return ( -
-
- onFilterChange(event.target.value)} - className="max-w-sm" - /> - - - - - - {table - .getAllColumns() - .filter( - (column) => column.getCanHide() - ) - .map((column) => { - return ( - - column.toggleVisibility(!!value) - } - > - {column.id} - + <> +
+
+ onFilterChange(e.target.value)} + className="max-w-sm border border-gray-300 rounded px-2 py-1" + /> + + + + + + {table + .getAllColumns() + .filter( + (column) => column.getCanHide() ) - })} - - -
-
- - - {table.getHeaderGroups().map((headerGroup) => ( - - {headerGroup.headers.map((header) => { + .map((column) => { return ( - - {header.isPlaceholder - ? null - : flexRender( - header.column.columnDef.header, - header.getContext() - )} - + + column.toggleVisibility(!!value) + } + > + {column.id} + ) })} - - ))} - - - {table.getRowModel().rows?.length ? ( - table.getRowModel().rows.map((row) => ( - - {row.getVisibleCells().map((cell) => ( - - {flexRender(cell.column.columnDef.cell, cell.getContext())} - - ))} - - )) - ) : ( - - - No results. - - - )} - -
-
-
-
- {table.getFilteredSelectedRowModel().rows.length} of{" "} - {table.getFilteredRowModel().rows.length} row(s) selected. + + +
+
+ + + {table.getHeaderGroups().map(headerGroup => ( + + {headerGroup.headers.map(header => ( + + ))} + + ))} + + + {table.getRowModel().rows.length ? ( + table.getRowModel().rows.map(row => { + const order = row.original as PurchaseOrder + return ( + + {row.getVisibleCells().map(cell => ( + + ))} + + + ) + }) + ) : ( + + + + )} + +
+ {header.isPlaceholder ? null : flexRender(header.column.columnDef.header, header.getContext())} +
+ {flexRender(cell.column.columnDef.cell, cell.getContext())} + + {order.status === "Pending Approval" && ( + <> + + + + )} + {order.status === "Approved" && ( + <> + + + + )} + {order.status === "Received" && ( + Received + )} + {order.status === "Rejected" && ( + Rejected + )} +
No results.
- -
-
+ + {selectedOrder && ( + ({ + stockItemId: item.stockItemId, + orderedQuantity: selectedOrder.quantities[idx] || 0, + itemName: item.itemName, + }))} + refreshData={refreshData} + /> + )} + ) } \ No newline at end of file diff --git a/src/components/purchase-orders/ReceiveStockDialog.tsx b/src/components/purchase-orders/ReceiveStockDialog.tsx new file mode 100644 index 0000000..f3cfcb3 --- /dev/null +++ b/src/components/purchase-orders/ReceiveStockDialog.tsx @@ -0,0 +1,124 @@ +"use client" + +import * as React from "react" +import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription } from "@/components/ui/dialog" +import { Button } from "@/components/ui/button" +import { useForm, useFieldArray } from "react-hook-form" +import { zodResolver } from "@hookform/resolvers/zod" +import * as z from "zod" +import pb from "@/lib/pocketbase" +import { showSuccess, showError } from "@/utils/toast" + +const formSchema = z.object({ + receivedItems: z.array(z.object({ + stockItemId: z.string(), + orderedQuantity: z.number().int().nonnegative(), + receivedQuantity: z.number().int().nonnegative(), + })).min(1), +}) + +type FormSchema = z.infer + +interface ReceiveStockDialogProps { + open: boolean + onOpenChange: (open: boolean) => void + purchaseOrderId: string + items: Array<{ stockItemId: string; orderedQuantity: number; itemName: string }> + refreshData: () => void +} + +export function ReceiveStockDialog({ open, onOpenChange, purchaseOrderId, items, refreshData }: ReceiveStockDialogProps) { + const form = useForm({ + resolver: zodResolver(formSchema), + defaultValues: { + receivedItems: items.map(i => ({ + stockItemId: i.stockItemId, + orderedQuantity: i.orderedQuantity, + receivedQuantity: i.orderedQuantity, + })), + }, + }) + + const { fields } = useFieldArray({ + control: form.control, + name: "receivedItems", + }) + + async function onSubmit(values: FormSchema) { + try { + // Update stock quantities and purchase order status + for (const item of values.receivedItems) { + if (item.receivedQuantity > 0) { + // Fetch current stock item + const stockItem = await pb.collection('stock_items').getOne(item.stockItemId) + if (stockItem) { + // Update quantity by adding receivedQuantity + const newQuantity = (stockItem.quantity || 0) + item.receivedQuantity + await pb.collection('stock_items').update(item.stockItemId, { quantity: newQuantity }) + } + } + } + + // Update purchase order status to 'Received' + await pb.collection('purchase_orders').update(purchaseOrderId, { status: "Received" }) + + showSuccess("Stock received and purchase order updated successfully!") + onOpenChange(false) + refreshData() + } catch (error) { + console.error("Error receiving stock:", error) + showError("Failed to receive stock. Please try again.") + } + } + + return ( + + + + Confirm Received Stock + + Confirm the quantities of items received. Discrepancies will be highlighted. + + +
+ {fields.map((field, index) => { + const orderedQty = field.orderedQuantity + const receivedQty = form.getValues(`receivedItems.${index}.receivedQuantity`) + const discrepancy = receivedQty !== orderedQty + + return ( +
+ +
+

{items[index].itemName}

+

Ordered: {orderedQty}

+
+
+ + + {discrepancy && ( +

Discrepancy detected

+ )} +
+
+ ) + })} +
+ + +
+
+
+
+ ) +} \ No newline at end of file diff --git a/src/pages/PurchaseOrders.tsx b/src/pages/PurchaseOrders.tsx index 039a614..d9f6c25 100644 --- a/src/pages/PurchaseOrders.tsx +++ b/src/pages/PurchaseOrders.tsx @@ -9,13 +9,13 @@ import { DialogTitle, DialogTrigger, } from "@/components/ui/dialog"; -import { columns, PurchaseOrder } from "@/components/purchase-orders/columns"; +import { columns } from "@/components/purchase-orders/columns"; import { PurchaseOrderDataTable } from "@/components/purchase-orders/PurchaseOrderDataTable"; import pb from "@/lib/pocketbase"; const PurchaseOrders = () => { const [purchaseOrderOpen, setPurchaseOrderOpen] = useState(false); - const [purchaseOrders, setPurchaseOrders] = useState([]); + const [purchaseOrders, setPurchaseOrders] = useState([]); const [refreshTrigger, setRefreshTrigger] = useState(0); const [filterValue, setFilterValue] = useState(""); @@ -23,14 +23,21 @@ const PurchaseOrders = () => { try { const records = await pb.collection('purchase_orders').getList(1, 50, { sort: '-orderDate', + expand: 'items', }); - const formattedData: PurchaseOrder[] = records.items.map((record: any) => ({ + const formattedData = records.items.map((record) => ({ id: record.id, supplierName: record.supplierName, orderDate: record.orderDate, status: record.status, itemCount: record.items?.length || 0, + items: (record.expand?.items || []).map((item) => ({ + stockItemId: item.id, + itemName: item.name, + quantity: item.quantity, + })), + quantities: record.quantities || [], })); setPurchaseOrders(formattedData); @@ -74,6 +81,7 @@ const PurchaseOrders = () => { data={purchaseOrders} filterValue={filterValue} onFilterChange={setFilterValue} + refreshData={handleRefreshData} />
);