feat(digital-signatures): simplify form, add TIFF support, subcategories, download options
- Remove 'initials' type, expirationDate, legalStatus, usageNotes fields - Add subcategory field with creatable input + 6 default categories - Add TIFF upload support with client-side utif2 decode for preview - Store original TIFF data separately for faithful downloads - Add download dropdown: Original file, Word (.docx), PDF (.pdf) - Group assets by subcategory in list view - Add subcategory filter in search bar - Install docx, jspdf, utif2 packages - Closes 3.07
This commit is contained in:
Generated
+341
-37
@@ -8,8 +8,11 @@
|
||||
"name": "architools",
|
||||
"version": "0.1.0",
|
||||
"dependencies": {
|
||||
"@prisma/client": "^6.19.2",
|
||||
"class-variance-authority": "^0.7.1",
|
||||
"clsx": "^2.1.1",
|
||||
"docx": "^9.6.0",
|
||||
"jspdf": "^4.2.0",
|
||||
"jszip": "^3.10.1",
|
||||
"lucide-react": "^0.564.0",
|
||||
"minio": "^8.0.6",
|
||||
@@ -21,10 +24,10 @@
|
||||
"react-dom": "19.2.3",
|
||||
"tailwind-merge": "^3.4.1",
|
||||
"tesseract.js": "^7.0.0",
|
||||
"utif2": "^4.1.0",
|
||||
"uuid": "^13.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@prisma/client": "^6.19.2",
|
||||
"@tailwindcss/postcss": "^4",
|
||||
"@types/jszip": "^3.4.0",
|
||||
"@types/node": "^20",
|
||||
@@ -1994,7 +1997,6 @@
|
||||
"version": "6.19.2",
|
||||
"resolved": "https://registry.npmjs.org/@prisma/client/-/client-6.19.2.tgz",
|
||||
"integrity": "sha512-gR2EMvfK/aTxsuooaDA32D8v+us/8AAet+C3J1cc04SW35FPdZYgLF+iN4NDLUgAaUGTKdAB0CYenu1TAgGdMg==",
|
||||
"dev": true,
|
||||
"hasInstallScript": true,
|
||||
"license": "Apache-2.0",
|
||||
"engines": {
|
||||
@@ -2017,7 +2019,7 @@
|
||||
"version": "6.19.2",
|
||||
"resolved": "https://registry.npmjs.org/@prisma/config/-/config-6.19.2.tgz",
|
||||
"integrity": "sha512-kadBGDl+aUswv/zZMk9Mx0C8UZs1kjao8H9/JpI4Wh4SHZaM7zkTwiKn/iFLfRg+XtOAo/Z/c6pAYhijKl0nzQ==",
|
||||
"dev": true,
|
||||
"devOptional": true,
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"c12": "3.1.0",
|
||||
@@ -2030,14 +2032,14 @@
|
||||
"version": "6.19.2",
|
||||
"resolved": "https://registry.npmjs.org/@prisma/debug/-/debug-6.19.2.tgz",
|
||||
"integrity": "sha512-lFnEZsLdFLmEVCVNdskLDCL8Uup41GDfU0LUfquw+ercJC8ODTuL0WNKgOKmYxCJVvFwf0OuZBzW99DuWmoH2A==",
|
||||
"dev": true,
|
||||
"devOptional": true,
|
||||
"license": "Apache-2.0"
|
||||
},
|
||||
"node_modules/@prisma/engines": {
|
||||
"version": "6.19.2",
|
||||
"resolved": "https://registry.npmjs.org/@prisma/engines/-/engines-6.19.2.tgz",
|
||||
"integrity": "sha512-TTkJ8r+uk/uqczX40wb+ODG0E0icVsMgwCTyTHXehaEfb0uo80M9g1aW1tEJrxmFHeOZFXdI2sTA1j1AgcHi4A==",
|
||||
"dev": true,
|
||||
"devOptional": true,
|
||||
"hasInstallScript": true,
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
@@ -2051,14 +2053,14 @@
|
||||
"version": "7.1.1-3.c2990dca591cba766e3b7ef5d9e8a84796e47ab7",
|
||||
"resolved": "https://registry.npmjs.org/@prisma/engines-version/-/engines-version-7.1.1-3.c2990dca591cba766e3b7ef5d9e8a84796e47ab7.tgz",
|
||||
"integrity": "sha512-03bgb1VD5gvuumNf+7fVGBzfpJPjmqV423l/WxsWk2cNQ42JD0/SsFBPhN6z8iAvdHs07/7ei77SKu7aZfq8bA==",
|
||||
"dev": true,
|
||||
"devOptional": true,
|
||||
"license": "Apache-2.0"
|
||||
},
|
||||
"node_modules/@prisma/fetch-engine": {
|
||||
"version": "6.19.2",
|
||||
"resolved": "https://registry.npmjs.org/@prisma/fetch-engine/-/fetch-engine-6.19.2.tgz",
|
||||
"integrity": "sha512-h4Ff4Pho+SR1S8XerMCC12X//oY2bG3Iug/fUnudfcXEUnIeRiBdXHFdGlGOgQ3HqKgosTEhkZMvGM9tWtYC+Q==",
|
||||
"dev": true,
|
||||
"devOptional": true,
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@prisma/debug": "6.19.2",
|
||||
@@ -2070,7 +2072,7 @@
|
||||
"version": "6.19.2",
|
||||
"resolved": "https://registry.npmjs.org/@prisma/get-platform/-/get-platform-6.19.2.tgz",
|
||||
"integrity": "sha512-PGLr06JUSTqIvztJtAzIxOwtWKtJm5WwOG6xpsgD37Rc84FpfUBGLKz65YpJBGtkRQGXTYEFie7pYALocC3MtA==",
|
||||
"dev": true,
|
||||
"devOptional": true,
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@prisma/debug": "6.19.2"
|
||||
@@ -3605,7 +3607,7 @@
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz",
|
||||
"integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==",
|
||||
"dev": true,
|
||||
"devOptional": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@swc/helpers": {
|
||||
@@ -4025,6 +4027,19 @@
|
||||
"undici-types": "~6.21.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/pako": {
|
||||
"version": "2.0.4",
|
||||
"resolved": "https://registry.npmjs.org/@types/pako/-/pako-2.0.4.tgz",
|
||||
"integrity": "sha512-VWDCbrLeVXJM9fihYodcLiIv0ku+AlOa/TQ1SvYOaBuyrSKgEcro95LJyIsJ4vSo6BXIxOKxiJAat04CmST9Fw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/raf": {
|
||||
"version": "3.4.3",
|
||||
"resolved": "https://registry.npmjs.org/@types/raf/-/raf-3.4.3.tgz",
|
||||
"integrity": "sha512-c4YAvMedbPZ5tEyxzQdMoOhhJ4RD3rngZIdwC2/qDN3d7JpEhB6fiBRKVY1lg5B7Wk+uPBjn5f39j1/2MY1oOw==",
|
||||
"license": "MIT",
|
||||
"optional": true
|
||||
},
|
||||
"node_modules/@types/react": {
|
||||
"version": "19.2.14",
|
||||
"resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.14.tgz",
|
||||
@@ -4054,6 +4069,13 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/trusted-types": {
|
||||
"version": "2.0.7",
|
||||
"resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz",
|
||||
"integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==",
|
||||
"license": "MIT",
|
||||
"optional": true
|
||||
},
|
||||
"node_modules/@types/uuid": {
|
||||
"version": "10.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-10.0.0.tgz",
|
||||
@@ -5040,6 +5062,16 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/base64-arraybuffer": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/base64-arraybuffer/-/base64-arraybuffer-1.0.2.tgz",
|
||||
"integrity": "sha512-I3yl4r9QB5ZRY3XuJVEPfc2XhZO6YweFPI+UovAzn+8/hb3oJ6lnysaFcjVpkCPfVWFUDvoZ8kmVDP7WyRtYtQ==",
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"engines": {
|
||||
"node": ">= 0.6.0"
|
||||
}
|
||||
},
|
||||
"node_modules/baseline-browser-mapping": {
|
||||
"version": "2.9.19",
|
||||
"resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.9.19.tgz",
|
||||
@@ -5207,7 +5239,7 @@
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/c12/-/c12-3.1.0.tgz",
|
||||
"integrity": "sha512-uWoS8OU1MEIsOv8p/5a82c3H31LsWVR5qiyXVfBNOzfffjUWtPnhAb4BYI2uG2HfGmZmFjCtui5XNWaps+iFuw==",
|
||||
"dev": true,
|
||||
"devOptional": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"chokidar": "^4.0.3",
|
||||
@@ -5236,7 +5268,7 @@
|
||||
"version": "16.6.1",
|
||||
"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.6.1.tgz",
|
||||
"integrity": "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==",
|
||||
"dev": true,
|
||||
"devOptional": true,
|
||||
"license": "BSD-2-Clause",
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
@@ -5322,6 +5354,26 @@
|
||||
],
|
||||
"license": "CC-BY-4.0"
|
||||
},
|
||||
"node_modules/canvg": {
|
||||
"version": "3.0.11",
|
||||
"resolved": "https://registry.npmjs.org/canvg/-/canvg-3.0.11.tgz",
|
||||
"integrity": "sha512-5ON+q7jCTgMp9cjpu4Jo6XbvfYwSB2Ow3kzHKfIyJfaCAOHLbdKPQqGKgfED/R5B+3TFFfe8pegYA+b423SRyA==",
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.12.5",
|
||||
"@types/raf": "^3.4.0",
|
||||
"core-js": "^3.8.3",
|
||||
"raf": "^3.4.1",
|
||||
"regenerator-runtime": "^0.13.7",
|
||||
"rgbcolor": "^1.0.1",
|
||||
"stackblur-canvas": "^2.0.0",
|
||||
"svg-pathdata": "^6.0.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/chalk": {
|
||||
"version": "4.1.2",
|
||||
"resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
|
||||
@@ -5343,7 +5395,7 @@
|
||||
"version": "4.0.3",
|
||||
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz",
|
||||
"integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==",
|
||||
"dev": true,
|
||||
"devOptional": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"readdirp": "^4.0.1"
|
||||
@@ -5359,7 +5411,7 @@
|
||||
"version": "0.1.6",
|
||||
"resolved": "https://registry.npmjs.org/citty/-/citty-0.1.6.tgz",
|
||||
"integrity": "sha512-tskPPKEs8D2KPafUypv2gxwJP8h/OaJmC82QQGGDQcHvXX43xF2VDACcJVmZ0EuSxkpO9Kc4MlrA3q0+FG58AQ==",
|
||||
"dev": true,
|
||||
"devOptional": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"consola": "^3.2.3"
|
||||
@@ -5557,14 +5609,14 @@
|
||||
"version": "0.2.4",
|
||||
"resolved": "https://registry.npmjs.org/confbox/-/confbox-0.2.4.tgz",
|
||||
"integrity": "sha512-ysOGlgTFbN2/Y6Cg3Iye8YKulHw+R2fNXHrgSmXISQdMnomY6eNDprVdW9R5xBguEqI954+S6709UyiO7B+6OQ==",
|
||||
"dev": true,
|
||||
"devOptional": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/consola": {
|
||||
"version": "3.4.2",
|
||||
"resolved": "https://registry.npmjs.org/consola/-/consola-3.4.2.tgz",
|
||||
"integrity": "sha512-5IKcdX0nnYavi6G7TtOhwkYzyjfJlatbjMjuLSfE2kYT5pMDOilZ4OvMhi637CcDICTmz3wARPoyhqyX1Y+XvA==",
|
||||
"dev": true,
|
||||
"devOptional": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": "^14.18.0 || >=16.10.0"
|
||||
@@ -5620,6 +5672,18 @@
|
||||
"node": ">=6.6.0"
|
||||
}
|
||||
},
|
||||
"node_modules/core-js": {
|
||||
"version": "3.48.0",
|
||||
"resolved": "https://registry.npmjs.org/core-js/-/core-js-3.48.0.tgz",
|
||||
"integrity": "sha512-zpEHTy1fjTMZCKLHUZoVeylt9XrzaIN2rbPXEt0k+q7JE5CkCZdo6bNq55bn24a69CH7ErAVLKijxJja4fw+UQ==",
|
||||
"hasInstallScript": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/core-js"
|
||||
}
|
||||
},
|
||||
"node_modules/core-util-is": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz",
|
||||
@@ -5686,6 +5750,16 @@
|
||||
"node": ">= 8"
|
||||
}
|
||||
},
|
||||
"node_modules/css-line-break": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/css-line-break/-/css-line-break-2.1.0.tgz",
|
||||
"integrity": "sha512-FHcKFCZcAha3LwfVBhCQbW2nCNbkZXn7KVUJcsT5/P8YmfsVja0FMPJr0B903j/E69HUphKiV9iQArX8SDYA4w==",
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"utrie": "^1.0.2"
|
||||
}
|
||||
},
|
||||
"node_modules/cssesc": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz",
|
||||
@@ -5840,7 +5914,7 @@
|
||||
"version": "7.1.5",
|
||||
"resolved": "https://registry.npmjs.org/deepmerge-ts/-/deepmerge-ts-7.1.5.tgz",
|
||||
"integrity": "sha512-HOJkrhaYsweh+W+e74Yn7YStZOilkoPb6fycpwNLKzSPtruFs48nYis0zy5yJz1+ktUhHxoRDJ27RQAWLIJVJw==",
|
||||
"dev": true,
|
||||
"devOptional": true,
|
||||
"license": "BSD-3-Clause",
|
||||
"engines": {
|
||||
"node": ">=16.0.0"
|
||||
@@ -5928,7 +6002,7 @@
|
||||
"version": "6.1.4",
|
||||
"resolved": "https://registry.npmjs.org/defu/-/defu-6.1.4.tgz",
|
||||
"integrity": "sha512-mEQCMmwJu317oSz8CwdIOdwf3xMif1ttiM8LTufzc3g6kR+9Pe236twL8j3IYT1F7GfRgGcW6MWxzZjLIkuHIg==",
|
||||
"dev": true,
|
||||
"devOptional": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/depd": {
|
||||
@@ -5945,7 +6019,7 @@
|
||||
"version": "2.0.5",
|
||||
"resolved": "https://registry.npmjs.org/destr/-/destr-2.0.5.tgz",
|
||||
"integrity": "sha512-ugFTXCtDZunbzasqBxrK93Ik/DRYsO6S/fedkWEMKqt04xZ4csmnmwGDBAb07QWNaGMAmnTIemsYZCksjATwsA==",
|
||||
"dev": true,
|
||||
"devOptional": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/detect-libc": {
|
||||
@@ -5987,6 +6061,66 @@
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/docx": {
|
||||
"version": "9.6.0",
|
||||
"resolved": "https://registry.npmjs.org/docx/-/docx-9.6.0.tgz",
|
||||
"integrity": "sha512-y6EaJJMDvt4P7wgGQB9KsZf4wsRkQMJfkc9LlNufRshggI5BT35hGNkXBCAeEoI3MLMwApKguxzjdqqVcBCqNA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/node": "^25.2.3",
|
||||
"hash.js": "^1.1.7",
|
||||
"jszip": "^3.10.1",
|
||||
"nanoid": "^5.1.3",
|
||||
"xml": "^1.0.1",
|
||||
"xml-js": "^1.6.8"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/docx/node_modules/@types/node": {
|
||||
"version": "25.3.2",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-25.3.2.tgz",
|
||||
"integrity": "sha512-RpV6r/ij22zRRdyBPcxDeKAzH43phWVKEjL2iksqo1Vz3CuBUrgmPpPhALKiRfU7OMCmeeO9vECBMsV0hMTG8Q==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"undici-types": "~7.18.0"
|
||||
}
|
||||
},
|
||||
"node_modules/docx/node_modules/nanoid": {
|
||||
"version": "5.1.6",
|
||||
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-5.1.6.tgz",
|
||||
"integrity": "sha512-c7+7RQ+dMB5dPwwCp4ee1/iV/q2P6aK1mTZcfr1BTuVlyW9hJYiMPybJCcnBlQtuSmTIWNeazm/zqNoZSSElBg==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/ai"
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
"nanoid": "bin/nanoid.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^18 || >=20"
|
||||
}
|
||||
},
|
||||
"node_modules/docx/node_modules/undici-types": {
|
||||
"version": "7.18.2",
|
||||
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.18.2.tgz",
|
||||
"integrity": "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/dompurify": {
|
||||
"version": "3.3.1",
|
||||
"resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.3.1.tgz",
|
||||
"integrity": "sha512-qkdCKzLNtrgPFP1Vo+98FRzJnBRGe4ffyCea9IwHB1fyxPOeNTHpLKYGd4Uk9xvNoH0ZoOjwZxNptyMwqrId1Q==",
|
||||
"license": "(MPL-2.0 OR Apache-2.0)",
|
||||
"optional": true,
|
||||
"optionalDependencies": {
|
||||
"@types/trusted-types": "^2.0.7"
|
||||
}
|
||||
},
|
||||
"node_modules/dotenv": {
|
||||
"version": "17.3.1",
|
||||
"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-17.3.1.tgz",
|
||||
@@ -6043,7 +6177,7 @@
|
||||
"version": "3.18.4",
|
||||
"resolved": "https://registry.npmjs.org/effect/-/effect-3.18.4.tgz",
|
||||
"integrity": "sha512-b1LXQJLe9D11wfnOKAk3PKxuqYshQ0Heez+y5pnkd3jLj1yx9QhM72zZ9uUrOQyNvrs2GZZd/3maL0ZV18YuDA==",
|
||||
"dev": true,
|
||||
"devOptional": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@standard-schema/spec": "^1.0.0",
|
||||
@@ -6068,7 +6202,7 @@
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/empathic/-/empathic-2.0.0.tgz",
|
||||
"integrity": "sha512-i6UzDscO/XfAcNYD75CfICkmfLedpyPDdozrLMmQc5ORaQcdMoc21OnlEylMIqI7U8eniKrPMxxtj8k0vhmJhA==",
|
||||
"dev": true,
|
||||
"devOptional": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=14"
|
||||
@@ -6896,14 +7030,14 @@
|
||||
"version": "1.0.8",
|
||||
"resolved": "https://registry.npmjs.org/exsolve/-/exsolve-1.0.8.tgz",
|
||||
"integrity": "sha512-LmDxfWXwcTArk8fUEnOfSZpHOJ6zOMUJKOtFLFqJLoKJetuQG874Uc7/Kki7zFLzYybmZhp1M7+98pfMqeX8yA==",
|
||||
"dev": true,
|
||||
"devOptional": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/fast-check": {
|
||||
"version": "3.23.2",
|
||||
"resolved": "https://registry.npmjs.org/fast-check/-/fast-check-3.23.2.tgz",
|
||||
"integrity": "sha512-h5+1OzzfCC3Ef7VbtKdcv7zsstUQwUDlYpUTvjeUsJAssPgLn7QzbboPtL5ro04Mq0rPOsMzl7q5hIbRs2wD1A==",
|
||||
"dev": true,
|
||||
"devOptional": true,
|
||||
"funding": [
|
||||
{
|
||||
"type": "individual",
|
||||
@@ -6973,6 +7107,23 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/fast-png": {
|
||||
"version": "6.4.0",
|
||||
"resolved": "https://registry.npmjs.org/fast-png/-/fast-png-6.4.0.tgz",
|
||||
"integrity": "sha512-kAqZq1TlgBjZcLr5mcN6NP5Rv4V2f22z00c3g8vRrwkcqjerx7BEhPbOnWCPqaHUl2XWQBJQvOT/FQhdMT7X/Q==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/pako": "^2.0.3",
|
||||
"iobuffer": "^5.3.2",
|
||||
"pako": "^2.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/fast-png/node_modules/pako": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/pako/-/pako-2.1.0.tgz",
|
||||
"integrity": "sha512-w+eufiZ1WuJYgPXbV/PO3NCMEc3xqylkKHzp8bxp1uW4qaSNQUkwmLLEc3kKsfz8lpV1F8Ht3U1Cm+9Srog2ug==",
|
||||
"license": "(MIT AND Zlib)"
|
||||
},
|
||||
"node_modules/fast-uri": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.0.tgz",
|
||||
@@ -7042,6 +7193,12 @@
|
||||
"node": "^12.20 || >= 14.13"
|
||||
}
|
||||
},
|
||||
"node_modules/fflate": {
|
||||
"version": "0.8.2",
|
||||
"resolved": "https://registry.npmjs.org/fflate/-/fflate-0.8.2.tgz",
|
||||
"integrity": "sha512-cPJU47OaAoCbg0pBvzsgpTPhmhqI5eJjh/JIu8tPj5q+T7iLvW/JAYUqmE7KOB4R1ZyEhzBaIQpQpardBF5z8A==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/figures": {
|
||||
"version": "6.1.0",
|
||||
"resolved": "https://registry.npmjs.org/figures/-/figures-6.1.0.tgz",
|
||||
@@ -7423,7 +7580,7 @@
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/giget/-/giget-2.0.0.tgz",
|
||||
"integrity": "sha512-L5bGsVkxJbJgdnwyuheIunkGatUF/zssUoxxjACCseZYAVbaqdh9Tsmmlkl8vYan09H7sbvKt4pS8GqKLBrEzA==",
|
||||
"dev": true,
|
||||
"devOptional": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"citty": "^0.1.6",
|
||||
@@ -7587,6 +7744,16 @@
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/hash.js": {
|
||||
"version": "1.1.7",
|
||||
"resolved": "https://registry.npmjs.org/hash.js/-/hash.js-1.1.7.tgz",
|
||||
"integrity": "sha512-taOaskGt4z4SOANNseOviYDvjEJinIkRgmp7LbKP2YTTmVxWBl87s/uzK9r+44BclBSp2X7K1hqeNfz9JbBeXA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"inherits": "^2.0.3",
|
||||
"minimalistic-assert": "^1.0.1"
|
||||
}
|
||||
},
|
||||
"node_modules/hasown": {
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz",
|
||||
@@ -7634,6 +7801,20 @@
|
||||
"node": ">=16.9.0"
|
||||
}
|
||||
},
|
||||
"node_modules/html2canvas": {
|
||||
"version": "1.4.1",
|
||||
"resolved": "https://registry.npmjs.org/html2canvas/-/html2canvas-1.4.1.tgz",
|
||||
"integrity": "sha512-fPU6BHNpsyIhr8yyMpTLLxAbkaK8ArIBcmZIRiBLiDhjeqvXolaEmDGmELFuX9I4xDcaKKcJl+TKZLqruBbmWA==",
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"css-line-break": "^2.1.0",
|
||||
"text-segmentation": "^1.0.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/http-errors": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz",
|
||||
@@ -7766,6 +7947,12 @@
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/iobuffer": {
|
||||
"version": "5.4.0",
|
||||
"resolved": "https://registry.npmjs.org/iobuffer/-/iobuffer-5.4.0.tgz",
|
||||
"integrity": "sha512-DRebOWuqDvxunfkNJAlc3IzWIPD5xVxwUNbHr7xKB8E6aLJxIPfNX3CoMJghcFjpv6RWQsrcJbghtEwSPoJqMA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/ip-address": {
|
||||
"version": "10.0.1",
|
||||
"resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.0.1.tgz",
|
||||
@@ -8444,7 +8631,7 @@
|
||||
"version": "2.6.1",
|
||||
"resolved": "https://registry.npmjs.org/jiti/-/jiti-2.6.1.tgz",
|
||||
"integrity": "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==",
|
||||
"dev": true,
|
||||
"devOptional": true,
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
"jiti": "lib/jiti-cli.mjs"
|
||||
@@ -8554,6 +8741,23 @@
|
||||
"graceful-fs": "^4.1.6"
|
||||
}
|
||||
},
|
||||
"node_modules/jspdf": {
|
||||
"version": "4.2.0",
|
||||
"resolved": "https://registry.npmjs.org/jspdf/-/jspdf-4.2.0.tgz",
|
||||
"integrity": "sha512-hR/hnRevAXXlrjeqU5oahOE+Ln9ORJUB5brLHHqH67A+RBQZuFr5GkbI9XQI8OUFSEezKegsi45QRpc4bGj75Q==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.28.6",
|
||||
"fast-png": "^6.2.0",
|
||||
"fflate": "^0.8.1"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"canvg": "^3.0.11",
|
||||
"core-js": "^3.6.0",
|
||||
"dompurify": "^3.3.1",
|
||||
"html2canvas": "^1.0.0-rc.5"
|
||||
}
|
||||
},
|
||||
"node_modules/jsx-ast-utils": {
|
||||
"version": "3.3.5",
|
||||
"resolved": "https://registry.npmjs.org/jsx-ast-utils/-/jsx-ast-utils-3.3.5.tgz",
|
||||
@@ -9140,6 +9344,12 @@
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/minimalistic-assert": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz",
|
||||
"integrity": "sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==",
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/minimatch": {
|
||||
"version": "3.1.2",
|
||||
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
|
||||
@@ -9530,7 +9740,7 @@
|
||||
"version": "1.6.7",
|
||||
"resolved": "https://registry.npmjs.org/node-fetch-native/-/node-fetch-native-1.6.7.tgz",
|
||||
"integrity": "sha512-g9yhqoedzIUm0nTnTqAQvueMPVOuIY16bqgAJJC8XOOubYFNwz6IER9qs0Gq2Xd0+CecCKFjtdDTMA4u4xG06Q==",
|
||||
"dev": true,
|
||||
"devOptional": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/node-releases": {
|
||||
@@ -9574,7 +9784,7 @@
|
||||
"version": "0.6.5",
|
||||
"resolved": "https://registry.npmjs.org/nypm/-/nypm-0.6.5.tgz",
|
||||
"integrity": "sha512-K6AJy1GMVyfyMXRVB88700BJqNUkByijGJM8kEHpLdcAt+vSQAVfkWWHYzuRXHSY6xA2sNc5RjTj0p9rE2izVQ==",
|
||||
"dev": true,
|
||||
"devOptional": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"citty": "^0.2.0",
|
||||
@@ -9592,7 +9802,7 @@
|
||||
"version": "0.2.1",
|
||||
"resolved": "https://registry.npmjs.org/citty/-/citty-0.2.1.tgz",
|
||||
"integrity": "sha512-kEV95lFBhQgtogAPlQfJJ0WGVSokvLr/UEoFPiKKOXF7pl98HfUVUD0ejsuTCld/9xH9vogSywZ5KqHzXrZpqg==",
|
||||
"dev": true,
|
||||
"devOptional": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/oauth": {
|
||||
@@ -9747,7 +9957,7 @@
|
||||
"version": "2.0.11",
|
||||
"resolved": "https://registry.npmjs.org/ohash/-/ohash-2.0.11.tgz",
|
||||
"integrity": "sha512-RdR9FQrFwNBNXAr4GixM8YaRZRJ5PUWbKYbE5eOsrwAjJW0q2REGcf79oYPsLyskQCZG1PLN+S/K1V00joZAoQ==",
|
||||
"dev": true,
|
||||
"devOptional": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/oidc-token-hash": {
|
||||
@@ -10095,16 +10305,23 @@
|
||||
"version": "2.0.3",
|
||||
"resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz",
|
||||
"integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==",
|
||||
"dev": true,
|
||||
"devOptional": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/perfect-debounce": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/perfect-debounce/-/perfect-debounce-1.0.0.tgz",
|
||||
"integrity": "sha512-xCy9V055GLEqoFaHoC1SoLIaLmWctgCUaBaWxDZ7/Zx4CTyX7cJQLJOok/orfjZAh9kEYpjJa4d0KcJmCbctZA==",
|
||||
"dev": true,
|
||||
"devOptional": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/performance-now": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz",
|
||||
"integrity": "sha512-7EAHlyLHI56VEIdK57uwHdHKIaAGbnXPiw0yWbarQZOKaKpvUIgW0jWRVLiatnM+XXlSwsanIBH/hzGMJulMow==",
|
||||
"license": "MIT",
|
||||
"optional": true
|
||||
},
|
||||
"node_modules/picocolors": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
|
||||
@@ -10138,7 +10355,7 @@
|
||||
"version": "2.3.0",
|
||||
"resolved": "https://registry.npmjs.org/pkg-types/-/pkg-types-2.3.0.tgz",
|
||||
"integrity": "sha512-SIqCzDRg0s9npO5XQ3tNZioRY1uK06lA41ynBC1YmFTmnY6FjUjVt6s4LoADmwoig1qqD0oK8h1p/8mlMx8Oig==",
|
||||
"dev": true,
|
||||
"devOptional": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"confbox": "^0.2.2",
|
||||
@@ -10270,7 +10487,7 @@
|
||||
"version": "6.19.2",
|
||||
"resolved": "https://registry.npmjs.org/prisma/-/prisma-6.19.2.tgz",
|
||||
"integrity": "sha512-XTKeKxtQElcq3U9/jHyxSPgiRgeYDKxWTPOf6NkXA0dNj5j40MfEsZkMbyNpwDWCUv7YBFUl7I2VK/6ALbmhEg==",
|
||||
"dev": true,
|
||||
"devOptional": true,
|
||||
"hasInstallScript": true,
|
||||
"license": "Apache-2.0",
|
||||
"peer": true,
|
||||
@@ -10363,7 +10580,7 @@
|
||||
"version": "6.1.0",
|
||||
"resolved": "https://registry.npmjs.org/pure-rand/-/pure-rand-6.1.0.tgz",
|
||||
"integrity": "sha512-bVWawvoZoBYpp6yIoQtQXHZjmz35RSVHnUOTefl8Vcjr8snTPY1wnpSPMWekcFwbxI6gtmT7rSYPFvz71ldiOA==",
|
||||
"dev": true,
|
||||
"devOptional": true,
|
||||
"funding": [
|
||||
{
|
||||
"type": "individual",
|
||||
@@ -10508,6 +10725,16 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/raf": {
|
||||
"version": "3.4.1",
|
||||
"resolved": "https://registry.npmjs.org/raf/-/raf-3.4.1.tgz",
|
||||
"integrity": "sha512-Sq4CW4QhwOHE8ucn6J34MqtZCeWFP2aQSmrlroYgqAV1PjStIhJXxYuTgUIfkEk7zTLjmIjLmU5q+fbD1NnOJA==",
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"performance-now": "^2.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/range-parser": {
|
||||
"version": "1.2.1",
|
||||
"resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz",
|
||||
@@ -10538,7 +10765,7 @@
|
||||
"version": "2.1.2",
|
||||
"resolved": "https://registry.npmjs.org/rc9/-/rc9-2.1.2.tgz",
|
||||
"integrity": "sha512-btXCnMmRIBINM2LDZoEmOogIZU7Qe7zn4BpomSKZ/ykbLObuBdvG+mFq11DL6fjH1DRwHhrlgtYWG96bJiC7Cg==",
|
||||
"dev": true,
|
||||
"devOptional": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"defu": "^6.1.4",
|
||||
@@ -10669,7 +10896,7 @@
|
||||
"version": "4.1.2",
|
||||
"resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz",
|
||||
"integrity": "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==",
|
||||
"dev": true,
|
||||
"devOptional": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 14.18.0"
|
||||
@@ -10842,6 +11069,16 @@
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/rgbcolor": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/rgbcolor/-/rgbcolor-1.0.1.tgz",
|
||||
"integrity": "sha512-9aZLIrhRaD97sgVhtJOW6ckOEh6/GnvQtdVNfdZ6s67+3/XwLS9lBcQYzEEhYVeUowN7pRzMLsyGhK2i/xvWbw==",
|
||||
"license": "MIT OR SEE LICENSE IN FEEL-FREE.md",
|
||||
"optional": true,
|
||||
"engines": {
|
||||
"node": ">= 0.8.15"
|
||||
}
|
||||
},
|
||||
"node_modules/router": {
|
||||
"version": "2.2.0",
|
||||
"resolved": "https://registry.npmjs.org/router/-/router-2.2.0.tgz",
|
||||
@@ -11421,6 +11658,16 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/stackblur-canvas": {
|
||||
"version": "2.7.0",
|
||||
"resolved": "https://registry.npmjs.org/stackblur-canvas/-/stackblur-canvas-2.7.0.tgz",
|
||||
"integrity": "sha512-yf7OENo23AGJhBriGx0QivY5JP6Y1HbrrDI6WLt6C5auYZXlQrheoY8hD4ibekFKz1HOfE48Ww8kMWMnJD/zcQ==",
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"engines": {
|
||||
"node": ">=0.1.14"
|
||||
}
|
||||
},
|
||||
"node_modules/statuses": {
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz",
|
||||
@@ -11767,6 +12014,16 @@
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/svg-pathdata": {
|
||||
"version": "6.0.3",
|
||||
"resolved": "https://registry.npmjs.org/svg-pathdata/-/svg-pathdata-6.0.3.tgz",
|
||||
"integrity": "sha512-qsjeeq5YjBZ5eMdFuUa4ZosMLxgr5RZ+F+Y1OrDhuOCEInRMA3x74XdBtggJcj9kOeInz0WE+LgCPDkZFlBYJw==",
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"engines": {
|
||||
"node": ">=12.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/tagged-tag": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/tagged-tag/-/tagged-tag-1.0.0.tgz",
|
||||
@@ -11855,6 +12112,16 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/text-segmentation": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/text-segmentation/-/text-segmentation-1.0.3.tgz",
|
||||
"integrity": "sha512-iOiPUo/BGnZ6+54OsWxZidGCsdU8YbE4PSpdPinp7DeMtUJNJBoJ/ouUSTJjHkh1KntHaltHl/gDs2FC4i5+Nw==",
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"utrie": "^1.0.2"
|
||||
}
|
||||
},
|
||||
"node_modules/through2": {
|
||||
"version": "4.0.2",
|
||||
"resolved": "https://registry.npmjs.org/through2/-/through2-4.0.2.tgz",
|
||||
@@ -11889,7 +12156,7 @@
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.0.2.tgz",
|
||||
"integrity": "sha512-W/KYk+NFhkmsYpuHq5JykngiOCnxeVL8v8dFnqxSD8qEEdRfXk1SDM6JzNqcERbcGYj9tMrDQBYV9cjgnunFIg==",
|
||||
"dev": true,
|
||||
"devOptional": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
@@ -12198,7 +12465,7 @@
|
||||
"version": "5.9.3",
|
||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
|
||||
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
|
||||
"dev": true,
|
||||
"devOptional": true,
|
||||
"license": "Apache-2.0",
|
||||
"peer": true,
|
||||
"bin": {
|
||||
@@ -12430,6 +12697,15 @@
|
||||
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/utif2": {
|
||||
"version": "4.1.0",
|
||||
"resolved": "https://registry.npmjs.org/utif2/-/utif2-4.1.0.tgz",
|
||||
"integrity": "sha512-+oknB9FHrJ7oW7A2WZYajOcv4FcDR4CfoGB0dPNfxbi4GO05RRnFmt5oa23+9w32EanrYcSJWspUiJkLMs+37w==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"pako": "^1.0.11"
|
||||
}
|
||||
},
|
||||
"node_modules/util": {
|
||||
"version": "0.12.5",
|
||||
"resolved": "https://registry.npmjs.org/util/-/util-0.12.5.tgz",
|
||||
@@ -12449,6 +12725,16 @@
|
||||
"integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/utrie": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/utrie/-/utrie-1.0.2.tgz",
|
||||
"integrity": "sha512-1MLa5ouZiOmQzUbjbu9VmjLzn1QLXBhwpUa7kdLUQK+KQ5KA9I1vk5U4YHe/X2Ch7PYnJfWuWT+VbuxbGwljhw==",
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"base64-arraybuffer": "^1.0.2"
|
||||
}
|
||||
},
|
||||
"node_modules/uuid": {
|
||||
"version": "13.0.0",
|
||||
"resolved": "https://registry.npmjs.org/uuid/-/uuid-13.0.0.tgz",
|
||||
@@ -12724,6 +13010,24 @@
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/xml": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/xml/-/xml-1.0.1.tgz",
|
||||
"integrity": "sha512-huCv9IH9Tcf95zuYCsQraZtWnJvBtLVE0QHMOs8bWyZAFZNDcYjsPq1nEx8jKA9y+Beo9v+7OBPRisQTjinQMw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/xml-js": {
|
||||
"version": "1.6.11",
|
||||
"resolved": "https://registry.npmjs.org/xml-js/-/xml-js-1.6.11.tgz",
|
||||
"integrity": "sha512-7rVi2KMfwfWFl+GpPg6m80IVMWXLRjO+PxTq7V2CDhoGak0wzYzFgUY2m4XJ47OGdXd8eLE8EmwfAmdjw7lC1g==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"sax": "^1.2.4"
|
||||
},
|
||||
"bin": {
|
||||
"xml-js": "bin/cli.js"
|
||||
}
|
||||
},
|
||||
"node_modules/xml2js": {
|
||||
"version": "0.6.2",
|
||||
"resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.6.2.tgz",
|
||||
|
||||
+4
-1
@@ -9,11 +9,13 @@
|
||||
"lint": "eslint"
|
||||
},
|
||||
"dependencies": {
|
||||
"@prisma/client": "^6.19.2",
|
||||
"class-variance-authority": "^0.7.1",
|
||||
"clsx": "^2.1.1",
|
||||
"docx": "^9.6.0",
|
||||
"jspdf": "^4.2.0",
|
||||
"jszip": "^3.10.1",
|
||||
"lucide-react": "^0.564.0",
|
||||
"@prisma/client": "^6.19.2",
|
||||
"minio": "^8.0.6",
|
||||
"next": "16.1.6",
|
||||
"next-auth": "^4.24.13",
|
||||
@@ -23,6 +25,7 @@
|
||||
"react-dom": "19.2.3",
|
||||
"tailwind-merge": "^3.4.1",
|
||||
"tesseract.js": "^7.0.0",
|
||||
"utif2": "^4.1.0",
|
||||
"uuid": "^13.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
"use client";
|
||||
|
||||
import { useState, useRef } from "react";
|
||||
import { useState, useRef, useMemo } from "react";
|
||||
import {
|
||||
Plus,
|
||||
Pencil,
|
||||
@@ -8,16 +8,17 @@ import {
|
||||
Search,
|
||||
PenTool,
|
||||
Stamp,
|
||||
Type,
|
||||
History,
|
||||
AlertTriangle,
|
||||
Upload,
|
||||
X,
|
||||
Download,
|
||||
FileText,
|
||||
FileDown,
|
||||
ChevronDown,
|
||||
} from "lucide-react";
|
||||
import { Button } from "@/shared/components/ui/button";
|
||||
import { Input } from "@/shared/components/ui/input";
|
||||
import { Label } from "@/shared/components/ui/label";
|
||||
import { Textarea } from "@/shared/components/ui/textarea";
|
||||
import { Badge } from "@/shared/components/ui/badge";
|
||||
import {
|
||||
Card,
|
||||
@@ -39,22 +40,146 @@ import {
|
||||
DialogTitle,
|
||||
DialogFooter,
|
||||
} from "@/shared/components/ui/dialog";
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuTrigger,
|
||||
} from "@/shared/components/ui/dropdown-menu";
|
||||
import type { CompanyId } from "@/core/auth/types";
|
||||
import type { SignatureAsset, SignatureAssetType } from "../types";
|
||||
import { DEFAULT_SUBCATEGORIES } from "../types";
|
||||
import { useSignatures } from "../hooks/use-signatures";
|
||||
|
||||
// --------------- constants ---------------
|
||||
|
||||
const TYPE_LABELS: Record<SignatureAssetType, string> = {
|
||||
signature: "Semnătură",
|
||||
stamp: "Ștampilă",
|
||||
initials: "Inițiale",
|
||||
};
|
||||
|
||||
const TYPE_ICONS: Record<SignatureAssetType, typeof PenTool> = {
|
||||
signature: PenTool,
|
||||
stamp: Stamp,
|
||||
initials: Type,
|
||||
};
|
||||
|
||||
// --------------- TIFF -> PNG preview ---------------
|
||||
|
||||
async function decodeTiffToPreview(file: File): Promise<string> {
|
||||
const UTIF = await import("utif2");
|
||||
const buffer = await file.arrayBuffer();
|
||||
const ifds = UTIF.decode(buffer);
|
||||
if (ifds.length === 0) throw new Error("TIFF gol");
|
||||
const page = ifds[0]!;
|
||||
UTIF.decodeImage(buffer, page);
|
||||
const rgba = UTIF.toRGBA8(page);
|
||||
|
||||
const canvas = document.createElement("canvas");
|
||||
canvas.width = page.width;
|
||||
canvas.height = page.height;
|
||||
const ctx = canvas.getContext("2d")!;
|
||||
const imageData = ctx.createImageData(page.width, page.height);
|
||||
imageData.data.set(rgba);
|
||||
ctx.putImageData(imageData, 0, 0);
|
||||
return canvas.toDataURL("image/png");
|
||||
}
|
||||
|
||||
// --------------- download helpers ---------------
|
||||
|
||||
function triggerDownload(url: string, filename: string) {
|
||||
const a = document.createElement("a");
|
||||
a.href = url;
|
||||
a.download = filename;
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
document.body.removeChild(a);
|
||||
}
|
||||
|
||||
function downloadOriginal(asset: SignatureAsset) {
|
||||
const dataUrl = asset.originalFileData || asset.imageUrl;
|
||||
if (!dataUrl) return;
|
||||
const name = asset.originalFileName || `${asset.label}.png`;
|
||||
triggerDownload(dataUrl, name);
|
||||
}
|
||||
|
||||
async function downloadAsWord(asset: SignatureAsset) {
|
||||
const { Document, Packer, Paragraph, ImageRun } = await import("docx");
|
||||
|
||||
// Use preview (PNG/JPG) for Word since TIFF is not supported
|
||||
const dataUrl = asset.imageUrl;
|
||||
if (!dataUrl) return;
|
||||
|
||||
const base64 = dataUrl.split(",")[1];
|
||||
if (!base64) return;
|
||||
const binary = atob(base64);
|
||||
const bytes = new Uint8Array(binary.length);
|
||||
for (let i = 0; i < binary.length; i++) bytes[i] = binary.charCodeAt(i);
|
||||
|
||||
// Load image to get natural dimensions
|
||||
const img = new Image();
|
||||
await new Promise<void>((resolve) => {
|
||||
img.onload = () => resolve();
|
||||
img.onerror = () => resolve();
|
||||
img.src = dataUrl;
|
||||
});
|
||||
|
||||
const maxW = 400;
|
||||
const ratio = img.naturalHeight / (img.naturalWidth || 1);
|
||||
const w = Math.min(maxW, img.naturalWidth || 200);
|
||||
const h = Math.round(w * ratio);
|
||||
|
||||
const doc = new Document({
|
||||
sections: [
|
||||
{
|
||||
children: [
|
||||
new Paragraph({
|
||||
children: [
|
||||
new ImageRun({
|
||||
data: bytes,
|
||||
transformation: { width: w, height: h },
|
||||
type: dataUrl.includes("image/png") ? "png" : "jpg",
|
||||
}),
|
||||
],
|
||||
}),
|
||||
],
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
const blob = await Packer.toBlob(doc);
|
||||
const objUrl = URL.createObjectURL(blob);
|
||||
triggerDownload(objUrl, `${asset.label}.docx`);
|
||||
URL.revokeObjectURL(objUrl);
|
||||
}
|
||||
|
||||
async function downloadAsPdf(asset: SignatureAsset) {
|
||||
const { jsPDF } = await import("jspdf");
|
||||
|
||||
const dataUrl = asset.imageUrl;
|
||||
if (!dataUrl) return;
|
||||
|
||||
// Load image to get natural dimensions
|
||||
const img = new Image();
|
||||
await new Promise<void>((resolve) => {
|
||||
img.onload = () => resolve();
|
||||
img.onerror = () => resolve();
|
||||
img.src = dataUrl;
|
||||
});
|
||||
|
||||
const doc = new jsPDF();
|
||||
const pageW = doc.internal.pageSize.getWidth();
|
||||
const maxW = pageW - 40;
|
||||
const ratio = img.naturalHeight / (img.naturalWidth || 1);
|
||||
const w = Math.min(maxW, 120);
|
||||
const h = w * ratio;
|
||||
|
||||
const format = dataUrl.includes("image/png") ? "PNG" : "JPEG";
|
||||
doc.addImage(dataUrl, format, 20, 20, w, h);
|
||||
doc.save(`${asset.label}.pdf`);
|
||||
}
|
||||
|
||||
// --------------- main module ---------------
|
||||
|
||||
type ViewMode = "list" | "add" | "edit";
|
||||
|
||||
export function DigitalSignaturesModule() {
|
||||
@@ -74,6 +199,32 @@ export function DigitalSignaturesModule() {
|
||||
const [deletingId, setDeletingId] = useState<string | null>(null);
|
||||
const [versionAsset, setVersionAsset] = useState<SignatureAsset | null>(null);
|
||||
|
||||
// Collect unique subcategories from all assets + defaults
|
||||
const allSubcategories = useMemo(() => {
|
||||
const set = new Set<string>(DEFAULT_SUBCATEGORIES);
|
||||
for (const a of allAssets) {
|
||||
if (a.subcategory) set.add(a.subcategory);
|
||||
}
|
||||
return Array.from(set).sort();
|
||||
}, [allAssets]);
|
||||
|
||||
// Group filtered assets by subcategory
|
||||
const groupedAssets = useMemo(() => {
|
||||
const groups = new Map<string, SignatureAsset[]>();
|
||||
for (const a of assets) {
|
||||
const key = a.subcategory || "Fără subcategorie";
|
||||
const list = groups.get(key) ?? [];
|
||||
list.push(a);
|
||||
groups.set(key, list);
|
||||
}
|
||||
// Sort groups alphabetically, "Fara subcategorie" last
|
||||
return Array.from(groups.entries()).sort((a, b) => {
|
||||
if (a[0] === "Fără subcategorie") return 1;
|
||||
if (b[0] === "Fără subcategorie") return -1;
|
||||
return a[0].localeCompare(b[0]);
|
||||
});
|
||||
}, [assets]);
|
||||
|
||||
const handleSubmit = async (
|
||||
data: Omit<SignatureAsset, "id" | "createdAt" | "updatedAt">,
|
||||
) => {
|
||||
@@ -100,21 +251,10 @@ export function DigitalSignaturesModule() {
|
||||
}
|
||||
};
|
||||
|
||||
const isExpiringSoon = (date?: string) => {
|
||||
if (!date) return false;
|
||||
const diff = new Date(date).getTime() - Date.now();
|
||||
return diff > 0 && diff < 30 * 24 * 60 * 60 * 1000; // 30 days
|
||||
};
|
||||
|
||||
const isExpired = (date?: string) => {
|
||||
if (!date) return false;
|
||||
return new Date(date).getTime() < Date.now();
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
{/* Stats */}
|
||||
<div className="grid grid-cols-2 gap-3 sm:grid-cols-4">
|
||||
<div className="grid grid-cols-2 gap-3 sm:grid-cols-3">
|
||||
<Card>
|
||||
<CardContent className="p-4">
|
||||
<p className="text-xs text-muted-foreground">Total</p>
|
||||
@@ -137,6 +277,7 @@ export function DigitalSignaturesModule() {
|
||||
|
||||
{viewMode === "list" && (
|
||||
<>
|
||||
{/* Filters */}
|
||||
<div className="flex flex-wrap items-center gap-3">
|
||||
<div className="relative min-w-[200px] flex-1">
|
||||
<Search className="absolute left-2.5 top-2.5 h-4 w-4 text-muted-foreground" />
|
||||
@@ -165,131 +306,62 @@ export function DigitalSignaturesModule() {
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<Select
|
||||
value={filters.subcategory || "__all__"}
|
||||
onValueChange={(v) =>
|
||||
updateFilter("subcategory", v === "__all__" ? "" : v)
|
||||
}
|
||||
>
|
||||
<SelectTrigger className="w-[180px]">
|
||||
<SelectValue placeholder="Subcategorie" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="__all__">Toate subcategoriile</SelectItem>
|
||||
{allSubcategories.map((s) => (
|
||||
<SelectItem key={s} value={s}>
|
||||
{s}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<Button onClick={() => setViewMode("add")} className="shrink-0">
|
||||
<Plus className="mr-1.5 h-4 w-4" /> Adaugă
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{/* Asset list grouped by subcategory */}
|
||||
{loading ? (
|
||||
<p className="py-8 text-center text-sm text-muted-foreground">
|
||||
Se încarcă...
|
||||
</p>
|
||||
) : assets.length === 0 ? (
|
||||
<p className="py-8 text-center text-sm text-muted-foreground">
|
||||
Niciun element găsit. Adaugă o semnătură, ștampilă sau inițiale.
|
||||
Niciun element găsit. Adaugă o semnătură sau ștampilă.
|
||||
</p>
|
||||
) : (
|
||||
<div className="grid gap-3 sm:grid-cols-2 lg:grid-cols-3">
|
||||
{assets.map((asset) => {
|
||||
const Icon = TYPE_ICONS[asset.type];
|
||||
const expired = isExpired(asset.expirationDate);
|
||||
const expiringSoon = isExpiringSoon(asset.expirationDate);
|
||||
return (
|
||||
<Card
|
||||
key={asset.id}
|
||||
className={`group relative ${expired ? "border-destructive/50" : expiringSoon ? "border-yellow-500/50" : ""}`}
|
||||
>
|
||||
<CardContent className="p-4">
|
||||
<div className="absolute right-2 top-2 flex gap-1 opacity-0 transition-opacity group-hover:opacity-100">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="h-7 w-7"
|
||||
title="Versiune nouă"
|
||||
onClick={() => setVersionAsset(asset)}
|
||||
>
|
||||
<History className="h-3.5 w-3.5" />
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="h-7 w-7"
|
||||
onClick={() => {
|
||||
setEditingAsset(asset);
|
||||
setViewMode("edit");
|
||||
}}
|
||||
>
|
||||
<Pencil className="h-3.5 w-3.5" />
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="h-7 w-7 text-destructive"
|
||||
onClick={() => setDeletingId(asset.id)}
|
||||
>
|
||||
<Trash2 className="h-3.5 w-3.5" />
|
||||
</Button>
|
||||
</div>
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="flex h-12 w-12 items-center justify-center rounded-lg border bg-muted/30">
|
||||
{asset.imageUrl ? (
|
||||
// eslint-disable-next-line @next/next/no-img-element
|
||||
<img
|
||||
src={asset.imageUrl}
|
||||
alt={asset.label}
|
||||
className="max-h-10 max-w-10 object-contain"
|
||||
/>
|
||||
) : (
|
||||
<Icon className="h-6 w-6 text-muted-foreground" />
|
||||
)}
|
||||
</div>
|
||||
<div className="min-w-0 flex-1">
|
||||
<p className="font-medium">{asset.label}</p>
|
||||
<div className="flex flex-wrap items-center gap-1">
|
||||
<Badge variant="outline" className="text-[10px]">
|
||||
{TYPE_LABELS[asset.type]}
|
||||
</Badge>
|
||||
<span className="text-xs text-muted-foreground">
|
||||
{asset.owner}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/* Metadata row */}
|
||||
<div className="mt-2 space-y-1">
|
||||
{asset.legalStatus && (
|
||||
<p className="text-xs text-muted-foreground">
|
||||
Status legal: {asset.legalStatus}
|
||||
</p>
|
||||
)}
|
||||
{asset.expirationDate && (
|
||||
<div className="flex items-center gap-1 text-xs">
|
||||
{(expired || expiringSoon) && (
|
||||
<AlertTriangle className="h-3 w-3 text-yellow-500" />
|
||||
)}
|
||||
<span
|
||||
className={
|
||||
expired
|
||||
? "text-destructive font-medium"
|
||||
: expiringSoon
|
||||
? "text-yellow-600 font-medium"
|
||||
: "text-muted-foreground"
|
||||
}
|
||||
>
|
||||
{expired
|
||||
? "Expirat"
|
||||
: expiringSoon
|
||||
? "Expiră curând"
|
||||
: "Expiră"}
|
||||
: {asset.expirationDate}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
{asset.usageNotes && (
|
||||
<p className="text-xs text-muted-foreground line-clamp-1">
|
||||
Note: {asset.usageNotes}
|
||||
</p>
|
||||
)}
|
||||
{(asset.versions ?? []).length > 0 && (
|
||||
<p className="text-xs text-muted-foreground">
|
||||
Versiuni: {(asset.versions ?? []).length + 1}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
})}
|
||||
<div className="space-y-6">
|
||||
{groupedAssets.map(([group, items]) => (
|
||||
<div key={group}>
|
||||
<h3 className="mb-3 text-sm font-medium text-muted-foreground">
|
||||
{group}{" "}
|
||||
<span className="text-xs">({items.length})</span>
|
||||
</h3>
|
||||
<div className="grid gap-3 sm:grid-cols-2 lg:grid-cols-3">
|
||||
{items.map((asset) => (
|
||||
<AssetCard
|
||||
key={asset.id}
|
||||
asset={asset}
|
||||
onEdit={() => {
|
||||
setEditingAsset(asset);
|
||||
setViewMode("edit");
|
||||
}}
|
||||
onDelete={() => setDeletingId(asset.id)}
|
||||
onVersion={() => setVersionAsset(asset)}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
@@ -305,6 +377,7 @@ export function DigitalSignaturesModule() {
|
||||
<CardContent>
|
||||
<AssetForm
|
||||
initial={editingAsset ?? undefined}
|
||||
allSubcategories={allSubcategories}
|
||||
onSubmit={handleSubmit}
|
||||
onCancel={() => {
|
||||
setViewMode("list");
|
||||
@@ -363,20 +436,182 @@ export function DigitalSignaturesModule() {
|
||||
);
|
||||
}
|
||||
|
||||
// --------------- asset card ---------------
|
||||
|
||||
function AssetCard({
|
||||
asset,
|
||||
onEdit,
|
||||
onDelete,
|
||||
onVersion,
|
||||
}: {
|
||||
asset: SignatureAsset;
|
||||
onEdit: () => void;
|
||||
onDelete: () => void;
|
||||
onVersion: () => void;
|
||||
}) {
|
||||
const Icon = TYPE_ICONS[asset.type];
|
||||
|
||||
return (
|
||||
<Card className="group relative">
|
||||
<CardContent className="p-4">
|
||||
{/* Hover actions */}
|
||||
<div className="absolute right-2 top-2 flex gap-1 opacity-0 transition-opacity group-hover:opacity-100">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="h-7 w-7"
|
||||
title="Versiune nouă"
|
||||
onClick={onVersion}
|
||||
>
|
||||
<History className="h-3.5 w-3.5" />
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="h-7 w-7"
|
||||
onClick={onEdit}
|
||||
>
|
||||
<Pencil className="h-3.5 w-3.5" />
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="h-7 w-7 text-destructive"
|
||||
onClick={onDelete}
|
||||
>
|
||||
<Trash2 className="h-3.5 w-3.5" />
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{/* Image + label */}
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="flex h-12 w-12 items-center justify-center rounded-lg border bg-muted/30">
|
||||
{asset.imageUrl ? (
|
||||
// eslint-disable-next-line @next/next/no-img-element
|
||||
<img
|
||||
src={asset.imageUrl}
|
||||
alt={asset.label}
|
||||
className="max-h-10 max-w-10 object-contain"
|
||||
/>
|
||||
) : (
|
||||
<Icon className="h-6 w-6 text-muted-foreground" />
|
||||
)}
|
||||
</div>
|
||||
<div className="min-w-0 flex-1">
|
||||
<p className="font-medium">{asset.label}</p>
|
||||
<div className="flex flex-wrap items-center gap-1">
|
||||
<Badge variant="outline" className="text-[10px]">
|
||||
{TYPE_LABELS[asset.type]}
|
||||
</Badge>
|
||||
<span className="text-xs text-muted-foreground">
|
||||
{asset.owner}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Metadata */}
|
||||
<div className="mt-2 space-y-1">
|
||||
{(asset.versions ?? []).length > 0 && (
|
||||
<p className="text-xs text-muted-foreground">
|
||||
Versiuni: {(asset.versions ?? []).length + 1}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Download buttons */}
|
||||
{asset.imageUrl && (
|
||||
<div className="mt-3 border-t pt-2">
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="h-7 w-full text-xs"
|
||||
>
|
||||
<Download className="mr-1.5 h-3 w-3" />
|
||||
Descarcă
|
||||
<ChevronDown className="ml-auto h-3 w-3" />
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="start">
|
||||
<DropdownMenuItem onClick={() => downloadOriginal(asset)}>
|
||||
<FileDown className="mr-2 h-4 w-4" />
|
||||
<span>Original</span>
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem
|
||||
onClick={() => void downloadAsWord(asset)}
|
||||
>
|
||||
<FileText className="mr-2 h-4 w-4" />
|
||||
<span>Word (.docx)</span>
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem
|
||||
onClick={() => void downloadAsPdf(asset)}
|
||||
>
|
||||
<FileDown className="mr-2 h-4 w-4" />
|
||||
<span>PDF (.pdf)</span>
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</div>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
// --------------- image upload (TIFF support) ---------------
|
||||
|
||||
function ImageUploadField({
|
||||
value,
|
||||
onChange,
|
||||
onOriginalFile,
|
||||
}: {
|
||||
value: string;
|
||||
onChange: (v: string) => void;
|
||||
onChange: (previewUrl: string) => void;
|
||||
onOriginalFile?: (dataUrl: string, fileName: string) => void;
|
||||
}) {
|
||||
const fileRef = useRef<HTMLInputElement>(null);
|
||||
const [processing, setProcessing] = useState(false);
|
||||
|
||||
const handleFile = (file: File) => {
|
||||
if (!file.type.startsWith("image/")) return;
|
||||
const reader = new FileReader();
|
||||
reader.onload = (e) => onChange(e.target?.result as string);
|
||||
reader.readAsDataURL(file);
|
||||
const handleFile = async (file: File) => {
|
||||
const isTiff =
|
||||
file.type === "image/tiff" || /\.tiff?$/i.test(file.name);
|
||||
|
||||
if (isTiff) {
|
||||
setProcessing(true);
|
||||
try {
|
||||
const previewUrl = await decodeTiffToPreview(file);
|
||||
onChange(previewUrl);
|
||||
|
||||
// Also store original TIFF
|
||||
if (onOriginalFile) {
|
||||
const reader = new FileReader();
|
||||
reader.onload = (e) =>
|
||||
onOriginalFile(e.target?.result as string, file.name);
|
||||
reader.readAsDataURL(file);
|
||||
}
|
||||
} catch {
|
||||
// Fallback: try as regular image
|
||||
const reader = new FileReader();
|
||||
reader.onload = (e) => {
|
||||
const url = e.target?.result as string;
|
||||
onChange(url);
|
||||
if (onOriginalFile) onOriginalFile(url, file.name);
|
||||
};
|
||||
reader.readAsDataURL(file);
|
||||
} finally {
|
||||
setProcessing(false);
|
||||
}
|
||||
} else if (file.type.startsWith("image/")) {
|
||||
const reader = new FileReader();
|
||||
reader.onload = (e) => {
|
||||
const url = e.target?.result as string;
|
||||
onChange(url);
|
||||
if (onOriginalFile) onOriginalFile(url, file.name);
|
||||
};
|
||||
reader.readAsDataURL(file);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
@@ -388,10 +623,12 @@ function ImageUploadField({
|
||||
onDrop={(e) => {
|
||||
e.preventDefault();
|
||||
const f = e.dataTransfer.files[0];
|
||||
if (f) handleFile(f);
|
||||
if (f) void handleFile(f);
|
||||
}}
|
||||
>
|
||||
{value ? (
|
||||
{processing ? (
|
||||
<span className="animate-pulse">Se procesează TIFF...</span>
|
||||
) : value ? (
|
||||
// eslint-disable-next-line @next/next/no-img-element
|
||||
<img
|
||||
src={value}
|
||||
@@ -401,18 +638,23 @@ function ImageUploadField({
|
||||
) : (
|
||||
<>
|
||||
<Upload className="h-6 w-6" />
|
||||
<span>Trage imaginea aici sau apasă pentru a selecta</span>
|
||||
<span>
|
||||
Trage imaginea aici sau apasă pentru a selecta
|
||||
</span>
|
||||
<span className="text-xs text-muted-foreground/60">
|
||||
PNG, JPG, TIFF
|
||||
</span>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
<input
|
||||
ref={fileRef}
|
||||
type="file"
|
||||
accept="image/*"
|
||||
accept="image/*,.tif,.tiff"
|
||||
className="hidden"
|
||||
onChange={(e) => {
|
||||
const f = e.target.files?.[0];
|
||||
if (f) handleFile(f);
|
||||
if (f) void handleFile(f);
|
||||
}}
|
||||
/>
|
||||
{value && (
|
||||
@@ -421,7 +663,10 @@ function ImageUploadField({
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="text-xs text-muted-foreground"
|
||||
onClick={() => onChange("")}
|
||||
onClick={() => {
|
||||
onChange("");
|
||||
if (onOriginalFile) onOriginalFile("", "");
|
||||
}}
|
||||
>
|
||||
<X className="mr-1 h-3 w-3" /> Elimină imaginea
|
||||
</Button>
|
||||
@@ -430,6 +675,8 @@ function ImageUploadField({
|
||||
);
|
||||
}
|
||||
|
||||
// --------------- add version form ---------------
|
||||
|
||||
function AddVersionForm({
|
||||
onSubmit,
|
||||
onCancel,
|
||||
@@ -501,12 +748,16 @@ function AddVersionForm({
|
||||
);
|
||||
}
|
||||
|
||||
// --------------- asset form ---------------
|
||||
|
||||
function AssetForm({
|
||||
initial,
|
||||
allSubcategories,
|
||||
onSubmit,
|
||||
onCancel,
|
||||
}: {
|
||||
initial?: SignatureAsset;
|
||||
allSubcategories: string[];
|
||||
onSubmit: (
|
||||
data: Omit<SignatureAsset, "id" | "createdAt" | "updatedAt">,
|
||||
) => void;
|
||||
@@ -517,15 +768,19 @@ function AssetForm({
|
||||
initial?.type ?? "signature",
|
||||
);
|
||||
const [imageUrl, setImageUrl] = useState(initial?.imageUrl ?? "");
|
||||
const [originalFileData, setOriginalFileData] = useState(
|
||||
initial?.originalFileData ?? "",
|
||||
);
|
||||
const [originalFileName, setOriginalFileName] = useState(
|
||||
initial?.originalFileName ?? "",
|
||||
);
|
||||
const [owner, setOwner] = useState(initial?.owner ?? "");
|
||||
const [company, setCompany] = useState<CompanyId>(
|
||||
initial?.company ?? "beletage",
|
||||
);
|
||||
const [expirationDate, setExpirationDate] = useState(
|
||||
initial?.expirationDate ?? "",
|
||||
const [subcategory, setSubcategory] = useState(
|
||||
initial?.subcategory ?? "",
|
||||
);
|
||||
const [legalStatus, setLegalStatus] = useState(initial?.legalStatus ?? "");
|
||||
const [usageNotes, setUsageNotes] = useState(initial?.usageNotes ?? "");
|
||||
const [tags, setTags] = useState<string[]>(initial?.tags ?? []);
|
||||
const [tagInput, setTagInput] = useState("");
|
||||
|
||||
@@ -552,11 +807,11 @@ function AssetForm({
|
||||
label,
|
||||
type,
|
||||
imageUrl,
|
||||
originalFileData: originalFileData || undefined,
|
||||
originalFileName: originalFileName || undefined,
|
||||
owner,
|
||||
company,
|
||||
expirationDate: expirationDate || undefined,
|
||||
legalStatus,
|
||||
usageNotes,
|
||||
subcategory,
|
||||
versions: initial?.versions ?? [],
|
||||
tags,
|
||||
visibility: initial?.visibility ?? "all",
|
||||
@@ -586,7 +841,6 @@ function AssetForm({
|
||||
<SelectContent>
|
||||
<SelectItem value="signature">Semnătură</SelectItem>
|
||||
<SelectItem value="stamp">Ștampilă</SelectItem>
|
||||
<SelectItem value="initials">Inițiale</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
@@ -619,37 +873,31 @@ function AssetForm({
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<Label>Imagine</Label>
|
||||
<Label>Subcategorie</Label>
|
||||
<div className="mt-1">
|
||||
<ImageUploadField value={imageUrl} onChange={setImageUrl} />
|
||||
<Input
|
||||
list="ds-subcategories"
|
||||
value={subcategory}
|
||||
onChange={(e) => setSubcategory(e.target.value)}
|
||||
placeholder="Selectează sau scrie o subcategorie..."
|
||||
/>
|
||||
<datalist id="ds-subcategories">
|
||||
{allSubcategories.map((s) => (
|
||||
<option key={s} value={s} />
|
||||
))}
|
||||
</datalist>
|
||||
</div>
|
||||
</div>
|
||||
<div className="grid gap-4 sm:grid-cols-3">
|
||||
<div>
|
||||
<Label>Data expirare</Label>
|
||||
<Input
|
||||
type="date"
|
||||
value={expirationDate}
|
||||
onChange={(e) => setExpirationDate(e.target.value)}
|
||||
className="mt-1"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<Label>Status legal</Label>
|
||||
<Input
|
||||
value={legalStatus}
|
||||
onChange={(e) => setLegalStatus(e.target.value)}
|
||||
className="mt-1"
|
||||
placeholder="Valid, Anulat..."
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<Label>Note utilizare</Label>
|
||||
<Input
|
||||
value={usageNotes}
|
||||
onChange={(e) => setUsageNotes(e.target.value)}
|
||||
className="mt-1"
|
||||
placeholder="Doar pentru contracte..."
|
||||
<div>
|
||||
<Label>Imagine</Label>
|
||||
<div className="mt-1">
|
||||
<ImageUploadField
|
||||
value={imageUrl}
|
||||
onChange={setImageUrl}
|
||||
onOriginalFile={(data, name) => {
|
||||
setOriginalFileData(data);
|
||||
setOriginalFileName(name);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@@ -679,7 +927,9 @@ function AssetForm({
|
||||
if (tagInput.trim()) addTag(tagInput);
|
||||
}}
|
||||
placeholder={
|
||||
tags.length === 0 ? "Adaugă etichete (Enter sau virgulă)..." : ""
|
||||
tags.length === 0
|
||||
? "Adaugă etichete (Enter sau virgulă)..."
|
||||
: ""
|
||||
}
|
||||
className="min-w-[120px] flex-1 bg-transparent text-sm outline-none placeholder:text-muted-foreground"
|
||||
/>
|
||||
|
||||
@@ -14,6 +14,7 @@ const PREFIX = "sig:";
|
||||
export interface SignatureFilters {
|
||||
search: string;
|
||||
type: SignatureAssetType | "all";
|
||||
subcategory: string; // "" means all
|
||||
}
|
||||
|
||||
export function useSignatures() {
|
||||
@@ -23,6 +24,7 @@ export function useSignatures() {
|
||||
const [filters, setFilters] = useState<SignatureFilters>({
|
||||
search: "",
|
||||
type: "all",
|
||||
subcategory: "",
|
||||
});
|
||||
|
||||
const refresh = useCallback(async () => {
|
||||
@@ -110,10 +112,14 @@ export function useSignatures() {
|
||||
|
||||
const filteredAssets = assets.filter((a) => {
|
||||
if (filters.type !== "all" && a.type !== filters.type) return false;
|
||||
if (filters.subcategory && a.subcategory !== filters.subcategory)
|
||||
return false;
|
||||
if (filters.search) {
|
||||
const q = filters.search.toLowerCase();
|
||||
return (
|
||||
a.label.toLowerCase().includes(q) || a.owner.toLowerCase().includes(q)
|
||||
a.label.toLowerCase().includes(q) ||
|
||||
a.owner.toLowerCase().includes(q) ||
|
||||
a.subcategory.toLowerCase().includes(q)
|
||||
);
|
||||
}
|
||||
return true;
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import type { Visibility } from '@/core/module-registry/types';
|
||||
import type { CompanyId } from '@/core/auth/types';
|
||||
import type { Visibility } from "@/core/module-registry/types";
|
||||
import type { CompanyId } from "@/core/auth/types";
|
||||
|
||||
export type SignatureAssetType = 'signature' | 'stamp' | 'initials';
|
||||
export type SignatureAssetType = "signature" | "stamp";
|
||||
|
||||
/** Version history entry */
|
||||
export interface AssetVersion {
|
||||
@@ -11,19 +11,30 @@ export interface AssetVersion {
|
||||
createdAt: string;
|
||||
}
|
||||
|
||||
/** Default subcategory options (users can add more) */
|
||||
export const DEFAULT_SUBCATEGORIES = [
|
||||
"Colaboratori",
|
||||
"Experți tehnici",
|
||||
"Verificatori de proiect",
|
||||
"Proiectanți",
|
||||
"Diriginți de șantier",
|
||||
"Responsabili tehnici",
|
||||
] as const;
|
||||
|
||||
export interface SignatureAsset {
|
||||
id: string;
|
||||
label: string;
|
||||
type: SignatureAssetType;
|
||||
/** Preview image data URL (PNG/JPG — browsers can render this) */
|
||||
imageUrl: string;
|
||||
/** Original file data URL (for TIFF originals or same as imageUrl) */
|
||||
originalFileData?: string;
|
||||
/** Original file name for downloads */
|
||||
originalFileName?: string;
|
||||
owner: string;
|
||||
company: CompanyId;
|
||||
/** Expiration date (YYYY-MM-DD) */
|
||||
expirationDate?: string;
|
||||
/** Legal status description */
|
||||
legalStatus: string;
|
||||
/** Usage notes */
|
||||
usageNotes: string;
|
||||
/** Subcategory for grouping (e.g. "Colaboratori firma X") */
|
||||
subcategory: string;
|
||||
/** Version history */
|
||||
versions: AssetVersion[];
|
||||
tags: string[];
|
||||
|
||||
Reference in New Issue
Block a user