Add export to pdf and add a badge component
This commit is contained in:
parent
06e6e7d120
commit
d1b97ee909
9 changed files with 435 additions and 115 deletions
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
}: {
|
||||
|
|
|
|||
|
|
@ -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'é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'é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>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
42
ui/src/ui-library/badge.tsx
Normal file
42
ui/src/ui-library/badge.tsx
Normal 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 };
|
||||
|
|
@ -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`);
|
||||
};
|
||||
|
|
|
|||
Loading…
Reference in a new issue