[dyad] wrote 5 file(s), added jspdf package(s)

This commit is contained in:
[dyad] 2025-09-12 08:35:10 +02:00
parent 883981a4be
commit afe1727b77
5 changed files with 466 additions and 112 deletions

View file

@ -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",

View file

@ -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)

View file

@ -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<TData, TValue> {
@ -45,6 +39,7 @@ interface PurchaseOrderDataTableProps<TData, TValue> {
data: TData[]
filterValue: string
onFilterChange: (value: string) => void
refreshData: () => void
}
export function PurchaseOrderDataTable<TData, TValue>({
@ -52,6 +47,7 @@ export function PurchaseOrderDataTable<TData, TValue>({
data,
filterValue,
onFilterChange,
refreshData,
}: PurchaseOrderDataTableProps<TData, TValue>) {
const [sorting, setSorting] = React.useState<SortingState>([])
const [columnFilters, setColumnFilters] = React.useState<ColumnFiltersState>([])
@ -66,6 +62,55 @@ export function PurchaseOrderDataTable<TData, TValue>({
return {};
});
const [rowSelection, setRowSelection] = React.useState({})
const [receiveDialogOpen, setReceiveDialogOpen] = React.useState(false)
const [selectedOrder, setSelectedOrder] = React.useState<PurchaseOrder | null>(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<TData, TValue>({
}, [filterValue, table])
return (
<div>
<div className="flex items-center py-4">
<Input
placeholder="Filter by supplier name..."
value={filterValue}
onChange={(event) => onFilterChange(event.target.value)}
className="max-w-sm"
/>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="outline" size="icon" className="ml-auto">
<Columns className="h-4 w-4" />
<span className="sr-only">Toggle columns</span>
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
{table
.getAllColumns()
.filter(
(column) => column.getCanHide()
)
.map((column) => {
return (
<DropdownMenuCheckboxItem
key={column.id}
className="capitalize"
checked={column.getIsVisible()}
onCheckedChange={(value) =>
column.toggleVisibility(!!value)
}
>
{column.id}
</DropdownMenuCheckboxItem>
<>
<div>
<div className="flex items-center py-4">
<input
type="text"
placeholder="Filter by supplier name..."
value={filterValue}
onChange={(e) => onFilterChange(e.target.value)}
className="max-w-sm border border-gray-300 rounded px-2 py-1"
/>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="outline" size="icon" className="ml-auto">
<Columns className="h-4 w-4" />
<span className="sr-only">Toggle columns</span>
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
{table
.getAllColumns()
.filter(
(column) => column.getCanHide()
)
})}
</DropdownMenuContent>
</DropdownMenu>
</div>
<div className="rounded-md border bg-white">
<Table>
<TableHeader>
{table.getHeaderGroups().map((headerGroup) => (
<TableRow key={headerGroup.id}>
{headerGroup.headers.map((header) => {
.map((column) => {
return (
<TableHead key={header.id}>
{header.isPlaceholder
? null
: flexRender(
header.column.columnDef.header,
header.getContext()
)}
</TableHead>
<DropdownMenuCheckboxItem
key={column.id}
className="capitalize"
checked={column.getIsVisible()}
onCheckedChange={(value) =>
column.toggleVisibility(!!value)
}
>
{column.id}
</DropdownMenuCheckboxItem>
)
})}
</TableRow>
))}
</TableHeader>
<TableBody>
{table.getRowModel().rows?.length ? (
table.getRowModel().rows.map((row) => (
<TableRow
key={row.id}
data-state={row.getIsSelected() && "selected"}
>
{row.getVisibleCells().map((cell) => (
<TableCell key={cell.id}>
{flexRender(cell.column.columnDef.cell, cell.getContext())}
</TableCell>
))}
</TableRow>
))
) : (
<TableRow>
<TableCell colSpan={columns.length} className="h-24 text-center">
No results.
</TableCell>
</TableRow>
)}
</TableBody>
</Table>
</div>
<div className="flex items-center justify-end space-x-2 py-4">
<div className="flex-1 text-sm text-muted-foreground">
{table.getFilteredSelectedRowModel().rows.length} of{" "}
{table.getFilteredRowModel().rows.length} row(s) selected.
</DropdownMenuContent>
</DropdownMenu>
</div>
<div className="rounded-md border bg-white">
<table className="w-full min-w-full table-auto">
<thead>
{table.getHeaderGroups().map(headerGroup => (
<tr key={headerGroup.id}>
{headerGroup.headers.map(header => (
<th key={header.id} className="border-b px-4 py-2 text-left font-medium text-gray-900">
{header.isPlaceholder ? null : flexRender(header.column.columnDef.header, header.getContext())}
</th>
))}
</tr>
))}
</thead>
<tbody>
{table.getRowModel().rows.length ? (
table.getRowModel().rows.map(row => {
const order = row.original as PurchaseOrder
return (
<tr key={row.id} className="border-b hover:bg-gray-50">
{row.getVisibleCells().map(cell => (
<td key={cell.id} className="px-4 py-2 align-top">
{flexRender(cell.column.columnDef.cell, cell.getContext())}
</td>
))}
<td className="px-4 py-2 space-x-2">
{order.status === "Pending Approval" && (
<>
<Button size="sm" variant="outline" onClick={() => approveOrder(order)}>Approve</Button>
<Button size="sm" variant="destructive" onClick={() => rejectOrder(order)}>Reject</Button>
</>
)}
{order.status === "Approved" && (
<>
<Button size="sm" onClick={() => generatePDF(order)}>Download PDF</Button>
<Button size="sm" variant="secondary" onClick={() => openReceiveDialog(order)}>Receive Stock</Button>
</>
)}
{order.status === "Received" && (
<span className="text-green-600 font-semibold">Received</span>
)}
{order.status === "Rejected" && (
<span className="text-red-600 font-semibold">Rejected</span>
)}
</td>
</tr>
)
})
) : (
<tr>
<td colSpan={columns.length + 1} className="h-24 text-center">No results.</td>
</tr>
)}
</tbody>
</table>
</div>
<Button
variant="outline"
size="sm"
onClick={() => table.previousPage()}
disabled={!table.getCanPreviousPage()}
>
Previous
</Button>
<Button
variant="outline"
size="sm"
onClick={() => table.nextPage()}
disabled={!table.getCanNextPage()}
>
Next
</Button>
</div>
</div>
{selectedOrder && (
<ReceiveStockDialog
open={receiveDialogOpen}
onOpenChange={setReceiveDialogOpen}
purchaseOrderId={selectedOrder.id}
items={selectedOrder.items.map((item, idx) => ({
stockItemId: item.stockItemId,
orderedQuantity: selectedOrder.quantities[idx] || 0,
itemName: item.itemName,
}))}
refreshData={refreshData}
/>
)}
</>
)
}

View file

@ -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<typeof formSchema>
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<FormSchema>({
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 (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent className="sm:max-w-lg">
<DialogHeader>
<DialogTitle>Confirm Received Stock</DialogTitle>
<DialogDescription>
Confirm the quantities of items received. Discrepancies will be highlighted.
</DialogDescription>
</DialogHeader>
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-4">
{fields.map((field, index) => {
const orderedQty = field.orderedQuantity
const receivedQty = form.getValues(`receivedItems.${index}.receivedQuantity`)
const discrepancy = receivedQty !== orderedQty
return (
<div key={field.id} className="flex items-center space-x-4">
<input type="hidden" {...form.register(`receivedItems.${index}.stockItemId` as const)} />
<div className="flex-1">
<p className="font-semibold">{items[index].itemName}</p>
<p className="text-sm text-muted-foreground">Ordered: {orderedQty}</p>
</div>
<div className="w-32">
<label htmlFor={`receivedQuantity-${index}`} className="block text-sm font-medium text-gray-700">
Received Qty
</label>
<input
id={`receivedQuantity-${index}`}
type="number"
min={0}
{...form.register(`receivedItems.${index}.receivedQuantity` as const, { valueAsNumber: true })}
className={`mt-1 block w-full rounded-md border border-gray-300 px-2 py-1 shadow-sm focus:border-primary focus:ring-1 focus:ring-primary ${
discrepancy ? "border-red-500" : ""
}`}
/>
{discrepancy && (
<p className="text-xs text-red-600">Discrepancy detected</p>
)}
</div>
</div>
)
})}
<div className="flex justify-end space-x-2 pt-4">
<Button variant="outline" onClick={() => onOpenChange(false)}>Cancel</Button>
<Button type="submit">Confirm Received</Button>
</div>
</form>
</DialogContent>
</Dialog>
)
}

View file

@ -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<PurchaseOrder[]>([]);
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}
/>
</div>
);