From 7774a3b6223a632c9d40e2ae07b529b48639c404 Mon Sep 17 00:00:00 2001 From: AI Assistant Date: Sat, 28 Feb 2026 00:18:29 +0200 Subject: [PATCH] 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 --- package-lock.json | 378 +++- package.json | 5 +- .../components/digital-signatures-module.tsx | 1642 ++++++++++------- .../hooks/use-signatures.ts | 8 +- src/modules/digital-signatures/types.ts | 29 +- 5 files changed, 1318 insertions(+), 744 deletions(-) diff --git a/package-lock.json b/package-lock.json index 2170d82..7927723 100644 --- a/package-lock.json +++ b/package-lock.json @@ -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", diff --git a/package.json b/package.json index d4247ca..b3a2fef 100644 --- a/package.json +++ b/package.json @@ -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": { diff --git a/src/modules/digital-signatures/components/digital-signatures-module.tsx b/src/modules/digital-signatures/components/digital-signatures-module.tsx index 9548d39..596a63a 100644 --- a/src/modules/digital-signatures/components/digital-signatures-module.tsx +++ b/src/modules/digital-signatures/components/digital-signatures-module.tsx @@ -1,696 +1,946 @@ -"use client"; - -import { useState, useRef } from "react"; -import { - Plus, - Pencil, - Trash2, - Search, - PenTool, - Stamp, - Type, - History, - AlertTriangle, - Upload, - X, -} 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, - CardContent, - CardHeader, - CardTitle, -} from "@/shared/components/ui/card"; -import { - Select, - SelectContent, - SelectItem, - SelectTrigger, - SelectValue, -} from "@/shared/components/ui/select"; -import { - Dialog, - DialogContent, - DialogHeader, - DialogTitle, - DialogFooter, -} from "@/shared/components/ui/dialog"; -import type { CompanyId } from "@/core/auth/types"; -import type { SignatureAsset, SignatureAssetType } from "../types"; -import { useSignatures } from "../hooks/use-signatures"; - -const TYPE_LABELS: Record = { - signature: "Semnătură", - stamp: "Ștampilă", - initials: "Inițiale", -}; - -const TYPE_ICONS: Record = { - signature: PenTool, - stamp: Stamp, - initials: Type, -}; - -type ViewMode = "list" | "add" | "edit"; - -export function DigitalSignaturesModule() { - const { - assets, - allAssets, - loading, - filters, - updateFilter, - addAsset, - updateAsset, - addVersion, - removeAsset, - } = useSignatures(); - const [viewMode, setViewMode] = useState("list"); - const [editingAsset, setEditingAsset] = useState(null); - const [deletingId, setDeletingId] = useState(null); - const [versionAsset, setVersionAsset] = useState(null); - - const handleSubmit = async ( - data: Omit, - ) => { - if (viewMode === "edit" && editingAsset) { - await updateAsset(editingAsset.id, data); - } else { - await addAsset(data); - } - setViewMode("list"); - setEditingAsset(null); - }; - - const handleDeleteConfirm = async () => { - if (deletingId) { - await removeAsset(deletingId); - setDeletingId(null); - } - }; - - const handleAddVersion = async (imageUrl: string, notes: string) => { - if (versionAsset) { - await addVersion(versionAsset.id, imageUrl, notes); - setVersionAsset(null); - } - }; - - 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 ( -
- {/* Stats */} -
- - -

Total

-

{allAssets.length}

-
-
- {(Object.keys(TYPE_LABELS) as SignatureAssetType[]).map((type) => ( - - -

- {TYPE_LABELS[type]} -

-

- {allAssets.filter((a) => a.type === type).length} -

-
-
- ))} -
- - {viewMode === "list" && ( - <> -
-
- - updateFilter("search", e.target.value)} - className="pl-9" - /> -
- - -
- - {loading ? ( -

- Se încarcă... -

- ) : assets.length === 0 ? ( -

- Niciun element găsit. Adaugă o semnătură, ștampilă sau inițiale. -

- ) : ( -
- {assets.map((asset) => { - const Icon = TYPE_ICONS[asset.type]; - const expired = isExpired(asset.expirationDate); - const expiringSoon = isExpiringSoon(asset.expirationDate); - return ( - - -
- - - -
-
-
- {asset.imageUrl ? ( - // eslint-disable-next-line @next/next/no-img-element - {asset.label} - ) : ( - - )} -
-
-

{asset.label}

-
- - {TYPE_LABELS[asset.type]} - - - {asset.owner} - -
-
-
- {/* Metadata row */} -
- {asset.legalStatus && ( -

- Status legal: {asset.legalStatus} -

- )} - {asset.expirationDate && ( -
- {(expired || expiringSoon) && ( - - )} - - {expired - ? "Expirat" - : expiringSoon - ? "Expiră curând" - : "Expiră"} - : {asset.expirationDate} - -
- )} - {asset.usageNotes && ( -

- Note: {asset.usageNotes} -

- )} - {(asset.versions ?? []).length > 0 && ( -

- Versiuni: {(asset.versions ?? []).length + 1} -

- )} -
-
-
- ); - })} -
- )} - - )} - - {(viewMode === "add" || viewMode === "edit") && ( - - - - {viewMode === "edit" ? "Editare" : "Element nou"} - - - - { - setViewMode("list"); - setEditingAsset(null); - }} - /> - - - )} - - {/* Delete confirmation */} - { - if (!open) setDeletingId(null); - }} - > - - - Confirmare ștergere - -

- Ești sigur că vrei să ștergi acest element? Acțiunea este - ireversibilă. -

- - - - -
-
- - {/* Add version dialog */} - { - if (!open) setVersionAsset(null); - }} - > - - - Versiune nouă — {versionAsset?.label} - - setVersionAsset(null)} - history={versionAsset?.versions ?? []} - /> - - -
- ); -} - -function ImageUploadField({ - value, - onChange, -}: { - value: string; - onChange: (v: string) => void; -}) { - const fileRef = useRef(null); - - 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); - }; - - return ( -
-
fileRef.current?.click()} - onDragOver={(e) => e.preventDefault()} - onDrop={(e) => { - e.preventDefault(); - const f = e.dataTransfer.files[0]; - if (f) handleFile(f); - }} - > - {value ? ( - // eslint-disable-next-line @next/next/no-img-element - preview - ) : ( - <> - - Trage imaginea aici sau apasă pentru a selecta - - )} -
- { - const f = e.target.files?.[0]; - if (f) handleFile(f); - }} - /> - {value && ( - - )} -
- ); -} - -function AddVersionForm({ - onSubmit, - onCancel, - history, -}: { - onSubmit: (imageUrl: string, notes: string) => void; - onCancel: () => void; - history: Array<{ - id: string; - imageUrl: string; - notes: string; - createdAt: string; - }>; -}) { - const [imageUrl, setImageUrl] = useState(""); - const [notes, setNotes] = useState(""); - - return ( -
- {history.length > 0 && ( -
-

- Istoric versiuni -

- {history.map((v) => ( -
- - {v.notes || "Fără note"} - - - {v.createdAt.slice(0, 10)} - -
- ))} -
- )} -
- -
- -
-
-
- - setNotes(e.target.value)} - className="mt-1" - placeholder="Ce s-a schimbat..." - /> -
-
- - -
-
- ); -} - -function AssetForm({ - initial, - onSubmit, - onCancel, -}: { - initial?: SignatureAsset; - onSubmit: ( - data: Omit, - ) => void; - onCancel: () => void; -}) { - const [label, setLabel] = useState(initial?.label ?? ""); - const [type, setType] = useState( - initial?.type ?? "signature", - ); - const [imageUrl, setImageUrl] = useState(initial?.imageUrl ?? ""); - const [owner, setOwner] = useState(initial?.owner ?? ""); - const [company, setCompany] = useState( - initial?.company ?? "beletage", - ); - const [expirationDate, setExpirationDate] = useState( - initial?.expirationDate ?? "", - ); - const [legalStatus, setLegalStatus] = useState(initial?.legalStatus ?? ""); - const [usageNotes, setUsageNotes] = useState(initial?.usageNotes ?? ""); - const [tags, setTags] = useState(initial?.tags ?? []); - const [tagInput, setTagInput] = useState(""); - - const addTag = (raw: string) => { - const t = raw.trim().toLowerCase(); - if (t && !tags.includes(t)) setTags((prev) => [...prev, t]); - setTagInput(""); - }; - - const handleTagKeyDown = (e: React.KeyboardEvent) => { - if (e.key === "Enter" || e.key === ",") { - e.preventDefault(); - addTag(tagInput); - } - if (e.key === "Backspace" && tagInput === "" && tags.length > 0) - setTags((prev) => prev.slice(0, -1)); - }; - - return ( -
{ - e.preventDefault(); - onSubmit({ - label, - type, - imageUrl, - owner, - company, - expirationDate: expirationDate || undefined, - legalStatus, - usageNotes, - versions: initial?.versions ?? [], - tags, - visibility: initial?.visibility ?? "all", - }); - }} - className="space-y-4" - > -
-
- - setLabel(e.target.value)} - className="mt-1" - required - /> -
-
- - -
-
-
-
- - setOwner(e.target.value)} - className="mt-1" - /> -
-
- - -
-
-
- -
- -
-
-
-
- - setExpirationDate(e.target.value)} - className="mt-1" - /> -
-
- - setLegalStatus(e.target.value)} - className="mt-1" - placeholder="Valid, Anulat..." - /> -
-
- - setUsageNotes(e.target.value)} - className="mt-1" - placeholder="Doar pentru contracte..." - /> -
-
-
- -
- {tags.map((tag) => ( - - {tag} - - - ))} - setTagInput(e.target.value)} - onKeyDown={handleTagKeyDown} - onBlur={() => { - if (tagInput.trim()) addTag(tagInput); - }} - placeholder={ - tags.length === 0 ? "Adaugă etichete (Enter sau virgulă)..." : "" - } - className="min-w-[120px] flex-1 bg-transparent text-sm outline-none placeholder:text-muted-foreground" - /> -
-
-
- - -
-
- ); -} +"use client"; + +import { useState, useRef, useMemo } from "react"; +import { + Plus, + Pencil, + Trash2, + Search, + PenTool, + Stamp, + History, + 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 { Badge } from "@/shared/components/ui/badge"; +import { + Card, + CardContent, + CardHeader, + CardTitle, +} from "@/shared/components/ui/card"; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/shared/components/ui/select"; +import { + Dialog, + DialogContent, + DialogHeader, + 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 = { + signature: "Semnătură", + stamp: "Ștampilă", +}; + +const TYPE_ICONS: Record = { + signature: PenTool, + stamp: Stamp, +}; + +// --------------- TIFF -> PNG preview --------------- + +async function decodeTiffToPreview(file: File): Promise { + 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((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((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() { + const { + assets, + allAssets, + loading, + filters, + updateFilter, + addAsset, + updateAsset, + addVersion, + removeAsset, + } = useSignatures(); + const [viewMode, setViewMode] = useState("list"); + const [editingAsset, setEditingAsset] = useState(null); + const [deletingId, setDeletingId] = useState(null); + const [versionAsset, setVersionAsset] = useState(null); + + // Collect unique subcategories from all assets + defaults + const allSubcategories = useMemo(() => { + const set = new Set(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(); + 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, + ) => { + if (viewMode === "edit" && editingAsset) { + await updateAsset(editingAsset.id, data); + } else { + await addAsset(data); + } + setViewMode("list"); + setEditingAsset(null); + }; + + const handleDeleteConfirm = async () => { + if (deletingId) { + await removeAsset(deletingId); + setDeletingId(null); + } + }; + + const handleAddVersion = async (imageUrl: string, notes: string) => { + if (versionAsset) { + await addVersion(versionAsset.id, imageUrl, notes); + setVersionAsset(null); + } + }; + + return ( +
+ {/* Stats */} +
+ + +

Total

+

{allAssets.length}

+
+
+ {(Object.keys(TYPE_LABELS) as SignatureAssetType[]).map((type) => ( + + +

+ {TYPE_LABELS[type]} +

+

+ {allAssets.filter((a) => a.type === type).length} +

+
+
+ ))} +
+ + {viewMode === "list" && ( + <> + {/* Filters */} +
+
+ + updateFilter("search", e.target.value)} + className="pl-9" + /> +
+ + + +
+ + {/* Asset list grouped by subcategory */} + {loading ? ( +

+ Se încarcă... +

+ ) : assets.length === 0 ? ( +

+ Niciun element găsit. Adaugă o semnătură sau ștampilă. +

+ ) : ( +
+ {groupedAssets.map(([group, items]) => ( +
+

+ {group}{" "} + ({items.length}) +

+
+ {items.map((asset) => ( + { + setEditingAsset(asset); + setViewMode("edit"); + }} + onDelete={() => setDeletingId(asset.id)} + onVersion={() => setVersionAsset(asset)} + /> + ))} +
+
+ ))} +
+ )} + + )} + + {(viewMode === "add" || viewMode === "edit") && ( + + + + {viewMode === "edit" ? "Editare" : "Element nou"} + + + + { + setViewMode("list"); + setEditingAsset(null); + }} + /> + + + )} + + {/* Delete confirmation */} + { + if (!open) setDeletingId(null); + }} + > + + + Confirmare ștergere + +

