Add export to pdf and add a badge component

This commit is contained in:
Arthur Belleville 2025-04-14 07:47:17 +02:00
parent 06e6e7d120
commit d1b97ee909
No known key found for this signature in database
9 changed files with 435 additions and 115 deletions

View file

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

View file

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

View file

@ -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<RowActionMenuProps> = ({
devis,
// onEdit,
onDelete,
onExport,
}) => {
return (
<div
@ -25,8 +29,16 @@ export const RowActionMenu: React.FC<RowActionMenuProps> = ({
onClick={(e) => e.stopPropagation()}
onDoubleClick={(e) => e.stopPropagation()}
>
<ViewDevisModal selectedDevis={devis} />
<DeleteDevisModal devisId={devis.id} onDelete={onDelete} />
<ViewDevisModalButton selectedDevis={devis} />
<Button
variant="outline"
size="sm"
onPress={() => onExport(devis)}
aria-label="Exporter en PDF"
>
<Download className="h-4 w-4" />
</Button>
<DeleteDevisModalButton devisId={devis.id} onDelete={onDelete} />
</div>
);
};

View file

@ -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,
}: {

View file

@ -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 }) => {
>
<EyeIcon className="w-4 h-4" />
</Button>
<Modal size="lg" isOpen={isOpen} onOpenChange={setIsOpen} isDismissable>
<Dialog>
<DialogHeader slot="title">
<h2 className="text-xl font-semibold">
Devis {selectedDevis.number}
</h2>
<DialogCloseButton />
</DialogHeader>
<DialogBody>
<div className="space-y-6">
<div className="grid grid-cols-2 gap-4">
<div>
<h3 className="text-sm font-medium text-gray-500 dark:text-gray-400">
Client
</h3>
<p className="mt-1">{selectedDevis.client_email}</p>
</div>
<div>
<h3 className="text-sm font-medium text-gray-500 dark:text-gray-400">
Statut
</h3>
<p className="mt-1">{selectedDevis.status}</p>
</div>
<div>
<h3 className="text-sm font-medium text-gray-500 dark:text-gray-400">
Date de création
</h3>
<p className="mt-1">
{selectedDevis.date
? new Date(selectedDevis.date).toLocaleDateString("fr-FR")
: ""}
</p>
</div>
<div>
<h3 className="text-sm font-medium text-gray-500 dark:text-gray-400">
Date d&apos;échéance
</h3>
<p className="mt-1">
{selectedDevis.due_date
? new Date(selectedDevis.due_date).toLocaleDateString(
"fr-FR"
)
: ""}
</p>
</div>
</div>
{selectedDevis.notes && (
<div>
<h3 className="text-sm font-medium text-gray-500 dark:text-gray-400">
Notes
</h3>
<p className="mt-1 whitespace-pre-wrap">
{selectedDevis.notes}
</p>
</div>
)}
{selectedDevis.terms && (
<div>
<h3 className="text-sm font-medium text-gray-500 dark:text-gray-400">
Conditions
</h3>
<p className="mt-1 whitespace-pre-wrap">
{selectedDevis.terms}
</p>
</div>
)}
<div className="border-t pt-4">
<div className="flex justify-between">
<span className="text-sm font-medium text-gray-500 dark:text-gray-400">
Sous-total
</span>
<span>{selectedDevis.subtotal.toFixed(2)} </span>
</div>
<div className="flex justify-between mt-2">
<span className="text-sm font-medium text-gray-500 dark:text-gray-400">
TVA
</span>
<span>{selectedDevis.tax.toFixed(2)} </span>
</div>
<div className="flex justify-between mt-2 font-semibold">
<span>Total</span>
<span>{selectedDevis.total.toFixed(2)} </span>
</div>
</div>
</div>
</DialogBody>
<DialogFooter>
<DialogCloseButton>Fermer</DialogCloseButton>
</DialogFooter>
</Dialog>
</Modal>
<ViewDevisModal
selectedDevis={selectedDevis}
isOpen={isOpen}
setIsOpen={setIsOpen}
/>
</>
);
};
export const ViewDevisModal = ({
selectedDevis,
isOpen,
setIsOpen,
}: {
selectedDevis: Devis | null;
isOpen: boolean;
setIsOpen: (isOpen: boolean) => void;
}) => {
return (
<Modal size="lg" isOpen={isOpen} onOpenChange={setIsOpen} isDismissable>
<Dialog>
<DialogHeader slot="title">
<h2 className="text-xl font-semibold">
Devis {selectedDevis?.number}
</h2>
<DialogCloseButton />
</DialogHeader>
<DialogBody>
<div className="space-y-6">
<div className="grid grid-cols-2 gap-4">
<div>
<h3 className="text-sm font-medium text-gray-500 dark:text-gray-400">
Client
</h3>
<p className="mt-1">{selectedDevis?.client_email}</p>
</div>
<div>
<h3 className="text-sm font-medium text-gray-500 dark:text-gray-400">
Statut
</h3>
<p className="mt-1">{selectedDevis?.status}</p>
</div>
<div>
<h3 className="text-sm font-medium text-gray-500 dark:text-gray-400">
Date de création
</h3>
<p className="mt-1">
{selectedDevis?.date
? new Date(selectedDevis.date).toLocaleDateString("fr-FR")
: ""}
</p>
</div>
<div>
<h3 className="text-sm font-medium text-gray-500 dark:text-gray-400">
Date d&apos;échéance
</h3>
<p className="mt-1">
{selectedDevis?.due_date
? new Date(selectedDevis.due_date).toLocaleDateString(
"fr-FR"
)
: ""}
</p>
</div>
</div>
{selectedDevis?.notes && (
<div>
<h3 className="text-sm font-medium text-gray-500 dark:text-gray-400">
Notes
</h3>
<p className="mt-1 whitespace-pre-wrap">
{selectedDevis.notes}
</p>
</div>
)}
{selectedDevis?.terms && (
<div>
<h3 className="text-sm font-medium text-gray-500 dark:text-gray-400">
Conditions
</h3>
<p className="mt-1 whitespace-pre-wrap">
{selectedDevis.terms}
</p>
</div>
)}
<div className="border-t pt-4">
<div className="flex justify-between">
<span className="text-sm font-medium text-gray-500 dark:text-gray-400">
Sous-total
</span>
<span>{selectedDevis?.subtotal.toFixed(2)} </span>
</div>
<div className="flex justify-between mt-2">
<span className="text-sm font-medium text-gray-500 dark:text-gray-400">
TVA
</span>
<span>{selectedDevis?.tax.toFixed(2)} </span>
</div>
<div className="flex justify-between mt-2 font-semibold">
<span>Total</span>
<span>{selectedDevis?.total.toFixed(2)} </span>
</div>
</div>
</div>
</DialogBody>
<DialogFooter>
<DialogCloseButton>Fermer</DialogCloseButton>
</DialogFooter>
</Dialog>
</Modal>
);
};

