diff --git a/ui/package.json b/ui/package.json index 1f50202..661e76d 100644 --- a/ui/package.json +++ b/ui/package.json @@ -62,6 +62,7 @@ "ag-grid-community": "^33.2.1", "ag-grid-react": "^33.2.1", "axios": "^1.8.4", + "jspdf": "^3.0.1", "jwt-decode": "^4.0.0", "react-router-dom": "^7.3.0", "react-stately": "^3.36.1", diff --git a/ui/pnpm-lock.yaml b/ui/pnpm-lock.yaml index ba04753..ae71bed 100644 --- a/ui/pnpm-lock.yaml +++ b/ui/pnpm-lock.yaml @@ -32,6 +32,9 @@ importers: axios: specifier: ^1.8.4 version: 1.8.4 + jspdf: + specifier: ^3.0.1 + version: 3.0.1 jwt-decode: specifier: ^4.0.0 version: 4.0.0 @@ -1602,6 +1605,9 @@ packages: '@types/phoenix@1.6.6': resolution: {integrity: sha512-PIzZZlEppgrpoT2QgbnDU+MMzuR6BbCjllj0bM70lWoejMeNJAxCchxnv7J3XFkI8MpygtRpzXrIlmWUBclP5A==} + '@types/raf@3.4.3': + resolution: {integrity: sha512-c4YAvMedbPZ5tEyxzQdMoOhhJ4RD3rngZIdwC2/qDN3d7JpEhB6fiBRKVY1lg5B7Wk+uPBjn5f39j1/2MY1oOw==} + '@types/react-dom@19.0.4': resolution: {integrity: sha512-4fSQ8vWFkg+TGhePfUzVmat3eC14TXYSsiiDSLI0dVLsrm9gZFABjPy/Qu6TKgl1tq1Bu1yDsuQgY3A3DOjCcg==} peerDependencies: @@ -1622,6 +1628,9 @@ packages: '@types/tough-cookie@4.0.5': resolution: {integrity: sha512-/Ad8+nIOV7Rl++6f1BdKxFSMgmoqEoYbHRpPcx3JEfv8VRsQe9Z4mCXeJBzxs7mbHY/XOZZuXlRNfhpVPbs6ZA==} + '@types/trusted-types@2.0.7': + resolution: {integrity: sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==} + '@types/ws@8.18.0': resolution: {integrity: sha512-8svvI3hMyvN0kKCJMvTJP/x6Y/EoQbepff882wL+Sn5QsXb3etnamgrJq4isrBxSJj5L2AuXcI0+bgkoAXGUJw==} @@ -1887,6 +1896,11 @@ packages: asynckit@0.4.0: resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==} + atob@2.1.2: + resolution: {integrity: sha512-Wm6ukoaOGJi/73p/cl2GvLjTI5JM1k/O14isD73YML8StrH/7/lRFgmg8nICZgD3bZZvjwCGxtMOD3wWNAu8cg==} + engines: {node: '>= 4.5.0'} + hasBin: true + available-typed-arrays@1.0.7: resolution: {integrity: sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==} engines: {node: '>= 0.4'} @@ -1922,6 +1936,10 @@ packages: balanced-match@1.0.2: resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} + base64-arraybuffer@1.0.2: + resolution: {integrity: sha512-I3yl4r9QB5ZRY3XuJVEPfc2XhZO6YweFPI+UovAzn+8/hb3oJ6lnysaFcjVpkCPfVWFUDvoZ8kmVDP7WyRtYtQ==} + engines: {node: '>= 0.6.0'} + brace-expansion@1.1.11: resolution: {integrity: sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==} @@ -1940,6 +1958,11 @@ packages: bser@2.1.1: resolution: {integrity: sha512-gQxTNE/GAfIIrmHLUE3oJyp5FO6HRBfhjnw4/wMmA63ZGDJnWBmgY/lyQBpnDUkGmAhbSe39tx2d/iTOAfglwQ==} + btoa@1.2.1: + resolution: {integrity: sha512-SB4/MIGlsiVkMcHmT+pSmIPoNDoHg+7cMzmt3Uxt628MTz2487DKSqK/fuhFBrkuqrYv5UCEnACpF4dTFNKc/g==} + engines: {node: '>= 0.4.0'} + hasBin: true + buffer-from@1.1.2: resolution: {integrity: sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==} @@ -1974,6 +1997,10 @@ packages: caniuse-lite@1.0.30001699: resolution: {integrity: sha512-b+uH5BakXZ9Do9iK+CkDmctUSEqZl+SP056vc5usa0PL+ev5OHw003rZXcnjNDv3L8P5j6rwT6C0BPKSikW08w==} + canvg@3.0.11: + resolution: {integrity: sha512-5ON+q7jCTgMp9cjpu4Jo6XbvfYwSB2Ow3kzHKfIyJfaCAOHLbdKPQqGKgfED/R5B+3TFFfe8pegYA+b423SRyA==} + engines: {node: '>=10.0.0'} + chai@5.2.0: resolution: {integrity: sha512-mCuXncKXk5iCLhfhwTc0izo0gtEmpz5CtG2y8GiOINBlMVS6v8TMRc5TaLWKS6692m9+dVVfzgeVxR5UxWHTYw==} engines: {node: '>=12'} @@ -2052,6 +2079,9 @@ packages: resolution: {integrity: sha512-9Kr/j4O16ISv8zBBhJoi4bXOYNTkFLOqSL3UDB0njXxCXNezjeyVrJyGOWtgfs/q2km1gwBcfH8q1yEGoMYunA==} engines: {node: '>=18'} + core-js@3.41.0: + resolution: {integrity: sha512-SJ4/EHwS36QMJd6h/Rg+GyR4A5xE0FSI3eZ+iBVpfqf1x0eTSg1smWLHrA+2jQThZSh97fmSgFSU8B61nxosxA==} + create-jest@29.7.0: resolution: {integrity: sha512-Adz2bdH0Vq3F53KEMJOoftQFutWCukm6J24wbPWRO4k1kMY7gS7ds/uoJkNuV8wDCtWWnuwGcJwpWcih+zEW1Q==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} @@ -2061,6 +2091,9 @@ packages: resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} engines: {node: '>= 8'} + css-line-break@2.1.0: + resolution: {integrity: sha512-FHcKFCZcAha3LwfVBhCQbW2nCNbkZXn7KVUJcsT5/P8YmfsVja0FMPJr0B903j/E69HUphKiV9iQArX8SDYA4w==} + css.escape@1.5.1: resolution: {integrity: sha512-YUifsXXuknHlUsmlgyY0PKzgPOr7/FjCePfHNt0jxm83wHZi44VDMQ7/fGNkjY3/jV1MC+1CmZbaHzugyeRtpg==} @@ -2175,6 +2208,9 @@ packages: engines: {node: '>=12'} deprecated: Use your platform's native DOMException instead + dompurify@3.2.5: + resolution: {integrity: sha512-mLPd29uoRe9HpvwP2TxClGQBzGXeEC/we/q+bFlmPPmj2p2Ugl3r6ATu/UU1v77DXNcehiBg9zsr1dREyA/dJQ==} + dunder-proto@1.0.1: resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==} engines: {node: '>= 0.4'} @@ -2348,6 +2384,9 @@ packages: fb-watchman@2.0.2: resolution: {integrity: sha512-p5161BqbuCaSnB8jIbzQHOlpgsPmK5rJVDfDKO91Axs5NC1uu3HRQm6wt9cd9/+GtQQIO53JdGXXoyDpTAsgYA==} + fflate@0.8.2: + resolution: {integrity: sha512-cPJU47OaAoCbg0pBvzsgpTPhmhqI5eJjh/JIu8tPj5q+T7iLvW/JAYUqmE7KOB4R1ZyEhzBaIQpQpardBF5z8A==} + file-entry-cache@8.0.0: resolution: {integrity: sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==} engines: {node: '>=16.0.0'} @@ -2513,6 +2552,10 @@ packages: html-escaper@2.0.2: resolution: {integrity: sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==} + html2canvas@1.4.1: + resolution: {integrity: sha512-fPU6BHNpsyIhr8yyMpTLLxAbkaK8ArIBcmZIRiBLiDhjeqvXolaEmDGmELFuX9I4xDcaKKcJl+TKZLqruBbmWA==} + engines: {node: '>=8.0.0'} + http-proxy-agent@5.0.0: resolution: {integrity: sha512-n2hY8YdoRE1i7r6M0w9DIw5GgZN0G25P8zLCRQ8rjXtTU3vsNFBI/vWK/UIeE6g5MUUz6avwAPXmL6Fy9D/90w==} engines: {node: '>= 6'} @@ -2905,6 +2948,9 @@ packages: engines: {node: '>=6'} hasBin: true + jspdf@3.0.1: + resolution: {integrity: sha512-qaGIxqxetdoNnFQQXxTKUD9/Z7AloLaw94fFsOiJMxbfYdBbrBuhWmbzI8TVjrw7s3jBY1PFHofBKMV/wZPapg==} + jsx-ast-utils@3.3.5: resolution: {integrity: sha512-ZZow9HBI5O6EPgSJLUb8n2NKgmVWTwCvHGwFuJlMjvLFqlGG6pjirPhtdsseaLZjSibD8eegzmYpUZwoIlj2cQ==} engines: {node: '>=4.0'} @@ -3208,6 +3254,9 @@ packages: resolution: {integrity: sha512-vE7JKRyES09KiunauX7nd2Q9/L7lhok4smP9RZTDeD4MVs72Dp2qNFVz39Nz5a0FVEW0BJR6C0DYrq6unoziZA==} engines: {node: '>= 14.16'} + performance-now@2.1.0: + resolution: {integrity: sha512-7EAHlyLHI56VEIdK57uwHdHKIaAGbnXPiw0yWbarQZOKaKpvUIgW0jWRVLiatnM+XXlSwsanIBH/hzGMJulMow==} + picocolors@1.1.1: resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} @@ -3333,6 +3382,9 @@ packages: queue-microtask@1.2.3: resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} + raf@3.4.1: + resolution: {integrity: sha512-Sq4CW4QhwOHE8ucn6J34MqtZCeWFP2aQSmrlroYgqAV1PjStIhJXxYuTgUIfkEk7zTLjmIjLmU5q+fbD1NnOJA==} + react-aria-components@1.7.1: resolution: {integrity: sha512-kTAlrxcW7n+rQDwlZSz5+o+HknjPGv/pn0OQ1FF92WsjoTaqQMJtWbEAHXrhrQaiW/3T4CANTpdR1soai4uK6g==} peerDependencies: @@ -3397,6 +3449,9 @@ packages: resolution: {integrity: sha512-00o4I+DVrefhv+nX0ulyi3biSHCPDe+yLv5o/p6d/UVlirijB8E16FtfwSAi4g3tcqrQ4lRAqQSoFEZJehYEcw==} engines: {node: '>= 0.4'} + regenerator-runtime@0.13.11: + resolution: {integrity: sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg==} + regenerator-runtime@0.14.1: resolution: {integrity: sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==} @@ -3440,6 +3495,10 @@ packages: resolution: {integrity: sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==} engines: {iojs: '>=1.0.0', node: '>=0.10.0'} + rgbcolor@1.0.1: + resolution: {integrity: sha512-9aZLIrhRaD97sgVhtJOW6ckOEh6/GnvQtdVNfdZ6s67+3/XwLS9lBcQYzEEhYVeUowN7pRzMLsyGhK2i/xvWbw==} + engines: {node: '>= 0.8.15'} + rollup-plugin-visualizer@5.14.0: resolution: {integrity: sha512-VlDXneTDaKsHIw8yzJAFWtrzguoJ/LnQ+lMpoVfYJ3jJF4Ihe5oYLAqLklIK/35lgUY+1yEzCkHyZ1j4A5w5fA==} engines: {node: '>=18'} @@ -3569,6 +3628,10 @@ packages: stackback@0.0.2: resolution: {integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==} + stackblur-canvas@2.7.0: + resolution: {integrity: sha512-yf7OENo23AGJhBriGx0QivY5JP6Y1HbrrDI6WLt6C5auYZXlQrheoY8hD4ibekFKz1HOfE48Ww8kMWMnJD/zcQ==} + engines: {node: '>=0.1.14'} + std-env@3.9.0: resolution: {integrity: sha512-UGvjygr6F6tpH7o2qyqR6QYpwraIjKSdtzyBdyytFOHmPZY917kwdwLG0RbOjWOnKmnm3PeHjaoLLMie7kPLQw==} @@ -3631,6 +3694,10 @@ packages: resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==} engines: {node: '>= 0.4'} + svg-pathdata@6.0.3: + resolution: {integrity: sha512-qsjeeq5YjBZ5eMdFuUa4ZosMLxgr5RZ+F+Y1OrDhuOCEInRMA3x74XdBtggJcj9kOeInz0WE+LgCPDkZFlBYJw==} + engines: {node: '>=12.0.0'} + symbol-tree@3.2.4: resolution: {integrity: sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==} @@ -3656,6 +3723,9 @@ packages: resolution: {integrity: sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w==} engines: {node: '>=8'} + text-segmentation@1.0.3: + resolution: {integrity: sha512-iOiPUo/BGnZ6+54OsWxZidGCsdU8YbE4PSpdPinp7DeMtUJNJBoJ/ouUSTJjHkh1KntHaltHl/gDs2FC4i5+Nw==} + tinybench@2.9.0: resolution: {integrity: sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==} @@ -3791,6 +3861,9 @@ packages: peerDependencies: react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + utrie@1.0.2: + resolution: {integrity: sha512-1MLa5ouZiOmQzUbjbu9VmjLzn1QLXBhwpUa7kdLUQK+KQ5KA9I1vk5U4YHe/X2Ch7PYnJfWuWT+VbuxbGwljhw==} + uuid@11.1.0: resolution: {integrity: sha512-0/A9rDy9P7cJ+8w1c9WD9V//9Wj15Ce2MPz8Ri6032usz+NfePxx5AcN3bN+r6ZL6jEo066/yNYB3tn4pQEx+A==} hasBin: true @@ -5928,6 +6001,9 @@ snapshots: '@types/phoenix@1.6.6': {} + '@types/raf@3.4.3': + optional: true + '@types/react-dom@19.0.4(@types/react@19.0.10)': dependencies: '@types/react': 19.0.10 @@ -5951,6 +6027,9 @@ snapshots: '@types/tough-cookie@4.0.5': {} + '@types/trusted-types@2.0.7': + optional: true + '@types/ws@8.18.0': dependencies: '@types/node': 22.13.10 @@ -6305,6 +6384,8 @@ snapshots: asynckit@0.4.0: {} + atob@2.1.2: {} + available-typed-arrays@1.0.7: dependencies: possible-typed-array-names: 1.1.0 @@ -6374,6 +6455,9 @@ snapshots: balanced-match@1.0.2: {} + base64-arraybuffer@1.0.2: + optional: true + brace-expansion@1.1.11: dependencies: balanced-match: 1.0.2 @@ -6398,6 +6482,8 @@ snapshots: dependencies: node-int64: 0.4.0 + btoa@1.2.1: {} + buffer-from@1.1.2: {} cac@6.7.14: {} @@ -6427,6 +6513,18 @@ snapshots: caniuse-lite@1.0.30001699: {} + canvg@3.0.11: + dependencies: + '@babel/runtime': 7.27.0 + '@types/raf': 3.4.3 + core-js: 3.41.0 + raf: 3.4.1 + regenerator-runtime: 0.13.11 + rgbcolor: 1.0.1 + stackblur-canvas: 2.7.0 + svg-pathdata: 6.0.3 + optional: true + chai@5.2.0: dependencies: assertion-error: 2.0.1 @@ -6485,6 +6583,9 @@ snapshots: cookie@1.0.2: {} + core-js@3.41.0: + optional: true + create-jest@29.7.0(@types/node@22.13.10): dependencies: '@jest/types': 29.6.3 @@ -6506,6 +6607,11 @@ snapshots: shebang-command: 2.0.0 which: 2.0.2 + css-line-break@2.1.0: + dependencies: + utrie: 1.0.2 + optional: true + css.escape@1.5.1: {} cssom@0.3.8: {} @@ -6596,6 +6702,11 @@ snapshots: dependencies: webidl-conversions: 7.0.0 + dompurify@3.2.5: + optionalDependencies: + '@types/trusted-types': 2.0.7 + optional: true + dunder-proto@1.0.1: dependencies: call-bind-apply-helpers: 1.0.2 @@ -6904,6 +7015,8 @@ snapshots: dependencies: bser: 2.1.1 + fflate@0.8.2: {} + file-entry-cache@8.0.0: dependencies: flat-cache: 4.0.1 @@ -7065,6 +7178,12 @@ snapshots: html-escaper@2.0.2: {} + html2canvas@1.4.1: + dependencies: + css-line-break: 2.1.0 + text-segmentation: 1.0.3 + optional: true + http-proxy-agent@5.0.0: dependencies: '@tootallnate/once': 2.0.0 @@ -7678,6 +7797,18 @@ snapshots: json5@2.2.3: {} + jspdf@3.0.1: + dependencies: + '@babel/runtime': 7.27.0 + atob: 2.1.2 + btoa: 1.2.1 + fflate: 0.8.2 + optionalDependencies: + canvg: 3.0.11 + core-js: 3.41.0 + dompurify: 3.2.5 + html2canvas: 1.4.1 + jsx-ast-utils@3.3.5: dependencies: array-includes: 3.1.8 @@ -7946,6 +8077,9 @@ snapshots: pathval@2.0.0: {} + performance-now@2.1.0: + optional: true + picocolors@1.1.1: {} picomatch@2.3.1: {} @@ -8011,6 +8145,11 @@ snapshots: queue-microtask@1.2.3: {} + raf@3.4.1: + dependencies: + performance-now: 2.1.0 + optional: true + react-aria-components@1.7.1(react-dom@19.0.0(react@19.0.0))(react@19.0.0): dependencies: '@internationalized/date': 3.7.0 @@ -8166,6 +8305,9 @@ snapshots: get-proto: 1.0.1 which-builtin-type: 1.2.1 + regenerator-runtime@0.13.11: + optional: true + regenerator-runtime@0.14.1: {} regexp.prototype.flags@1.5.4: @@ -8205,6 +8347,9 @@ snapshots: reusify@1.0.4: {} + rgbcolor@1.0.1: + optional: true + rollup-plugin-visualizer@5.14.0(rollup@4.34.6): dependencies: open: 8.4.2 @@ -8359,6 +8504,9 @@ snapshots: stackback@0.0.2: {} + stackblur-canvas@2.7.0: + optional: true + std-env@3.9.0: {} string-length@4.0.2: @@ -8440,6 +8588,9 @@ snapshots: supports-preserve-symlinks-flag@1.0.0: {} + svg-pathdata@6.0.3: + optional: true + symbol-tree@3.2.4: {} tabbable@6.2.0: {} @@ -8460,6 +8611,11 @@ snapshots: glob: 7.2.3 minimatch: 3.1.2 + text-segmentation@1.0.3: + dependencies: + utrie: 1.0.2 + optional: true + tinybench@2.9.0: {} tinyexec@0.3.2: {} @@ -8590,6 +8746,11 @@ snapshots: dependencies: react: 19.0.0 + utrie@1.0.2: + dependencies: + base64-arraybuffer: 1.0.2 + optional: true + uuid@11.1.0: {} v8-to-istanbul@9.3.0: diff --git a/ui/src/components/RowActionMenu.tsx b/ui/src/components/RowActionMenu.tsx index f53d53f..dcd3330 100644 --- a/ui/src/components/RowActionMenu.tsx +++ b/ui/src/components/RowActionMenu.tsx @@ -1,19 +1,23 @@ import React from "react"; -import { DeleteDevisModal } from "@ui/components/devis/DeleteDevisModal"; -import { ViewDevisModal } from "./devis/ViewDevisModal"; +import { DeleteDevisModalButton } from "@ui/components/devis/DeleteDevisModal"; +import { ViewDevisModalButton } from "@ui/components/devis/ViewDevisModal"; import { Database } from "@ui/types/db"; +import { Button } from "@ui/ui-library/button"; +import { Download } from "lucide-react"; type Devis = Database["public"]["Tables"]["devis"]["Row"]; interface RowActionMenuProps { devis: Devis; // onEdit: (devis: Devis) => void; onDelete: (devisId: string) => void; + onExport: (devis: Devis) => void; } export const RowActionMenu: React.FC = ({ devis, // onEdit, onDelete, + onExport, }) => { return (
= ({ onClick={(e) => e.stopPropagation()} onDoubleClick={(e) => e.stopPropagation()} > - - + + +
); }; diff --git a/ui/src/components/devis/DeleteDevisModal.tsx b/ui/src/components/devis/DeleteDevisModal.tsx index f8bf007..b07837b 100644 --- a/ui/src/components/devis/DeleteDevisModal.tsx +++ b/ui/src/components/devis/DeleteDevisModal.tsx @@ -8,7 +8,7 @@ import { TrashIcon } from "lucide-react"; import { Modal } from "@ui/ui-library/modal"; import { useState } from "react"; -export const DeleteDevisModal = ({ +export const DeleteDevisModalButton = ({ devisId, onDelete, }: { diff --git a/ui/src/components/devis/ViewDevisModal.tsx b/ui/src/components/devis/ViewDevisModal.tsx index 6df926d..f3aa00c 100644 --- a/ui/src/components/devis/ViewDevisModal.tsx +++ b/ui/src/components/devis/ViewDevisModal.tsx @@ -11,7 +11,11 @@ import { useState } from "react"; type Devis = Database["public"]["Tables"]["devis"]["Row"]; -export const ViewDevisModal = ({ selectedDevis }: { selectedDevis: Devis }) => { +export const ViewDevisModalButton = ({ + selectedDevis, +}: { + selectedDevis: Devis; +}) => { const [isOpen, setIsOpen] = useState(false); return ( <> @@ -23,97 +27,115 @@ export const ViewDevisModal = ({ selectedDevis }: { selectedDevis: Devis }) => { > - - - -

- Devis {selectedDevis.number} -

- -
- -
-
-
-

- Client -

-

{selectedDevis.client_email}

-
-
-

- Statut -

-

{selectedDevis.status}

-
-
-

- Date de création -

-

- {selectedDevis.date - ? new Date(selectedDevis.date).toLocaleDateString("fr-FR") - : ""} -

-
-
-

- Date d'échéance -

-

- {selectedDevis.due_date - ? new Date(selectedDevis.due_date).toLocaleDateString( - "fr-FR" - ) - : ""} -

-
-
- {selectedDevis.notes && ( -
-

- Notes -

-

- {selectedDevis.notes} -

-
- )} - {selectedDevis.terms && ( -
-

- Conditions -

-

- {selectedDevis.terms} -

-
- )} -
-
- - Sous-total - - {selectedDevis.subtotal.toFixed(2)} € -
-
- - TVA - - {selectedDevis.tax.toFixed(2)} € -
-
- Total - {selectedDevis.total.toFixed(2)} € -
-
-
-
- - Fermer - -
-
+ ); }; + +export const ViewDevisModal = ({ + selectedDevis, + isOpen, + setIsOpen, +}: { + selectedDevis: Devis | null; + isOpen: boolean; + setIsOpen: (isOpen: boolean) => void; +}) => { + return ( + + + +

+ Devis {selectedDevis?.number} +

+ +
+ +
+
+
+

+ Client +

+

{selectedDevis?.client_email}

+
+
+

+ Statut +

+

{selectedDevis?.status}

+
+
+

+ Date de création +

+

+ {selectedDevis?.date + ? new Date(selectedDevis.date).toLocaleDateString("fr-FR") + : ""} +

+
+
+

+ Date d'échéance +

+

+ {selectedDevis?.due_date + ? new Date(selectedDevis.due_date).toLocaleDateString( + "fr-FR" + ) + : ""} +

+
+
+ {selectedDevis?.notes && ( +
+

+ Notes +

+

+ {selectedDevis.notes} +

+
+ )} + {selectedDevis?.terms && ( +
+

+ Conditions +

+

+ {selectedDevis.terms} +

+
+ )} +
+
+ + Sous-total + + {selectedDevis?.subtotal.toFixed(2)} € +
+
+ + TVA + + {selectedDevis?.tax.toFixed(2)} € +
+
+ Total + {selectedDevis?.total.toFixed(2)} € +
+
+
+
+ + Fermer + +
+
+ ); +}; diff --git a/ui/src/pages/__tests__/devis.test.tsx b/ui/src/pages/devis.test.tsx similarity index 99% rename from ui/src/pages/__tests__/devis.test.tsx rename to ui/src/pages/devis.test.tsx index d6ab124..c5438b2 100644 --- a/ui/src/pages/__tests__/devis.test.tsx +++ b/ui/src/pages/devis.test.tsx @@ -1,6 +1,6 @@ import { screen, waitFor, within } from "@testing-library/react"; import { describe, it, expect, beforeEach, vi } from "vitest"; -import { DevisPage } from "../devis"; +import { DevisPage } from "@ui/pages/devis"; import { useDevisList, useCreateDevis, useDeleteDevis } from "@ui/hooks/devis"; import userEvent from "@testing-library/user-event"; import { diff --git a/ui/src/pages/devis.tsx b/ui/src/pages/devis.tsx index 6fa770c..2284d61 100644 --- a/ui/src/pages/devis.tsx +++ b/ui/src/pages/devis.tsx @@ -20,8 +20,13 @@ import { EmptyStateIcon, } from "@ui/ui-library/empty-state"; import { CreateDevisModal } from "@ui/components/devis/CreateDevisModal"; -import { calculateTotal } from "@ui/utils/helpers"; -import { calculateTax } from "@ui/utils/helpers"; +import { + calculateTotal, + calculateTax, + exportDevisToPdf, +} from "@ui/utils/helpers"; +import { ViewDevisModal } from "@ui/components/devis/ViewDevisModal"; +import { Badge } from "@ui/ui-library/badge"; ModuleRegistry.registerModules([AllCommunityModule]); @@ -32,9 +37,8 @@ export const DevisPage = () => { const createDevis = useCreateDevis(); const deleteDevis = useDeleteDevis(); const [dueDateError, setDueDateError] = useState(""); - // const [selectedDevis, setSelectedDevis] = useState(null); - // const [devisIdToDelete, setDevisIdToDelete] = useState(null); - + const [selectedDevis, setSelectedDevis] = useState(null); + const [isViewModalOpen, setIsViewModalOpen] = useState(false); const validateDueDate = (date: DateValue, dueDate: DateValue) => { if (dueDate.compare(date) < 0) { return "La date d'échéance doit être postérieure à la date de création"; @@ -101,6 +105,9 @@ export const DevisPage = () => { deleteDevis.mutate(devisId); }; + // Add handler for exporting + const handleExport = exportDevisToPdf; + const columnDefs: ColDef[] = [ { field: "date", @@ -127,18 +134,28 @@ export const DevisPage = () => { return params.value.toFixed(2) + " €"; }, }, - { field: "status", headerName: "Status" }, + { + field: "status", + headerName: "Status", + cellRenderer: (params: { data: Devis }) => { + return {params.data.status}; + }, + }, { headerName: "Actions", pinned: "right", - width: 80, + width: 120, cellStyle: { padding: 0 }, colId: "actions-column", cellRenderer: (params: { data: Devis; node: { id: string | null } }) => { if (!params.data) return null; return (
- +
); }, @@ -193,11 +210,12 @@ export const DevisPage = () => { loading={isLoading} gridOptions={{ theme: themeQuartz, - // onRowDoubleClicked: (event) => { - // if (event.data) { - // setSelectedDevis(event.data); - // } - // }, + onRowDoubleClicked: (event) => { + if (event.data) { + setSelectedDevis(event.data); + setIsViewModalOpen(true); + } + }, suppressHorizontalScroll: true, domLayout: "autoHeight", loadingOverlayComponent: CustomLoadingOverlay, @@ -220,10 +238,11 @@ export const DevisPage = () => { - {/* */} + ); }; diff --git a/ui/src/ui-library/badge.tsx b/ui/src/ui-library/badge.tsx new file mode 100644 index 0000000..ad9dd4a --- /dev/null +++ b/ui/src/ui-library/badge.tsx @@ -0,0 +1,42 @@ +import React from "react"; + +const baseStyles = + "inline-flex items-center rounded-full border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2"; + +const variantStyles = { + neutral: + "border-transparent bg-gray-100 text-gray-800 hover:bg-gray-100/80 dark:bg-gray-800 dark:text-gray-50 dark:hover:bg-gray-800/80", + positive: + "border-transparent bg-green-100 text-green-800 hover:bg-green-100/80 dark:bg-green-900 dark:text-green-50 dark:hover:bg-green-900/80", + negative: + "border-transparent bg-red-100 text-red-800 hover:bg-red-100/80 dark:bg-red-900 dark:text-red-50 dark:hover:bg-red-900/80", + notice: + "border-transparent bg-yellow-100 text-yellow-800 hover:bg-yellow-100/80 dark:bg-yellow-900 dark:text-yellow-50 dark:hover:bg-yellow-900/80", + info: "border-transparent bg-blue-100 text-blue-800 hover:bg-blue-100/80 dark:bg-blue-900 dark:text-blue-50 dark:hover:bg-blue-900/80", +}; + +export type BadgeVariant = keyof typeof variantStyles; + +export interface BadgeProps extends React.HTMLAttributes { + variant?: BadgeVariant; + children: React.ReactNode; +} + +function Badge({ + className, + variant = "neutral", + children, + ...props +}: BadgeProps) { + const combinedClassName = [baseStyles, variantStyles[variant], className] + .filter(Boolean) + .join(" "); + + return ( +
+ {children} +
+ ); +} + +export { Badge }; diff --git a/ui/src/utils/helpers.ts b/ui/src/utils/helpers.ts index 51a48b1..d339f9b 100644 --- a/ui/src/utils/helpers.ts +++ b/ui/src/utils/helpers.ts @@ -1,3 +1,6 @@ +import { Database } from "@ui/types/db"; +import jsPDF from "jspdf"; + export const calculateTax = (amount: number, taxRate: number) => { return (amount * taxRate) / 100; }; @@ -5,3 +8,63 @@ export const calculateTax = (amount: number, taxRate: number) => { export const calculateTotal = (amount: number, tax: number) => { return amount + tax; }; + +type Devis = Database["public"]["Tables"]["devis"]["Row"]; + +export const exportDevisToPdf = (devis: Devis) => { + const doc = new jsPDF(); + + // --- Basic PDF Content --- + doc.setFontSize(22); + doc.text(`Devis ${devis.number}`, 20, 20); + + doc.setFontSize(12); + doc.text(`Client: ${devis.client_email}`, 20, 40); + + doc.text( + `Date: ${new Date(devis.date).toLocaleDateString("fr-FR")}`, + 140, + 40 + ); + doc.text( + `Date d'échéance: ${new Date(devis.due_date).toLocaleDateString("fr-FR")}`, + 140, + 48 + ); + + // --- Amounts --- + const startYAmounts = 70; + doc.line(20, startYAmounts - 5, 190, startYAmounts - 5); // Separator line + doc.text(`Sous-total HT: ${devis.subtotal.toFixed(2)} €`, 140, startYAmounts); + doc.text(`TVA: ${devis.tax.toFixed(2)} €`, 140, startYAmounts + 8); + doc.setFontSize(14); + doc.setFont("helvetica", "bold"); + doc.text(`Total TTC: ${devis.total.toFixed(2)} €`, 140, startYAmounts + 18); + doc.setFont("helvetica", "normal"); // Reset font + doc.line(20, startYAmounts + 25, 190, startYAmounts + 25); // Separator line + + // --- Notes & Terms (if available) --- + let currentY = startYAmounts + 40; + if (devis.notes) { + doc.setFontSize(12); + doc.setFont("helvetica", "bold"); + doc.text("Notes:", 20, currentY); + doc.setFont("helvetica", "normal"); + // Use splitTextToSize for wrapping long text + const notesLines = doc.splitTextToSize(devis.notes, 170); // 170 is max width + doc.text(notesLines, 20, currentY + 8); + currentY += notesLines.length * 5 + 15; // Adjust Y position based on lines + } + + if (devis.terms) { + doc.setFontSize(12); + doc.setFont("helvetica", "bold"); + doc.text("Conditions:", 20, currentY); + doc.setFont("helvetica", "normal"); + const termsLines = doc.splitTextToSize(devis.terms, 170); + doc.text(termsLines, 20, currentY + 8); + } + + // --- Save the PDF --- + doc.save(`devis_${devis.number}.pdf`); +};