+ Ești sigur că vrei să ștergi acest element? Acțiunea este + ireversibilă. +

+ + + + +
+
+ + {/* Add version dialog */} + { + if (!open) setVersionAsset(null); + }} + > + + + Versiune nouă — {versionAsset?.label} + + setVersionAsset(null)} + history={versionAsset?.versions ?? []} + /> + + +
+ ); +} + +// --------------- asset card --------------- + +function AssetCard({ + asset, + onEdit, + onDelete, + onVersion, +}: { + asset: SignatureAsset; + onEdit: () => void; + onDelete: () => void; + onVersion: () => void; +}) { + const Icon = TYPE_ICONS[asset.type]; + + return ( + + + {/* Hover actions */} +
+ + + +
+ + {/* Image + label */} +
+
+ {asset.imageUrl ? ( + // eslint-disable-next-line @next/next/no-img-element + {asset.label} + ) : ( + + )} +
+
+

{asset.label}

+
+ + {TYPE_LABELS[asset.type]} + + + {asset.owner} + +
+
+
+ + {/* Metadata */} +
+ {(asset.versions ?? []).length > 0 && ( +

+ Versiuni: {(asset.versions ?? []).length + 1} +

+ )} +
+ + {/* Download buttons */} + {asset.imageUrl && ( +
+ + + + + + downloadOriginal(asset)}> + + Original + + void downloadAsWord(asset)} + > + + Word (.docx) + + void downloadAsPdf(asset)} + > + + PDF (.pdf) + + + +
+ )} +
+
+ ); +} + +// --------------- image upload (TIFF support) --------------- + +function ImageUploadField({ + value, + onChange, + onOriginalFile, +}: { + value: string; + onChange: (previewUrl: string) => void; + onOriginalFile?: (dataUrl: string, fileName: string) => void; +}) { + const fileRef = useRef(null); + const [processing, setProcessing] = useState(false); + + 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 ( +
+
fileRef.current?.click()} + onDragOver={(e) => e.preventDefault()} + onDrop={(e) => { + e.preventDefault(); + const f = e.dataTransfer.files[0]; + if (f) void handleFile(f); + }} + > + {processing ? ( + Se procesează TIFF... + ) : value ? ( + // eslint-disable-next-line @next/next/no-img-element + preview + ) : ( + <> + + + Trage imaginea aici sau apasă pentru a selecta + + + PNG, JPG, TIFF + + + )} +
+ { + const f = e.target.files?.[0]; + if (f) void handleFile(f); + }} + /> + {value && ( + + )} +
+ ); +} + +// --------------- add version form --------------- + +function AddVersionForm({ + onSubmit, + onCancel, + history, +}: { + onSubmit: (imageUrl: string, notes: string) => void; + onCancel: () => void; + history: Array<{ + id: string; + imageUrl: string; + notes: string; + createdAt: string; + }>; +}) { + const [imageUrl, setImageUrl] = useState(""); + const [notes, setNotes] = useState(""); + + return ( +
+ {history.length > 0 && ( +
+

+ Istoric versiuni +

+ {history.map((v) => ( +
+ + {v.notes || "Fără note"} + + + {v.createdAt.slice(0, 10)} + +
+ ))} +
+ )} +
+ +
+ +
+
+
+ + setNotes(e.target.value)} + className="mt-1" + placeholder="Ce s-a schimbat..." + /> +
+
+ + +
+
+ ); +} + +// --------------- asset form --------------- + +function AssetForm({ + initial, + allSubcategories, + onSubmit, + onCancel, +}: { + initial?: SignatureAsset; + allSubcategories: string[]; + onSubmit: ( + data: Omit, + ) => void; + onCancel: () => void; +}) { + const [label, setLabel] = useState(initial?.label ?? ""); + const [type, setType] = useState( + 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( + initial?.company ?? "beletage", + ); + const [subcategory, setSubcategory] = useState( + initial?.subcategory ?? "", + ); + const [tags, setTags] = useState(initial?.tags ?? []); + const [tagInput, setTagInput] = useState(""); + + const addTag = (raw: string) => { + const t = raw.trim().toLowerCase(); + if (t && !tags.includes(t)) setTags((prev) => [...prev, t]); + setTagInput(""); + }; + + const handleTagKeyDown = (e: React.KeyboardEvent) => { + if (e.key === "Enter" || e.key === ",") { + e.preventDefault(); + addTag(tagInput); + } + if (e.key === "Backspace" && tagInput === "" && tags.length > 0) + setTags((prev) => prev.slice(0, -1)); + }; + + return ( +
{ + e.preventDefault(); + onSubmit({ + label, + type, + imageUrl, + originalFileData: originalFileData || undefined, + originalFileName: originalFileName || undefined, + owner, + company, + subcategory, + versions: initial?.versions ?? [], + tags, + visibility: initial?.visibility ?? "all", + }); + }} + className="space-y-4" + > +
+
+ + setLabel(e.target.value)} + className="mt-1" + required + /> +
+
+ + +
+
+
+
+ + setOwner(e.target.value)} + className="mt-1" + /> +
+
+ + +
+
+
+ +
+ setSubcategory(e.target.value)} + placeholder="Selectează sau scrie o subcategorie..." + /> + + {allSubcategories.map((s) => ( + +
+
+
+ +
+ { + setOriginalFileData(data); + setOriginalFileName(name); + }} + /> +
+
+
+ +
+ {tags.map((tag) => ( + + {tag} + + + ))} + setTagInput(e.target.value)} + onKeyDown={handleTagKeyDown} + onBlur={() => { + if (tagInput.trim()) addTag(tagInput); + }} + placeholder={ + tags.length === 0 + ? "Adaugă etichete (Enter sau virgulă)..." + : "" + } + className="min-w-[120px] flex-1 bg-transparent text-sm outline-none placeholder:text-muted-foreground" + /> +
+
+
+ + +
+
+ ); +} diff --git a/src/modules/digital-signatures/hooks/use-signatures.ts b/src/modules/digital-signatures/hooks/use-signatures.ts index 2e1353b..52e0349 100644 --- a/src/modules/digital-signatures/hooks/use-signatures.ts +++ b/src/modules/digital-signatures/hooks/use-signatures.ts @@ -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({ 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; diff --git a/src/modules/digital-signatures/types.ts b/src/modules/digital-signatures/types.ts index 423a3f5..5649226 100644 --- a/src/modules/digital-signatures/types.ts +++ b/src/modules/digital-signatures/types.ts @@ -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[];