View file

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

View file

@ -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<Devis | null>(null);
// const [devisIdToDelete, setDevisIdToDelete] = useState<string | null>(null);
const [selectedDevis, setSelectedDevis] = useState<Devis | null>(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<Devis>[] = [
{
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 <Badge variant="neutral">{params.data.status}</Badge>;
},
},
{
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 (
<div className="flex justify-center items-center h-full">
<RowActionMenu devis={params.data} onDelete={confirmDeleteAction} />
<RowActionMenu
devis={params.data}
onDelete={confirmDeleteAction}
onExport={handleExport}
/>
</div>
);
},
@ -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 = () => {
</div>
</main>
{/* <ViewDevisModal
selectedDevis={selectedDevis}
setSelectedDevis={setSelectedDevis}
/> */}
<ViewDevisModal
selectedDevis={selectedDevis as Devis}
isOpen={isViewModalOpen}
setIsOpen={setIsViewModalOpen}
/>
</div>
);
};

View file

@ -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<HTMLDivElement> {
variant?: BadgeVariant;
children: React.ReactNode;
}
function Badge({
className,
variant = "neutral",
children,
...props
}: BadgeProps) {
const combinedClassName = [baseStyles, variantStyles[variant], className]
.filter(Boolean)
.join(" ");
return (
<div className={combinedClassName} {...props}>
{children}
</div>
);
}
export { Badge };

View file

@ -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`);
};