Compare commits
1216 Commits
ddc9258dfe
...
af5bd12577
| Author | SHA1 | Date | |
|---|---|---|---|
| af5bd12577 | |||
| 6f3d4c0bff | |||
| 125c16a20c | |||
| d01bd7fc0c | |||
| d3fc2e6ad3 | |||
| c1ed8cd028 | |||
| 786ac06e09 | |||
| a00fd6d51f | |||
| c439396595 | |||
| b013ef8f4b | |||
| 7c3968636d | |||
| bf3f0cc784 | |||
| 7f09ef2911 | |||
| 8b9b5744bf | |||
| fa369f7b81 | |||
| 5485d2c0cc | |||
| c1ff4d1a08 | |||
| 8a2e91e702 | |||
| 65acad1c0d | |||
| bc3b944f5d | |||
| e8fe6940c2 | |||
| 7df88ef5e4 | |||
| 8a87113275 | |||
| 94ed9ac343 | |||
| 848c9e4b2f | |||
| b26c9432f1 | |||
| 3c573380a8 | |||
| a5f82c3ef3 | |||
| 78ae8989ba | |||
| cebe20fd43 | |||
| 3da3aee50d | |||
| 2c7449ef7a | |||
| fb746b4d59 | |||
| b3fd5eafbc | |||
| 4121666c70 | |||
| a3df87caee | |||
| 8db4ad2203 | |||
| 0d1b4b7eb8 | |||
| e0f73c20c9 | |||
| 3e5c6ca8da | |||
| bffc5666fe | |||
| 7d7689999e | |||
| fb52f88ec3 | |||
| 902e3ac59a | |||
| 97f66f808a | |||
| 240264aafb | |||
| d5999bfb20 | |||
| bded9189e3 | |||
| 282d1eb052 | |||
| 7d4adbb3e4 | |||
| 8e9b9dfc98 | |||
| bb1e73443b | |||
| 2b12e6ada7 | |||
| 1ae640dc02 | |||
| 9d6f2de4ab | |||
| 280cf4518a | |||
| 439ec3438d | |||
| 049a5b2264 | |||
| 5aac81f601 | |||
| 6053b7340f | |||
| 8e1a28577c | |||
| e6bc123481 | |||
| 6d8193296e | |||
| 6ded3b859f | |||
| 94c252ec6c | |||
| 9a0a757757 | |||
| 780b899d42 | |||
| 5270313b3d | |||
| 1281976ec3 | |||
| 267a57c4ea | |||
| e5c3863eee | |||
| c8f85496ce | |||
| 9ddda1a933 | |||
| ca393d1625 | |||
| d916ed471b | |||
| 3d51a1aa6d | |||
| 3328279581 | |||
| 053f3ce557 | |||
| 6d936c4be7 | |||
| d210eb857f | |||
| 1e0f18c854 | |||
| 9263c24ede | |||
| 3f7f8a4498 | |||
| 1ab124ac7a | |||
| 65da63d1c8 | |||
| eeceb4a8f6 | |||
| 58836fb0ef | |||
| 4016c8b6b5 | |||
| ab2b5135c0 | |||
| ac2ed79e10 | |||
| 7ce5616e65 | |||
| aefd8ffba4 | |||
| c3467bdd9d | |||
| 641130e506 | |||
| 6d05001812 | |||
| 4a3515f6f3 | |||
| b88e840ff5 | |||
| 84c88ef69e | |||
| 8b70b77b85 | |||
| 76941abbf2 | |||
| a161306359 | |||
| 8a528f0711 | |||
| 6c1f95db9f | |||
| 861984e4b1 | |||
| c6354ac656 | |||
| 0c1287d3b9 | |||
| 862912cd66 | |||
| a605fdc351 | |||
| d234a2371b | |||
| 183fefb557 | |||
| 64b3ec626c | |||
| fd52cadbff | |||
| b694340f38 | |||
| 888336dd04 | |||
| e35e857380 | |||
| c1120d1878 | |||
| 428a002e9f | |||
| 1d3bf94b88 | |||
| 8f0efc0d72 | |||
| e760bd5f97 | |||
| 899f8dce20 | |||
| aa1f3b1cb3 | |||
| f5825f9a18 | |||
| 607024c813 | |||
| 206bdc6392 | |||
| eda8d357da | |||
| 32b46f58a4 | |||
| e92eccc7d3 | |||
| af5f9567f1 | |||
| 8faaef1ef5 | |||
| d33b908775 | |||
| 5968329c8f | |||
| 09d67a7522 | |||
| 0b32b69d10 | |||
| 9bfb880f7b | |||
| b90e056e7c | |||
| dc0b49355d | |||
| 76e40cd9e1 | |||
| 0f3c8c862f | |||
| dff2b05401 | |||
| 765f3c42c1 | |||
| 1e71e54314 | |||
| d895583ea2 | |||
| 7450f90a0f | |||
| 63af22b671 | |||
| 407592680a | |||
| d6badafeb9 | |||
| d408dadd88 | |||
| 253b04ec0d | |||
| de2692f704 | |||
| 665f0d1454 | |||
| 24f576aeaa | |||
| f8c62a90f0 | |||
| 951011bc0e | |||
| 5fd683eacc | |||
| 5f6df7c69d | |||
| da074df63c | |||
| 32c71fe49b | |||
| c4d0a20bbc | |||
| 263f389e91 | |||
| 24e80a6a0a | |||
| e0638b5a98 | |||
| 57abbe8534 | |||
| f61c780185 | |||
| 48263b28b5 | |||
| 7cd9a3e074 | |||
| 914c7e4fc1 | |||
| 94aee4540a | |||
| aaf61760a3 | |||
| 8a88c6878e | |||
| 4f01d2407b | |||
| d6dd0a1602 | |||
| c1dc88cdd9 | |||
| 1beb0dbaf3 | |||
| de32d72eda | |||
| 87aaa28d92 | |||
| 70347920f2 | |||
| 2f177ad603 | |||
| 06b97cbdae | |||
| 2382b2dfae | |||
| fc3ed1fb11 | |||
| 29cfaccd02 | |||
| d6e11766da | |||
| 3cd7120b4b | |||
| 433bb7d05e | |||
| 65598bceac | |||
| 67389a5f78 | |||
| 0909f34649 | |||
| 4cfa4dbffd | |||
| f3892861b1 | |||
| 572d564135 | |||
| 7eb4a32552 | |||
| 9d2cecd55b | |||
| 16962a6df8 | |||
| cf2b88199a | |||
| 75e2f84726 | |||
| 8b34609e53 | |||
| d7939a9ca4 | |||
| ee8e17d80e | |||
| 29bebe6149 | |||
| 6d0b764ee2 | |||
| 89abbde753 | |||
| 61d22dbfa1 | |||
| 62146ff970 | |||
| 802c0c3f09 | |||
| d54a3aab7a | |||
| 6d22aec2f4 | |||
| 86a5b7e63e | |||
| 3a0c0fc9a7 | |||
| 10b29c336a | |||
| e4ae514830 | |||
| d9f35602b2 | |||
| 1838d07082 | |||
| 655601dddb | |||
| 3ac8b63164 | |||
| 43a6d1d631 | |||
| 1526879a38 | |||
| 157533bb9d | |||
| cf31d43702 | |||
| 26b7dfc06c | |||
| 61835c3f26 | |||
| 55c101df12 | |||
| c439eec66c | |||
| 75518897f1 | |||
| 3bd4ac1f56 | |||
| 2eb19b36a6 | |||
| bbd5bbab9b | |||
| 8ff63fadec | |||
| 8af09e1841 | |||
| 5d3cdeb960 | |||
| d4fe665462 | |||
| 560b15ec93 | |||
| efaebb2f4e | |||
| 9658ad621a | |||
| f918e735a5 | |||
| 4907dd478a | |||
| 31c7bd34b7 | |||
| e0ed8f41bb | |||
| ef0af906ff | |||
| 2d332f1ded | |||
| 6c3189c8f0 | |||
| 4abb3917e6 | |||
| 59c32ef8d8 | |||
| 4514627ec0 | |||
| ceae4972b8 | |||
| 4b754b5ef5 | |||
| f7f239fcb9 | |||
| 78846fd85a | |||
| 33411c4e7d | |||
| 0a9731be37 | |||
| 7bf29ffe32 | |||
| ce8e35b628 | |||
| c85b61000a | |||
| 39820d7122 | |||
| 4054d1acf8 | |||
| 02330e2173 | |||
| 7ab53557ce | |||
| 50f1ed8d41 | |||
| f9b69be791 | |||
| c092696a9a | |||
| d1cf575845 | |||
| ce3f6323ea | |||
| 008d96d0b8 | |||
| 473980d6b4 | |||
| 625bb4be4a | |||
| f74b717bc0 | |||
| ffb91586a0 | |||
| 103499a163 | |||
| 1fd568c562 | |||
| b61a3de40a | |||
| c729c595ee | |||
| f05c222c42 | |||
| 3b9c77f321 | |||
| ccf25a69f0 | |||
| 5744e19344 | |||
| 29a6d885b4 | |||
| 80d1d205a1 | |||
| f81d9ebb40 | |||
| 4588c2b9e9 | |||
| 22ce0d6e7a | |||
| 26cb53b231 | |||
| 183313b550 | |||
| f917d8e370 | |||
| fc6e76d756 | |||
| 8750ccbb7d | |||
| 87411d9b87 | |||
| 1dfcc694ef | |||
| 67a86ed9ab | |||
| 5f31d0a89c | |||
| 6d6f8b419a | |||
| 2cfdafa507 | |||
| 6d0f83fab1 | |||
| bae41efdf5 | |||
| 467410af6a | |||
| 5b09a11a6e | |||
| 72f8064d06 | |||
| a222da1970 | |||
| 3cbd6b265e | |||
| 9aefa5d08f | |||
| f51ced1c31 | |||
| f683f149e6 | |||
| 7482e974f2 | |||
| 83fc24be0c | |||
| 4e61b20ba8 | |||
| 1c3c6adcc2 | |||
| 2a2078a85f | |||
| 3e17709998 | |||
| e2b24964bc | |||
| cfcbdfb749 | |||
| fbf8e883d9 | |||
| 969fa60518 | |||
| 63ee3c7b9f | |||
| 6bb1222e0b | |||
| baa59f16ce | |||
| 906e893bf0 | |||
| c68fe87622 | |||
| 7851dd80ca | |||
| c95cc818b5 | |||
| 25483dd8f2 | |||
| c81ab45919 | |||
| 4d9b1f1dff | |||
| 3aed09d023 | |||
| 68763996ad | |||
| 347bc0cf7b | |||
| 29b12cdc46 | |||
| 0e03a5914b | |||
| 936c6cdf00 | |||
| efba076c41 | |||
| 7f0f6567b0 | |||
| e5696191af | |||
| 62c403307f | |||
| 7f2a6ba438 | |||
| 9afd5c3bf5 | |||
| dd2bf7a8ff | |||
| 4a05bd5bc3 | |||
| 850ef006c5 | |||
| ce39e96c0a | |||
| 2366790cc0 | |||
| 4fa752f7c9 | |||
| 76e1a333dc | |||
| 0da2beaa64 | |||
| 40c12ae487 | |||
| 2656341956 | |||
| 9894f003e3 | |||
| b57b11ca9a | |||
| 46d0cbfd41 | |||
| a207237bc2 | |||
| 476b4b5d4f | |||
| 911f5f049a | |||
| 5588d5a76c | |||
| 0ae247ac98 | |||
| 8480ce1512 | |||
| 07b914675d | |||
| 64d4569655 | |||
| 8ab20cee51 | |||
| 3f4636d698 | |||
| cc9f7e58ec | |||
| 168ebead92 | |||
| f79c569383 | |||
| b67e4cc6d2 | |||
| cb19fa63f8 | |||
| fd3ac28ae2 | |||
| e504ca60ce | |||
| d3f70942b2 | |||
| 51ca916056 | |||
| 4ae55a4956 | |||
| a23376c727 | |||
| 55ac79c717 | |||
| 95a122c4bf | |||
| f30ccd6c45 | |||
| cd1d9f4cf2 | |||
| caa92843b3 | |||
| 717eaf0851 | |||
| 975acb9833 | |||
| 85479435b0 | |||
| d134f5541d | |||
| 18b63272b7 | |||
| e60c0df77b | |||
| eea7937225 | |||
| 9fad0eafd0 | |||
| 91a62f88d2 | |||
| 8f0e5ad37a | |||
| cf42caa519 | |||
| 8f7ce314fb | |||
| d45d16a810 | |||
| 0fe16ad79e | |||
| af1bf82c75 | |||
| 9b6af75e98 | |||
| f35d375c5f | |||
| 522af71f9b | |||
| 1dfc1fd2c1 | |||
| a9d0a060b8 | |||
| d3104bb5a2 | |||
| 800b7d883e | |||
| 6d190d2501 | |||
| 0f03deeadd | |||
| fc46e807e7 | |||
| c98394b5bf | |||
| 95e8da2e39 | |||
| a19aa47e34 | |||
| 00d9361aad | |||
| 577e5d947a | |||
| 1985f7b725 | |||
| 30da08689d | |||
| 9a210bceb9 | |||
| 8302b00a9c | |||
| 0d918b8719 | |||
| 278afb5ee8 | |||
| c7e18d689e | |||
| 6d233f5bfb | |||
| 949b094490 | |||
| 34c5764472 | |||
| 9f59b94336 | |||
| f5f2949545 | |||
| 01e3d878d3 | |||
| 45741a6295 | |||
| 42edd626fd | |||
| af86fdd8ed | |||
| dc385b8422 | |||
| 8d5e158d59 | |||
| 7c4272ffe9 | |||
| 27af6a0953 | |||
| 97a095b422 | |||
| 6d76acc0bc | |||
| aeaba64865 | |||
| c492442d3d | |||
| b0497142ed | |||
| 95b1c16cf1 | |||
| 7265164b0a | |||
| 841bb67d60 | |||
| c70cd797ac | |||
| 80d68e0aa9 | |||
| 23573caceb | |||
| 661e826767 | |||
| 8989975be1 | |||
| a4d68cafd8 | |||
| 472ee0fd53 | |||
| 86a12cc223 | |||
| 01469d97f0 | |||
| c58b6ca2aa | |||
| 58ef4ec620 | |||
| e3c3a5c444 | |||
| a7336999ef | |||
| 0e3586dccf | |||
| 2490c69beb | |||
| 986f27a48c | |||
| 6bc8e4e7f0 | |||
| dcac52ff9d | |||
| 6cfaf3fdbe | |||
| 10c686bfa1 | |||
| f77ffeaf4c | |||
| ff6ee91075 | |||
| 93d0f97a56 | |||
| e5b4409df7 | |||
| f4fae86d31 | |||
| fe8aa2e758 | |||
| 0209c49503 | |||
| 2b1e2a12eb | |||
| e98e1dc634 | |||
| 583ca15dcc | |||
| 1264b987fe | |||
| ffec9b7dc4 | |||
| a742fc8c54 | |||
| cf8845848a | |||
| 6b1e697209 | |||
| a6b9247305 | |||
| bfc11bcace | |||
| 132016f562 | |||
| 96cd94d77f | |||
| bab0187467 | |||
| 8d4ff3c88e | |||
| dd89a70789 | |||
| 50e583389e | |||
| ef8e0d0e83 | |||
| 08f104d4c9 | |||
| 4346fbffe5 | |||
| 03231ab3f3 | |||
| 80f05c7029 | |||
| 5199afb9b5 | |||
| 3fa50c158c | |||
| cdbf1785a3 | |||
| 3eadedcb19 | |||
| 4e469a3ffc | |||
| f93eb9ec71 | |||
| eb37257ae8 | |||
| bb99d5ae1c | |||
| e982684d95 | |||
| e2e368e9f8 | |||
| 4a3c4f4bd7 | |||
| fd3b96a11c | |||
| 9d3aba9179 | |||
| 1b30825b76 | |||
| 6f6754db39 | |||
| f1eb857eda | |||
| cad2ed9dba | |||
| c94e4c3194 | |||
| 6599fdc6c2 | |||
| f5431ddade | |||
| 835486d04f | |||
| db53043b19 | |||
| 131a7a95b9 | |||
| 6a68c2fbcd | |||
| 5606858dc7 | |||
| b17af2620b | |||
| 55038d2bcb | |||
| a0ffdce1bb | |||
| a9ea5982cf | |||
| 557d0eff2f | |||
| 241a912f08 | |||
| caba8348bf | |||
| 3332b7f258 | |||
| f1ef5fce58 | |||
| b647518d2f | |||
| 834f96f258 | |||
| 57b7d0b35b | |||
| 1679bfdc7d | |||
| d9dc7c67be | |||
| 3986c9cd0a | |||
| e71118ad09 | |||
| 795a8662f9 | |||
| 81165c6954 | |||
| c419fe610d | |||
| 87dc8ead08 | |||
| c231994ad1 | |||
| fe8902ba7f | |||
| f4f28c095d | |||
| 50a22877e4 | |||
| 1b2bc1424d | |||
| e6c9942d7d | |||
| 3998c997d4 | |||
| 207ed7ce36 | |||
| 415dbed67a | |||
| e3196460b9 | |||
| 34b566343f | |||
| 7b4d6d7858 | |||
| b310d96482 | |||
| 9877206825 | |||
| 7fd088b758 | |||
| 8220db14f3 | |||
| dc79787f70 | |||
| 0272c9434c | |||
| 658e3f66b3 | |||
| 0d8f403058 | |||
| 4623bb3bb7 | |||
| ec741acf7c | |||
| 8edd996b89 | |||
| d762bb3228 | |||
| 842afcfdb2 | |||
| e17f8aa734 | |||
| 577b2dd035 | |||
| 8f6ca1593e | |||
| 7e95547eb1 | |||
| 8d39c16e26 | |||
| fa25923e62 | |||
| b9cd79cabe | |||
| 6049fcbb1b | |||
| 93b36be42c | |||
| e21e46b2c8 | |||
| 78b16d2c1c | |||
| 9afe62eae5 | |||
| bd2fe9bb03 | |||
| dd10760da1 | |||
| 3bf54a4e32 | |||
| 7750415e9a | |||
| ba1aa84ac4 | |||
| 9d89b547b2 | |||
| 808df9a6ad | |||
| abc10a77f8 | |||
| bc5ba9c352 | |||
| 554f871a1a | |||
| af90c6fcf3 | |||
| 977e9e0344 | |||
| b924c92908 | |||
| b7ff4a915b | |||
| 56334128ae | |||
| fe14591e02 | |||
| ab7f8b0846 | |||
| bb4aea512d | |||
| f49d6f45b6 | |||
| 74be47b84c | |||
| 1a29b47583 | |||
| d3b4f217e2 | |||
| cfcfceb9e8 | |||
| 5b913799d0 | |||
| f0c96f5e10 | |||
| e45ff93100 | |||
| 05b47fa2e9 | |||
| c53154d115 | |||
| e79d034c2c | |||
| 1136d59f20 | |||
| b1ed0b17ed | |||
| 8604a18ed5 | |||
| 267b68283f | |||
| a47426af70 | |||
| 035b5f2eac | |||
| 70000c776e | |||
| 8593720c15 | |||
| 9f5409cc70 | |||
| 621c61c094 | |||
| b5287f4a7d | |||
| 5f483b4980 | |||
| ff5593ebc5 | |||
| 684f65250f | |||
| 7a986189df | |||
| a3bec6a9b1 | |||
| ea6181265d | |||
| 65e1025289 | |||
| a144bba54b | |||
| 7f8efcf32f | |||
| 95968681ce | |||
| 338bcd0df9 | |||
| eeec6d2f92 | |||
| 6ea58d7990 | |||
| 7d3cba7e79 | |||
| f4ab1b382d | |||
| d9a2384701 | |||
| 235f3690cb | |||
| 548e85a00a | |||
| 4d2401ff59 | |||
| eac0a6573a | |||
| c77198625c | |||
| b3fd996f3f | |||
| 830c71ada7 | |||
| 4fbde89251 | |||
| 9eae5740a8 | |||
| c019b2608c | |||
| 57a4512a0e | |||
| 7deffc885e | |||
| b33ad857b1 | |||
| 207d9ce5f6 | |||
| ee1aa1bbed | |||
| 856ebd1b60 | |||
| bb7d7ae3a9 | |||
| 5418c0195e | |||
| 35c98ebb97 | |||
| 6cb9653dea | |||
| f0550f5233 | |||
| f21cd63367 | |||
| 325c37e26e | |||
| 4ff9fed3df | |||
| ff69796eab | |||
| cd7cecb61b | |||
| e85a1def90 | |||
| ba72a896bb | |||
| b5b1741f21 | |||
| 19a5d906da | |||
| 34e6fb7b64 | |||
| bd15ac8b2d | |||
| 76e207cd20 | |||
| bd1916bdaa | |||
| 3dcaeb1d4f | |||
| f1fa6d2762 | |||
| bb4b9734aa | |||
| 78b0ee6f9b | |||
| 6d234ffb5b | |||
| be4f586613 | |||
| 02b8768f8e | |||
| 38be27a89c | |||
| cff5d9be29 | |||
| 11594e6dc7 | |||
| 44e8ce0105 | |||
| ea7ffaca5d | |||
| 898663344b | |||
| 15a889dfcf | |||
| 5442accb48 | |||
| b7d64b1f9e | |||
| aa849f591b | |||
| c3fcb83da2 | |||
| 0d75678ffd | |||
| d907f1ca01 | |||
| e42dd9cc6e | |||
| 14e4e79c43 | |||
| d83f31c3c2 | |||
| 6f47b12501 | |||
| 99cd9dd195 | |||
| 5fb62313d4 | |||
| 41aa40b237 | |||
| 1f5d4434be | |||
| 3d83100eda | |||
| 29860684cb | |||
| 4c6cce416d | |||
| 449386a906 | |||
| 3be4b4eb50 | |||
| f8af82d0a8 | |||
| 855f527a87 | |||
| 69c056a35f | |||
| 11e43e6a9b | |||
| 8b49ae373e | |||
| b084f13f0c | |||
| c616fe1154 | |||
| 89cb068db6 | |||
| c96ff65bc7 | |||
| 57b631ad72 | |||
| 7980efec50 | |||
| 79c1d2d361 | |||
| ba0646e768 | |||
| 58a9d72946 | |||
| f3ce52d247 | |||
| 08acee792b | |||
| 183bac752e | |||
| dc587029e1 | |||
| bb7cd71a07 | |||
| bb68f29d4d | |||
| 1b1ef80983 | |||
| eb99adafab | |||
| 96341185c0 | |||
| 07e7c43a2e | |||
| f1e50f7c38 | |||
| 288163aded | |||
| 4e37915516 | |||
| 53b13bc6a5 | |||
| ff27862bdd | |||
| 3eda49d468 | |||
| 62e891f594 | |||
| 2ed4d9d1d4 | |||
| 1beeb4ea6d | |||
| 48ee83a77e | |||
| f01b307865 | |||
| f315c920e7 | |||
| 79c169ef2a | |||
| ac6aa470af | |||
| e8263a8bb5 | |||
| 7c64447c9d | |||
| 2a728e0ee7 | |||
| 855e137376 | |||
| 074985a2ac | |||
| eedc8fe67f | |||
| c63bcb9246 | |||
| efd929b153 | |||
| a7ed13ae9a | |||
| 8ffc070622 | |||
| 9e67654272 | |||
| d81d2d89d4 | |||
| c1591099c6 | |||
| a84f906d43 | |||
| f6c1dc1cab | |||
| e448bac0d8 | |||
| 1c22561acc | |||
| e1ee5a89f0 | |||
| c75e97252c | |||
| 20ec16c3d1 | |||
| 36a039f72e | |||
| 21f5b17673 | |||
| d675eb96a6 | |||
| 4401692de0 | |||
| 85ffd61c16 | |||
| abbc8bf945 | |||
| 291762a49e | |||
| 05d2575544 | |||
| 83b2120ef5 | |||
| b0d99f0a86 | |||
| 80af069669 | |||
| 1dc71d5791 | |||
| abc5b8ff38 | |||
| 6dc61da519 | |||
| 52f3eb15a7 | |||
| 524b4b17c8 | |||
| c109344b07 | |||
| 15e480d26d | |||
| 807503c88f | |||
| d05d5c687c | |||
| 8b6aa4b502 | |||
| d586f8d9a6 | |||
| 4f72c53648 | |||
| 4bcce61829 | |||
| aebfe7dac6 | |||
| 2e45ff3561 | |||
| d3d7799812 | |||
| 984831eaff | |||
| ceb0f7c33e | |||
| 75b6e630fb | |||
| 70cca68f30 | |||
| 2a63ff827b | |||
| ce52c983a8 | |||
| cc81d18344 | |||
| c207329a3e | |||
| f4b0964e25 | |||
| 162f516647 | |||
| e459c0a4e5 | |||
| d5378f1f83 | |||
| 1d5471f463 | |||
| 673f1b8d58 | |||
| 85c7a18b1a | |||
| 6676d6314b | |||
| 8048d73551 | |||
| 88103c01eb | |||
| 06aa639ae4 | |||
| 7385fc1a7e | |||
| 3711fa75cf | |||
| 66feca3323 | |||
| 148619eb05 | |||
| 30a658b083 | |||
| 4f45bd2b76 | |||
| 5098332d20 | |||
| 335a01d448 | |||
| 61de673741 | |||
| 7ea5da8d0f | |||
| eb1eacdad9 | |||
| 3188409bac | |||
| cbc1f5a84d | |||
| 6139f12e0d | |||
| ce4efeaf2d | |||
| 7dcddba401 | |||
| 0bd894a61b | |||
| ab3437989a | |||
| 540fe100cf | |||
| b388f29df5 | |||
| b3f1295809 | |||
| 80a465fad8 | |||
| 00e7e7e01e | |||
| 43a28c9b2e | |||
| 06c775f563 | |||
| 2f9bad0f8f | |||
| ec9fae9e5a | |||
| 9efa596059 | |||
| 01914fcc1b | |||
| 33818033a4 | |||
| e655d9cecd | |||
| 94d61a6d52 | |||
| 830e5d3602 | |||
| ada3143ffb | |||
| 879a8da1f4 | |||
| 96a66f24e6 | |||
| f6aae59e72 | |||
| 9758c7b406 | |||
| bec94e4c6a | |||
| e71281dcee | |||
| 6626bd568d | |||
| cdb3131185 | |||
| 57c0f81263 | |||
| b40553cc54 | |||
| 62b29fb644 | |||
| 5555239fb3 | |||
| b2bc69f782 | |||
| eff2b8a0bb | |||
| f2d4107172 | |||
| 0911678db0 | |||
| 120aa12727 | |||
| f48bcdcd7b | |||
| 39b6ad28d0 | |||
| 2bd8b2b03a | |||
| 0d4d5227a0 | |||
| 07909550c3 | |||
| 4f7bffe6fe | |||
| f3590ebf7c | |||
| 8b1950fc54 | |||
| 9a61a309a7 | |||
| 542f941879 | |||
| c57ea6add6 | |||
| 0d949c7894 | |||
| f6accf0aa7 | |||
| 1ecf89a2b1 | |||
| 630d04fb46 | |||
| 8a00dffce0 | |||
| 423733967e | |||
| 38c6e01d9f | |||
| 566efb0d2c | |||
| 06725ee497 | |||
| 4df416a818 | |||
| 26fef320ef | |||
| ff1685a721 | |||
| 0baba63542 | |||
| 2c175ad1b2 | |||
| cd190132f9 | |||
| 0bab558c75 | |||
| af38afc5b5 | |||
| 96184e4853 | |||
| 46fb5de260 | |||
| ceed45e409 | |||
| 8d9aed0ee1 | |||
| 308e9629b5 | |||
| 778308940a | |||
| 5ee5e671e7 | |||
| 7f23d8efd8 | |||
| 639cdb4c1f | |||
| 3a4b1a7a56 | |||
| c103e9402a | |||
| a326c6ab3a | |||
| efbb97967a | |||
| 1c6c6e4a33 | |||
| 159b77334b | |||
| cae33c92c9 | |||
| cbdf3edd8d | |||
| 8723540b0d | |||
| 4f395a01d3 | |||
| b877d5f91b | |||
| 595531683b | |||
| 6989dd61f7 | |||
| 4ef7d410f4 | |||
| cacdb442ca | |||
| 9a70879778 | |||
| d5ef6469bd | |||
| f44ac5960c | |||
| 803b155f65 | |||
| a55d4817bd | |||
| da3dc1e663 | |||
| 9884a08e83 | |||
| cd463bd1d1 | |||
| ee5ebfe0b9 | |||
| 92ec684066 | |||
| 4dcce6238f | |||
| 98619a9082 | |||
| 334ee2f897 | |||
| 156ca348aa | |||
| 8cfa5eaa43 | |||
| d3c865a10f | |||
| 42686efbe7 | |||
| a1e6061579 | |||
| 2bda15d264 | |||
| 43c7148637 | |||
| 1a79f9dbd9 | |||
| 325d25034d | |||
| 4a0e092115 | |||
| 1ba3d9c3e9 | |||
| 61110da453 | |||
| 565d531376 | |||
| b465f4a75a | |||
| 813944fc23 | |||
| c8521ad1f6 | |||
| 0d849f5fcb | |||
| 8abfce0fa1 | |||
| acf5d1c2ea | |||
| a6c1eaf69f | |||
| 756bfb8478 | |||
| 1af63705ff | |||
| 9e02e1f99d | |||
| 05a4ecc654 | |||
| 847d8512e7 | |||
| 6b8bf96bec | |||
| 586a2f5fc1 | |||
| ff5d20421e | |||
| 8e68858489 | |||
| a6712f7c98 | |||
| 9993217ed1 | |||
| 5928c34b03 | |||
| 5817931099 | |||
| 024adba069 | |||
| c093f9d191 | |||
| 60658a2ef9 | |||
| 3633b8ee37 | |||
| 46ddd5659b | |||
| f9ab8f4050 | |||
| 4b6857958c | |||
| d9a22518da | |||
| 818bfd5b1c | |||
| fbf1f78d64 | |||
| 59424f067c | |||
| 869f381a1b | |||
| 988c33d07d | |||
| fc9984eb3a | |||
| ce4a6fcd67 | |||
| 60c2870123 | |||
| 1c15f02d34 | |||
| 0140ffaf5e | |||
| 2899633436 | |||
| 9812c0a122 | |||
| 947fe710a3 | |||
| a246263424 | |||
| f69b30b5ba | |||
| 928e4d0ca9 | |||
| 45a81d90df | |||
| 27ccfd2c72 | |||
| a61382486f | |||
| c67c4a70c4 | |||
| 53b59bd95f | |||
| 3a55c80ae2 | |||
| 34533b401c | |||
| ecf7110781 | |||
| d3a7f0636b | |||
| 13c91fe728 | |||
| d3c2c8f642 | |||
| a905b1f966 | |||
| 8dbf43b672 | |||
| ca15cfbd0b | |||
| 2cbc30de6f | |||
| 163c6232b8 | |||
| 3efbd9065a | |||
| 7fb9744847 | |||
| 06badd590c | |||
| e1f94cfb72 | |||
| 16932ad71c | |||
| 856628bce6 | |||
| 1e1f82cc2d | |||
| 0f92dbe61c | |||
| 42bfb2d8eb | |||
| f461bb7694 | |||
| da62213579 | |||
| c1282c613e | |||
| 782f97afcc | |||
| a75506b183 | |||
| 5d7714519a | |||
| 082573f6d9 | |||
| ffea7627cf | |||
| f9995505a8 | |||
| f31e76ac3a | |||
| 2458b3573f | |||
| 3c5f80f8b5 | |||
| eee6060e58 | |||
| b0f186dc4e | |||
| db3880a31a | |||
| 673bcb279a | |||
| 805ed8e9f4 | |||
| 51dbb10b45 | |||
| 516bf5dd7f | |||
| fc0a6eaa76 | |||
| 93a2d96c21 | |||
| c546e2cf3c | |||
| 1e4d0153b4 | |||
| d5c2ed86f7 | |||
| c70b172b08 | |||
| 6f965b1704 | |||
| 9cad48cb31 | |||
| 5a8d8fd5a9 | |||
| acd8fc8290 | |||
| c8265aebea | |||
| ab7df093c4 | |||
| 876c8774fa | |||
| 26f8d8f710 | |||
| 133fd1d892 | |||
| 60a860d9b5 | |||
| 725fe01b69 | |||
| 3b8aeb3b87 | |||
| 22d99ba39e | |||
| 5e4118acdd | |||
| 734c9ec8f3 | |||
| 4127c6d4fb | |||
| b71814369d | |||
| 1c76d8e63c | |||
| 7457abb173 | |||
| a349b1eb4f | |||
| 0bc0a59939 | |||
| 75963ce8b4 | |||
| db096e462c | |||
| 9846e91c2f | |||
| 98d14a3e34 | |||
| 4ded159c49 | |||
| baa06b60fb | |||
| c776100ed0 | |||
| 8f7cfe8362 | |||
| d915cce277 | |||
| d31478a858 | |||
| dc480f38b6 | |||
| 510ce73ecd | |||
| 91bc893001 | |||
| 269c5f4b59 | |||
| 013ae3c69c | |||
| de327850ae | |||
| 53000988d9 | |||
| 02beb31e43 | |||
| c27fd4cd2d | |||
| fc0caf2d00 | |||
| d01869fca8 | |||
| 1e952e008e | |||
| f9909a87aa | |||
| dc3c8a2b60 | |||
| eb4b8a8d9b | |||
| 101b8d3361 | |||
| 669f50de6c | |||
| 10c6e72d87 | |||
| d327277bac | |||
| d2da96eaa3 | |||
| e8b75956d0 | |||
| de1fe083d8 | |||
| de70c4ca80 | |||
| bc24f49476 | |||
| e139eb771c | |||
| 4cd4a9c7c4 | |||
| 26d0148525 | |||
| eca1cd380b | |||
| 28ff274107 | |||
| 491d502c40 | |||
| 7966173385 | |||
| 86cd55102c | |||
| 7a337482d4 | |||
| 1cbb8cbacf | |||
| 633d75b798 | |||
| ac9fadf922 | |||
| eddaae2d87 | |||
| 1269d3b838 | |||
| c1108314c1 | |||
| 0fd9db2357 | |||
| aa322a3234 | |||
| 34dfb334ec | |||
| f9d7c93d21 | |||
| 6114c96b14 | |||
| bb158c1353 | |||
| bf04b9c563 | |||
| 9fa1059301 | |||
| 0f7555907b | |||
| ca62d492ba | |||
| 44a0e10a94 | |||
| 3a0f3f75b4 | |||
| e2a15644ce | |||
| 129c0e4d25 | |||
| 2fe1aeb4f2 | |||
| d51ad2c4eb | |||
| 895f508c29 | |||
| e0750e755f | |||
| 491cbf15b6 | |||
| d7fafda78e | |||
| 092f3aa6bd | |||
| 281f3562ec | |||
| 7c162f157a | |||
| e4b02af524 | |||
| cd79725a27 | |||
| 88bca67745 | |||
| c0e0345faa | |||
| 34096f877a | |||
| 66ee33cdde | |||
| edf1de189b | |||
| d4edc66c2c | |||
| 6ef8573032 | |||
| e4a41d9126 | |||
| 39a2f19b0d | |||
| 73d3c6311a | |||
| aef8cce755 | |||
| 874ff01551 | |||
| d1419fdad1 | |||
| 85dd60c197 | |||
| 6791342879 | |||
| 423b2638aa | |||
| 99b8738c31 | |||
| 62b5e1bc57 | |||
| 5783fb1f2e | |||
| 5cbfbff4ac | |||
| 1cbea757d0 | |||
| fb8041c32e | |||
| 8f444bd917 | |||
| 8e69cda09b | |||
| b4b70d8e7c | |||
| 3924fd79d2 | |||
| 0d86e4c4f9 | |||
| 96d4ee7356 | |||
| c6e0854544 | |||
| 4e3ac183d4 | |||
| 436cb2c163 | |||
| ddb3b90788 | |||
| 6e2e419a1c | |||
| d5c3034758 | |||
| 0ccd5635e7 | |||
| 0495f40bef | |||
| 630798c89f | |||
| fa0eb73363 | |||
| 1a0b7288df | |||
| 2fc45b3ea0 | |||
| 22a3b8698c | |||
| fa661ff6b3 | |||
| bd09d07698 | |||
| 34d1eb9c71 | |||
| fe74e7d91b | |||
| 3167b6a20a | |||
| 6f6e835b0a | |||
| 497d768d4e | |||
| 9f5a142680 | |||
| f6c1f4219b | |||
| f5e7700809 | |||
| 05130052af | |||
| 291b0350e8 | |||
| 7aa06f595d | |||
| 3227414a92 | |||
| 12323382a5 | |||
| d62fc5d668 | |||
| 8822854040 | |||
| 61793838bb | |||
| 991cac18f2 | |||
| 57e856c71c | |||
| c41b99f29d | |||
| 9e092823e4 | |||
| 8a1e2384d1 | |||
| 8d777a3d50 | |||
| 3625db30ec | |||
| 1573cb2b1e | |||
| 537896503f | |||
| e792ed39c9 | |||
| cc636ce040 | |||
| c82a0e5e1c | |||
| b9772def05 | |||
| 0590fa0875 | |||
| cd36514e1c | |||
| 2dd31690e5 | |||
| 2721a3b2d4 | |||
| 5503c572f1 | |||
| 6ffc4f01d9 | |||
| 5182959881 | |||
| 36371f94e8 | |||
| 2b7bf12bc7 | |||
| ce3a013f86 | |||
| 987f8a0bec | |||
| 73678f0507 | |||
| 0d6357af26 | |||
| e863bea269 | |||
| 6d13d02efb | |||
| 6a5238d2bb | |||
| 76aa6631f3 | |||
| 4b413e0209 | |||
| 04f4ccbe03 | |||
| ef9105e286 | |||
| 68a5775717 | |||
| fb6e8a89c0 | |||
| 999738ed4b | |||
|
|
19401d27c0 | ||
| 465a531bf4 | |||
|
|
5da6b4ae99 | ||
| f63bda171f | |||
| 45da05c9a4 | |||
| 098bc97fa4 | |||
| d74d7abc90 | |||
| ff966418b2 | |||
| 7cc942b42f | |||
| dea4f7eddc | |||
| cb3d48d42c | |||
| 8b76434b41 | |||
| 2ed3ed4b45 | |||
| f400833213 | |||
|
|
832a0b9f29 | ||
|
|
677030f712 |
5
backend/.gitignore
vendored
Normal file
5
backend/.gitignore
vendored
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
node_modules
|
||||||
|
# Keep environment variables out of version control
|
||||||
|
.env
|
||||||
|
|
||||||
|
/src/generated/prisma
|
||||||
18
backend/.gitlab-ci.yml
Normal file
18
backend/.gitlab-ci.yml
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
before_script:
|
||||||
|
- docker info
|
||||||
|
|
||||||
|
stages:
|
||||||
|
- build
|
||||||
|
|
||||||
|
build-backend:
|
||||||
|
stage: build
|
||||||
|
tags:
|
||||||
|
- shell
|
||||||
|
- docker-daemon
|
||||||
|
variables:
|
||||||
|
IMAGE_TAG: $CI_REGISTRY_IMAGE:$CI_COMMIT_REF_SLUG
|
||||||
|
script:
|
||||||
|
- echo $IMAGE_TAG
|
||||||
|
- docker login -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD $CI_REGISTRY
|
||||||
|
- docker build -t $IMAGE_TAG .
|
||||||
|
- docker push $IMAGE_TAG
|
||||||
20
backend/Dockerfile
Normal file
20
backend/Dockerfile
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
FROM node:20-alpine
|
||||||
|
WORKDIR /usr/src/app
|
||||||
|
|
||||||
|
# Package-Dateien
|
||||||
|
COPY package*.json ./
|
||||||
|
|
||||||
|
# Dev + Prod Dependencies (für TS-Build nötig)
|
||||||
|
RUN npm install
|
||||||
|
|
||||||
|
# Restlicher Sourcecode
|
||||||
|
COPY . .
|
||||||
|
|
||||||
|
# TypeScript Build
|
||||||
|
RUN npm run build
|
||||||
|
|
||||||
|
# Port freigeben
|
||||||
|
EXPOSE 3100
|
||||||
|
|
||||||
|
# Start der App
|
||||||
|
CMD ["node", "dist/src/index.js"]
|
||||||
32
backend/TODO.md
Normal file
32
backend/TODO.md
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
# Projekt To-Do Liste
|
||||||
|
|
||||||
|
## ✅ Erledigt
|
||||||
|
- JWT-basierte Authentifizierung mit Cookie
|
||||||
|
- Prefix für Auth-Tabellen (`auth_users`, `auth_roles`, …)
|
||||||
|
- `/me` liefert User + Rechte (via `auth_get_user_permissions`)
|
||||||
|
- Basis-Seed für Standardrollen + Rechte eingespielt
|
||||||
|
- Auth Middleware im Frontend korrigiert (Login-Redirects)
|
||||||
|
- Nuxt API Plugin unterstützt JWT im Header
|
||||||
|
- Login-Seite an Nuxt UI Pro (v2) anpassen
|
||||||
|
- `usePermission()` Composable im Frontend vorbereitet
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🔄 Offene Punkte
|
||||||
|
|
||||||
|
### Backend
|
||||||
|
- [ ] `/me` erweitern: Rollen + deren Rechte strukturiert zurückgeben (`{ role: "manager", permissions: [...] }`)
|
||||||
|
- [ ] Loading Flag im Auth-Flow berücksichtigen (damit `me` nicht doppelt feuert)
|
||||||
|
- [ ] Gemeinsames Schema für Entities (Backend stellt per Endpoint bereit)
|
||||||
|
- [ ] Soft Delete vereinheitlichen (`archived = true` statt DELETE)
|
||||||
|
- [ ] Swagger-Doku verbessern (Schemas, Beispiele)
|
||||||
|
|
||||||
|
### Frontend
|
||||||
|
- [ ] Loading Flag in Middleware/Store einbauen
|
||||||
|
- [ ] Einheitliches Laden des Schemas beim Start
|
||||||
|
- [ ] Pinia-Store für Auth/User/Tenant konsolidieren
|
||||||
|
- [ ] Composable `usePermission(key)` implementieren, um Rechte einfach im Template zu prüfen
|
||||||
|
- [ ] Entity-Seiten schrittweise auf API-Routen umstellen
|
||||||
|
- [ ] Page Guards für Routen einbauen (z. B. `/projects/create` nur bei `projects-create`)
|
||||||
|
|
||||||
|
---
|
||||||
10
backend/db/index.ts
Normal file
10
backend/db/index.ts
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
import { drizzle } from "drizzle-orm/node-postgres"
|
||||||
|
import { Pool } from "pg"
|
||||||
|
import {secrets} from "../src/utils/secrets";
|
||||||
|
|
||||||
|
const pool = new Pool({
|
||||||
|
connectionString: secrets.DATABASE_URL,
|
||||||
|
max: 10, // je nach Last
|
||||||
|
})
|
||||||
|
|
||||||
|
export const db = drizzle(pool)
|
||||||
1312
backend/db/migrations/0000_brief_dark_beast.sql
Normal file
1312
backend/db/migrations/0000_brief_dark_beast.sql
Normal file
File diff suppressed because it is too large
Load Diff
32
backend/db/migrations/0001_medical_big_bertha.sql
Normal file
32
backend/db/migrations/0001_medical_big_bertha.sql
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
CREATE TABLE "time_events" (
|
||||||
|
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
|
||||||
|
"tenant_id" bigint NOT NULL,
|
||||||
|
"user_id" uuid NOT NULL,
|
||||||
|
"actor_type" text NOT NULL,
|
||||||
|
"actor_user_id" uuid,
|
||||||
|
"event_time" timestamp with time zone NOT NULL,
|
||||||
|
"event_type" text NOT NULL,
|
||||||
|
"source" text NOT NULL,
|
||||||
|
"invalidates_event_id" uuid,
|
||||||
|
"metadata" jsonb,
|
||||||
|
"created_at" timestamp with time zone DEFAULT now() NOT NULL,
|
||||||
|
CONSTRAINT "time_events_actor_user_check" CHECK (
|
||||||
|
(actor_type = 'system' AND actor_user_id IS NULL)
|
||||||
|
OR
|
||||||
|
(actor_type = 'user' AND actor_user_id IS NOT NULL)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
--> statement-breakpoint
|
||||||
|
ALTER TABLE "bankstatements" DROP CONSTRAINT "bankstatements_incomingInvoice_incominginvoices_id_fk";
|
||||||
|
--> statement-breakpoint
|
||||||
|
ALTER TABLE "bankstatements" DROP CONSTRAINT "bankstatements_createdDocument_createddocuments_id_fk";
|
||||||
|
--> statement-breakpoint
|
||||||
|
ALTER TABLE "time_events" ADD CONSTRAINT "time_events_tenant_id_tenants_id_fk" FOREIGN KEY ("tenant_id") REFERENCES "public"."tenants"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint
|
||||||
|
ALTER TABLE "time_events" ADD CONSTRAINT "time_events_user_id_auth_users_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."auth_users"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint
|
||||||
|
ALTER TABLE "time_events" ADD CONSTRAINT "time_events_actor_user_id_auth_users_id_fk" FOREIGN KEY ("actor_user_id") REFERENCES "public"."auth_users"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint
|
||||||
|
ALTER TABLE "time_events" ADD CONSTRAINT "time_events_invalidates_event_id_time_events_id_fk" FOREIGN KEY ("invalidates_event_id") REFERENCES "public"."time_events"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint
|
||||||
|
CREATE INDEX "idx_time_events_tenant_user_time" ON "time_events" USING btree ("tenant_id","user_id","event_time");--> statement-breakpoint
|
||||||
|
CREATE INDEX "idx_time_events_created_at" ON "time_events" USING btree ("created_at");--> statement-breakpoint
|
||||||
|
CREATE INDEX "idx_time_events_invalidates" ON "time_events" USING btree ("invalidates_event_id");--> statement-breakpoint
|
||||||
|
ALTER TABLE "bankstatements" DROP COLUMN "incomingInvoice";--> statement-breakpoint
|
||||||
|
ALTER TABLE "bankstatements" DROP COLUMN "createdDocument";
|
||||||
13
backend/db/migrations/0002_silent_christian_walker.sql
Normal file
13
backend/db/migrations/0002_silent_christian_walker.sql
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
ALTER TABLE "time_events" RENAME TO "staff_time_events";--> statement-breakpoint
|
||||||
|
ALTER TABLE "staff_time_events" DROP CONSTRAINT "time_events_tenant_id_tenants_id_fk";
|
||||||
|
--> statement-breakpoint
|
||||||
|
ALTER TABLE "staff_time_events" DROP CONSTRAINT "time_events_user_id_auth_users_id_fk";
|
||||||
|
--> statement-breakpoint
|
||||||
|
ALTER TABLE "staff_time_events" DROP CONSTRAINT "time_events_actor_user_id_auth_users_id_fk";
|
||||||
|
--> statement-breakpoint
|
||||||
|
ALTER TABLE "staff_time_events" DROP CONSTRAINT "time_events_invalidates_event_id_time_events_id_fk";
|
||||||
|
--> statement-breakpoint
|
||||||
|
ALTER TABLE "staff_time_events" ADD CONSTRAINT "staff_time_events_tenant_id_tenants_id_fk" FOREIGN KEY ("tenant_id") REFERENCES "public"."tenants"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint
|
||||||
|
ALTER TABLE "staff_time_events" ADD CONSTRAINT "staff_time_events_user_id_auth_users_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."auth_users"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint
|
||||||
|
ALTER TABLE "staff_time_events" ADD CONSTRAINT "staff_time_events_actor_user_id_auth_users_id_fk" FOREIGN KEY ("actor_user_id") REFERENCES "public"."auth_users"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint
|
||||||
|
ALTER TABLE "staff_time_events" ADD CONSTRAINT "staff_time_events_invalidates_event_id_staff_time_events_id_fk" FOREIGN KEY ("invalidates_event_id") REFERENCES "public"."staff_time_events"("id") ON DELETE no action ON UPDATE no action;
|
||||||
9788
backend/db/migrations/meta/0000_snapshot.json
Normal file
9788
backend/db/migrations/meta/0000_snapshot.json
Normal file
File diff suppressed because it is too large
Load Diff
9947
backend/db/migrations/meta/0001_snapshot.json
Normal file
9947
backend/db/migrations/meta/0001_snapshot.json
Normal file
File diff suppressed because it is too large
Load Diff
41
backend/db/migrations/meta/_journal.json
Normal file
41
backend/db/migrations/meta/_journal.json
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
{
|
||||||
|
"version": "7",
|
||||||
|
"dialect": "postgresql",
|
||||||
|
"entries": [
|
||||||
|
{
|
||||||
|
"idx": 0,
|
||||||
|
"version": "7",
|
||||||
|
"when": 1764947303113,
|
||||||
|
"tag": "0000_brief_dark_beast",
|
||||||
|
"breakpoints": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idx": 1,
|
||||||
|
"version": "7",
|
||||||
|
"when": 1765641431341,
|
||||||
|
"tag": "0001_medical_big_bertha",
|
||||||
|
"breakpoints": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idx": 2,
|
||||||
|
"version": "7",
|
||||||
|
"when": 1765642446738,
|
||||||
|
"tag": "0002_silent_christian_walker",
|
||||||
|
"breakpoints": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idx": 3,
|
||||||
|
"version": "7",
|
||||||
|
"when": 1765716484200,
|
||||||
|
"tag": "0003_woozy_adam_destine",
|
||||||
|
"breakpoints": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idx": 4,
|
||||||
|
"version": "7",
|
||||||
|
"when": 1765716877146,
|
||||||
|
"tag": "0004_stormy_onslaught",
|
||||||
|
"breakpoints": true
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
24
backend/db/schema/accounts.ts
Normal file
24
backend/db/schema/accounts.ts
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
import {
|
||||||
|
pgTable,
|
||||||
|
bigint,
|
||||||
|
timestamp,
|
||||||
|
text,
|
||||||
|
} from "drizzle-orm/pg-core"
|
||||||
|
|
||||||
|
export const accounts = pgTable("accounts", {
|
||||||
|
id: bigint("id", { mode: "number" })
|
||||||
|
.primaryKey()
|
||||||
|
.generatedByDefaultAsIdentity(),
|
||||||
|
|
||||||
|
createdAt: timestamp("created_at", { withTimezone: true })
|
||||||
|
.notNull()
|
||||||
|
.defaultNow(),
|
||||||
|
|
||||||
|
number: text("number").notNull(),
|
||||||
|
label: text("label").notNull(),
|
||||||
|
|
||||||
|
description: text("description"),
|
||||||
|
})
|
||||||
|
|
||||||
|
export type Account = typeof accounts.$inferSelect
|
||||||
|
export type NewAccount = typeof accounts.$inferInsert
|
||||||
83
backend/db/schema/auth_profiles.ts
Normal file
83
backend/db/schema/auth_profiles.ts
Normal file
@@ -0,0 +1,83 @@
|
|||||||
|
import {
|
||||||
|
pgTable,
|
||||||
|
uuid,
|
||||||
|
text,
|
||||||
|
timestamp,
|
||||||
|
date,
|
||||||
|
boolean,
|
||||||
|
bigint,
|
||||||
|
doublePrecision,
|
||||||
|
jsonb,
|
||||||
|
} from "drizzle-orm/pg-core"
|
||||||
|
import { authUsers } from "./auth_users"
|
||||||
|
|
||||||
|
export const authProfiles = pgTable("auth_profiles", {
|
||||||
|
id: uuid("id").primaryKey().defaultRandom(),
|
||||||
|
|
||||||
|
user_id: uuid("user_id").references(() => authUsers.id),
|
||||||
|
|
||||||
|
tenant_id: bigint("tenant_id", { mode: "number" }).notNull(),
|
||||||
|
|
||||||
|
created_at: timestamp("created_at", { withTimezone: true })
|
||||||
|
.notNull()
|
||||||
|
.defaultNow(),
|
||||||
|
|
||||||
|
first_name: text("first_name").notNull(),
|
||||||
|
last_name: text("last_name").notNull(),
|
||||||
|
|
||||||
|
full_name: text("full_name").generatedAlwaysAs(
|
||||||
|
`((first_name || ' ') || last_name)`
|
||||||
|
),
|
||||||
|
|
||||||
|
mobile_tel: text("mobile_tel"),
|
||||||
|
fixed_tel: text("fixed_tel"),
|
||||||
|
salutation: text("salutation"),
|
||||||
|
employee_number: text("employee_number"),
|
||||||
|
|
||||||
|
weekly_working_hours: doublePrecision("weekly_working_hours").default(0),
|
||||||
|
annual_paid_leave_days: bigint("annual_paid_leave_days", { mode: "number" }),
|
||||||
|
|
||||||
|
weekly_regular_working_hours: jsonb("weekly_regular_working_hours").default("{}"),
|
||||||
|
|
||||||
|
clothing_size_top: text("clothing_size_top"),
|
||||||
|
clothing_size_bottom: text("clothing_size_bottom"),
|
||||||
|
clothing_size_shoe: text("clothing_size_shoe"),
|
||||||
|
|
||||||
|
email_signature: text("email_signature").default("<p>Mit freundlichen Grüßen</p>"),
|
||||||
|
|
||||||
|
birthday: date("birthday"),
|
||||||
|
entry_date: date("entry_date").defaultNow(),
|
||||||
|
|
||||||
|
automatic_hour_corrections: jsonb("automatic_hour_corrections").default("[]"),
|
||||||
|
|
||||||
|
recreation_days_compensation: boolean("recreation_days_compensation")
|
||||||
|
.notNull()
|
||||||
|
.default(true),
|
||||||
|
|
||||||
|
customer_for_portal: bigint("customer_for_portal", { mode: "number" }),
|
||||||
|
|
||||||
|
pinned_on_navigation: jsonb("pinned_on_navigation").notNull().default("[]"),
|
||||||
|
|
||||||
|
email: text("email"),
|
||||||
|
token_id: text("token_id"),
|
||||||
|
|
||||||
|
weekly_working_days: doublePrecision("weekly_working_days"),
|
||||||
|
|
||||||
|
old_profile_id: uuid("old_profile_id"),
|
||||||
|
temp_config: jsonb("temp_config"),
|
||||||
|
|
||||||
|
state_code: text("state_code").default("DE-NI"),
|
||||||
|
|
||||||
|
contract_type: text("contract_type"),
|
||||||
|
position: text("position"),
|
||||||
|
qualification: text("qualification"),
|
||||||
|
|
||||||
|
address_street: text("address_street"),
|
||||||
|
address_zip: text("address_zip"),
|
||||||
|
address_city: text("address_city"),
|
||||||
|
|
||||||
|
active: boolean("active").notNull().default(true),
|
||||||
|
})
|
||||||
|
|
||||||
|
export type AuthProfile = typeof authProfiles.$inferSelect
|
||||||
|
export type NewAuthProfile = typeof authProfiles.$inferInsert
|
||||||
23
backend/db/schema/auth_role_permisssions.ts
Normal file
23
backend/db/schema/auth_role_permisssions.ts
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
import { pgTable, uuid, text, timestamp } from "drizzle-orm/pg-core"
|
||||||
|
import { authRoles } from "./auth_roles"
|
||||||
|
|
||||||
|
export const authRolePermissions = pgTable(
|
||||||
|
"auth_role_permissions",
|
||||||
|
{
|
||||||
|
created_at: timestamp("created_at", { withTimezone: true })
|
||||||
|
.notNull()
|
||||||
|
.defaultNow(),
|
||||||
|
|
||||||
|
role_id: uuid("role_id")
|
||||||
|
.notNull()
|
||||||
|
.references(() => authRoles.id),
|
||||||
|
|
||||||
|
permission: text("permission").notNull(),
|
||||||
|
},
|
||||||
|
(table) => ({
|
||||||
|
primaryKey: [table.role_id, table.permission],
|
||||||
|
})
|
||||||
|
)
|
||||||
|
|
||||||
|
export type AuthRolePermission = typeof authRolePermissions.$inferSelect
|
||||||
|
export type NewAuthRolePermission = typeof authRolePermissions.$inferInsert
|
||||||
19
backend/db/schema/auth_roles.ts
Normal file
19
backend/db/schema/auth_roles.ts
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
import { pgTable, uuid, text, timestamp, bigint } from "drizzle-orm/pg-core"
|
||||||
|
import { authUsers } from "./auth_users"
|
||||||
|
|
||||||
|
export const authRoles = pgTable("auth_roles", {
|
||||||
|
id: uuid("id").primaryKey().defaultRandom(),
|
||||||
|
|
||||||
|
created_at: timestamp("created_at", { withTimezone: true })
|
||||||
|
.notNull()
|
||||||
|
.defaultNow(),
|
||||||
|
|
||||||
|
name: text("name").notNull(),
|
||||||
|
description: text("description"),
|
||||||
|
|
||||||
|
created_by: uuid("created_by").references(() => authUsers.id),
|
||||||
|
tenant_id: bigint("tenant_id", {mode: "number"}),
|
||||||
|
})
|
||||||
|
|
||||||
|
export type AuthRole = typeof authRoles.$inferSelect
|
||||||
|
export type NewAuthRole = typeof authRoles.$inferInsert
|
||||||
22
backend/db/schema/auth_tenant_users.ts
Normal file
22
backend/db/schema/auth_tenant_users.ts
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
import { pgTable, uuid, bigint, timestamp } from "drizzle-orm/pg-core"
|
||||||
|
import { authUsers } from "./auth_users"
|
||||||
|
|
||||||
|
export const authTenantUsers = pgTable(
|
||||||
|
"auth_tenant_users",
|
||||||
|
{
|
||||||
|
created_at: timestamp("created_at", { withTimezone: true })
|
||||||
|
.notNull()
|
||||||
|
.defaultNow(),
|
||||||
|
|
||||||
|
tenant_id: bigint("tenant_id", { mode: "number" }).notNull(),
|
||||||
|
user_id: uuid("user_id").notNull(),
|
||||||
|
|
||||||
|
created_by: uuid("created_by").references(() => authUsers.id),
|
||||||
|
},
|
||||||
|
(table) => ({
|
||||||
|
primaryKey: [table.tenant_id, table.user_id],
|
||||||
|
})
|
||||||
|
)
|
||||||
|
|
||||||
|
export type AuthTenantUser = typeof authTenantUsers.$inferSelect
|
||||||
|
export type NewAuthTenantUser = typeof authTenantUsers.$inferInsert
|
||||||
30
backend/db/schema/auth_user_roles.ts
Normal file
30
backend/db/schema/auth_user_roles.ts
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
import { pgTable, uuid, bigint, timestamp } from "drizzle-orm/pg-core"
|
||||||
|
import { authUsers } from "./auth_users"
|
||||||
|
import { authRoles } from "./auth_roles"
|
||||||
|
|
||||||
|
export const authUserRoles = pgTable(
|
||||||
|
"auth_user_roles",
|
||||||
|
{
|
||||||
|
created_at: timestamp("created_at", { withTimezone: true })
|
||||||
|
.notNull()
|
||||||
|
.defaultNow(),
|
||||||
|
|
||||||
|
user_id: uuid("user_id")
|
||||||
|
.notNull()
|
||||||
|
.references(() => authUsers.id),
|
||||||
|
|
||||||
|
role_id: uuid("role_id")
|
||||||
|
.notNull()
|
||||||
|
.references(() => authRoles.id),
|
||||||
|
|
||||||
|
tenant_id: bigint("tenant_id", { mode: "number" }).notNull(),
|
||||||
|
|
||||||
|
created_by: uuid("created_by").references(() => authUsers.id),
|
||||||
|
},
|
||||||
|
(table) => ({
|
||||||
|
primaryKey: [table.user_id, table.role_id, table.tenant_id],
|
||||||
|
})
|
||||||
|
)
|
||||||
|
|
||||||
|
export type AuthUserRole = typeof authUserRoles.$inferSelect
|
||||||
|
export type NewAuthUserRole = typeof authUserRoles.$inferInsert
|
||||||
22
backend/db/schema/auth_users.ts
Normal file
22
backend/db/schema/auth_users.ts
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
import { pgTable, uuid, text, boolean, timestamp } from "drizzle-orm/pg-core"
|
||||||
|
|
||||||
|
export const authUsers = pgTable("auth_users", {
|
||||||
|
id: uuid("id").primaryKey().defaultRandom(),
|
||||||
|
|
||||||
|
created_at: timestamp("created_at", { withTimezone: true })
|
||||||
|
.notNull()
|
||||||
|
.defaultNow(),
|
||||||
|
|
||||||
|
email: text("email").notNull(),
|
||||||
|
passwordHash: text("password_hash").notNull(),
|
||||||
|
|
||||||
|
multiTenant: boolean("multi_tenant").notNull().default(true),
|
||||||
|
must_change_password: boolean("must_change_password").notNull().default(false),
|
||||||
|
|
||||||
|
updatedAt: timestamp("updated_at", { withTimezone: true }),
|
||||||
|
|
||||||
|
ported: boolean("ported").notNull().default(true),
|
||||||
|
})
|
||||||
|
|
||||||
|
export type AuthUser = typeof authUsers.$inferSelect
|
||||||
|
export type NewAuthUser = typeof authUsers.$inferInsert
|
||||||
52
backend/db/schema/bankaccounts.ts
Normal file
52
backend/db/schema/bankaccounts.ts
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
import {
|
||||||
|
pgTable,
|
||||||
|
bigint,
|
||||||
|
timestamp,
|
||||||
|
text,
|
||||||
|
doublePrecision,
|
||||||
|
boolean,
|
||||||
|
uuid,
|
||||||
|
} from "drizzle-orm/pg-core"
|
||||||
|
|
||||||
|
import { tenants } from "./tenants"
|
||||||
|
import { authUsers } from "./auth_users"
|
||||||
|
|
||||||
|
export const bankaccounts = pgTable("bankaccounts", {
|
||||||
|
id: bigint("id", { mode: "number" })
|
||||||
|
.primaryKey()
|
||||||
|
.generatedByDefaultAsIdentity(),
|
||||||
|
|
||||||
|
createdAt: timestamp("created_at", { withTimezone: true })
|
||||||
|
.notNull()
|
||||||
|
.defaultNow(),
|
||||||
|
|
||||||
|
name: text("name"),
|
||||||
|
iban: text("iban").notNull(),
|
||||||
|
|
||||||
|
tenant: bigint("tenant", { mode: "number" })
|
||||||
|
.notNull()
|
||||||
|
.references(() => tenants.id),
|
||||||
|
|
||||||
|
bankId: text("bankId").notNull(),
|
||||||
|
ownerName: text("ownerName"),
|
||||||
|
|
||||||
|
accountId: text("accountId").notNull(),
|
||||||
|
|
||||||
|
balance: doublePrecision("balance"),
|
||||||
|
|
||||||
|
expired: boolean("expired").notNull().default(false),
|
||||||
|
|
||||||
|
datevNumber: text("datevNumber"),
|
||||||
|
|
||||||
|
syncedAt: timestamp("synced_at", { withTimezone: true })
|
||||||
|
.notNull()
|
||||||
|
.defaultNow(),
|
||||||
|
|
||||||
|
updatedAt: timestamp("updated_at", { withTimezone: true }),
|
||||||
|
updatedBy: uuid("updated_by").references(() => authUsers.id),
|
||||||
|
|
||||||
|
archived: boolean("archived").notNull().default(false),
|
||||||
|
})
|
||||||
|
|
||||||
|
export type BankAccount = typeof bankaccounts.$inferSelect
|
||||||
|
export type NewBankAccount = typeof bankaccounts.$inferInsert
|
||||||
30
backend/db/schema/bankrequisitions.ts
Normal file
30
backend/db/schema/bankrequisitions.ts
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
import {
|
||||||
|
pgTable,
|
||||||
|
uuid,
|
||||||
|
timestamp,
|
||||||
|
text,
|
||||||
|
bigint,
|
||||||
|
} from "drizzle-orm/pg-core"
|
||||||
|
|
||||||
|
import { tenants } from "./tenants"
|
||||||
|
import { authUsers } from "./auth_users"
|
||||||
|
|
||||||
|
export const bankrequisitions = pgTable("bankrequisitions", {
|
||||||
|
id: uuid("id").primaryKey(),
|
||||||
|
|
||||||
|
createdAt: timestamp("created_at", { withTimezone: true })
|
||||||
|
.notNull()
|
||||||
|
.defaultNow(),
|
||||||
|
|
||||||
|
institutionId: text("institutionId"),
|
||||||
|
|
||||||
|
tenant: bigint("tenant", { mode: "number" }).references(() => tenants.id),
|
||||||
|
|
||||||
|
status: text("status"),
|
||||||
|
|
||||||
|
updatedAt: timestamp("updated_at", { withTimezone: true }),
|
||||||
|
updatedBy: uuid("updated_by").references(() => authUsers.id),
|
||||||
|
})
|
||||||
|
|
||||||
|
export type BankRequisition = typeof bankrequisitions.$inferSelect
|
||||||
|
export type NewBankRequisition = typeof bankrequisitions.$inferInsert
|
||||||
62
backend/db/schema/bankstatements.ts
Normal file
62
backend/db/schema/bankstatements.ts
Normal file
@@ -0,0 +1,62 @@
|
|||||||
|
import {
|
||||||
|
pgTable,
|
||||||
|
bigint,
|
||||||
|
timestamp,
|
||||||
|
text,
|
||||||
|
doublePrecision,
|
||||||
|
boolean,
|
||||||
|
uuid,
|
||||||
|
} from "drizzle-orm/pg-core"
|
||||||
|
|
||||||
|
import { bankaccounts } from "./bankaccounts"
|
||||||
|
import { createddocuments } from "./createddocuments"
|
||||||
|
import { tenants } from "./tenants"
|
||||||
|
import { incominginvoices } from "./incominginvoices"
|
||||||
|
import { contracts } from "./contracts"
|
||||||
|
import { authUsers } from "./auth_users"
|
||||||
|
|
||||||
|
export const bankstatements = pgTable("bankstatements", {
|
||||||
|
id: bigint("id", { mode: "number" })
|
||||||
|
.primaryKey()
|
||||||
|
.generatedByDefaultAsIdentity(),
|
||||||
|
|
||||||
|
createdAt: timestamp("created_at", { withTimezone: true })
|
||||||
|
.notNull()
|
||||||
|
.defaultNow(),
|
||||||
|
|
||||||
|
account: bigint("account", { mode: "number" })
|
||||||
|
.notNull()
|
||||||
|
.references(() => bankaccounts.id),
|
||||||
|
|
||||||
|
date: text("date").notNull(),
|
||||||
|
|
||||||
|
credIban: text("credIban"),
|
||||||
|
credName: text("credName"),
|
||||||
|
|
||||||
|
text: text("text"),
|
||||||
|
amount: doublePrecision("amount").notNull(),
|
||||||
|
|
||||||
|
tenant: bigint("tenant", { mode: "number" })
|
||||||
|
.notNull()
|
||||||
|
.references(() => tenants.id),
|
||||||
|
|
||||||
|
debIban: text("debIban"),
|
||||||
|
debName: text("debName"),
|
||||||
|
gocardlessId: text("gocardlessId"),
|
||||||
|
currency: text("currency"),
|
||||||
|
valueDate: text("valueDate"),
|
||||||
|
|
||||||
|
mandateId: text("mandateId"),
|
||||||
|
|
||||||
|
contract: bigint("contract", { mode: "number" }).references(
|
||||||
|
() => contracts.id
|
||||||
|
),
|
||||||
|
|
||||||
|
archived: boolean("archived").notNull().default(false),
|
||||||
|
|
||||||
|
updatedAt: timestamp("updated_at", { withTimezone: true }),
|
||||||
|
updatedBy: uuid("updated_by").references(() => authUsers.id),
|
||||||
|
})
|
||||||
|
|
||||||
|
export type BankStatement = typeof bankstatements.$inferSelect
|
||||||
|
export type NewBankStatement = typeof bankstatements.$inferInsert
|
||||||
27
backend/db/schema/checkexecutions.ts
Normal file
27
backend/db/schema/checkexecutions.ts
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
import {
|
||||||
|
pgTable,
|
||||||
|
uuid,
|
||||||
|
timestamp,
|
||||||
|
text,
|
||||||
|
} from "drizzle-orm/pg-core"
|
||||||
|
|
||||||
|
import { checks } from "./checks"
|
||||||
|
|
||||||
|
export const checkexecutions = pgTable("checkexecutions", {
|
||||||
|
id: uuid("id").primaryKey().defaultRandom(),
|
||||||
|
|
||||||
|
createdAt: timestamp("created_at", { withTimezone: true })
|
||||||
|
.notNull()
|
||||||
|
.defaultNow(),
|
||||||
|
|
||||||
|
check: uuid("check").references(() => checks.id),
|
||||||
|
|
||||||
|
executedAt: timestamp("executed_at"),
|
||||||
|
|
||||||
|
// ❌ executed_by removed (was 0_profiles)
|
||||||
|
|
||||||
|
description: text("description"),
|
||||||
|
})
|
||||||
|
|
||||||
|
export type CheckExecution = typeof checkexecutions.$inferSelect
|
||||||
|
export type NewCheckExecution = typeof checkexecutions.$inferInsert
|
||||||
52
backend/db/schema/checks.ts
Normal file
52
backend/db/schema/checks.ts
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
import {
|
||||||
|
pgTable,
|
||||||
|
uuid,
|
||||||
|
timestamp,
|
||||||
|
text,
|
||||||
|
bigint,
|
||||||
|
boolean,
|
||||||
|
jsonb,
|
||||||
|
} from "drizzle-orm/pg-core"
|
||||||
|
|
||||||
|
import { tenants } from "./tenants"
|
||||||
|
import { vehicles } from "./vehicles"
|
||||||
|
import { inventoryitems } from "./inventoryitems"
|
||||||
|
import { authUsers } from "./auth_users"
|
||||||
|
|
||||||
|
export const checks = pgTable("checks", {
|
||||||
|
id: uuid("id").primaryKey().defaultRandom(),
|
||||||
|
|
||||||
|
createdAt: timestamp("created_at", { withTimezone: true })
|
||||||
|
.notNull()
|
||||||
|
.defaultNow(),
|
||||||
|
|
||||||
|
vehicle: bigint("vehicle", { mode: "number" })
|
||||||
|
.references(() => vehicles.id),
|
||||||
|
|
||||||
|
// ❌ profile removed (old 0_profiles reference)
|
||||||
|
|
||||||
|
inventoryItem: bigint("inventoryitem", { mode: "number" })
|
||||||
|
.references(() => inventoryitems.id),
|
||||||
|
|
||||||
|
tenant: bigint("tenant", { mode: "number" })
|
||||||
|
.references(() => tenants.id),
|
||||||
|
|
||||||
|
name: text("name"),
|
||||||
|
type: text("type"),
|
||||||
|
|
||||||
|
distance: bigint("distance", { mode: "number" }).default(1),
|
||||||
|
|
||||||
|
distanceUnit: text("distanceUnit").default("days"),
|
||||||
|
|
||||||
|
description: text("description"),
|
||||||
|
|
||||||
|
profiles: jsonb("profiles").notNull().default([]),
|
||||||
|
|
||||||
|
archived: boolean("archived").notNull().default(false),
|
||||||
|
|
||||||
|
updatedAt: timestamp("updated_at", { withTimezone: true }),
|
||||||
|
updatedBy: uuid("updated_by").references(() => authUsers.id),
|
||||||
|
})
|
||||||
|
|
||||||
|
export type Check = typeof checks.$inferSelect
|
||||||
|
export type NewCheck = typeof checks.$inferInsert
|
||||||
32
backend/db/schema/citys.ts
Normal file
32
backend/db/schema/citys.ts
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
import {
|
||||||
|
pgTable,
|
||||||
|
bigint,
|
||||||
|
text,
|
||||||
|
jsonb,
|
||||||
|
} from "drizzle-orm/pg-core"
|
||||||
|
|
||||||
|
export const citys = pgTable("citys", {
|
||||||
|
id: bigint("id", { mode: "number" })
|
||||||
|
.primaryKey()
|
||||||
|
.generatedByDefaultAsIdentity(),
|
||||||
|
|
||||||
|
name: text("name"),
|
||||||
|
short: text("short"),
|
||||||
|
long: text("long"),
|
||||||
|
|
||||||
|
geometry: jsonb("geometry"),
|
||||||
|
|
||||||
|
zip: bigint("zip", { mode: "number" }),
|
||||||
|
|
||||||
|
districtCode: bigint("districtCode", { mode: "number" }),
|
||||||
|
|
||||||
|
countryName: text("countryName"),
|
||||||
|
countryCode: bigint("countryCode", { mode: "number" }),
|
||||||
|
|
||||||
|
districtName: text("districtName"),
|
||||||
|
|
||||||
|
geopoint: text("geopoint"),
|
||||||
|
})
|
||||||
|
|
||||||
|
export type City = typeof citys.$inferSelect
|
||||||
|
export type NewCity = typeof citys.$inferInsert
|
||||||
66
backend/db/schema/contacts.ts
Normal file
66
backend/db/schema/contacts.ts
Normal file
@@ -0,0 +1,66 @@
|
|||||||
|
import {
|
||||||
|
pgTable,
|
||||||
|
bigint,
|
||||||
|
text,
|
||||||
|
timestamp,
|
||||||
|
boolean,
|
||||||
|
jsonb,
|
||||||
|
date,
|
||||||
|
uuid,
|
||||||
|
} from "drizzle-orm/pg-core"
|
||||||
|
|
||||||
|
import { customers } from "./customers"
|
||||||
|
import { tenants } from "./tenants"
|
||||||
|
import { authUsers } from "./auth_users"
|
||||||
|
|
||||||
|
export const contacts = pgTable(
|
||||||
|
"contacts",
|
||||||
|
{
|
||||||
|
id: bigint("id", { mode: "number" })
|
||||||
|
.primaryKey()
|
||||||
|
.generatedByDefaultAsIdentity(),
|
||||||
|
|
||||||
|
createdAt: timestamp("created_at", { withTimezone: true })
|
||||||
|
.notNull()
|
||||||
|
.defaultNow(),
|
||||||
|
|
||||||
|
firstName: text("firstName"),
|
||||||
|
lastName: text("lastName"),
|
||||||
|
email: text("email"),
|
||||||
|
|
||||||
|
customer: bigint("customer", { mode: "number" }).references(
|
||||||
|
() => customers.id
|
||||||
|
),
|
||||||
|
|
||||||
|
tenant: bigint("tenant", { mode: "number" }).notNull(),
|
||||||
|
|
||||||
|
phoneMobile: text("phoneMobile"),
|
||||||
|
phoneHome: text("phoneHome"),
|
||||||
|
|
||||||
|
heroId: text("heroId"),
|
||||||
|
role: text("role"),
|
||||||
|
|
||||||
|
fullName: text("fullName"),
|
||||||
|
|
||||||
|
salutation: text("salutation"),
|
||||||
|
|
||||||
|
vendor: bigint("vendor", { mode: "number" }), // vendors folgt separat
|
||||||
|
|
||||||
|
active: boolean("active").notNull().default(true),
|
||||||
|
|
||||||
|
birthday: date("birthday"),
|
||||||
|
notes: text("notes"),
|
||||||
|
|
||||||
|
profiles: jsonb("profiles").notNull().default([]),
|
||||||
|
|
||||||
|
archived: boolean("archived").notNull().default(false),
|
||||||
|
|
||||||
|
title: text("title"),
|
||||||
|
|
||||||
|
updatedAt: timestamp("updated_at", { withTimezone: true }),
|
||||||
|
updatedBy: uuid("updated_by").references(() => authUsers.id),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
export type Contact = typeof contacts.$inferSelect
|
||||||
|
export type NewContact = typeof contacts.$inferInsert
|
||||||
76
backend/db/schema/contracts.ts
Normal file
76
backend/db/schema/contracts.ts
Normal file
@@ -0,0 +1,76 @@
|
|||||||
|
import {
|
||||||
|
pgTable,
|
||||||
|
bigint,
|
||||||
|
text,
|
||||||
|
timestamp,
|
||||||
|
boolean,
|
||||||
|
jsonb,
|
||||||
|
uuid,
|
||||||
|
} from "drizzle-orm/pg-core"
|
||||||
|
|
||||||
|
import { tenants } from "./tenants"
|
||||||
|
import { customers } from "./customers"
|
||||||
|
import { contacts } from "./contacts"
|
||||||
|
import { authUsers } from "./auth_users"
|
||||||
|
|
||||||
|
export const contracts = pgTable(
|
||||||
|
"contracts",
|
||||||
|
{
|
||||||
|
id: bigint("id", { mode: "number" })
|
||||||
|
.primaryKey()
|
||||||
|
.generatedByDefaultAsIdentity(),
|
||||||
|
|
||||||
|
createdAt: timestamp("created_at", { withTimezone: true })
|
||||||
|
.notNull()
|
||||||
|
.defaultNow(),
|
||||||
|
|
||||||
|
tenant: bigint("tenant", { mode: "number" }).notNull(),
|
||||||
|
|
||||||
|
name: text("name").notNull(),
|
||||||
|
|
||||||
|
customer: bigint("customer", { mode: "number" })
|
||||||
|
.notNull()
|
||||||
|
.references(() => customers.id),
|
||||||
|
|
||||||
|
notes: text("notes"),
|
||||||
|
|
||||||
|
active: boolean("active").notNull().default(true),
|
||||||
|
recurring: boolean("recurring").notNull().default(false),
|
||||||
|
|
||||||
|
rhythm: jsonb("rhythm"),
|
||||||
|
|
||||||
|
startDate: timestamp("startDate", { withTimezone: true }),
|
||||||
|
endDate: timestamp("endDate", { withTimezone: true }),
|
||||||
|
signDate: timestamp("signDate", { withTimezone: true }),
|
||||||
|
|
||||||
|
duration: text("duration"),
|
||||||
|
|
||||||
|
contact: bigint("contact", { mode: "number" }).references(
|
||||||
|
() => contacts.id
|
||||||
|
),
|
||||||
|
|
||||||
|
bankingIban: text("bankingIban"),
|
||||||
|
bankingBIC: text("bankingBIC"),
|
||||||
|
bankingName: text("bankingName"),
|
||||||
|
bankingOwner: text("bankingOwner"),
|
||||||
|
sepaRef: text("sepaRef"),
|
||||||
|
sepaDate: timestamp("sepaDate", { withTimezone: true }),
|
||||||
|
|
||||||
|
paymentType: text("paymentType"),
|
||||||
|
invoiceDispatch: text("invoiceDispatch"),
|
||||||
|
|
||||||
|
ownFields: jsonb("ownFields").notNull().default({}),
|
||||||
|
profiles: jsonb("profiles").notNull().default([]),
|
||||||
|
|
||||||
|
archived: boolean("archived").notNull().default(false),
|
||||||
|
|
||||||
|
contractNumber: text("contractNumber"),
|
||||||
|
|
||||||
|
updatedAt: timestamp("updated_at", { withTimezone: true }),
|
||||||
|
|
||||||
|
updatedBy: uuid("updated_by").references(() => authUsers.id),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
export type Contract = typeof contracts.$inferSelect
|
||||||
|
export type NewContract = typeof contracts.$inferInsert
|
||||||
50
backend/db/schema/costcentres.ts
Normal file
50
backend/db/schema/costcentres.ts
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
import {
|
||||||
|
pgTable,
|
||||||
|
uuid,
|
||||||
|
timestamp,
|
||||||
|
text,
|
||||||
|
boolean,
|
||||||
|
jsonb,
|
||||||
|
bigint,
|
||||||
|
} from "drizzle-orm/pg-core"
|
||||||
|
|
||||||
|
import { tenants } from "./tenants"
|
||||||
|
import { inventoryitems } from "./inventoryitems"
|
||||||
|
import { projects } from "./projects"
|
||||||
|
import { vehicles } from "./vehicles"
|
||||||
|
import { authUsers } from "./auth_users"
|
||||||
|
|
||||||
|
export const costcentres = pgTable("costcentres", {
|
||||||
|
id: uuid("id").primaryKey().defaultRandom(),
|
||||||
|
|
||||||
|
createdAt: timestamp("created_at", { withTimezone: true })
|
||||||
|
.notNull()
|
||||||
|
.defaultNow(),
|
||||||
|
|
||||||
|
tenant: bigint("tenant", { mode: "number" })
|
||||||
|
.notNull()
|
||||||
|
.references(() => tenants.id),
|
||||||
|
|
||||||
|
number: text("number").notNull(),
|
||||||
|
name: text("name").notNull(),
|
||||||
|
|
||||||
|
vehicle: bigint("vehicle", { mode: "number" }).references(() => vehicles.id),
|
||||||
|
|
||||||
|
project: bigint("project", { mode: "number" }).references(() => projects.id),
|
||||||
|
|
||||||
|
inventoryitem: bigint("inventoryitem", { mode: "number" }).references(
|
||||||
|
() => inventoryitems.id
|
||||||
|
),
|
||||||
|
|
||||||
|
description: text("description"),
|
||||||
|
|
||||||
|
archived: boolean("archived").notNull().default(false),
|
||||||
|
|
||||||
|
profiles: jsonb("profiles").notNull().default([]),
|
||||||
|
|
||||||
|
updatedAt: timestamp("updated_at", { withTimezone: true }),
|
||||||
|
updatedBy: uuid("updated_by").references(() => authUsers.id),
|
||||||
|
})
|
||||||
|
|
||||||
|
export type CostCentre = typeof costcentres.$inferSelect
|
||||||
|
export type NewCostCentre = typeof costcentres.$inferInsert
|
||||||
21
backend/db/schema/countrys.ts
Normal file
21
backend/db/schema/countrys.ts
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
import {
|
||||||
|
pgTable,
|
||||||
|
bigint,
|
||||||
|
timestamp,
|
||||||
|
text,
|
||||||
|
} from "drizzle-orm/pg-core"
|
||||||
|
|
||||||
|
export const countrys = pgTable("countrys", {
|
||||||
|
id: bigint("id", { mode: "number" })
|
||||||
|
.primaryKey()
|
||||||
|
.generatedByDefaultAsIdentity(),
|
||||||
|
|
||||||
|
createdAt: timestamp("created_at", { withTimezone: true })
|
||||||
|
.notNull()
|
||||||
|
.defaultNow(),
|
||||||
|
|
||||||
|
name: text("name").notNull(),
|
||||||
|
})
|
||||||
|
|
||||||
|
export type Country = typeof countrys.$inferSelect
|
||||||
|
export type NewCountry = typeof countrys.$inferInsert
|
||||||
124
backend/db/schema/createddocuments.ts
Normal file
124
backend/db/schema/createddocuments.ts
Normal file
@@ -0,0 +1,124 @@
|
|||||||
|
import {
|
||||||
|
pgTable,
|
||||||
|
bigint,
|
||||||
|
timestamp,
|
||||||
|
text,
|
||||||
|
jsonb,
|
||||||
|
boolean,
|
||||||
|
smallint,
|
||||||
|
uuid,
|
||||||
|
} from "drizzle-orm/pg-core"
|
||||||
|
|
||||||
|
import { tenants } from "./tenants"
|
||||||
|
import { customers } from "./customers"
|
||||||
|
import { contacts } from "./contacts"
|
||||||
|
import { contracts } from "./contracts"
|
||||||
|
import { letterheads } from "./letterheads"
|
||||||
|
import { projects } from "./projects"
|
||||||
|
import { plants } from "./plants"
|
||||||
|
import { authUsers } from "./auth_users"
|
||||||
|
import {serialExecutions} from "./serialexecutions";
|
||||||
|
|
||||||
|
export const createddocuments = pgTable("createddocuments", {
|
||||||
|
id: bigint("id", { mode: "number" })
|
||||||
|
.primaryKey()
|
||||||
|
.generatedByDefaultAsIdentity(),
|
||||||
|
|
||||||
|
createdAt: timestamp("created_at", { withTimezone: true })
|
||||||
|
.notNull()
|
||||||
|
.defaultNow(),
|
||||||
|
|
||||||
|
tenant: bigint("tenant", { mode: "number" })
|
||||||
|
.notNull()
|
||||||
|
.references(() => tenants.id),
|
||||||
|
|
||||||
|
type: text("type").notNull().default("INVOICE"),
|
||||||
|
|
||||||
|
customer: bigint("customer", { mode: "number" }).references(
|
||||||
|
() => customers.id
|
||||||
|
),
|
||||||
|
|
||||||
|
contact: bigint("contact", { mode: "number" }).references(
|
||||||
|
() => contacts.id
|
||||||
|
),
|
||||||
|
|
||||||
|
address: jsonb("address"),
|
||||||
|
project: bigint("project", { mode: "number" }).references(
|
||||||
|
() => projects.id
|
||||||
|
),
|
||||||
|
|
||||||
|
documentNumber: text("documentNumber"),
|
||||||
|
documentDate: text("documentDate"),
|
||||||
|
|
||||||
|
state: text("state").notNull().default("Entwurf"),
|
||||||
|
|
||||||
|
info: jsonb("info"),
|
||||||
|
|
||||||
|
createdBy: uuid("createdBy").references(() => authUsers.id),
|
||||||
|
|
||||||
|
title: text("title"),
|
||||||
|
description: text("description"),
|
||||||
|
|
||||||
|
startText: text("startText"),
|
||||||
|
endText: text("endText"),
|
||||||
|
|
||||||
|
rows: jsonb("rows").default([]),
|
||||||
|
|
||||||
|
deliveryDateType: text("deliveryDateType"),
|
||||||
|
paymentDays: smallint("paymentDays"),
|
||||||
|
deliveryDate: text("deliveryDate"),
|
||||||
|
|
||||||
|
contactPerson: uuid("contactPerson"),
|
||||||
|
|
||||||
|
serialConfig: jsonb("serialConfig").default({}),
|
||||||
|
|
||||||
|
createddocument: bigint("linkedDocument", { mode: "number" }).references(
|
||||||
|
() => createddocuments.id
|
||||||
|
),
|
||||||
|
|
||||||
|
agriculture: jsonb("agriculture"),
|
||||||
|
|
||||||
|
letterhead: bigint("letterhead", { mode: "number" }).references(
|
||||||
|
() => letterheads.id
|
||||||
|
),
|
||||||
|
|
||||||
|
advanceInvoiceResolved: boolean("advanceInvoiceResolved")
|
||||||
|
.notNull()
|
||||||
|
.default(false),
|
||||||
|
|
||||||
|
usedAdvanceInvoices: jsonb("usedAdvanceInvoices").notNull().default([]),
|
||||||
|
|
||||||
|
archived: boolean("archived").notNull().default(false),
|
||||||
|
|
||||||
|
deliveryDateEnd: text("deliveryDateEnd"),
|
||||||
|
|
||||||
|
plant: bigint("plant", { mode: "number" }).references(() => plants.id),
|
||||||
|
|
||||||
|
taxType: text("taxType"),
|
||||||
|
|
||||||
|
customSurchargePercentage: smallint("customSurchargePercentage")
|
||||||
|
.notNull()
|
||||||
|
.default(0),
|
||||||
|
|
||||||
|
report: jsonb("report").notNull().default({}),
|
||||||
|
|
||||||
|
availableInPortal: boolean("availableInPortal")
|
||||||
|
.notNull()
|
||||||
|
.default(false),
|
||||||
|
|
||||||
|
updatedAt: timestamp("updated_at", { withTimezone: true }),
|
||||||
|
updatedBy: uuid("updated_by").references(() => authUsers.id),
|
||||||
|
|
||||||
|
created_by: uuid("created_by").references(() => authUsers.id),
|
||||||
|
|
||||||
|
payment_type: text("payment_type").default("transfer"),
|
||||||
|
|
||||||
|
contract: bigint("contract", { mode: "number" }).references(
|
||||||
|
() => contracts.id
|
||||||
|
),
|
||||||
|
|
||||||
|
serialexecution: uuid("serialexecution").references(() => serialExecutions.id)
|
||||||
|
})
|
||||||
|
|
||||||
|
export type CreatedDocument = typeof createddocuments.$inferSelect
|
||||||
|
export type NewCreatedDocument = typeof createddocuments.$inferInsert
|
||||||
43
backend/db/schema/createdletters.ts
Normal file
43
backend/db/schema/createdletters.ts
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
import {
|
||||||
|
pgTable,
|
||||||
|
uuid,
|
||||||
|
timestamp,
|
||||||
|
bigint,
|
||||||
|
text,
|
||||||
|
jsonb,
|
||||||
|
boolean,
|
||||||
|
} from "drizzle-orm/pg-core"
|
||||||
|
|
||||||
|
import { tenants } from "./tenants"
|
||||||
|
import { customers } from "./customers"
|
||||||
|
import { vendors } from "./vendors"
|
||||||
|
import { authUsers } from "./auth_users"
|
||||||
|
|
||||||
|
export const createdletters = pgTable("createdletters", {
|
||||||
|
id: uuid("id").primaryKey().defaultRandom(),
|
||||||
|
|
||||||
|
createdAt: timestamp("created_at", { withTimezone: true })
|
||||||
|
.notNull()
|
||||||
|
.defaultNow(),
|
||||||
|
|
||||||
|
tenant: bigint("tenant", { mode: "number" }).references(() => tenants.id),
|
||||||
|
|
||||||
|
customer: bigint("customer", { mode: "number" }).references(
|
||||||
|
() => customers.id
|
||||||
|
),
|
||||||
|
|
||||||
|
vendor: bigint("vendor", { mode: "number" }).references(() => vendors.id),
|
||||||
|
|
||||||
|
contentJson: jsonb("content_json").default([]),
|
||||||
|
|
||||||
|
contentText: text("content_text"),
|
||||||
|
|
||||||
|
updatedAt: timestamp("updated_at", { withTimezone: true }),
|
||||||
|
|
||||||
|
updatedBy: uuid("updated_by").references(() => authUsers.id),
|
||||||
|
|
||||||
|
archived: boolean("archived").notNull().default(false),
|
||||||
|
})
|
||||||
|
|
||||||
|
export type CreatedLetter = typeof createdletters.$inferSelect
|
||||||
|
export type NewCreatedLetter = typeof createdletters.$inferInsert
|
||||||
69
backend/db/schema/customers.ts
Normal file
69
backend/db/schema/customers.ts
Normal file
@@ -0,0 +1,69 @@
|
|||||||
|
import {
|
||||||
|
pgTable,
|
||||||
|
bigint,
|
||||||
|
text,
|
||||||
|
timestamp,
|
||||||
|
boolean,
|
||||||
|
jsonb,
|
||||||
|
smallint,
|
||||||
|
uuid,
|
||||||
|
} from "drizzle-orm/pg-core"
|
||||||
|
import { tenants } from "./tenants"
|
||||||
|
import { authUsers } from "./auth_users"
|
||||||
|
|
||||||
|
export const customers = pgTable(
|
||||||
|
"customers",
|
||||||
|
{
|
||||||
|
id: bigint("id", { mode: "number" })
|
||||||
|
.primaryKey()
|
||||||
|
.generatedByDefaultAsIdentity(),
|
||||||
|
|
||||||
|
createdAt: timestamp("created_at", { withTimezone: true })
|
||||||
|
.notNull()
|
||||||
|
.defaultNow(),
|
||||||
|
|
||||||
|
customerNumber: text("customerNumber").notNull(),
|
||||||
|
name: text("name").notNull(),
|
||||||
|
|
||||||
|
tenant: bigint("tenant", { mode: "number" }).notNull(),
|
||||||
|
|
||||||
|
infoData: jsonb("infoData").default({}),
|
||||||
|
active: boolean("active").notNull().default(true),
|
||||||
|
|
||||||
|
notes: text("notes"),
|
||||||
|
|
||||||
|
type: text("type").default("Privat"),
|
||||||
|
heroId: text("heroId"),
|
||||||
|
|
||||||
|
isCompany: boolean("isCompany").notNull().default(false),
|
||||||
|
|
||||||
|
profiles: jsonb("profiles").notNull().default([]),
|
||||||
|
|
||||||
|
customPaymentDays: smallint("customPaymentDays"),
|
||||||
|
|
||||||
|
firstname: text("firstname"),
|
||||||
|
lastname: text("lastname"),
|
||||||
|
|
||||||
|
archived: boolean("archived").notNull().default(false),
|
||||||
|
|
||||||
|
customSurchargePercentage: smallint("customSurchargePercentage")
|
||||||
|
.notNull()
|
||||||
|
.default(0),
|
||||||
|
|
||||||
|
salutation: text("salutation"),
|
||||||
|
title: text("title"),
|
||||||
|
nameAddition: text("nameAddition"),
|
||||||
|
|
||||||
|
availableInPortal: boolean("availableInPortal")
|
||||||
|
.notNull()
|
||||||
|
.default(false),
|
||||||
|
|
||||||
|
updatedAt: timestamp("updated_at", { withTimezone: true }),
|
||||||
|
updatedBy: uuid("updated_by").references(() => authUsers.id),
|
||||||
|
|
||||||
|
customPaymentType: text("custom_payment_type"), // ENUM payment_types separat?
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
export type Customer = typeof customers.$inferSelect
|
||||||
|
export type NewCustomer = typeof customers.$inferInsert
|
||||||
29
backend/db/schema/devices.ts
Normal file
29
backend/db/schema/devices.ts
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
import {
|
||||||
|
pgTable,
|
||||||
|
uuid,
|
||||||
|
timestamp,
|
||||||
|
text,
|
||||||
|
bigint,
|
||||||
|
} from "drizzle-orm/pg-core"
|
||||||
|
|
||||||
|
import { tenants } from "./tenants"
|
||||||
|
|
||||||
|
export const devices = pgTable("devices", {
|
||||||
|
id: uuid("id").primaryKey().defaultRandom(),
|
||||||
|
|
||||||
|
createdAt: timestamp("created_at", { withTimezone: true })
|
||||||
|
.notNull()
|
||||||
|
.defaultNow(),
|
||||||
|
|
||||||
|
name: text("name").notNull(),
|
||||||
|
type: text("type").notNull(),
|
||||||
|
|
||||||
|
tenant: bigint("tenant", { mode: "number" }).references(() => tenants.id),
|
||||||
|
|
||||||
|
password: text("password"),
|
||||||
|
|
||||||
|
externalId: text("externalId"),
|
||||||
|
})
|
||||||
|
|
||||||
|
export type Device = typeof devices.$inferSelect
|
||||||
|
export type NewDevice = typeof devices.$inferInsert
|
||||||
28
backend/db/schema/documentboxes.ts
Normal file
28
backend/db/schema/documentboxes.ts
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
import { pgTable, uuid, timestamp, text, boolean, bigint } from "drizzle-orm/pg-core"
|
||||||
|
|
||||||
|
import { spaces } from "./spaces"
|
||||||
|
import { tenants } from "./tenants"
|
||||||
|
import { authUsers } from "./auth_users"
|
||||||
|
|
||||||
|
export const documentboxes = pgTable("documentboxes", {
|
||||||
|
id: uuid("id").primaryKey().defaultRandom(),
|
||||||
|
|
||||||
|
createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
|
||||||
|
|
||||||
|
space: bigint("space", { mode: "number" }).references(() => spaces.id),
|
||||||
|
|
||||||
|
key: text("key").notNull(),
|
||||||
|
|
||||||
|
tenant: bigint("tenant", { mode: "number" })
|
||||||
|
.notNull()
|
||||||
|
.references(() => tenants.id),
|
||||||
|
|
||||||
|
archived: boolean("archived").notNull().default(false),
|
||||||
|
|
||||||
|
updatedAt: timestamp("updated_at", { withTimezone: true }),
|
||||||
|
|
||||||
|
updatedBy: uuid("updated_by").references(() => authUsers.id),
|
||||||
|
})
|
||||||
|
|
||||||
|
export type DocumentBox = typeof documentboxes.$inferSelect
|
||||||
|
export type NewDocumentBox = typeof documentboxes.$inferInsert
|
||||||
97
backend/db/schema/enums.ts
Normal file
97
backend/db/schema/enums.ts
Normal file
@@ -0,0 +1,97 @@
|
|||||||
|
import { pgEnum } from "drizzle-orm/pg-core"
|
||||||
|
|
||||||
|
// public.textTemplatePositions
|
||||||
|
export const textTemplatePositionsEnum = pgEnum("texttemplatepositions", [
|
||||||
|
"startText",
|
||||||
|
"endText",
|
||||||
|
])
|
||||||
|
|
||||||
|
// public.folderFunctions
|
||||||
|
export const folderFunctionsEnum = pgEnum("folderfunctions", [
|
||||||
|
"none",
|
||||||
|
"yearSubCategory",
|
||||||
|
"incomingInvoices",
|
||||||
|
"invoices",
|
||||||
|
"quotes",
|
||||||
|
"confirmationOrders",
|
||||||
|
"deliveryNotes",
|
||||||
|
"vehicleData",
|
||||||
|
"reminders",
|
||||||
|
"taxData",
|
||||||
|
"deposit",
|
||||||
|
"timeEvaluations",
|
||||||
|
])
|
||||||
|
|
||||||
|
// public.locked_tenant
|
||||||
|
export const lockedTenantEnum = pgEnum("locked_tenant", [
|
||||||
|
"maintenance_tenant",
|
||||||
|
"maintenance",
|
||||||
|
"general",
|
||||||
|
"no_subscription",
|
||||||
|
])
|
||||||
|
|
||||||
|
// public.credential_types
|
||||||
|
export const credentialTypesEnum = pgEnum("credential_types", [
|
||||||
|
"mail",
|
||||||
|
"m365",
|
||||||
|
])
|
||||||
|
|
||||||
|
// public.payment_types
|
||||||
|
export const paymentTypesEnum = pgEnum("payment_types", [
|
||||||
|
"transfer",
|
||||||
|
"direct_debit",
|
||||||
|
])
|
||||||
|
|
||||||
|
// public.notification_status
|
||||||
|
export const notificationStatusEnum = pgEnum("notification_status", [
|
||||||
|
"queued",
|
||||||
|
"sent",
|
||||||
|
"failed",
|
||||||
|
"read",
|
||||||
|
])
|
||||||
|
|
||||||
|
// public.notification_channel
|
||||||
|
export const notificationChannelEnum = pgEnum("notification_channel", [
|
||||||
|
"email",
|
||||||
|
"inapp",
|
||||||
|
"sms",
|
||||||
|
"push",
|
||||||
|
"webhook",
|
||||||
|
])
|
||||||
|
|
||||||
|
// public.notification_severity
|
||||||
|
export const notificationSeverityEnum = pgEnum("notification_severity", [
|
||||||
|
"info",
|
||||||
|
"success",
|
||||||
|
"warning",
|
||||||
|
"error",
|
||||||
|
])
|
||||||
|
|
||||||
|
// public.times_state
|
||||||
|
export const timesStateEnum = pgEnum("times_state", [
|
||||||
|
"submitted",
|
||||||
|
"approved",
|
||||||
|
"draft",
|
||||||
|
])
|
||||||
|
|
||||||
|
export const helpdeskStatusEnum = [
|
||||||
|
"open",
|
||||||
|
"in_progress",
|
||||||
|
"waiting_for_customer",
|
||||||
|
"answered",
|
||||||
|
"closed",
|
||||||
|
] as const
|
||||||
|
|
||||||
|
export const helpdeskPriorityEnum = [
|
||||||
|
"low",
|
||||||
|
"normal",
|
||||||
|
"high",
|
||||||
|
] as const
|
||||||
|
|
||||||
|
export const helpdeskDirectionEnum = [
|
||||||
|
"incoming",
|
||||||
|
"outgoing",
|
||||||
|
"internal",
|
||||||
|
"system",
|
||||||
|
] as const
|
||||||
|
|
||||||
60
backend/db/schema/events.ts
Normal file
60
backend/db/schema/events.ts
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
import {
|
||||||
|
pgTable,
|
||||||
|
bigint,
|
||||||
|
text,
|
||||||
|
timestamp,
|
||||||
|
boolean,
|
||||||
|
jsonb,
|
||||||
|
uuid,
|
||||||
|
} from "drizzle-orm/pg-core"
|
||||||
|
|
||||||
|
import { tenants } from "./tenants"
|
||||||
|
import { customers } from "./customers"
|
||||||
|
import { authUsers } from "./auth_users"
|
||||||
|
|
||||||
|
export const events = pgTable(
|
||||||
|
"events",
|
||||||
|
{
|
||||||
|
id: bigint("id", { mode: "number" })
|
||||||
|
.primaryKey()
|
||||||
|
.generatedByDefaultAsIdentity(),
|
||||||
|
|
||||||
|
createdAt: timestamp("created_at", { withTimezone: true })
|
||||||
|
.notNull()
|
||||||
|
.defaultNow(),
|
||||||
|
|
||||||
|
tenant: bigint("tenant", { mode: "number" }).notNull(),
|
||||||
|
|
||||||
|
name: text("name").notNull(),
|
||||||
|
|
||||||
|
startDate: timestamp("startDate", { withTimezone: true }).notNull(),
|
||||||
|
endDate: timestamp("endDate", { withTimezone: true }),
|
||||||
|
|
||||||
|
eventtype: text("eventtype").default("Umsetzung"),
|
||||||
|
|
||||||
|
project: bigint("project", { mode: "number" }), // FK follows when projects.ts exists
|
||||||
|
|
||||||
|
resources: jsonb("resources").default([]),
|
||||||
|
notes: text("notes"),
|
||||||
|
link: text("link"),
|
||||||
|
|
||||||
|
profiles: jsonb("profiles").notNull().default([]),
|
||||||
|
archived: boolean("archived").notNull().default(false),
|
||||||
|
|
||||||
|
vehicles: jsonb("vehicles").notNull().default([]),
|
||||||
|
inventoryitems: jsonb("inventoryitems").notNull().default([]),
|
||||||
|
inventoryitemgroups: jsonb("inventoryitemgroups").notNull().default([]),
|
||||||
|
|
||||||
|
customer: bigint("customer", { mode: "number" }).references(
|
||||||
|
() => customers.id
|
||||||
|
),
|
||||||
|
|
||||||
|
vendor: bigint("vendor", { mode: "number" }), // will link once vendors.ts is created
|
||||||
|
|
||||||
|
updatedAt: timestamp("updated_at", { withTimezone: true }),
|
||||||
|
updatedBy: uuid("updated_by").references(() => authUsers.id),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
export type Event = typeof events.$inferSelect
|
||||||
|
export type NewEvent = typeof events.$inferInsert
|
||||||
79
backend/db/schema/files.ts
Normal file
79
backend/db/schema/files.ts
Normal file
@@ -0,0 +1,79 @@
|
|||||||
|
import {
|
||||||
|
pgTable,
|
||||||
|
uuid,
|
||||||
|
timestamp,
|
||||||
|
text,
|
||||||
|
boolean,
|
||||||
|
bigint,
|
||||||
|
} from "drizzle-orm/pg-core"
|
||||||
|
|
||||||
|
import { tenants } from "./tenants"
|
||||||
|
import { projects } from "./projects"
|
||||||
|
import { customers } from "./customers"
|
||||||
|
import { contracts } from "./contracts"
|
||||||
|
import { vendors } from "./vendors"
|
||||||
|
import { incominginvoices } from "./incominginvoices"
|
||||||
|
import { plants } from "./plants"
|
||||||
|
import { createddocuments } from "./createddocuments"
|
||||||
|
import { vehicles } from "./vehicles"
|
||||||
|
import { products } from "./products"
|
||||||
|
import { inventoryitems } from "./inventoryitems"
|
||||||
|
import { folders } from "./folders"
|
||||||
|
import { filetags } from "./filetags"
|
||||||
|
import { authUsers } from "./auth_users"
|
||||||
|
import { authProfiles } from "./auth_profiles"
|
||||||
|
import { spaces } from "./spaces"
|
||||||
|
import { documentboxes } from "./documentboxes"
|
||||||
|
import { checks } from "./checks"
|
||||||
|
|
||||||
|
export const files = pgTable("files", {
|
||||||
|
id: uuid("id").primaryKey().defaultRandom(),
|
||||||
|
|
||||||
|
createdAt: timestamp("created_at", { withTimezone: true })
|
||||||
|
.notNull()
|
||||||
|
.defaultNow(),
|
||||||
|
|
||||||
|
path: text("path"),
|
||||||
|
|
||||||
|
tenant: bigint("tenant", { mode: "number" })
|
||||||
|
.notNull()
|
||||||
|
.references(() => tenants.id),
|
||||||
|
|
||||||
|
project: bigint("project", { mode: "number" }).references(() => projects.id),
|
||||||
|
customer: bigint("customer", { mode: "number" }).references(() => customers.id),
|
||||||
|
contract: bigint("contract", { mode: "number" }).references(() => contracts.id),
|
||||||
|
vendor: bigint("vendor", { mode: "number" }).references(() => vendors.id),
|
||||||
|
incominginvoice: bigint("incominginvoice", { mode: "number" }).references(() => incominginvoices.id),
|
||||||
|
plant: bigint("plant", { mode: "number" }).references(() => plants.id),
|
||||||
|
createddocument: bigint("createddocument", { mode: "number" }).references(() => createddocuments.id),
|
||||||
|
vehicle: bigint("vehicle", { mode: "number" }).references(() => vehicles.id),
|
||||||
|
product: bigint("product", { mode: "number" }).references(() => products.id),
|
||||||
|
|
||||||
|
check: uuid("check").references(() => checks.id),
|
||||||
|
|
||||||
|
inventoryitem: bigint("inventoryitem", { mode: "number" }).references(() => inventoryitems.id),
|
||||||
|
|
||||||
|
folder: uuid("folder").references(() => folders.id),
|
||||||
|
|
||||||
|
mimeType: text("mimeType"),
|
||||||
|
|
||||||
|
archived: boolean("archived").notNull().default(false),
|
||||||
|
|
||||||
|
space: bigint("space", { mode: "number" }).references(() => spaces.id),
|
||||||
|
|
||||||
|
type: uuid("type").references(() => filetags.id),
|
||||||
|
|
||||||
|
documentbox: uuid("documentbox").references(() => documentboxes.id),
|
||||||
|
|
||||||
|
name: text("name"),
|
||||||
|
|
||||||
|
updatedAt: timestamp("updated_at", { withTimezone: true }),
|
||||||
|
updatedBy: uuid("updated_by").references(() => authUsers.id),
|
||||||
|
|
||||||
|
createdBy: uuid("created_by").references(() => authUsers.id),
|
||||||
|
|
||||||
|
authProfile: uuid("auth_profile").references(() => authProfiles.id),
|
||||||
|
})
|
||||||
|
|
||||||
|
export type File = typeof files.$inferSelect
|
||||||
|
export type NewFile = typeof files.$inferInsert
|
||||||
33
backend/db/schema/filetags.ts
Normal file
33
backend/db/schema/filetags.ts
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
import {
|
||||||
|
pgTable,
|
||||||
|
uuid,
|
||||||
|
timestamp,
|
||||||
|
text,
|
||||||
|
boolean,
|
||||||
|
bigint,
|
||||||
|
} from "drizzle-orm/pg-core"
|
||||||
|
|
||||||
|
import { tenants } from "./tenants"
|
||||||
|
|
||||||
|
export const filetags = pgTable("filetags", {
|
||||||
|
id: uuid("id").primaryKey().defaultRandom(),
|
||||||
|
|
||||||
|
createdAt: timestamp("created_at", { withTimezone: true })
|
||||||
|
.notNull()
|
||||||
|
.defaultNow(),
|
||||||
|
|
||||||
|
name: text("name").notNull(),
|
||||||
|
color: text("color"),
|
||||||
|
|
||||||
|
tenant: bigint("tenant", { mode: "number" })
|
||||||
|
.notNull()
|
||||||
|
.references(() => tenants.id),
|
||||||
|
|
||||||
|
createdDocumentType: text("createddocumenttype").default(""),
|
||||||
|
incomingDocumentType: text("incomingDocumentType"),
|
||||||
|
|
||||||
|
archived: boolean("archived").notNull().default(false),
|
||||||
|
})
|
||||||
|
|
||||||
|
export type FileTag = typeof filetags.$inferSelect
|
||||||
|
export type NewFileTag = typeof filetags.$inferInsert
|
||||||
51
backend/db/schema/folders.ts
Normal file
51
backend/db/schema/folders.ts
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
import {
|
||||||
|
pgTable,
|
||||||
|
uuid,
|
||||||
|
timestamp,
|
||||||
|
text,
|
||||||
|
boolean,
|
||||||
|
integer,
|
||||||
|
bigint,
|
||||||
|
} from "drizzle-orm/pg-core"
|
||||||
|
|
||||||
|
import { tenants } from "./tenants"
|
||||||
|
import { authUsers } from "./auth_users"
|
||||||
|
import { filetags } from "./filetags"
|
||||||
|
import { folderFunctionsEnum } from "./enums"
|
||||||
|
|
||||||
|
export const folders = pgTable("folders", {
|
||||||
|
id: uuid("id").primaryKey().defaultRandom(),
|
||||||
|
|
||||||
|
createdAt: timestamp("created_at", { withTimezone: true })
|
||||||
|
.notNull()
|
||||||
|
.defaultNow(),
|
||||||
|
|
||||||
|
tenant: bigint("tenant", { mode: "number" })
|
||||||
|
.notNull()
|
||||||
|
.references(() => tenants.id),
|
||||||
|
|
||||||
|
name: text("name").notNull(),
|
||||||
|
icon: text("icon"),
|
||||||
|
|
||||||
|
parent: uuid("parent").references(() => folders.id),
|
||||||
|
|
||||||
|
isSystemUsed: boolean("isSystemUsed").notNull().default(false),
|
||||||
|
|
||||||
|
function: folderFunctionsEnum("function"),
|
||||||
|
|
||||||
|
year: integer("year"),
|
||||||
|
|
||||||
|
standardFiletype: uuid("standardFiletype").references(() => filetags.id),
|
||||||
|
|
||||||
|
standardFiletypeIsOptional: boolean("standardFiletypeIsOptional")
|
||||||
|
.notNull()
|
||||||
|
.default(true),
|
||||||
|
|
||||||
|
updatedAt: timestamp("updated_at", { withTimezone: true }),
|
||||||
|
updatedBy: uuid("updated_by").references(() => authUsers.id),
|
||||||
|
|
||||||
|
archived: boolean("archived").notNull().default(false),
|
||||||
|
})
|
||||||
|
|
||||||
|
export type Folder = typeof folders.$inferSelect
|
||||||
|
export type NewFolder = typeof folders.$inferInsert
|
||||||
35
backend/db/schema/generatedexports.ts
Normal file
35
backend/db/schema/generatedexports.ts
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
import {
|
||||||
|
pgTable,
|
||||||
|
bigint,
|
||||||
|
timestamp,
|
||||||
|
text,
|
||||||
|
} from "drizzle-orm/pg-core"
|
||||||
|
|
||||||
|
import { tenants } from "./tenants"
|
||||||
|
|
||||||
|
export const generatedexports = pgTable("exports", {
|
||||||
|
id: bigint("id", { mode: "number" })
|
||||||
|
.primaryKey()
|
||||||
|
.generatedByDefaultAsIdentity(),
|
||||||
|
|
||||||
|
createdAt: timestamp("created_at", { withTimezone: true })
|
||||||
|
.notNull()
|
||||||
|
.defaultNow(),
|
||||||
|
|
||||||
|
tenantId: bigint("tenant_id", { mode: "number" })
|
||||||
|
.notNull()
|
||||||
|
.references(() => tenants.id),
|
||||||
|
|
||||||
|
startDate: timestamp("start_date", { withTimezone: true }).notNull(),
|
||||||
|
endDate: timestamp("end_date", { withTimezone: true }).notNull(),
|
||||||
|
|
||||||
|
validUntil: timestamp("valid_until", { withTimezone: true }),
|
||||||
|
|
||||||
|
type: text("type").notNull().default("datev"),
|
||||||
|
|
||||||
|
url: text("url").notNull(),
|
||||||
|
filePath: text("file_path"),
|
||||||
|
})
|
||||||
|
|
||||||
|
export type Export = typeof generatedexports.$inferSelect
|
||||||
|
export type NewExport = typeof generatedexports.$inferInsert
|
||||||
22
backend/db/schema/globalmessages.ts
Normal file
22
backend/db/schema/globalmessages.ts
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
import {
|
||||||
|
pgTable,
|
||||||
|
bigint,
|
||||||
|
timestamp,
|
||||||
|
text,
|
||||||
|
} from "drizzle-orm/pg-core"
|
||||||
|
|
||||||
|
export const globalmessages = pgTable("globalmessages", {
|
||||||
|
id: bigint("id", { mode: "number" })
|
||||||
|
.primaryKey()
|
||||||
|
.generatedByDefaultAsIdentity(),
|
||||||
|
|
||||||
|
createdAt: timestamp("created_at", { withTimezone: true })
|
||||||
|
.notNull()
|
||||||
|
.defaultNow(),
|
||||||
|
|
||||||
|
title: text("title"),
|
||||||
|
description: text("description"),
|
||||||
|
})
|
||||||
|
|
||||||
|
export type GlobalMessage = typeof globalmessages.$inferSelect
|
||||||
|
export type NewGlobalMessage = typeof globalmessages.$inferInsert
|
||||||
17
backend/db/schema/globalmessagesseen.ts
Normal file
17
backend/db/schema/globalmessagesseen.ts
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
import {
|
||||||
|
pgTable,
|
||||||
|
timestamp,
|
||||||
|
bigint,
|
||||||
|
} from "drizzle-orm/pg-core"
|
||||||
|
|
||||||
|
import { globalmessages } from "./globalmessages"
|
||||||
|
|
||||||
|
export const globalmessagesseen = pgTable("globalmessagesseen", {
|
||||||
|
message: bigint("message", { mode: "number" })
|
||||||
|
.notNull()
|
||||||
|
.references(() => globalmessages.id),
|
||||||
|
|
||||||
|
seenAt: timestamp("seen_at", { withTimezone: true })
|
||||||
|
.notNull()
|
||||||
|
.defaultNow(),
|
||||||
|
})
|
||||||
44
backend/db/schema/helpdesk_channel_instances.ts
Normal file
44
backend/db/schema/helpdesk_channel_instances.ts
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
import {
|
||||||
|
pgTable,
|
||||||
|
uuid,
|
||||||
|
timestamp,
|
||||||
|
text,
|
||||||
|
boolean,
|
||||||
|
jsonb,
|
||||||
|
bigint,
|
||||||
|
} from "drizzle-orm/pg-core"
|
||||||
|
|
||||||
|
import { tenants } from "./tenants"
|
||||||
|
import { authUsers } from "./auth_users"
|
||||||
|
import { helpdesk_channel_types } from "./helpdesk_channel_types"
|
||||||
|
|
||||||
|
export const helpdesk_channel_instances = pgTable("helpdesk_channel_instances", {
|
||||||
|
id: uuid("id").primaryKey().defaultRandom(),
|
||||||
|
|
||||||
|
tenantId: bigint("tenant_id", { mode: "number" })
|
||||||
|
.notNull()
|
||||||
|
.references(() => tenants.id, { onDelete: "cascade" }),
|
||||||
|
|
||||||
|
typeId: text("type_id")
|
||||||
|
.notNull()
|
||||||
|
.references(() => helpdesk_channel_types.id),
|
||||||
|
|
||||||
|
name: text("name").notNull(),
|
||||||
|
|
||||||
|
isActive: boolean("is_active").notNull().default(true),
|
||||||
|
|
||||||
|
config: jsonb("config").notNull(),
|
||||||
|
publicConfig: jsonb("public_config").notNull().default({}),
|
||||||
|
|
||||||
|
publicToken: text("public_token").unique(),
|
||||||
|
secretToken: text("secret_token"),
|
||||||
|
|
||||||
|
createdBy: uuid("created_by").references(() => authUsers.id),
|
||||||
|
|
||||||
|
createdAt: timestamp("created_at", { withTimezone: true }).defaultNow(),
|
||||||
|
})
|
||||||
|
|
||||||
|
export type HelpdeskChannelInstance =
|
||||||
|
typeof helpdesk_channel_instances.$inferSelect
|
||||||
|
export type NewHelpdeskChannelInstance =
|
||||||
|
typeof helpdesk_channel_instances.$inferInsert
|
||||||
9
backend/db/schema/helpdesk_channel_types.ts
Normal file
9
backend/db/schema/helpdesk_channel_types.ts
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
import { pgTable, text } from "drizzle-orm/pg-core"
|
||||||
|
|
||||||
|
export const helpdesk_channel_types = pgTable("helpdesk_channel_types", {
|
||||||
|
id: text("id").primaryKey(),
|
||||||
|
description: text("description").notNull(),
|
||||||
|
})
|
||||||
|
|
||||||
|
export type HelpdeskChannelType = typeof helpdesk_channel_types.$inferSelect
|
||||||
|
export type NewHelpdeskChannelType = typeof helpdesk_channel_types.$inferInsert
|
||||||
45
backend/db/schema/helpdesk_contacts.ts
Normal file
45
backend/db/schema/helpdesk_contacts.ts
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
import {
|
||||||
|
pgTable,
|
||||||
|
uuid,
|
||||||
|
timestamp,
|
||||||
|
text,
|
||||||
|
jsonb,
|
||||||
|
bigint,
|
||||||
|
} from "drizzle-orm/pg-core"
|
||||||
|
|
||||||
|
import { tenants } from "./tenants"
|
||||||
|
import { customers } from "./customers"
|
||||||
|
import { contacts } from "./contacts"
|
||||||
|
import { helpdesk_channel_instances } from "./helpdesk_channel_instances" // placeholder
|
||||||
|
|
||||||
|
export const helpdesk_contacts = pgTable("helpdesk_contacts", {
|
||||||
|
id: uuid("id").primaryKey().defaultRandom(),
|
||||||
|
|
||||||
|
tenantId: bigint("tenant_id", { mode: "number" })
|
||||||
|
.notNull()
|
||||||
|
.references(() => tenants.id, { onDelete: "cascade" }),
|
||||||
|
|
||||||
|
customerId: bigint("customer_id", { mode: "number" })
|
||||||
|
.references(() => customers.id, { onDelete: "set null" }),
|
||||||
|
|
||||||
|
email: text("email"),
|
||||||
|
phone: text("phone"),
|
||||||
|
|
||||||
|
externalRef: jsonb("external_ref"),
|
||||||
|
displayName: text("display_name"),
|
||||||
|
|
||||||
|
createdAt: timestamp("created_at", { withTimezone: true }).defaultNow(),
|
||||||
|
|
||||||
|
sourceChannelId: uuid("source_channel_id").references(
|
||||||
|
() => helpdesk_channel_instances.id,
|
||||||
|
{ onDelete: "set null" }
|
||||||
|
),
|
||||||
|
|
||||||
|
contactId: bigint("contact_id", { mode: "number" }).references(
|
||||||
|
() => contacts.id,
|
||||||
|
{ onDelete: "set null" }
|
||||||
|
),
|
||||||
|
})
|
||||||
|
|
||||||
|
export type HelpdeskContact = typeof helpdesk_contacts.$inferSelect
|
||||||
|
export type NewHelpdeskContact = typeof helpdesk_contacts.$inferInsert
|
||||||
34
backend/db/schema/helpdesk_conversation_participants.ts
Normal file
34
backend/db/schema/helpdesk_conversation_participants.ts
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
import {
|
||||||
|
pgTable,
|
||||||
|
uuid,
|
||||||
|
text,
|
||||||
|
} from "drizzle-orm/pg-core"
|
||||||
|
|
||||||
|
import { helpdesk_conversations } from "./helpdesk_conversations"
|
||||||
|
import { authUsers } from "./auth_users"
|
||||||
|
|
||||||
|
export const helpdesk_conversation_participants = pgTable(
|
||||||
|
"helpdesk_conversation_participants",
|
||||||
|
{
|
||||||
|
conversationId: uuid("conversation_id")
|
||||||
|
.notNull()
|
||||||
|
.references(() => helpdesk_conversations.id, { onDelete: "cascade" }),
|
||||||
|
|
||||||
|
userId: uuid("user_id")
|
||||||
|
.notNull()
|
||||||
|
.references(() => authUsers.id, { onDelete: "cascade" }),
|
||||||
|
|
||||||
|
role: text("role"),
|
||||||
|
},
|
||||||
|
(table) => ({
|
||||||
|
pk: {
|
||||||
|
name: "helpdesk_conversation_participants_pkey",
|
||||||
|
columns: [table.conversationId, table.userId],
|
||||||
|
},
|
||||||
|
})
|
||||||
|
)
|
||||||
|
|
||||||
|
export type HelpdeskConversationParticipant =
|
||||||
|
typeof helpdesk_conversation_participants.$inferSelect
|
||||||
|
export type NewHelpdeskConversationParticipant =
|
||||||
|
typeof helpdesk_conversation_participants.$inferInsert
|
||||||
59
backend/db/schema/helpdesk_conversations.ts
Normal file
59
backend/db/schema/helpdesk_conversations.ts
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
import {
|
||||||
|
pgTable,
|
||||||
|
uuid,
|
||||||
|
timestamp,
|
||||||
|
text,
|
||||||
|
bigint,
|
||||||
|
} from "drizzle-orm/pg-core"
|
||||||
|
|
||||||
|
import { tenants } from "./tenants"
|
||||||
|
import { helpdesk_contacts } from "./helpdesk_contacts"
|
||||||
|
import { contacts } from "./contacts"
|
||||||
|
import { customers } from "./customers"
|
||||||
|
import { authUsers } from "./auth_users"
|
||||||
|
import { helpdesk_channel_instances } from "./helpdesk_channel_instances"
|
||||||
|
|
||||||
|
export const helpdesk_conversations = pgTable("helpdesk_conversations", {
|
||||||
|
id: uuid("id").primaryKey().defaultRandom(),
|
||||||
|
|
||||||
|
tenantId: bigint("tenant_id", { mode: "number" })
|
||||||
|
.notNull()
|
||||||
|
.references(() => tenants.id, { onDelete: "cascade" }),
|
||||||
|
|
||||||
|
channelInstanceId: uuid("channel_instance_id")
|
||||||
|
.notNull()
|
||||||
|
.references(() => helpdesk_channel_instances.id, { onDelete: "cascade" }),
|
||||||
|
|
||||||
|
contactId: uuid("contact_id").references(() => helpdesk_contacts.id, {
|
||||||
|
onDelete: "set null",
|
||||||
|
}),
|
||||||
|
|
||||||
|
subject: text("subject"),
|
||||||
|
|
||||||
|
status: text("status").notNull().default("open"),
|
||||||
|
|
||||||
|
priority: text("priority").default("normal"),
|
||||||
|
|
||||||
|
assigneeUserId: uuid("assignee_user_id").references(() => authUsers.id),
|
||||||
|
|
||||||
|
lastMessageAt: timestamp("last_message_at", { withTimezone: true }),
|
||||||
|
|
||||||
|
createdAt: timestamp("created_at", { withTimezone: true }).defaultNow(),
|
||||||
|
|
||||||
|
customerId: bigint("customer_id", { mode: "number" }).references(
|
||||||
|
() => customers.id,
|
||||||
|
{ onDelete: "set null" }
|
||||||
|
),
|
||||||
|
|
||||||
|
contactPersonId: bigint("contact_person_id", { mode: "number" }).references(
|
||||||
|
() => contacts.id,
|
||||||
|
{ onDelete: "set null" }
|
||||||
|
),
|
||||||
|
|
||||||
|
ticketNumber: text("ticket_number"),
|
||||||
|
})
|
||||||
|
|
||||||
|
export type HelpdeskConversation =
|
||||||
|
typeof helpdesk_conversations.$inferSelect
|
||||||
|
export type NewHelpdeskConversation =
|
||||||
|
typeof helpdesk_conversations.$inferInsert
|
||||||
46
backend/db/schema/helpdesk_messages.ts
Normal file
46
backend/db/schema/helpdesk_messages.ts
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
import {
|
||||||
|
pgTable,
|
||||||
|
uuid,
|
||||||
|
timestamp,
|
||||||
|
text,
|
||||||
|
jsonb,
|
||||||
|
bigint,
|
||||||
|
} from "drizzle-orm/pg-core"
|
||||||
|
|
||||||
|
import { tenants } from "./tenants"
|
||||||
|
import { helpdesk_contacts } from "./helpdesk_contacts"
|
||||||
|
import { helpdesk_conversations } from "./helpdesk_conversations"
|
||||||
|
import { authUsers } from "./auth_users"
|
||||||
|
|
||||||
|
export const helpdesk_messages = pgTable("helpdesk_messages", {
|
||||||
|
id: uuid("id").primaryKey().defaultRandom(),
|
||||||
|
|
||||||
|
tenantId: bigint("tenant_id", { mode: "number" })
|
||||||
|
.notNull()
|
||||||
|
.references(() => tenants.id, { onDelete: "cascade" }),
|
||||||
|
|
||||||
|
conversationId: uuid("conversation_id")
|
||||||
|
.notNull()
|
||||||
|
.references(() => helpdesk_conversations.id, { onDelete: "cascade" }),
|
||||||
|
|
||||||
|
direction: text("direction").notNull(),
|
||||||
|
|
||||||
|
authorUserId: uuid("author_user_id").references(() => authUsers.id),
|
||||||
|
|
||||||
|
payload: jsonb("payload").notNull(),
|
||||||
|
|
||||||
|
rawMeta: jsonb("raw_meta"),
|
||||||
|
|
||||||
|
createdAt: timestamp("created_at", { withTimezone: true }).defaultNow(),
|
||||||
|
|
||||||
|
contactId: uuid("contact_id").references(() => helpdesk_contacts.id, {
|
||||||
|
onDelete: "set null",
|
||||||
|
}),
|
||||||
|
|
||||||
|
externalMessageId: text("external_message_id").unique(),
|
||||||
|
|
||||||
|
receivedAt: timestamp("received_at", { withTimezone: true }).defaultNow(),
|
||||||
|
})
|
||||||
|
|
||||||
|
export type HelpdeskMessage = typeof helpdesk_messages.$inferSelect
|
||||||
|
export type NewHelpdeskMessage = typeof helpdesk_messages.$inferInsert
|
||||||
33
backend/db/schema/helpdesk_routing_rules.ts
Normal file
33
backend/db/schema/helpdesk_routing_rules.ts
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
import {
|
||||||
|
pgTable,
|
||||||
|
uuid,
|
||||||
|
timestamp,
|
||||||
|
text,
|
||||||
|
jsonb,
|
||||||
|
bigint,
|
||||||
|
} from "drizzle-orm/pg-core"
|
||||||
|
|
||||||
|
import { tenants } from "./tenants"
|
||||||
|
import { authUsers } from "./auth_users"
|
||||||
|
|
||||||
|
export const helpdesk_routing_rules = pgTable("helpdesk_routing_rules", {
|
||||||
|
id: uuid("id").primaryKey().defaultRandom(),
|
||||||
|
|
||||||
|
tenantId: bigint("tenant_id", { mode: "number" })
|
||||||
|
.notNull()
|
||||||
|
.references(() => tenants.id, { onDelete: "cascade" }),
|
||||||
|
|
||||||
|
name: text("name").notNull(),
|
||||||
|
|
||||||
|
condition: jsonb("condition").notNull(),
|
||||||
|
action: jsonb("action").notNull(),
|
||||||
|
|
||||||
|
createdBy: uuid("created_by").references(() => authUsers.id),
|
||||||
|
|
||||||
|
createdAt: timestamp("created_at", { withTimezone: true }).defaultNow(),
|
||||||
|
})
|
||||||
|
|
||||||
|
export type HelpdeskRoutingRule =
|
||||||
|
typeof helpdesk_routing_rules.$inferSelect
|
||||||
|
export type NewHelpdeskRoutingRule =
|
||||||
|
typeof helpdesk_routing_rules.$inferInsert
|
||||||
140
backend/db/schema/historyitems.ts
Normal file
140
backend/db/schema/historyitems.ts
Normal file
@@ -0,0 +1,140 @@
|
|||||||
|
import {
|
||||||
|
pgTable,
|
||||||
|
bigint,
|
||||||
|
uuid,
|
||||||
|
timestamp,
|
||||||
|
text,
|
||||||
|
jsonb,
|
||||||
|
} from "drizzle-orm/pg-core"
|
||||||
|
|
||||||
|
import { tenants } from "./tenants"
|
||||||
|
import { customers } from "./customers"
|
||||||
|
import { vendors } from "./vendors"
|
||||||
|
import { projects } from "./projects"
|
||||||
|
import { plants } from "./plants"
|
||||||
|
import { incominginvoices } from "./incominginvoices"
|
||||||
|
import { contacts } from "./contacts"
|
||||||
|
import { inventoryitems } from "./inventoryitems"
|
||||||
|
import { products } from "./products"
|
||||||
|
import { tasks } from "./tasks"
|
||||||
|
import { vehicles } from "./vehicles"
|
||||||
|
import { bankstatements } from "./bankstatements"
|
||||||
|
import { spaces } from "./spaces"
|
||||||
|
import { costcentres } from "./costcentres"
|
||||||
|
import { ownaccounts } from "./ownaccounts"
|
||||||
|
import { createddocuments } from "./createddocuments"
|
||||||
|
import { documentboxes } from "./documentboxes"
|
||||||
|
import { hourrates } from "./hourrates"
|
||||||
|
import { projecttypes } from "./projecttypes"
|
||||||
|
import { checks } from "./checks"
|
||||||
|
import { services } from "./services"
|
||||||
|
import { events } from "./events"
|
||||||
|
import { inventoryitemgroups } from "./inventoryitemgroups"
|
||||||
|
import { authUsers } from "./auth_users"
|
||||||
|
import {files} from "./files";
|
||||||
|
|
||||||
|
export const historyitems = pgTable("historyitems", {
|
||||||
|
id: bigint("id", { mode: "number" })
|
||||||
|
.primaryKey()
|
||||||
|
.generatedByDefaultAsIdentity(),
|
||||||
|
|
||||||
|
createdAt: timestamp("created_at", { withTimezone: true })
|
||||||
|
.notNull()
|
||||||
|
.defaultNow(),
|
||||||
|
|
||||||
|
text: text("text").notNull(),
|
||||||
|
|
||||||
|
customer: bigint("customer", { mode: "number" }).references(
|
||||||
|
() => customers.id,
|
||||||
|
{ onDelete: "cascade" }
|
||||||
|
),
|
||||||
|
|
||||||
|
tenant: bigint("tenant", { mode: "number" })
|
||||||
|
.notNull()
|
||||||
|
.references(() => tenants.id),
|
||||||
|
|
||||||
|
vendor: bigint("vendor", { mode: "number" }).references(() => vendors.id),
|
||||||
|
|
||||||
|
project: bigint("project", { mode: "number" }).references(
|
||||||
|
() => projects.id,
|
||||||
|
{ onDelete: "cascade" }
|
||||||
|
),
|
||||||
|
|
||||||
|
plant: bigint("plant", { mode: "number" }).references(
|
||||||
|
() => plants.id,
|
||||||
|
{ onDelete: "cascade" }
|
||||||
|
),
|
||||||
|
|
||||||
|
incomingInvoice: bigint("incomingInvoice", { mode: "number" }).references(
|
||||||
|
() => incominginvoices.id,
|
||||||
|
{ onDelete: "cascade" }
|
||||||
|
),
|
||||||
|
|
||||||
|
contact: bigint("contact", { mode: "number" }).references(() => contacts.id, {
|
||||||
|
onDelete: "cascade",
|
||||||
|
}),
|
||||||
|
|
||||||
|
inventoryitem: bigint("inventoryitem", { mode: "number" }).references(
|
||||||
|
() => inventoryitems.id,
|
||||||
|
{ onDelete: "cascade" }
|
||||||
|
),
|
||||||
|
|
||||||
|
product: bigint("product", { mode: "number" }).references(
|
||||||
|
() => products.id,
|
||||||
|
{ onDelete: "cascade" }
|
||||||
|
),
|
||||||
|
|
||||||
|
event: bigint("event", { mode: "number" }).references(() => events.id),
|
||||||
|
|
||||||
|
newVal: text("newVal"),
|
||||||
|
oldVal: text("oldVal"),
|
||||||
|
|
||||||
|
task: bigint("task", { mode: "number" }).references(() => tasks.id),
|
||||||
|
|
||||||
|
vehicle: bigint("vehicle", { mode: "number" }).references(() => vehicles.id),
|
||||||
|
|
||||||
|
bankstatement: bigint("bankstatement", { mode: "number" }).references(
|
||||||
|
() => bankstatements.id
|
||||||
|
),
|
||||||
|
|
||||||
|
space: bigint("space", { mode: "number" }).references(() => spaces.id),
|
||||||
|
|
||||||
|
config: jsonb("config"),
|
||||||
|
|
||||||
|
projecttype: bigint("projecttype", { mode: "number" }).references(
|
||||||
|
() => projecttypes.id
|
||||||
|
),
|
||||||
|
|
||||||
|
check: uuid("check").references(() => checks.id),
|
||||||
|
|
||||||
|
service: bigint("service", { mode: "number" }).references(
|
||||||
|
() => services.id
|
||||||
|
),
|
||||||
|
|
||||||
|
createddocument: bigint("createddocument", { mode: "number" }).references(
|
||||||
|
() => createddocuments.id
|
||||||
|
),
|
||||||
|
|
||||||
|
file: uuid("file").references(() => files.id),
|
||||||
|
|
||||||
|
inventoryitemgroup: uuid("inventoryitemgroup").references(
|
||||||
|
() => inventoryitemgroups.id
|
||||||
|
),
|
||||||
|
|
||||||
|
source: text("source").default("Software"),
|
||||||
|
|
||||||
|
costcentre: uuid("costcentre").references(() => costcentres.id),
|
||||||
|
|
||||||
|
ownaccount: uuid("ownaccount").references(() => ownaccounts.id),
|
||||||
|
|
||||||
|
documentbox: uuid("documentbox").references(() => documentboxes.id),
|
||||||
|
|
||||||
|
hourrate: uuid("hourrate").references(() => hourrates.id),
|
||||||
|
|
||||||
|
createdBy: uuid("created_by").references(() => authUsers.id),
|
||||||
|
|
||||||
|
action: text("action"),
|
||||||
|
})
|
||||||
|
|
||||||
|
export type HistoryItem = typeof historyitems.$inferSelect
|
||||||
|
export type NewHistoryItem = typeof historyitems.$inferInsert
|
||||||
18
backend/db/schema/holidays.ts
Normal file
18
backend/db/schema/holidays.ts
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
import { pgTable, bigint, date, text, timestamp } from "drizzle-orm/pg-core"
|
||||||
|
|
||||||
|
export const holidays = pgTable("holidays", {
|
||||||
|
id: bigint("id", { mode: "number" })
|
||||||
|
.primaryKey()
|
||||||
|
.generatedAlwaysAsIdentity(),
|
||||||
|
|
||||||
|
date: date("date").notNull(),
|
||||||
|
|
||||||
|
name: text("name").notNull(),
|
||||||
|
|
||||||
|
state_code: text("state_code").notNull(),
|
||||||
|
|
||||||
|
createdAt: timestamp("created_at", { withTimezone: true }).defaultNow(),
|
||||||
|
})
|
||||||
|
|
||||||
|
export type Holiday = typeof holidays.$inferSelect
|
||||||
|
export type NewHoliday = typeof holidays.$inferInsert
|
||||||
27
backend/db/schema/hourrates.ts
Normal file
27
backend/db/schema/hourrates.ts
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
import { pgTable, uuid, timestamp, text, boolean, bigint, doublePrecision } from "drizzle-orm/pg-core"
|
||||||
|
|
||||||
|
import { tenants } from "./tenants"
|
||||||
|
import { authUsers } from "./auth_users"
|
||||||
|
|
||||||
|
export const hourrates = pgTable("hourrates", {
|
||||||
|
id: uuid("id").primaryKey().defaultRandom(),
|
||||||
|
|
||||||
|
createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
|
||||||
|
|
||||||
|
tenant: bigint("tenant", { mode: "number" })
|
||||||
|
.notNull()
|
||||||
|
.references(() => tenants.id),
|
||||||
|
|
||||||
|
name: text("name").notNull(),
|
||||||
|
|
||||||
|
purchasePrice: doublePrecision("purchasePrice").notNull(),
|
||||||
|
sellingPrice: doublePrecision("sellingPrice").notNull(),
|
||||||
|
|
||||||
|
archived: boolean("archived").notNull().default(false),
|
||||||
|
|
||||||
|
updatedAt: timestamp("updated_at", { withTimezone: true }),
|
||||||
|
updatedBy: uuid("updated_by").references(() => authUsers.id),
|
||||||
|
})
|
||||||
|
|
||||||
|
export type HourRate = typeof hourrates.$inferSelect
|
||||||
|
export type NewHourRate = typeof hourrates.$inferInsert
|
||||||
63
backend/db/schema/incominginvoices.ts
Normal file
63
backend/db/schema/incominginvoices.ts
Normal file
@@ -0,0 +1,63 @@
|
|||||||
|
import {
|
||||||
|
pgTable,
|
||||||
|
bigint,
|
||||||
|
timestamp,
|
||||||
|
text,
|
||||||
|
boolean,
|
||||||
|
jsonb,
|
||||||
|
uuid,
|
||||||
|
} from "drizzle-orm/pg-core"
|
||||||
|
|
||||||
|
import { vendors } from "./vendors"
|
||||||
|
import { tenants } from "./tenants"
|
||||||
|
import { authUsers } from "./auth_users"
|
||||||
|
|
||||||
|
export const incominginvoices = pgTable("incominginvoices", {
|
||||||
|
id: bigint("id", { mode: "number" })
|
||||||
|
.primaryKey()
|
||||||
|
.generatedByDefaultAsIdentity(),
|
||||||
|
|
||||||
|
createdAt: timestamp("created_at", { withTimezone: true })
|
||||||
|
.notNull()
|
||||||
|
.defaultNow(),
|
||||||
|
|
||||||
|
tenant: bigint("tenant", { mode: "number" })
|
||||||
|
.notNull()
|
||||||
|
.references(() => tenants.id),
|
||||||
|
|
||||||
|
state: text("state").notNull().default("Entwurf"),
|
||||||
|
|
||||||
|
vendor: bigint("vendor", { mode: "number" }).references(() => vendors.id),
|
||||||
|
|
||||||
|
reference: text("reference"),
|
||||||
|
date: text("date"),
|
||||||
|
|
||||||
|
document: bigint("document", { mode: "number" }),
|
||||||
|
|
||||||
|
dueDate: text("dueDate"),
|
||||||
|
|
||||||
|
description: text("description"),
|
||||||
|
|
||||||
|
paymentType: text("paymentType"),
|
||||||
|
|
||||||
|
accounts: jsonb("accounts").notNull().default([
|
||||||
|
{
|
||||||
|
account: null,
|
||||||
|
taxType: null,
|
||||||
|
amountNet: null,
|
||||||
|
amountTax: 19,
|
||||||
|
costCentre: null,
|
||||||
|
},
|
||||||
|
]),
|
||||||
|
|
||||||
|
paid: boolean("paid").notNull().default(false),
|
||||||
|
expense: boolean("expense").notNull().default(true),
|
||||||
|
|
||||||
|
updatedAt: timestamp("updated_at", { withTimezone: true }),
|
||||||
|
updatedBy: uuid("updated_by").references(() => authUsers.id),
|
||||||
|
|
||||||
|
archived: boolean("archived").notNull().default(false),
|
||||||
|
})
|
||||||
|
|
||||||
|
export type IncomingInvoice = typeof incominginvoices.$inferSelect
|
||||||
|
export type NewIncomingInvoice = typeof incominginvoices.$inferInsert
|
||||||
74
backend/db/schema/index.ts
Normal file
74
backend/db/schema/index.ts
Normal file
@@ -0,0 +1,74 @@
|
|||||||
|
export * from "./accounts"
|
||||||
|
export * from "./auth_profiles"
|
||||||
|
export * from "./auth_role_permisssions"
|
||||||
|
export * from "./auth_roles"
|
||||||
|
export * from "./auth_tenant_users"
|
||||||
|
export * from "./auth_user_roles"
|
||||||
|
export * from "./auth_users"
|
||||||
|
export * from "./bankaccounts"
|
||||||
|
export * from "./bankrequisitions"
|
||||||
|
export * from "./bankstatements"
|
||||||
|
export * from "./checkexecutions"
|
||||||
|
export * from "./checks"
|
||||||
|
export * from "./citys"
|
||||||
|
export * from "./contacts"
|
||||||
|
export * from "./contracts"
|
||||||
|
export * from "./costcentres"
|
||||||
|
export * from "./countrys"
|
||||||
|
export * from "./createddocuments"
|
||||||
|
export * from "./createdletters"
|
||||||
|
export * from "./customers"
|
||||||
|
export * from "./devices"
|
||||||
|
export * from "./documentboxes"
|
||||||
|
export * from "./enums"
|
||||||
|
export * from "./events"
|
||||||
|
export * from "./files"
|
||||||
|
export * from "./filetags"
|
||||||
|
export * from "./folders"
|
||||||
|
export * from "./generatedexports"
|
||||||
|
export * from "./globalmessages"
|
||||||
|
export * from "./globalmessagesseen"
|
||||||
|
export * from "./helpdesk_channel_instances"
|
||||||
|
export * from "./helpdesk_channel_types"
|
||||||
|
export * from "./helpdesk_contacts"
|
||||||
|
export * from "./helpdesk_conversation_participants"
|
||||||
|
export * from "./helpdesk_conversations"
|
||||||
|
export * from "./helpdesk_messages"
|
||||||
|
export * from "./helpdesk_routing_rules"
|
||||||
|
export * from "./historyitems"
|
||||||
|
export * from "./holidays"
|
||||||
|
export * from "./hourrates"
|
||||||
|
export * from "./incominginvoices"
|
||||||
|
export * from "./inventoryitemgroups"
|
||||||
|
export * from "./inventoryitems"
|
||||||
|
export * from "./letterheads"
|
||||||
|
export * from "./movements"
|
||||||
|
export * from "./notifications_event_types"
|
||||||
|
export * from "./notifications_items"
|
||||||
|
export * from "./notifications_preferences"
|
||||||
|
export * from "./notifications_preferences_defaults"
|
||||||
|
export * from "./ownaccounts"
|
||||||
|
export * from "./plants"
|
||||||
|
export * from "./productcategories"
|
||||||
|
export * from "./products"
|
||||||
|
export * from "./projects"
|
||||||
|
export * from "./projecttypes"
|
||||||
|
export * from "./servicecategories"
|
||||||
|
export * from "./services"
|
||||||
|
export * from "./spaces"
|
||||||
|
export * from "./staff_time_entries"
|
||||||
|
export * from "./staff_time_entry_connects"
|
||||||
|
export * from "./staff_zeitstromtimestamps"
|
||||||
|
export * from "./statementallocations"
|
||||||
|
export * from "./tasks"
|
||||||
|
export * from "./taxtypes"
|
||||||
|
export * from "./tenants"
|
||||||
|
export * from "./texttemplates"
|
||||||
|
export * from "./units"
|
||||||
|
export * from "./user_credentials"
|
||||||
|
export * from "./vehicles"
|
||||||
|
export * from "./vendors"
|
||||||
|
export * from "./staff_time_events"
|
||||||
|
export * from "./serialtypes"
|
||||||
|
export * from "./serialexecutions"
|
||||||
|
export * from "./public_links"
|
||||||
39
backend/db/schema/inventoryitemgroups.ts
Normal file
39
backend/db/schema/inventoryitemgroups.ts
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
import {
|
||||||
|
pgTable,
|
||||||
|
uuid,
|
||||||
|
timestamp,
|
||||||
|
text,
|
||||||
|
boolean,
|
||||||
|
jsonb, bigint,
|
||||||
|
} from "drizzle-orm/pg-core"
|
||||||
|
|
||||||
|
import { tenants } from "./tenants"
|
||||||
|
import { authUsers } from "./auth_users"
|
||||||
|
|
||||||
|
export const inventoryitemgroups = pgTable("inventoryitemgroups", {
|
||||||
|
id: uuid("id").primaryKey().defaultRandom(),
|
||||||
|
|
||||||
|
createdAt: timestamp("created_at", { withTimezone: true })
|
||||||
|
.notNull()
|
||||||
|
.defaultNow(),
|
||||||
|
|
||||||
|
tenant: bigint("tenant", { mode: "number" }).notNull().references(() => tenants.id),
|
||||||
|
|
||||||
|
name: text("name").notNull(),
|
||||||
|
|
||||||
|
inventoryitems: jsonb("inventoryitems").notNull().default([]),
|
||||||
|
|
||||||
|
description: text("description"),
|
||||||
|
|
||||||
|
archived: boolean("archived").notNull().default(false),
|
||||||
|
|
||||||
|
profiles: jsonb("profiles").notNull().default([]),
|
||||||
|
|
||||||
|
usePlanning: boolean("usePlanning").notNull().default(false),
|
||||||
|
|
||||||
|
updatedAt: timestamp("updated_at", { withTimezone: true }),
|
||||||
|
updatedBy: uuid("updated_by").references(() => authUsers.id),
|
||||||
|
})
|
||||||
|
|
||||||
|
export type InventoryItemGroup = typeof inventoryitemgroups.$inferSelect
|
||||||
|
export type NewInventoryItemGroup = typeof inventoryitemgroups.$inferInsert
|
||||||
68
backend/db/schema/inventoryitems.ts
Normal file
68
backend/db/schema/inventoryitems.ts
Normal file
@@ -0,0 +1,68 @@
|
|||||||
|
import {
|
||||||
|
pgTable,
|
||||||
|
bigint,
|
||||||
|
timestamp,
|
||||||
|
text,
|
||||||
|
boolean,
|
||||||
|
doublePrecision,
|
||||||
|
uuid,
|
||||||
|
jsonb,
|
||||||
|
date,
|
||||||
|
} from "drizzle-orm/pg-core"
|
||||||
|
|
||||||
|
import { tenants } from "./tenants"
|
||||||
|
import { vendors } from "./vendors"
|
||||||
|
import { spaces } from "./spaces"
|
||||||
|
import { authUsers } from "./auth_users"
|
||||||
|
|
||||||
|
export const inventoryitems = pgTable("inventoryitems", {
|
||||||
|
id: bigint("id", { mode: "number" })
|
||||||
|
.primaryKey()
|
||||||
|
.generatedByDefaultAsIdentity(),
|
||||||
|
|
||||||
|
createdAt: timestamp("created_at", { withTimezone: true })
|
||||||
|
.notNull()
|
||||||
|
.defaultNow(),
|
||||||
|
|
||||||
|
name: text("name").notNull(),
|
||||||
|
|
||||||
|
usePlanning: boolean("usePlanning").notNull().default(false),
|
||||||
|
|
||||||
|
description: text("description"),
|
||||||
|
|
||||||
|
tenant: bigint("tenant", { mode: "number" })
|
||||||
|
.notNull()
|
||||||
|
.references(() => tenants.id),
|
||||||
|
|
||||||
|
currentSpace: bigint("currentSpace", { mode: "number" }).references(
|
||||||
|
() => spaces.id
|
||||||
|
),
|
||||||
|
|
||||||
|
articleNumber: text("articleNumber"),
|
||||||
|
serialNumber: text("serialNumber"),
|
||||||
|
|
||||||
|
purchaseDate: date("purchaseDate"),
|
||||||
|
|
||||||
|
vendor: bigint("vendor", { mode: "number" }).references(() => vendors.id),
|
||||||
|
|
||||||
|
quantity: bigint("quantity", { mode: "number" }).notNull().default(0),
|
||||||
|
|
||||||
|
purchasePrice: doublePrecision("purchasePrice").default(0),
|
||||||
|
|
||||||
|
manufacturer: text("manufacturer"),
|
||||||
|
manufacturerNumber: text("manufacturerNumber"),
|
||||||
|
|
||||||
|
currentValue: doublePrecision("currentValue"),
|
||||||
|
|
||||||
|
archived: boolean("archived").notNull().default(false),
|
||||||
|
|
||||||
|
profiles: jsonb("profiles").notNull().default([]),
|
||||||
|
|
||||||
|
updatedAt: timestamp("updated_at", { withTimezone: true }),
|
||||||
|
updatedBy: uuid("updated_by").references(() =>
|
||||||
|
authUsers.id
|
||||||
|
),
|
||||||
|
})
|
||||||
|
|
||||||
|
export type InventoryItem = typeof inventoryitems.$inferSelect
|
||||||
|
export type NewInventoryItem = typeof inventoryitems.$inferInsert
|
||||||
39
backend/db/schema/letterheads.ts
Normal file
39
backend/db/schema/letterheads.ts
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
import {
|
||||||
|
pgTable,
|
||||||
|
bigint,
|
||||||
|
timestamp,
|
||||||
|
text,
|
||||||
|
boolean,
|
||||||
|
uuid,
|
||||||
|
} from "drizzle-orm/pg-core"
|
||||||
|
|
||||||
|
import { tenants } from "./tenants"
|
||||||
|
import { authUsers } from "./auth_users"
|
||||||
|
|
||||||
|
export const letterheads = pgTable("letterheads", {
|
||||||
|
id: bigint("id", { mode: "number" })
|
||||||
|
.primaryKey()
|
||||||
|
.generatedByDefaultAsIdentity(),
|
||||||
|
|
||||||
|
createdAt: timestamp("created_at", { withTimezone: true })
|
||||||
|
.notNull()
|
||||||
|
.defaultNow(),
|
||||||
|
|
||||||
|
tenant: bigint("tenant", { mode: "number" })
|
||||||
|
.notNull()
|
||||||
|
.references(() => tenants.id),
|
||||||
|
|
||||||
|
name: text("name").default("Standard"),
|
||||||
|
|
||||||
|
path: text("path").notNull(),
|
||||||
|
|
||||||
|
documentTypes: text("documentTypes").array().notNull().default([]),
|
||||||
|
|
||||||
|
updatedAt: timestamp("updated_at", { withTimezone: true }),
|
||||||
|
updatedBy: uuid("updated_by").references(() => authUsers.id),
|
||||||
|
|
||||||
|
archived: boolean("archived").notNull().default(false),
|
||||||
|
})
|
||||||
|
|
||||||
|
export type Letterhead = typeof letterheads.$inferSelect
|
||||||
|
export type NewLetterhead = typeof letterheads.$inferInsert
|
||||||
49
backend/db/schema/movements.ts
Normal file
49
backend/db/schema/movements.ts
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
import {
|
||||||
|
pgTable,
|
||||||
|
bigint,
|
||||||
|
timestamp,
|
||||||
|
text,
|
||||||
|
uuid,
|
||||||
|
} from "drizzle-orm/pg-core"
|
||||||
|
|
||||||
|
import { products } from "./products"
|
||||||
|
import { spaces } from "./spaces"
|
||||||
|
import { tenants } from "./tenants"
|
||||||
|
import { projects } from "./projects"
|
||||||
|
import { authUsers } from "./auth_users"
|
||||||
|
|
||||||
|
export const movements = pgTable("movements", {
|
||||||
|
id: bigint("id", { mode: "number" })
|
||||||
|
.primaryKey()
|
||||||
|
.generatedByDefaultAsIdentity(),
|
||||||
|
|
||||||
|
createdAt: timestamp("created_at", { withTimezone: true })
|
||||||
|
.notNull()
|
||||||
|
.defaultNow(),
|
||||||
|
|
||||||
|
quantity: bigint("quantity", { mode: "number" }).notNull(),
|
||||||
|
|
||||||
|
productId: bigint("productId", { mode: "number" })
|
||||||
|
.notNull()
|
||||||
|
.references(() => products.id),
|
||||||
|
|
||||||
|
spaceId: bigint("spaceId", { mode: "number" }).references(() => spaces.id),
|
||||||
|
|
||||||
|
tenant: bigint("tenant", { mode: "number" })
|
||||||
|
.notNull()
|
||||||
|
.references(() => tenants.id),
|
||||||
|
|
||||||
|
projectId: bigint("projectId", { mode: "number" }).references(
|
||||||
|
() => projects.id
|
||||||
|
),
|
||||||
|
|
||||||
|
notes: text("notes"),
|
||||||
|
|
||||||
|
serials: text("serials").array(),
|
||||||
|
|
||||||
|
updatedAt: timestamp("updated_at", { withTimezone: true }),
|
||||||
|
updatedBy: uuid("updated_by").references(() => authUsers.id),
|
||||||
|
})
|
||||||
|
|
||||||
|
export type Movement = typeof movements.$inferSelect
|
||||||
|
export type NewMovement = typeof movements.$inferInsert
|
||||||
34
backend/db/schema/notifications_event_types.ts
Normal file
34
backend/db/schema/notifications_event_types.ts
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
import {
|
||||||
|
pgTable,
|
||||||
|
text,
|
||||||
|
jsonb,
|
||||||
|
boolean,
|
||||||
|
timestamp,
|
||||||
|
} from "drizzle-orm/pg-core"
|
||||||
|
import {notificationSeverityEnum} from "./enums";
|
||||||
|
|
||||||
|
|
||||||
|
export const notificationsEventTypes = pgTable("notifications_event_types", {
|
||||||
|
eventKey: text("event_key").primaryKey(),
|
||||||
|
|
||||||
|
displayName: text("display_name").notNull(),
|
||||||
|
description: text("description"),
|
||||||
|
category: text("category"),
|
||||||
|
|
||||||
|
severity: notificationSeverityEnum("severity").notNull().default("info"),
|
||||||
|
|
||||||
|
allowedChannels: jsonb("allowed_channels").notNull().default(["inapp", "email"]),
|
||||||
|
|
||||||
|
payloadSchema: jsonb("payload_schema"),
|
||||||
|
|
||||||
|
isActive: boolean("is_active").notNull().default(true),
|
||||||
|
|
||||||
|
createdAt: timestamp("created_at", { withTimezone: true })
|
||||||
|
.notNull()
|
||||||
|
.defaultNow(),
|
||||||
|
})
|
||||||
|
|
||||||
|
export type NotificationsEventType =
|
||||||
|
typeof notificationsEventTypes.$inferSelect
|
||||||
|
export type NewNotificationsEventType =
|
||||||
|
typeof notificationsEventTypes.$inferInsert
|
||||||
54
backend/db/schema/notifications_items.ts
Normal file
54
backend/db/schema/notifications_items.ts
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
import {
|
||||||
|
pgTable,
|
||||||
|
uuid,
|
||||||
|
bigint,
|
||||||
|
text,
|
||||||
|
jsonb,
|
||||||
|
timestamp,
|
||||||
|
} from "drizzle-orm/pg-core"
|
||||||
|
|
||||||
|
import { tenants } from "./tenants"
|
||||||
|
import { authUsers } from "./auth_users"
|
||||||
|
import { notificationsEventTypes } from "./notifications_event_types"
|
||||||
|
import {notificationChannelEnum, notificationStatusEnum} from "./enums";
|
||||||
|
|
||||||
|
|
||||||
|
export const notificationsItems = pgTable("notifications_items", {
|
||||||
|
id: uuid("id").primaryKey().defaultRandom(),
|
||||||
|
|
||||||
|
tenantId: bigint("tenant_id", { mode: "number" })
|
||||||
|
.notNull()
|
||||||
|
.references(() => tenants.id, { onDelete: "cascade", onUpdate: "cascade" }),
|
||||||
|
|
||||||
|
userId: uuid("user_id")
|
||||||
|
.notNull()
|
||||||
|
.references(() => authUsers.id, { onDelete: "cascade", onUpdate: "cascade" }),
|
||||||
|
|
||||||
|
eventType: text("event_type")
|
||||||
|
.notNull()
|
||||||
|
.references(() => notificationsEventTypes.eventKey, {
|
||||||
|
onUpdate: "cascade",
|
||||||
|
onDelete: "restrict",
|
||||||
|
}),
|
||||||
|
|
||||||
|
title: text("title").notNull(),
|
||||||
|
message: text("message").notNull(),
|
||||||
|
|
||||||
|
payload: jsonb("payload"),
|
||||||
|
|
||||||
|
channel: notificationChannelEnum("channel").notNull(),
|
||||||
|
|
||||||
|
status: notificationStatusEnum("status").notNull().default("queued"),
|
||||||
|
|
||||||
|
error: text("error"),
|
||||||
|
|
||||||
|
createdAt: timestamp("created_at", { withTimezone: true })
|
||||||
|
.notNull()
|
||||||
|
.defaultNow(),
|
||||||
|
|
||||||
|
sentAt: timestamp("sent_at", { withTimezone: true }),
|
||||||
|
readAt: timestamp("read_at", { withTimezone: true }),
|
||||||
|
})
|
||||||
|
|
||||||
|
export type NotificationItem = typeof notificationsItems.$inferSelect
|
||||||
|
export type NewNotificationItem = typeof notificationsItems.$inferInsert
|
||||||
60
backend/db/schema/notifications_preferences.ts
Normal file
60
backend/db/schema/notifications_preferences.ts
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
import {
|
||||||
|
pgTable,
|
||||||
|
uuid,
|
||||||
|
bigint,
|
||||||
|
text,
|
||||||
|
boolean,
|
||||||
|
timestamp,
|
||||||
|
uniqueIndex,
|
||||||
|
} from "drizzle-orm/pg-core"
|
||||||
|
|
||||||
|
import { tenants } from "./tenants"
|
||||||
|
import { authUsers } from "./auth_users"
|
||||||
|
import { notificationsEventTypes } from "./notifications_event_types"
|
||||||
|
import {notificationChannelEnum} from "./enums";
|
||||||
|
|
||||||
|
export const notificationsPreferences = pgTable(
|
||||||
|
"notifications_preferences",
|
||||||
|
{
|
||||||
|
id: uuid("id").primaryKey().defaultRandom(),
|
||||||
|
|
||||||
|
tenantId: bigint("tenant_id", { mode: "number" })
|
||||||
|
.notNull()
|
||||||
|
.references(() => tenants.id, {
|
||||||
|
onDelete: "cascade",
|
||||||
|
onUpdate: "cascade",
|
||||||
|
}),
|
||||||
|
|
||||||
|
userId: uuid("user_id")
|
||||||
|
.notNull()
|
||||||
|
.references(() => authUsers.id, {
|
||||||
|
onDelete: "cascade",
|
||||||
|
onUpdate: "cascade",
|
||||||
|
}),
|
||||||
|
|
||||||
|
eventType: text("event_type")
|
||||||
|
.notNull()
|
||||||
|
.references(() => notificationsEventTypes.eventKey, {
|
||||||
|
onDelete: "restrict",
|
||||||
|
onUpdate: "cascade",
|
||||||
|
}),
|
||||||
|
|
||||||
|
channel: notificationChannelEnum("channel").notNull(),
|
||||||
|
|
||||||
|
enabled: boolean("enabled").notNull().default(true),
|
||||||
|
|
||||||
|
createdAt: timestamp("created_at", { withTimezone: true })
|
||||||
|
.notNull()
|
||||||
|
.defaultNow(),
|
||||||
|
},
|
||||||
|
(table) => ({
|
||||||
|
uniquePrefs: uniqueIndex(
|
||||||
|
"notifications_preferences_tenant_id_user_id_event_type_chan_key",
|
||||||
|
).on(table.tenantId, table.userId, table.eventType, table.channel),
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
|
export type NotificationPreference =
|
||||||
|
typeof notificationsPreferences.$inferSelect
|
||||||
|
export type NewNotificationPreference =
|
||||||
|
typeof notificationsPreferences.$inferInsert
|
||||||
52
backend/db/schema/notifications_preferences_defaults.ts
Normal file
52
backend/db/schema/notifications_preferences_defaults.ts
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
import {
|
||||||
|
pgTable,
|
||||||
|
uuid,
|
||||||
|
bigint,
|
||||||
|
text,
|
||||||
|
boolean,
|
||||||
|
timestamp,
|
||||||
|
uniqueIndex,
|
||||||
|
} from "drizzle-orm/pg-core"
|
||||||
|
|
||||||
|
import { tenants } from "./tenants"
|
||||||
|
import { notificationsEventTypes } from "./notifications_event_types"
|
||||||
|
import {notificationChannelEnum} from "./enums";
|
||||||
|
|
||||||
|
export const notificationsPreferencesDefaults = pgTable(
|
||||||
|
"notifications_preferences_defaults",
|
||||||
|
{
|
||||||
|
id: uuid("id").primaryKey().defaultRandom(),
|
||||||
|
|
||||||
|
tenantId: bigint("tenant_id", { mode: "number" })
|
||||||
|
.notNull()
|
||||||
|
.references(() => tenants.id, {
|
||||||
|
onDelete: "cascade",
|
||||||
|
onUpdate: "cascade",
|
||||||
|
}),
|
||||||
|
|
||||||
|
eventKey: text("event_key")
|
||||||
|
.notNull()
|
||||||
|
.references(() => notificationsEventTypes.eventKey, {
|
||||||
|
onDelete: "restrict",
|
||||||
|
onUpdate: "cascade",
|
||||||
|
}),
|
||||||
|
|
||||||
|
channel: notificationChannelEnum("channel").notNull(),
|
||||||
|
|
||||||
|
enabled: boolean("enabled").notNull().default(true),
|
||||||
|
|
||||||
|
createdAt: timestamp("created_at", { withTimezone: true })
|
||||||
|
.notNull()
|
||||||
|
.defaultNow(),
|
||||||
|
},
|
||||||
|
(table) => ({
|
||||||
|
uniqueDefaults: uniqueIndex(
|
||||||
|
"notifications_preferences_defau_tenant_id_event_key_channel_key",
|
||||||
|
).on(table.tenantId, table.eventKey, table.channel),
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
|
export type NotificationPreferenceDefault =
|
||||||
|
typeof notificationsPreferencesDefaults.$inferSelect
|
||||||
|
export type NewNotificationPreferenceDefault =
|
||||||
|
typeof notificationsPreferencesDefaults.$inferInsert
|
||||||
39
backend/db/schema/ownaccounts.ts
Normal file
39
backend/db/schema/ownaccounts.ts
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
import {
|
||||||
|
pgTable,
|
||||||
|
uuid,
|
||||||
|
timestamp,
|
||||||
|
text,
|
||||||
|
boolean,
|
||||||
|
jsonb,
|
||||||
|
bigint,
|
||||||
|
} from "drizzle-orm/pg-core"
|
||||||
|
|
||||||
|
import { tenants } from "./tenants"
|
||||||
|
import { authUsers } from "./auth_users"
|
||||||
|
|
||||||
|
export const ownaccounts = pgTable("ownaccounts", {
|
||||||
|
id: uuid("id").primaryKey().defaultRandom(),
|
||||||
|
|
||||||
|
createdAt: timestamp("created_at", { withTimezone: true })
|
||||||
|
.notNull()
|
||||||
|
.defaultNow(),
|
||||||
|
|
||||||
|
tenant: bigint("tenant", { mode: "number" })
|
||||||
|
.notNull()
|
||||||
|
.references(() => tenants.id),
|
||||||
|
|
||||||
|
number: text("number").notNull(),
|
||||||
|
name: text("name").notNull(),
|
||||||
|
|
||||||
|
description: text("description"),
|
||||||
|
|
||||||
|
archived: boolean("archived").notNull().default(false),
|
||||||
|
|
||||||
|
profiles: jsonb("profiles").notNull().default([]),
|
||||||
|
|
||||||
|
updatedAt: timestamp("updated_at", { withTimezone: true }),
|
||||||
|
updatedBy: uuid("updated_by").references(() => authUsers.id),
|
||||||
|
})
|
||||||
|
|
||||||
|
export type OwnAccount = typeof ownaccounts.$inferSelect
|
||||||
|
export type NewOwnAccount = typeof ownaccounts.$inferInsert
|
||||||
56
backend/db/schema/plants.ts
Normal file
56
backend/db/schema/plants.ts
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
import {
|
||||||
|
pgTable,
|
||||||
|
bigint,
|
||||||
|
timestamp,
|
||||||
|
text,
|
||||||
|
jsonb,
|
||||||
|
boolean,
|
||||||
|
uuid,
|
||||||
|
date,
|
||||||
|
} from "drizzle-orm/pg-core"
|
||||||
|
|
||||||
|
import { tenants } from "./tenants"
|
||||||
|
import { customers } from "./customers"
|
||||||
|
import { contracts } from "./contracts"
|
||||||
|
import { authUsers } from "./auth_users"
|
||||||
|
|
||||||
|
export const plants = pgTable("plants", {
|
||||||
|
id: bigint("id", { mode: "number" })
|
||||||
|
.primaryKey()
|
||||||
|
.generatedByDefaultAsIdentity(),
|
||||||
|
|
||||||
|
createdAt: timestamp("created_at", { withTimezone: true })
|
||||||
|
.notNull()
|
||||||
|
.defaultNow(),
|
||||||
|
|
||||||
|
tenant: bigint("tenant", { mode: "number" })
|
||||||
|
.notNull()
|
||||||
|
.references(() => tenants.id),
|
||||||
|
|
||||||
|
name: text("name").notNull(),
|
||||||
|
|
||||||
|
customer: bigint("customer", { mode: "number" }).references(
|
||||||
|
() => customers.id
|
||||||
|
),
|
||||||
|
|
||||||
|
infoData: jsonb("infoData"),
|
||||||
|
contract: bigint("contract", { mode: "number" }).references(
|
||||||
|
() => contracts.id
|
||||||
|
),
|
||||||
|
|
||||||
|
description: jsonb("description").default({
|
||||||
|
html: "",
|
||||||
|
json: [],
|
||||||
|
text: "",
|
||||||
|
}),
|
||||||
|
|
||||||
|
archived: boolean("archived").notNull().default(false),
|
||||||
|
|
||||||
|
profiles: jsonb("profiles").notNull().default([]),
|
||||||
|
|
||||||
|
updatedAt: timestamp("updated_at", { withTimezone: true }),
|
||||||
|
updatedBy: uuid("updated_by").references(() => authUsers.id),
|
||||||
|
})
|
||||||
|
|
||||||
|
export type Plant = typeof plants.$inferSelect
|
||||||
|
export type NewPlant = typeof plants.$inferInsert
|
||||||
37
backend/db/schema/productcategories.ts
Normal file
37
backend/db/schema/productcategories.ts
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
import {
|
||||||
|
pgTable,
|
||||||
|
bigint,
|
||||||
|
timestamp,
|
||||||
|
text,
|
||||||
|
boolean,
|
||||||
|
uuid,
|
||||||
|
} from "drizzle-orm/pg-core"
|
||||||
|
|
||||||
|
import { tenants } from "./tenants"
|
||||||
|
import { authUsers } from "./auth_users"
|
||||||
|
|
||||||
|
export const productcategories = pgTable("productcategories", {
|
||||||
|
id: bigint("id", { mode: "number" })
|
||||||
|
.primaryKey()
|
||||||
|
.generatedByDefaultAsIdentity(),
|
||||||
|
|
||||||
|
createdAt: timestamp("created_at", { withTimezone: true })
|
||||||
|
.notNull()
|
||||||
|
.defaultNow(),
|
||||||
|
|
||||||
|
tenant: bigint("tenant", { mode: "number" })
|
||||||
|
.notNull()
|
||||||
|
.references(() => tenants.id),
|
||||||
|
|
||||||
|
name: text("name").notNull(),
|
||||||
|
|
||||||
|
description: text("description"),
|
||||||
|
|
||||||
|
archived: boolean("archived").notNull().default(false),
|
||||||
|
|
||||||
|
updatedAt: timestamp("updated_at", { withTimezone: true }),
|
||||||
|
updatedBy: uuid("updated_by").references(() => authUsers.id),
|
||||||
|
})
|
||||||
|
|
||||||
|
export type ProductCategory = typeof productcategories.$inferSelect
|
||||||
|
export type NewProductCategory = typeof productcategories.$inferInsert
|
||||||
69
backend/db/schema/products.ts
Normal file
69
backend/db/schema/products.ts
Normal file
@@ -0,0 +1,69 @@
|
|||||||
|
import {
|
||||||
|
pgTable,
|
||||||
|
bigint,
|
||||||
|
timestamp,
|
||||||
|
text,
|
||||||
|
doublePrecision,
|
||||||
|
boolean,
|
||||||
|
smallint,
|
||||||
|
uuid,
|
||||||
|
jsonb,
|
||||||
|
json,
|
||||||
|
} from "drizzle-orm/pg-core"
|
||||||
|
|
||||||
|
import { tenants } from "./tenants"
|
||||||
|
import { units } from "./units"
|
||||||
|
import { authUsers } from "./auth_users"
|
||||||
|
|
||||||
|
export const products = pgTable("products", {
|
||||||
|
id: bigint("id", { mode: "number" })
|
||||||
|
.primaryKey()
|
||||||
|
.generatedByDefaultAsIdentity(),
|
||||||
|
|
||||||
|
createdAt: timestamp("created_at", { withTimezone: true })
|
||||||
|
.notNull()
|
||||||
|
.defaultNow(),
|
||||||
|
|
||||||
|
name: text("name").notNull(),
|
||||||
|
manufacturer: text("manufacturer"),
|
||||||
|
|
||||||
|
unit: bigint("unit", { mode: "number" })
|
||||||
|
.notNull()
|
||||||
|
.references(() => units.id),
|
||||||
|
|
||||||
|
tags: json("tags").notNull().default([]),
|
||||||
|
|
||||||
|
tenant: bigint("tenant", { mode: "number" })
|
||||||
|
.notNull()
|
||||||
|
.references(() => tenants.id),
|
||||||
|
|
||||||
|
ean: text("ean"),
|
||||||
|
barcode: text("barcode"),
|
||||||
|
|
||||||
|
purchase_price: doublePrecision("purchasePrice"),
|
||||||
|
selling_price: doublePrecision("sellingPrice"),
|
||||||
|
|
||||||
|
description: text("description"),
|
||||||
|
|
||||||
|
manufacturer_number: text("manufacturerNumber"),
|
||||||
|
|
||||||
|
vendor_allocation: jsonb("vendorAllocation").default([]),
|
||||||
|
|
||||||
|
article_number: text("articleNumber"),
|
||||||
|
|
||||||
|
barcodes: text("barcodes").array().notNull().default([]),
|
||||||
|
|
||||||
|
productcategories: jsonb("productcategories").default([]),
|
||||||
|
|
||||||
|
archived: boolean("archived").notNull().default(false),
|
||||||
|
|
||||||
|
tax_percentage: smallint("taxPercentage").notNull().default(19),
|
||||||
|
|
||||||
|
markup_percentage: doublePrecision("markupPercentage"),
|
||||||
|
|
||||||
|
updated_at: timestamp("updated_at", { withTimezone: true }),
|
||||||
|
updated_by: uuid("updated_by").references(() => authUsers.id),
|
||||||
|
})
|
||||||
|
|
||||||
|
export type Product = typeof products.$inferSelect
|
||||||
|
export type NewProduct = typeof products.$inferInsert
|
||||||
78
backend/db/schema/projects.ts
Normal file
78
backend/db/schema/projects.ts
Normal file
@@ -0,0 +1,78 @@
|
|||||||
|
import {
|
||||||
|
pgTable,
|
||||||
|
bigint,
|
||||||
|
timestamp,
|
||||||
|
text,
|
||||||
|
jsonb,
|
||||||
|
json,
|
||||||
|
boolean,
|
||||||
|
uuid,
|
||||||
|
} from "drizzle-orm/pg-core"
|
||||||
|
|
||||||
|
import { tenants } from "./tenants"
|
||||||
|
import { customers } from "./customers"
|
||||||
|
import { contracts } from "./contracts"
|
||||||
|
import { projecttypes } from "./projecttypes"
|
||||||
|
import { authUsers } from "./auth_users"
|
||||||
|
|
||||||
|
export const projects = pgTable("projects", {
|
||||||
|
id: bigint("id", { mode: "number" })
|
||||||
|
.primaryKey()
|
||||||
|
.generatedByDefaultAsIdentity(),
|
||||||
|
|
||||||
|
createdAt: timestamp("created_at", { withTimezone: true })
|
||||||
|
.notNull()
|
||||||
|
.defaultNow(),
|
||||||
|
|
||||||
|
tenant: bigint("tenant", { mode: "number" })
|
||||||
|
.notNull()
|
||||||
|
.references(() => tenants.id),
|
||||||
|
|
||||||
|
name: text("name").notNull(),
|
||||||
|
|
||||||
|
notes: text("notes"),
|
||||||
|
|
||||||
|
customer: bigint("customer", { mode: "number" }).references(
|
||||||
|
() => customers.id
|
||||||
|
),
|
||||||
|
|
||||||
|
phases: jsonb("phases").default([]),
|
||||||
|
|
||||||
|
description: json("description"),
|
||||||
|
|
||||||
|
forms: jsonb("forms").default([]),
|
||||||
|
|
||||||
|
heroId: text("heroId"),
|
||||||
|
|
||||||
|
measure: text("measure"),
|
||||||
|
|
||||||
|
material: jsonb("material"),
|
||||||
|
|
||||||
|
plant: bigint("plant", { mode: "number" }),
|
||||||
|
|
||||||
|
profiles: uuid("profiles").array().notNull().default([]),
|
||||||
|
|
||||||
|
projectNumber: text("projectNumber"),
|
||||||
|
|
||||||
|
contract: bigint("contract", { mode: "number" }).references(
|
||||||
|
() => contracts.id
|
||||||
|
),
|
||||||
|
|
||||||
|
projectType: text("projectType").default("Projekt"),
|
||||||
|
|
||||||
|
projecttype: bigint("projecttype", { mode: "number" }).references(
|
||||||
|
() => projecttypes.id
|
||||||
|
),
|
||||||
|
|
||||||
|
archived: boolean("archived").notNull().default(false),
|
||||||
|
|
||||||
|
customerRef: text("customerRef"),
|
||||||
|
|
||||||
|
updatedAt: timestamp("updated_at", { withTimezone: true }),
|
||||||
|
updatedBy: uuid("updated_by").references(() => authUsers.id),
|
||||||
|
|
||||||
|
active_phase: text("active_phase"),
|
||||||
|
})
|
||||||
|
|
||||||
|
export type Project = typeof projects.$inferSelect
|
||||||
|
export type NewProject = typeof projects.$inferInsert
|
||||||
41
backend/db/schema/projecttypes.ts
Normal file
41
backend/db/schema/projecttypes.ts
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
import {
|
||||||
|
pgTable,
|
||||||
|
bigint,
|
||||||
|
timestamp,
|
||||||
|
text,
|
||||||
|
jsonb,
|
||||||
|
boolean,
|
||||||
|
uuid,
|
||||||
|
} from "drizzle-orm/pg-core"
|
||||||
|
|
||||||
|
import { tenants } from "./tenants"
|
||||||
|
import { authUsers } from "./auth_users"
|
||||||
|
|
||||||
|
export const projecttypes = pgTable("projecttypes", {
|
||||||
|
id: bigint("id", { mode: "number" })
|
||||||
|
.primaryKey()
|
||||||
|
.generatedByDefaultAsIdentity(),
|
||||||
|
|
||||||
|
createdAt: timestamp("created_at", { withTimezone: true })
|
||||||
|
.notNull()
|
||||||
|
.defaultNow(),
|
||||||
|
|
||||||
|
name: text("name").notNull(),
|
||||||
|
|
||||||
|
initialPhases: jsonb("initialPhases"),
|
||||||
|
addablePhases: jsonb("addablePhases"),
|
||||||
|
|
||||||
|
icon: text("icon"),
|
||||||
|
|
||||||
|
tenant: bigint("tenant", { mode: "number" })
|
||||||
|
.notNull()
|
||||||
|
.references(() => tenants.id),
|
||||||
|
|
||||||
|
archived: boolean("archived").notNull().default(false),
|
||||||
|
|
||||||
|
updatedAt: timestamp("updated_at", { withTimezone: true }),
|
||||||
|
updatedBy: uuid("updated_by").references(() => authUsers.id),
|
||||||
|
})
|
||||||
|
|
||||||
|
export type ProjectType = typeof projecttypes.$inferSelect
|
||||||
|
export type NewProjectType = typeof projecttypes.$inferInsert
|
||||||
30
backend/db/schema/public_links.ts
Normal file
30
backend/db/schema/public_links.ts
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
import { pgTable, text, integer, boolean, jsonb, timestamp, uuid } from 'drizzle-orm/pg-core';
|
||||||
|
import { tenants } from './tenants';
|
||||||
|
import { authProfiles } from './auth_profiles';
|
||||||
|
|
||||||
|
export const publicLinks = pgTable('public_links', {
|
||||||
|
id: uuid("id").primaryKey().defaultRandom(),
|
||||||
|
|
||||||
|
// Der öffentliche Token (z.B. "werkstatt-tablet-01")
|
||||||
|
token: text('token').notNull().unique(),
|
||||||
|
|
||||||
|
// Zuordnung zum Tenant (WICHTIG für die Datentrennung)
|
||||||
|
tenant: integer('tenant').references(() => tenants.id).notNull(),
|
||||||
|
|
||||||
|
defaultProfile: uuid('default_profile').references(() => authProfiles.id),
|
||||||
|
|
||||||
|
// Sicherheit
|
||||||
|
isProtected: boolean('is_protected').default(false).notNull(),
|
||||||
|
pinHash: text('pin_hash'),
|
||||||
|
|
||||||
|
// Konfiguration (JSON)
|
||||||
|
config: jsonb('config').default({}),
|
||||||
|
|
||||||
|
// Metadaten
|
||||||
|
name: text('name').notNull(),
|
||||||
|
description: text('description'),
|
||||||
|
|
||||||
|
active: boolean('active').default(true).notNull(),
|
||||||
|
createdAt: timestamp('created_at').defaultNow(),
|
||||||
|
updatedAt: timestamp('updated_at').defaultNow(),
|
||||||
|
});
|
||||||
21
backend/db/schema/serialexecutions.ts
Normal file
21
backend/db/schema/serialexecutions.ts
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
import {
|
||||||
|
pgTable,
|
||||||
|
bigint,
|
||||||
|
timestamp,
|
||||||
|
text,
|
||||||
|
jsonb,
|
||||||
|
boolean,
|
||||||
|
uuid,
|
||||||
|
} from "drizzle-orm/pg-core"
|
||||||
|
import {tenants} from "./tenants";
|
||||||
|
|
||||||
|
export const serialExecutions = pgTable("serial_executions", {
|
||||||
|
id: uuid("id").primaryKey().defaultRandom(),
|
||||||
|
tenant: bigint("tenant", { mode: "number" })
|
||||||
|
.notNull()
|
||||||
|
.references(() => tenants.id), executionDate: timestamp("execution_date").notNull(),
|
||||||
|
status: text("status").default("draft"), // 'draft', 'completed'
|
||||||
|
createdBy: text("created_by"), // oder UUID, je nach Auth-System
|
||||||
|
createdAt: timestamp("created_at").defaultNow(),
|
||||||
|
summary: text("summary"), // z.B. "25 Rechnungen erstellt"
|
||||||
|
});
|
||||||
40
backend/db/schema/serialtypes.ts
Normal file
40
backend/db/schema/serialtypes.ts
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
import {
|
||||||
|
pgTable,
|
||||||
|
bigint,
|
||||||
|
timestamp,
|
||||||
|
text,
|
||||||
|
jsonb,
|
||||||
|
boolean,
|
||||||
|
uuid,
|
||||||
|
} from "drizzle-orm/pg-core"
|
||||||
|
|
||||||
|
import { tenants } from "./tenants"
|
||||||
|
import { authUsers } from "./auth_users"
|
||||||
|
|
||||||
|
export const serialtypes = pgTable("serialtypes", {
|
||||||
|
id: bigint("id", { mode: "number" })
|
||||||
|
.primaryKey()
|
||||||
|
.generatedByDefaultAsIdentity(),
|
||||||
|
|
||||||
|
createdAt: timestamp("created_at", { withTimezone: true })
|
||||||
|
.notNull()
|
||||||
|
.defaultNow(),
|
||||||
|
|
||||||
|
name: text("name").notNull(),
|
||||||
|
|
||||||
|
intervall: text("intervall"),
|
||||||
|
|
||||||
|
icon: text("icon"),
|
||||||
|
|
||||||
|
tenant: bigint("tenant", { mode: "number" })
|
||||||
|
.notNull()
|
||||||
|
.references(() => tenants.id),
|
||||||
|
|
||||||
|
archived: boolean("archived").notNull().default(false),
|
||||||
|
|
||||||
|
updatedAt: timestamp("updated_at", { withTimezone: true }),
|
||||||
|
updatedBy: uuid("updated_by").references(() => authUsers.id),
|
||||||
|
})
|
||||||
|
|
||||||
|
export type SerialType = typeof serialtypes.$inferSelect
|
||||||
|
export type NewSerialType = typeof serialtypes.$inferInsert
|
||||||
39
backend/db/schema/servicecategories.ts
Normal file
39
backend/db/schema/servicecategories.ts
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
import {
|
||||||
|
pgTable,
|
||||||
|
bigint,
|
||||||
|
timestamp,
|
||||||
|
text,
|
||||||
|
doublePrecision,
|
||||||
|
boolean,
|
||||||
|
uuid,
|
||||||
|
} from "drizzle-orm/pg-core"
|
||||||
|
|
||||||
|
import { tenants } from "./tenants"
|
||||||
|
import { authUsers } from "./auth_users"
|
||||||
|
|
||||||
|
export const servicecategories = pgTable("servicecategories", {
|
||||||
|
id: bigint("id", { mode: "number" })
|
||||||
|
.primaryKey()
|
||||||
|
.generatedByDefaultAsIdentity(),
|
||||||
|
|
||||||
|
created_at: timestamp("created_at", { withTimezone: true })
|
||||||
|
.notNull()
|
||||||
|
.defaultNow(),
|
||||||
|
|
||||||
|
tenant: bigint("tenant", { mode: "number" })
|
||||||
|
.notNull()
|
||||||
|
.references(() => tenants.id),
|
||||||
|
|
||||||
|
name: text("name").notNull(),
|
||||||
|
description: text("description"),
|
||||||
|
|
||||||
|
discount: doublePrecision("discount").default(0),
|
||||||
|
|
||||||
|
archived: boolean("archived").notNull().default(false),
|
||||||
|
|
||||||
|
updated_at: timestamp("updated_at", { withTimezone: true }),
|
||||||
|
updated_by: uuid("updated_by").references(() => authUsers.id),
|
||||||
|
})
|
||||||
|
|
||||||
|
export type ServiceCategory = typeof servicecategories.$inferSelect
|
||||||
|
export type NewServiceCategory = typeof servicecategories.$inferInsert
|
||||||
63
backend/db/schema/services.ts
Normal file
63
backend/db/schema/services.ts
Normal file
@@ -0,0 +1,63 @@
|
|||||||
|
import {
|
||||||
|
pgTable,
|
||||||
|
bigint,
|
||||||
|
timestamp,
|
||||||
|
text,
|
||||||
|
doublePrecision,
|
||||||
|
jsonb,
|
||||||
|
boolean,
|
||||||
|
smallint,
|
||||||
|
uuid,
|
||||||
|
} from "drizzle-orm/pg-core"
|
||||||
|
|
||||||
|
import { tenants } from "./tenants"
|
||||||
|
import { units } from "./units"
|
||||||
|
import { authUsers } from "./auth_users"
|
||||||
|
|
||||||
|
export const services = pgTable("services", {
|
||||||
|
id: bigint("id", { mode: "number" })
|
||||||
|
.primaryKey()
|
||||||
|
.generatedByDefaultAsIdentity(),
|
||||||
|
|
||||||
|
createdAt: timestamp("created_at", { withTimezone: true })
|
||||||
|
.notNull()
|
||||||
|
.defaultNow(),
|
||||||
|
|
||||||
|
name: text("name").notNull(),
|
||||||
|
|
||||||
|
sellingPrice: doublePrecision("sellingPrice"),
|
||||||
|
|
||||||
|
description: text("description"),
|
||||||
|
|
||||||
|
tenant: bigint("tenant", { mode: "number" })
|
||||||
|
.notNull()
|
||||||
|
.references(() => tenants.id),
|
||||||
|
|
||||||
|
unit: bigint("unit", { mode: "number" }).references(() => units.id),
|
||||||
|
|
||||||
|
serviceNumber: bigint("serviceNumber", { mode: "number" }),
|
||||||
|
|
||||||
|
tags: jsonb("tags").default([]),
|
||||||
|
servicecategories: jsonb("servicecategories").notNull().default([]),
|
||||||
|
|
||||||
|
archived: boolean("archived").notNull().default(false),
|
||||||
|
|
||||||
|
purchasePriceComposed: jsonb("purchasePriceComposed")
|
||||||
|
.notNull()
|
||||||
|
.default({ total: 0 }),
|
||||||
|
|
||||||
|
sellingPriceComposed: jsonb("sellingPriceComposed")
|
||||||
|
.notNull()
|
||||||
|
.default({ total: 0 }),
|
||||||
|
|
||||||
|
taxPercentage: smallint("taxPercentage").notNull().default(19),
|
||||||
|
|
||||||
|
materialComposition: jsonb("materialComposition").notNull().default([]),
|
||||||
|
personalComposition: jsonb("personalComposition").notNull().default([]),
|
||||||
|
|
||||||
|
updatedAt: timestamp("updated_at", { withTimezone: true }),
|
||||||
|
updatedBy: uuid("updated_by").references(() => authUsers.id),
|
||||||
|
})
|
||||||
|
|
||||||
|
export type Service = typeof services.$inferSelect
|
||||||
|
export type NewService = typeof services.$inferInsert
|
||||||
49
backend/db/schema/spaces.ts
Normal file
49
backend/db/schema/spaces.ts
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
import {
|
||||||
|
pgTable,
|
||||||
|
bigint,
|
||||||
|
timestamp,
|
||||||
|
text,
|
||||||
|
boolean,
|
||||||
|
jsonb,
|
||||||
|
uuid,
|
||||||
|
} from "drizzle-orm/pg-core"
|
||||||
|
|
||||||
|
import { tenants } from "./tenants"
|
||||||
|
import { authUsers } from "./auth_users"
|
||||||
|
|
||||||
|
export const spaces = pgTable("spaces", {
|
||||||
|
id: bigint("id", { mode: "number" })
|
||||||
|
.primaryKey()
|
||||||
|
.generatedByDefaultAsIdentity(),
|
||||||
|
|
||||||
|
createdAt: timestamp("created_at", { withTimezone: true })
|
||||||
|
.notNull()
|
||||||
|
.defaultNow(),
|
||||||
|
|
||||||
|
name: text("name"),
|
||||||
|
type: text("type").notNull(),
|
||||||
|
|
||||||
|
tenant: bigint("tenant", { mode: "number" })
|
||||||
|
.notNull()
|
||||||
|
.references(() => tenants.id),
|
||||||
|
|
||||||
|
space_number: text("spaceNumber").notNull(),
|
||||||
|
|
||||||
|
parentSpace: bigint("parentSpace", { mode: "number" }).references(
|
||||||
|
() => spaces.id
|
||||||
|
),
|
||||||
|
|
||||||
|
info_data: jsonb("infoData")
|
||||||
|
.notNull()
|
||||||
|
.default({ zip: "", city: "", streetNumber: "" }),
|
||||||
|
|
||||||
|
description: text("description"),
|
||||||
|
|
||||||
|
archived: boolean("archived").notNull().default(false),
|
||||||
|
|
||||||
|
updatedAt: timestamp("updated_at", { withTimezone: true }),
|
||||||
|
updatedBy: uuid("updated_by").references(() => authUsers.id),
|
||||||
|
})
|
||||||
|
|
||||||
|
export type Space = typeof spaces.$inferSelect
|
||||||
|
export type NewSpace = typeof spaces.$inferInsert
|
||||||
68
backend/db/schema/staff_time_entries.ts
Normal file
68
backend/db/schema/staff_time_entries.ts
Normal file
@@ -0,0 +1,68 @@
|
|||||||
|
import {
|
||||||
|
pgTable,
|
||||||
|
uuid,
|
||||||
|
bigint,
|
||||||
|
timestamp,
|
||||||
|
integer,
|
||||||
|
text,
|
||||||
|
boolean,
|
||||||
|
numeric,
|
||||||
|
} from "drizzle-orm/pg-core"
|
||||||
|
|
||||||
|
import { tenants } from "./tenants"
|
||||||
|
import { authUsers } from "./auth_users"
|
||||||
|
import { timesStateEnum } from "./enums"
|
||||||
|
import {sql} from "drizzle-orm";
|
||||||
|
|
||||||
|
export const stafftimeentries = pgTable("staff_time_entries", {
|
||||||
|
id: uuid("id").primaryKey().defaultRandom(),
|
||||||
|
|
||||||
|
tenant_id: bigint("tenant_id", { mode: "number" })
|
||||||
|
.notNull()
|
||||||
|
.references(() => tenants.id),
|
||||||
|
|
||||||
|
user_id: uuid("user_id")
|
||||||
|
.notNull()
|
||||||
|
.references(() => authUsers.id, { onDelete: "cascade" }),
|
||||||
|
|
||||||
|
started_at: timestamp("started_at", { withTimezone: true }).notNull(),
|
||||||
|
stopped_at: timestamp("stopped_at", { withTimezone: true }),
|
||||||
|
|
||||||
|
duration_minutes: integer("duration_minutes").generatedAlwaysAs(
|
||||||
|
sql`CASE
|
||||||
|
WHEN stopped_at IS NOT NULL
|
||||||
|
THEN (EXTRACT(epoch FROM (stopped_at - started_at)) / 60)
|
||||||
|
ELSE NULL
|
||||||
|
END`
|
||||||
|
),
|
||||||
|
|
||||||
|
type: text("type").default("work"),
|
||||||
|
|
||||||
|
description: text("description"),
|
||||||
|
|
||||||
|
created_at: timestamp("created_at", { withTimezone: true }).defaultNow(),
|
||||||
|
updated_at: timestamp("updated_at", { withTimezone: true }).defaultNow(),
|
||||||
|
|
||||||
|
archived: boolean("archived").notNull().default(false),
|
||||||
|
|
||||||
|
updated_by: uuid("updated_by").references(() => authUsers.id),
|
||||||
|
|
||||||
|
source: text("source"),
|
||||||
|
|
||||||
|
state: timesStateEnum("state").notNull().default("draft"),
|
||||||
|
|
||||||
|
device: uuid("device"),
|
||||||
|
|
||||||
|
internal_note: text("internal_note"),
|
||||||
|
|
||||||
|
vacation_reason: text("vacation_reason"),
|
||||||
|
vacation_days: numeric("vacation_days", { precision: 5, scale: 2 }),
|
||||||
|
|
||||||
|
approved_by: uuid("approved_by").references(() => authUsers.id),
|
||||||
|
approved_at: timestamp("approved_at", { withTimezone: true }),
|
||||||
|
|
||||||
|
sick_reason: text("sick_reason"),
|
||||||
|
})
|
||||||
|
|
||||||
|
export type StaffTimeEntry = typeof stafftimeentries.$inferSelect
|
||||||
|
export type NewStaffTimeEntry = typeof stafftimeentries.$inferInsert
|
||||||
38
backend/db/schema/staff_time_entry_connects.ts
Normal file
38
backend/db/schema/staff_time_entry_connects.ts
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
import {
|
||||||
|
pgTable,
|
||||||
|
uuid,
|
||||||
|
bigint,
|
||||||
|
timestamp,
|
||||||
|
integer,
|
||||||
|
text,
|
||||||
|
} from "drizzle-orm/pg-core"
|
||||||
|
|
||||||
|
import { stafftimeentries } from "./staff_time_entries"
|
||||||
|
import {sql} from "drizzle-orm";
|
||||||
|
|
||||||
|
export const stafftimenetryconnects = pgTable("staff_time_entry_connects", {
|
||||||
|
id: uuid("id").primaryKey().defaultRandom(),
|
||||||
|
|
||||||
|
stafftimeentry: uuid("time_entry_id")
|
||||||
|
.notNull()
|
||||||
|
.references(() => stafftimeentries.id, { onDelete: "cascade" }),
|
||||||
|
|
||||||
|
project_id: bigint("project_id", { mode: "number" }), // referenziert später projects.id
|
||||||
|
|
||||||
|
started_at: timestamp("started_at", { withTimezone: true }).notNull(),
|
||||||
|
stopped_at: timestamp("stopped_at", { withTimezone: true }).notNull(),
|
||||||
|
|
||||||
|
durationMinutes: integer("duration_minutes").generatedAlwaysAs(
|
||||||
|
sql`(EXTRACT(epoch FROM (stopped_at - started_at)) / 60)`
|
||||||
|
),
|
||||||
|
|
||||||
|
notes: text("notes"),
|
||||||
|
|
||||||
|
created_at: timestamp("created_at", { withTimezone: true }).defaultNow(),
|
||||||
|
updated_at: timestamp("updated_at", { withTimezone: true }).defaultNow(),
|
||||||
|
})
|
||||||
|
|
||||||
|
export type StaffTimeEntryConnect =
|
||||||
|
typeof stafftimenetryconnects.$inferSelect
|
||||||
|
export type NewStaffTimeEntryConnect =
|
||||||
|
typeof stafftimenetryconnects.$inferInsert
|
||||||
85
backend/db/schema/staff_time_events.ts
Normal file
85
backend/db/schema/staff_time_events.ts
Normal file
@@ -0,0 +1,85 @@
|
|||||||
|
import {
|
||||||
|
pgTable,
|
||||||
|
uuid,
|
||||||
|
bigint,
|
||||||
|
text,
|
||||||
|
timestamp,
|
||||||
|
jsonb,
|
||||||
|
index,
|
||||||
|
check,
|
||||||
|
} from "drizzle-orm/pg-core";
|
||||||
|
import { sql } from "drizzle-orm";
|
||||||
|
import {tenants} from "./tenants";
|
||||||
|
import {authUsers} from "./auth_users";
|
||||||
|
|
||||||
|
export const stafftimeevents = pgTable(
|
||||||
|
"staff_time_events",
|
||||||
|
{
|
||||||
|
id: uuid("id").primaryKey().defaultRandom(),
|
||||||
|
|
||||||
|
tenant_id: bigint("tenant_id", { mode: "number" })
|
||||||
|
.notNull()
|
||||||
|
.references(() => tenants.id),
|
||||||
|
|
||||||
|
user_id: uuid("user_id")
|
||||||
|
.notNull()
|
||||||
|
.references(() => authUsers.id),
|
||||||
|
|
||||||
|
// Akteur
|
||||||
|
actortype: text("actor_type").notNull(), // 'user' | 'system'
|
||||||
|
actoruser_id: uuid("actor_user_id").references(() => authUsers.id),
|
||||||
|
|
||||||
|
// Zeit
|
||||||
|
eventtime: timestamp("event_time", {
|
||||||
|
withTimezone: true,
|
||||||
|
}).notNull(),
|
||||||
|
|
||||||
|
// Fachliche Bedeutung
|
||||||
|
eventtype: text("event_type").notNull(),
|
||||||
|
|
||||||
|
// Quelle
|
||||||
|
source: text("source").notNull(), // web | mobile | terminal | system
|
||||||
|
|
||||||
|
// Entkräftung
|
||||||
|
invalidates_event_id: uuid("invalidates_event_id")
|
||||||
|
.references(() => stafftimeevents.id),
|
||||||
|
|
||||||
|
//Beziehung Approval etc
|
||||||
|
related_event_id: uuid("related_event_id")
|
||||||
|
.references(() => stafftimeevents.id),
|
||||||
|
|
||||||
|
// Zusatzdaten
|
||||||
|
metadata: jsonb("metadata"),
|
||||||
|
|
||||||
|
// Technisch
|
||||||
|
created_at: timestamp("created_at", {
|
||||||
|
withTimezone: true,
|
||||||
|
})
|
||||||
|
.defaultNow()
|
||||||
|
.notNull(),
|
||||||
|
},
|
||||||
|
(table) => ({
|
||||||
|
// Indizes
|
||||||
|
tenantUserTimeIdx: index("idx_time_events_tenant_user_time").on(
|
||||||
|
table.tenant_id,
|
||||||
|
table.user_id,
|
||||||
|
table.eventtime
|
||||||
|
),
|
||||||
|
|
||||||
|
createdAtIdx: index("idx_time_events_created_at").on(table.created_at),
|
||||||
|
|
||||||
|
invalidatesIdx: index("idx_time_events_invalidates").on(
|
||||||
|
table.invalidates_event_id
|
||||||
|
),
|
||||||
|
|
||||||
|
// Constraints
|
||||||
|
actorUserCheck: check(
|
||||||
|
"time_events_actor_user_check",
|
||||||
|
sql`
|
||||||
|
(actor_type = 'system' AND actor_user_id IS NULL)
|
||||||
|
OR
|
||||||
|
(actor_type = 'user' AND actor_user_id IS NOT NULL)
|
||||||
|
`
|
||||||
|
),
|
||||||
|
})
|
||||||
|
);
|
||||||
44
backend/db/schema/staff_zeitstromtimestamps.ts
Normal file
44
backend/db/schema/staff_zeitstromtimestamps.ts
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
import {
|
||||||
|
pgTable,
|
||||||
|
uuid,
|
||||||
|
timestamp,
|
||||||
|
bigint,
|
||||||
|
text,
|
||||||
|
} from "drizzle-orm/pg-core"
|
||||||
|
|
||||||
|
import { tenants } from "./tenants"
|
||||||
|
import { authProfiles } from "./auth_profiles"
|
||||||
|
import { stafftimeentries } from "./staff_time_entries"
|
||||||
|
|
||||||
|
export const staffZeitstromTimestamps = pgTable("staff_zeitstromtimestamps", {
|
||||||
|
id: uuid("id").primaryKey().defaultRandom(),
|
||||||
|
|
||||||
|
createdAt: timestamp("created_at", { withTimezone: true })
|
||||||
|
.notNull()
|
||||||
|
.defaultNow(),
|
||||||
|
|
||||||
|
tenant: bigint("tenant", { mode: "number" })
|
||||||
|
.notNull()
|
||||||
|
.references(() => tenants.id),
|
||||||
|
|
||||||
|
profile: uuid("profile")
|
||||||
|
.notNull()
|
||||||
|
.references(() => authProfiles.id),
|
||||||
|
|
||||||
|
key: text("key").notNull(),
|
||||||
|
|
||||||
|
intent: text("intent").notNull(),
|
||||||
|
|
||||||
|
time: timestamp("time", { withTimezone: true }).notNull(),
|
||||||
|
|
||||||
|
staffTimeEntry: uuid("staff_time_entry").references(
|
||||||
|
() => stafftimeentries.id
|
||||||
|
),
|
||||||
|
|
||||||
|
internalNote: text("internal_note"),
|
||||||
|
})
|
||||||
|
|
||||||
|
export type StaffZeitstromTimestamp =
|
||||||
|
typeof staffZeitstromTimestamps.$inferSelect
|
||||||
|
export type NewStaffZeitstromTimestamp =
|
||||||
|
typeof staffZeitstromTimestamps.$inferInsert
|
||||||
69
backend/db/schema/statementallocations.ts
Normal file
69
backend/db/schema/statementallocations.ts
Normal file
@@ -0,0 +1,69 @@
|
|||||||
|
import {
|
||||||
|
pgTable,
|
||||||
|
uuid,
|
||||||
|
bigint,
|
||||||
|
integer,
|
||||||
|
text,
|
||||||
|
timestamp,
|
||||||
|
boolean,
|
||||||
|
doublePrecision,
|
||||||
|
} from "drizzle-orm/pg-core"
|
||||||
|
|
||||||
|
import { tenants } from "./tenants"
|
||||||
|
import { authUsers } from "./auth_users"
|
||||||
|
import { customers } from "./customers"
|
||||||
|
import { vendors } from "./vendors"
|
||||||
|
import { ownaccounts } from "./ownaccounts"
|
||||||
|
import { incominginvoices } from "./incominginvoices"
|
||||||
|
import { createddocuments } from "./createddocuments"
|
||||||
|
import { bankstatements } from "./bankstatements"
|
||||||
|
import { accounts } from "./accounts" // Falls noch nicht erstellt → bitte melden!
|
||||||
|
|
||||||
|
export const statementallocations = pgTable("statementallocations", {
|
||||||
|
id: uuid("id").primaryKey().defaultRandom(),
|
||||||
|
|
||||||
|
// foreign keys
|
||||||
|
bankstatement: integer("bs_id")
|
||||||
|
.notNull()
|
||||||
|
.references(() => bankstatements.id),
|
||||||
|
|
||||||
|
createddocument: integer("cd_id").references(() => createddocuments.id),
|
||||||
|
|
||||||
|
amount: doublePrecision("amount").notNull().default(0),
|
||||||
|
|
||||||
|
incominginvoice: bigint("ii_id", { mode: "number" }).references(
|
||||||
|
() => incominginvoices.id
|
||||||
|
),
|
||||||
|
|
||||||
|
tenant: bigint("tenant", { mode: "number" })
|
||||||
|
.notNull()
|
||||||
|
.references(() => tenants.id),
|
||||||
|
|
||||||
|
account: bigint("account", { mode: "number" }).references(
|
||||||
|
() => accounts.id
|
||||||
|
),
|
||||||
|
|
||||||
|
created_at: timestamp("created_at", {
|
||||||
|
withTimezone: false,
|
||||||
|
}).defaultNow(),
|
||||||
|
|
||||||
|
ownaccount: uuid("ownaccount").references(() => ownaccounts.id),
|
||||||
|
|
||||||
|
description: text("description"),
|
||||||
|
|
||||||
|
customer: bigint("customer", { mode: "number" }).references(
|
||||||
|
() => customers.id
|
||||||
|
),
|
||||||
|
|
||||||
|
vendor: bigint("vendor", { mode: "number" }).references(() => vendors.id),
|
||||||
|
|
||||||
|
updated_at: timestamp("updated_at", { withTimezone: true }),
|
||||||
|
|
||||||
|
updated_by: uuid("updated_by").references(() => authUsers.id),
|
||||||
|
|
||||||
|
archived: boolean("archived").notNull().default(false),
|
||||||
|
})
|
||||||
|
|
||||||
|
export type StatementAllocation = typeof statementallocations.$inferSelect
|
||||||
|
export type NewStatementAllocation =
|
||||||
|
typeof statementallocations.$inferInsert
|
||||||
51
backend/db/schema/tasks.ts
Normal file
51
backend/db/schema/tasks.ts
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
import {
|
||||||
|
pgTable,
|
||||||
|
bigint,
|
||||||
|
text,
|
||||||
|
timestamp,
|
||||||
|
boolean,
|
||||||
|
jsonb,
|
||||||
|
uuid,
|
||||||
|
} from "drizzle-orm/pg-core"
|
||||||
|
|
||||||
|
import { tenants } from "./tenants"
|
||||||
|
import { authUsers } from "./auth_users"
|
||||||
|
import { customers } from "./customers"
|
||||||
|
|
||||||
|
export const tasks = pgTable("tasks", {
|
||||||
|
id: bigint("id", { mode: "number" })
|
||||||
|
.primaryKey()
|
||||||
|
.generatedByDefaultAsIdentity(),
|
||||||
|
|
||||||
|
createdAt: timestamp("created_at", { withTimezone: true })
|
||||||
|
.notNull()
|
||||||
|
.defaultNow(),
|
||||||
|
|
||||||
|
name: text("name").notNull(),
|
||||||
|
description: text("description"),
|
||||||
|
categorie: text("categorie"),
|
||||||
|
|
||||||
|
tenant: bigint("tenant", { mode: "number" })
|
||||||
|
.notNull()
|
||||||
|
.references(() => tenants.id),
|
||||||
|
|
||||||
|
// FIXED: user_id statt profile, verweist auf auth_users.id
|
||||||
|
userId: uuid("user_id").references(() => authUsers.id),
|
||||||
|
|
||||||
|
project: bigint("project", { mode: "number" }),
|
||||||
|
plant: bigint("plant", { mode: "number" }),
|
||||||
|
|
||||||
|
customer: bigint("customer", { mode: "number" }).references(
|
||||||
|
() => customers.id
|
||||||
|
),
|
||||||
|
|
||||||
|
profiles: jsonb("profiles").notNull().default([]),
|
||||||
|
|
||||||
|
archived: boolean("archived").notNull().default(false),
|
||||||
|
|
||||||
|
updatedAt: timestamp("updated_at", { withTimezone: true }),
|
||||||
|
updatedBy: uuid("updated_by").references(() => authUsers.id),
|
||||||
|
})
|
||||||
|
|
||||||
|
export type Task = typeof tasks.$inferSelect
|
||||||
|
export type NewTask = typeof tasks.$inferInsert
|
||||||
28
backend/db/schema/taxtypes.ts
Normal file
28
backend/db/schema/taxtypes.ts
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
import {
|
||||||
|
pgTable,
|
||||||
|
bigint,
|
||||||
|
timestamp,
|
||||||
|
text,
|
||||||
|
uuid,
|
||||||
|
} from "drizzle-orm/pg-core"
|
||||||
|
|
||||||
|
import { authUsers } from "./auth_users"
|
||||||
|
|
||||||
|
export const taxTypes = pgTable("taxtypes", {
|
||||||
|
id: bigint("id", { mode: "number" })
|
||||||
|
.primaryKey()
|
||||||
|
.generatedByDefaultAsIdentity(),
|
||||||
|
|
||||||
|
createdAt: timestamp("created_at", { withTimezone: true })
|
||||||
|
.notNull()
|
||||||
|
.defaultNow(),
|
||||||
|
|
||||||
|
label: text("label").notNull(),
|
||||||
|
percentage: bigint("percentage", { mode: "number" }).notNull(),
|
||||||
|
|
||||||
|
updatedAt: timestamp("updated_at", { withTimezone: true }),
|
||||||
|
updatedBy: uuid("updated_by").references(() => authUsers.id),
|
||||||
|
})
|
||||||
|
|
||||||
|
export type TaxType = typeof taxTypes.$inferSelect
|
||||||
|
export type NewTaxType = typeof taxTypes.$inferInsert
|
||||||
140
backend/db/schema/tenants.ts
Normal file
140
backend/db/schema/tenants.ts
Normal file
@@ -0,0 +1,140 @@
|
|||||||
|
import {
|
||||||
|
pgTable,
|
||||||
|
bigint,
|
||||||
|
text,
|
||||||
|
timestamp,
|
||||||
|
boolean,
|
||||||
|
jsonb,
|
||||||
|
integer,
|
||||||
|
smallint,
|
||||||
|
date,
|
||||||
|
uuid,
|
||||||
|
pgEnum,
|
||||||
|
} from "drizzle-orm/pg-core"
|
||||||
|
import { authUsers } from "./auth_users"
|
||||||
|
import {lockedTenantEnum} from "./enums";
|
||||||
|
|
||||||
|
export const tenants = pgTable(
|
||||||
|
"tenants",
|
||||||
|
{
|
||||||
|
id: bigint("id", { mode: "number" })
|
||||||
|
.primaryKey()
|
||||||
|
.generatedByDefaultAsIdentity(),
|
||||||
|
|
||||||
|
createdAt: timestamp("created_at", { withTimezone: true })
|
||||||
|
.notNull()
|
||||||
|
.defaultNow(),
|
||||||
|
|
||||||
|
name: text("name").notNull(),
|
||||||
|
short: text("short").notNull(),
|
||||||
|
|
||||||
|
calendarConfig: jsonb("calendarConfig").default({
|
||||||
|
eventTypes: [
|
||||||
|
{ color: "blue", label: "Büro" },
|
||||||
|
{ color: "yellow", label: "Besprechung" },
|
||||||
|
{ color: "green", label: "Umsetzung" },
|
||||||
|
{ color: "red", label: "Vor Ort Termin" },
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
|
||||||
|
timeConfig: jsonb("timeConfig").notNull().default({}),
|
||||||
|
|
||||||
|
tags: jsonb("tags").notNull().default({
|
||||||
|
products: [],
|
||||||
|
documents: [],
|
||||||
|
}),
|
||||||
|
|
||||||
|
measures: jsonb("measures")
|
||||||
|
.notNull()
|
||||||
|
.default([
|
||||||
|
{ name: "Netzwerktechnik", short: "NWT" },
|
||||||
|
{ name: "Elektrotechnik", short: "ELT" },
|
||||||
|
{ name: "Photovoltaik", short: "PV" },
|
||||||
|
{ name: "Videüberwachung", short: "VÜA" },
|
||||||
|
{ name: "Projekt", short: "PRJ" },
|
||||||
|
{ name: "Smart Home", short: "SHO" },
|
||||||
|
]),
|
||||||
|
|
||||||
|
businessInfo: jsonb("businessInfo").default({
|
||||||
|
zip: "",
|
||||||
|
city: "",
|
||||||
|
name: "",
|
||||||
|
street: "",
|
||||||
|
}),
|
||||||
|
|
||||||
|
features: jsonb("features").default({
|
||||||
|
objects: true,
|
||||||
|
calendar: true,
|
||||||
|
contacts: true,
|
||||||
|
projects: true,
|
||||||
|
vehicles: true,
|
||||||
|
contracts: true,
|
||||||
|
inventory: true,
|
||||||
|
accounting: true,
|
||||||
|
timeTracking: true,
|
||||||
|
planningBoard: true,
|
||||||
|
workingTimeTracking: true,
|
||||||
|
}),
|
||||||
|
|
||||||
|
ownFields: jsonb("ownFields"),
|
||||||
|
|
||||||
|
numberRanges: jsonb("numberRanges")
|
||||||
|
.notNull()
|
||||||
|
.default({
|
||||||
|
vendors: { prefix: "", suffix: "", nextNumber: 10000 },
|
||||||
|
customers: { prefix: "", suffix: "", nextNumber: 10000 },
|
||||||
|
products: { prefix: "AT-", suffix: "", nextNumber: 1000 },
|
||||||
|
quotes: { prefix: "AN-", suffix: "", nextNumber: 1000 },
|
||||||
|
confirmationOrders: { prefix: "AB-", suffix: "", nextNumber: 1000 },
|
||||||
|
invoices: { prefix: "RE-", suffix: "", nextNumber: 1000 },
|
||||||
|
spaces: { prefix: "LP-", suffix: "", nextNumber: 1000 },
|
||||||
|
inventoryitems: { prefix: "IA-", suffix: "", nextNumber: 1000 },
|
||||||
|
projects: { prefix: "PRJ-", suffix: "", nextNumber: 1000 },
|
||||||
|
costcentres: { prefix: "KST-", suffix: "", nextNumber: 1000 },
|
||||||
|
}),
|
||||||
|
|
||||||
|
standardEmailForInvoices: text("standardEmailForInvoices"),
|
||||||
|
|
||||||
|
extraModules: jsonb("extraModules").notNull().default([]),
|
||||||
|
|
||||||
|
isInTrial: boolean("isInTrial").default(false),
|
||||||
|
trialEndDate: date("trialEndDate"),
|
||||||
|
|
||||||
|
stripeCustomerId: text("stripeCustomerId"),
|
||||||
|
|
||||||
|
hasActiveLicense: boolean("hasActiveLicense").notNull().default(false),
|
||||||
|
|
||||||
|
userLicenseCount: integer("userLicenseCount")
|
||||||
|
.notNull()
|
||||||
|
.default(0),
|
||||||
|
|
||||||
|
workstationLicenseCount: integer("workstationLicenseCount")
|
||||||
|
.notNull()
|
||||||
|
.default(0),
|
||||||
|
|
||||||
|
standardPaymentDays: smallint("standardPaymentDays")
|
||||||
|
.notNull()
|
||||||
|
.default(14),
|
||||||
|
|
||||||
|
dokuboxEmailAddresses: jsonb("dokuboxEmailAddresses").default([]),
|
||||||
|
|
||||||
|
dokuboxkey: uuid("dokuboxkey").notNull().defaultRandom(),
|
||||||
|
|
||||||
|
autoPrepareIncomingInvoices: boolean("autoPrepareIncomingInvoices")
|
||||||
|
.default(true),
|
||||||
|
|
||||||
|
portalDomain: text("portalDomain"),
|
||||||
|
|
||||||
|
portalConfig: jsonb("portalConfig")
|
||||||
|
.notNull()
|
||||||
|
.default({ primayColor: "#69c350" }),
|
||||||
|
|
||||||
|
updatedAt: timestamp("updated_at", { withTimezone: true }),
|
||||||
|
updatedBy: uuid("updated_by").references(() => authUsers.id),
|
||||||
|
|
||||||
|
locked: lockedTenantEnum("locked"),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
export type Tenant = typeof tenants.$inferSelect
|
||||||
|
export type NewTenant = typeof tenants.$inferInsert
|
||||||
44
backend/db/schema/texttemplates.ts
Normal file
44
backend/db/schema/texttemplates.ts
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
import {
|
||||||
|
pgTable,
|
||||||
|
bigint,
|
||||||
|
text,
|
||||||
|
timestamp,
|
||||||
|
boolean,
|
||||||
|
jsonb,
|
||||||
|
uuid,
|
||||||
|
} from "drizzle-orm/pg-core"
|
||||||
|
|
||||||
|
import { tenants } from "./tenants"
|
||||||
|
import { authUsers } from "./auth_users"
|
||||||
|
import { textTemplatePositionsEnum } from "./enums"
|
||||||
|
|
||||||
|
export const texttemplates = pgTable("texttemplates", {
|
||||||
|
id: bigint("id", { mode: "number" })
|
||||||
|
.primaryKey()
|
||||||
|
.generatedByDefaultAsIdentity(),
|
||||||
|
|
||||||
|
createdAt: timestamp("created_at", { withTimezone: true })
|
||||||
|
.notNull()
|
||||||
|
.defaultNow(),
|
||||||
|
|
||||||
|
tenant: bigint("tenant", { mode: "number" })
|
||||||
|
.notNull()
|
||||||
|
.references(() => tenants.id),
|
||||||
|
|
||||||
|
name: text("name").notNull(),
|
||||||
|
text: text("text").notNull(),
|
||||||
|
|
||||||
|
documentType: text("documentType").default(""),
|
||||||
|
|
||||||
|
default: boolean("default").notNull().default(false),
|
||||||
|
|
||||||
|
pos: textTemplatePositionsEnum("pos").notNull(),
|
||||||
|
|
||||||
|
archived: boolean("archived").notNull().default(false),
|
||||||
|
|
||||||
|
updatedAt: timestamp("updated_at", { withTimezone: true }),
|
||||||
|
updatedBy: uuid("updated_by").references(() => authUsers.id),
|
||||||
|
})
|
||||||
|
|
||||||
|
export type TextTemplate = typeof texttemplates.$inferSelect
|
||||||
|
export type NewTextTemplate = typeof texttemplates.$inferInsert
|
||||||
27
backend/db/schema/units.ts
Normal file
27
backend/db/schema/units.ts
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
import {
|
||||||
|
pgTable,
|
||||||
|
bigint,
|
||||||
|
timestamp,
|
||||||
|
text,
|
||||||
|
} from "drizzle-orm/pg-core"
|
||||||
|
|
||||||
|
export const units = pgTable("units", {
|
||||||
|
id: bigint("id", { mode: "number" })
|
||||||
|
.primaryKey()
|
||||||
|
.generatedByDefaultAsIdentity(),
|
||||||
|
|
||||||
|
createdAt: timestamp("created_at", { withTimezone: true })
|
||||||
|
.notNull()
|
||||||
|
.defaultNow(),
|
||||||
|
|
||||||
|
name: text("name").notNull(),
|
||||||
|
single: text("single").notNull(),
|
||||||
|
|
||||||
|
multiple: text("multiple"),
|
||||||
|
short: text("short"),
|
||||||
|
|
||||||
|
step: text("step").notNull().default("1"),
|
||||||
|
})
|
||||||
|
|
||||||
|
export type Unit = typeof units.$inferSelect
|
||||||
|
export type NewUnit = typeof units.$inferInsert
|
||||||
53
backend/db/schema/user_credentials.ts
Normal file
53
backend/db/schema/user_credentials.ts
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
import {
|
||||||
|
pgTable,
|
||||||
|
uuid,
|
||||||
|
timestamp,
|
||||||
|
bigint,
|
||||||
|
boolean,
|
||||||
|
jsonb,
|
||||||
|
numeric, pgEnum,
|
||||||
|
} from "drizzle-orm/pg-core"
|
||||||
|
|
||||||
|
import { tenants } from "./tenants"
|
||||||
|
import { authUsers } from "./auth_users"
|
||||||
|
import {credentialTypesEnum} from "./enums";
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
export const userCredentials = pgTable("user_credentials", {
|
||||||
|
id: uuid("id").primaryKey().defaultRandom(),
|
||||||
|
|
||||||
|
createdAt: timestamp("created_at", { withTimezone: true })
|
||||||
|
.notNull()
|
||||||
|
.defaultNow(),
|
||||||
|
|
||||||
|
userId: uuid("user_id")
|
||||||
|
.notNull()
|
||||||
|
.references(() => authUsers.id),
|
||||||
|
|
||||||
|
updatedAt: timestamp("updated_at", { withTimezone: true }),
|
||||||
|
|
||||||
|
tenantId: bigint("tenant_id", { mode: "number" })
|
||||||
|
.notNull()
|
||||||
|
.references(() => tenants.id),
|
||||||
|
|
||||||
|
smtpPort: numeric("smtp_port"),
|
||||||
|
smtpSsl: boolean("smtp_ssl"),
|
||||||
|
|
||||||
|
type: credentialTypesEnum("type").notNull(),
|
||||||
|
|
||||||
|
imapPort: numeric("imap_port"),
|
||||||
|
imapSsl: boolean("imap_ssl"),
|
||||||
|
|
||||||
|
emailEncrypted: jsonb("email_encrypted"),
|
||||||
|
passwordEncrypted: jsonb("password_encrypted"),
|
||||||
|
|
||||||
|
smtpHostEncrypted: jsonb("smtp_host_encrypted"),
|
||||||
|
imapHostEncrypted: jsonb("imap_host_encrypted"),
|
||||||
|
|
||||||
|
accessTokenEncrypted: jsonb("access_token_encrypted"),
|
||||||
|
refreshTokenEncrypted: jsonb("refresh_token_encrypted"),
|
||||||
|
})
|
||||||
|
|
||||||
|
export type UserCredential = typeof userCredentials.$inferSelect
|
||||||
|
export type NewUserCredential = typeof userCredentials.$inferInsert
|
||||||
57
backend/db/schema/vehicles.ts
Normal file
57
backend/db/schema/vehicles.ts
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
import {
|
||||||
|
pgTable,
|
||||||
|
bigint,
|
||||||
|
text,
|
||||||
|
timestamp,
|
||||||
|
boolean,
|
||||||
|
jsonb,
|
||||||
|
uuid,
|
||||||
|
doublePrecision,
|
||||||
|
} from "drizzle-orm/pg-core"
|
||||||
|
|
||||||
|
import { tenants } from "./tenants"
|
||||||
|
import { authUsers } from "./auth_users"
|
||||||
|
|
||||||
|
export const vehicles = pgTable("vehicles", {
|
||||||
|
id: bigint("id", { mode: "number" })
|
||||||
|
.primaryKey()
|
||||||
|
.generatedByDefaultAsIdentity(),
|
||||||
|
|
||||||
|
created_at: timestamp("created_at", { withTimezone: true })
|
||||||
|
.notNull()
|
||||||
|
.defaultNow(),
|
||||||
|
|
||||||
|
tenant: bigint("tenant", { mode: "number" })
|
||||||
|
.notNull()
|
||||||
|
.references(() => tenants.id),
|
||||||
|
|
||||||
|
license_plate: text("licensePlate"),
|
||||||
|
name: text("name"),
|
||||||
|
type: text("type"),
|
||||||
|
|
||||||
|
active: boolean("active").default(true),
|
||||||
|
|
||||||
|
// FIXED: driver references auth_users.id
|
||||||
|
driver: uuid("driver").references(() => authUsers.id),
|
||||||
|
|
||||||
|
vin: text("vin"),
|
||||||
|
|
||||||
|
tank_size: doublePrecision("tankSize").notNull().default(0),
|
||||||
|
|
||||||
|
archived: boolean("archived").notNull().default(false),
|
||||||
|
|
||||||
|
build_year: text("buildYear"),
|
||||||
|
|
||||||
|
towing_capacity: bigint("towingCapacity", { mode: "number" }),
|
||||||
|
power_in_kw: bigint("powerInKW", { mode: "number" }),
|
||||||
|
|
||||||
|
color: text("color"),
|
||||||
|
|
||||||
|
profiles: jsonb("profiles").notNull().default([]),
|
||||||
|
|
||||||
|
updated_at: timestamp("updated_at", { withTimezone: true }),
|
||||||
|
updated_by: uuid("updated_by").references(() => authUsers.id),
|
||||||
|
})
|
||||||
|
|
||||||
|
export type Vehicle = typeof vehicles.$inferSelect
|
||||||
|
export type NewVehicle = typeof vehicles.$inferInsert
|
||||||
45
backend/db/schema/vendors.ts
Normal file
45
backend/db/schema/vendors.ts
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
import {
|
||||||
|
pgTable,
|
||||||
|
bigint,
|
||||||
|
text,
|
||||||
|
timestamp,
|
||||||
|
boolean,
|
||||||
|
jsonb,
|
||||||
|
uuid,
|
||||||
|
} from "drizzle-orm/pg-core"
|
||||||
|
|
||||||
|
import { tenants } from "./tenants"
|
||||||
|
import { authUsers } from "./auth_users"
|
||||||
|
|
||||||
|
export const vendors = pgTable("vendors", {
|
||||||
|
id: bigint("id", { mode: "number" })
|
||||||
|
.primaryKey()
|
||||||
|
.generatedByDefaultAsIdentity(),
|
||||||
|
|
||||||
|
createdAt: timestamp("created_at", { withTimezone: true })
|
||||||
|
.notNull()
|
||||||
|
.defaultNow(),
|
||||||
|
|
||||||
|
name: text("name").notNull(),
|
||||||
|
vendorNumber: text("vendorNumber").notNull(),
|
||||||
|
|
||||||
|
tenant: bigint("tenant", { mode: "number" })
|
||||||
|
.notNull()
|
||||||
|
.references(() => tenants.id),
|
||||||
|
|
||||||
|
infoData: jsonb("infoData").notNull().default({}),
|
||||||
|
notes: text("notes"),
|
||||||
|
|
||||||
|
hasSEPA: boolean("hasSEPA").notNull().default(false),
|
||||||
|
|
||||||
|
profiles: jsonb("profiles").notNull().default([]),
|
||||||
|
archived: boolean("archived").notNull().default(false),
|
||||||
|
|
||||||
|
defaultPaymentMethod: text("defaultPaymentMethod"),
|
||||||
|
|
||||||
|
updatedAt: timestamp("updated_at", { withTimezone: true }),
|
||||||
|
updatedBy: uuid("updated_by").references(() => authUsers.id),
|
||||||
|
})
|
||||||
|
|
||||||
|
export type Vendor = typeof vendors.$inferSelect
|
||||||
|
export type NewVendor = typeof vendors.$inferInsert
|
||||||
7
backend/docker-compose.yml
Normal file
7
backend/docker-compose.yml
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
services:
|
||||||
|
backend:
|
||||||
|
image: reg.federspiel.software/fedeo/backend:main
|
||||||
|
restart: always
|
||||||
|
|
||||||
|
environment:
|
||||||
|
|
||||||
11
backend/drizzle.config.ts
Normal file
11
backend/drizzle.config.ts
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
import { defineConfig } from "drizzle-kit"
|
||||||
|
import {secrets} from "./src/utils/secrets";
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
dialect: "postgresql",
|
||||||
|
schema: "./db/schema",
|
||||||
|
out: "./db/migrations",
|
||||||
|
dbCredentials: {
|
||||||
|
url: secrets.DATABASE_URL || process.env.DATABASE_URL,
|
||||||
|
},
|
||||||
|
})
|
||||||
64
backend/package.json
Normal file
64
backend/package.json
Normal file
@@ -0,0 +1,64 @@
|
|||||||
|
{
|
||||||
|
"name": "backend",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"description": "",
|
||||||
|
"main": "index.js",
|
||||||
|
"scripts": {
|
||||||
|
"dev": "tsx watch src/index.ts",
|
||||||
|
"build": "tsc",
|
||||||
|
"start": "node dist/src/index.js",
|
||||||
|
"schema:index": "ts-node scripts/generate-schema-index.ts"
|
||||||
|
},
|
||||||
|
"repository": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "https://git.federspiel.software/fedeo/backend.git"
|
||||||
|
},
|
||||||
|
"keywords": [],
|
||||||
|
"author": "",
|
||||||
|
"license": "ISC",
|
||||||
|
"dependencies": {
|
||||||
|
"@aws-sdk/client-s3": "^3.879.0",
|
||||||
|
"@aws-sdk/s3-request-presigner": "^3.879.0",
|
||||||
|
"@fastify/cookie": "^11.0.2",
|
||||||
|
"@fastify/cors": "^11.1.0",
|
||||||
|
"@fastify/multipart": "^9.0.3",
|
||||||
|
"@fastify/swagger": "^9.5.1",
|
||||||
|
"@fastify/swagger-ui": "^5.2.3",
|
||||||
|
"@infisical/sdk": "^4.0.6",
|
||||||
|
"@mmote/niimbluelib": "^0.0.1-alpha.29",
|
||||||
|
"@prisma/client": "^6.15.0",
|
||||||
|
"@supabase/supabase-js": "^2.56.1",
|
||||||
|
"@zip.js/zip.js": "^2.7.73",
|
||||||
|
"archiver": "^7.0.1",
|
||||||
|
"axios": "^1.12.1",
|
||||||
|
"bcrypt": "^6.0.0",
|
||||||
|
"bwip-js": "^4.8.0",
|
||||||
|
"crypto": "^1.0.1",
|
||||||
|
"dayjs": "^1.11.18",
|
||||||
|
"drizzle-orm": "^0.45.0",
|
||||||
|
"fastify": "^5.5.0",
|
||||||
|
"fastify-plugin": "^5.0.1",
|
||||||
|
"handlebars": "^4.7.8",
|
||||||
|
"imapflow": "^1.1.1",
|
||||||
|
"jsonwebtoken": "^9.0.2",
|
||||||
|
"mailparser": "^3.9.0",
|
||||||
|
"nodemailer": "^7.0.6",
|
||||||
|
"openai": "^6.10.0",
|
||||||
|
"pdf-lib": "^1.17.1",
|
||||||
|
"pg": "^8.16.3",
|
||||||
|
"pngjs": "^7.0.0",
|
||||||
|
"sharp": "^0.34.5",
|
||||||
|
"xmlbuilder": "^15.1.1",
|
||||||
|
"zpl-image": "^0.2.0",
|
||||||
|
"zpl-renderer-js": "^2.0.2"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/bcrypt": "^6.0.0",
|
||||||
|
"@types/jsonwebtoken": "^9.0.10",
|
||||||
|
"@types/node": "^24.3.0",
|
||||||
|
"drizzle-kit": "^0.31.8",
|
||||||
|
"prisma": "^6.15.0",
|
||||||
|
"tsx": "^4.20.5",
|
||||||
|
"typescript": "^5.9.2"
|
||||||
|
}
|
||||||
|
}
|
||||||
16
backend/scripts/generate-schema-index.ts
Normal file
16
backend/scripts/generate-schema-index.ts
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
import fs from "node:fs"
|
||||||
|
import path from "node:path"
|
||||||
|
|
||||||
|
const schemaDir = path.resolve("db/schema")
|
||||||
|
const indexFile = path.join(schemaDir, "index.ts")
|
||||||
|
|
||||||
|
const files = fs
|
||||||
|
.readdirSync(schemaDir)
|
||||||
|
.filter((f) => f.endsWith(".ts") && f !== "index.ts")
|
||||||
|
|
||||||
|
const exportsToWrite = files
|
||||||
|
.map((f) => `export * from "./${f.replace(".ts", "")}"`)
|
||||||
|
.join("\n")
|
||||||
|
|
||||||
|
fs.writeFileSync(indexFile, exportsToWrite)
|
||||||
|
console.log("✓ schema/index.ts generated")
|
||||||
166
backend/src/index.ts
Normal file
166
backend/src/index.ts
Normal file
@@ -0,0 +1,166 @@
|
|||||||
|
import Fastify from "fastify";
|
||||||
|
import swaggerPlugin from "./plugins/swagger"
|
||||||
|
import supabasePlugin from "./plugins/supabase";
|
||||||
|
import dayjsPlugin from "./plugins/dayjs";
|
||||||
|
import healthRoutes from "./routes/health";
|
||||||
|
import meRoutes from "./routes/auth/me";
|
||||||
|
import tenantRoutes from "./routes/tenant";
|
||||||
|
import tenantPlugin from "./plugins/tenant";
|
||||||
|
import authRoutes from "./routes/auth/auth";
|
||||||
|
import authRoutesAuthenticated from "./routes/auth/auth-authenticated";
|
||||||
|
import authPlugin from "./plugins/auth";
|
||||||
|
import adminRoutes from "./routes/admin";
|
||||||
|
import corsPlugin from "./plugins/cors";
|
||||||
|
import queryConfigPlugin from "./plugins/queryconfig";
|
||||||
|
import dbPlugin from "./plugins/db";
|
||||||
|
import resourceRoutesSpecial from "./routes/resourcesSpecial";
|
||||||
|
import fastifyCookie from "@fastify/cookie";
|
||||||
|
import historyRoutes from "./routes/history";
|
||||||
|
import fileRoutes from "./routes/files";
|
||||||
|
import functionRoutes from "./routes/functions";
|
||||||
|
import bankingRoutes from "./routes/banking";
|
||||||
|
import exportRoutes from "./routes/exports"
|
||||||
|
import emailAsUserRoutes from "./routes/emailAsUser";
|
||||||
|
import authProfilesRoutes from "./routes/profiles";
|
||||||
|
import helpdeskRoutes from "./routes/helpdesk";
|
||||||
|
import helpdeskInboundRoutes from "./routes/helpdesk.inbound";
|
||||||
|
import notificationsRoutes from "./routes/notifications";
|
||||||
|
import staffTimeRoutes from "./routes/staff/time";
|
||||||
|
import staffTimeConnectRoutes from "./routes/staff/timeconnects";
|
||||||
|
import userRoutes from "./routes/auth/user";
|
||||||
|
import publiclinksAuthenticatedRoutes from "./routes/publiclinks/publiclinks-authenticated";
|
||||||
|
|
||||||
|
//Public Links
|
||||||
|
import publiclinksNonAuthenticatedRoutes from "./routes/publiclinks/publiclinks-non-authenticated";
|
||||||
|
|
||||||
|
//Resources
|
||||||
|
import resourceRoutes from "./routes/resources/main";
|
||||||
|
|
||||||
|
//M2M
|
||||||
|
import authM2m from "./plugins/auth.m2m";
|
||||||
|
import helpdeskInboundEmailRoutes from "./routes/helpdesk.inbound.email";
|
||||||
|
import deviceRoutes from "./routes/internal/devices";
|
||||||
|
import tenantRoutesInternal from "./routes/internal/tenant";
|
||||||
|
import staffTimeRoutesInternal from "./routes/internal/time";
|
||||||
|
|
||||||
|
//Devices
|
||||||
|
import devicesRFIDRoutes from "./routes/devices/rfid";
|
||||||
|
|
||||||
|
|
||||||
|
import {sendMail} from "./utils/mailer";
|
||||||
|
import {loadSecrets, secrets} from "./utils/secrets";
|
||||||
|
import {initMailer} from "./utils/mailer"
|
||||||
|
import {initS3} from "./utils/s3";
|
||||||
|
|
||||||
|
//Services
|
||||||
|
import servicesPlugin from "./plugins/services";
|
||||||
|
|
||||||
|
async function main() {
|
||||||
|
const app = Fastify({ logger: false });
|
||||||
|
await loadSecrets();
|
||||||
|
await initMailer();
|
||||||
|
await initS3();
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/*app.addHook("onRequest", (req, reply, done) => {
|
||||||
|
console.log("Incoming:", req.method, req.url, "Headers:", req.headers)
|
||||||
|
done()
|
||||||
|
})*/
|
||||||
|
|
||||||
|
// Plugins Global verfügbar
|
||||||
|
await app.register(swaggerPlugin);
|
||||||
|
await app.register(corsPlugin);
|
||||||
|
await app.register(supabasePlugin);
|
||||||
|
await app.register(tenantPlugin);
|
||||||
|
await app.register(dayjsPlugin);
|
||||||
|
await app.register(dbPlugin);
|
||||||
|
await app.register(servicesPlugin);
|
||||||
|
|
||||||
|
app.addHook('preHandler', (req, reply, done) => {
|
||||||
|
console.log(req.method)
|
||||||
|
console.log('Matched path:', req.routeOptions.url)
|
||||||
|
console.log('Exact URL:', req.url)
|
||||||
|
done()
|
||||||
|
})
|
||||||
|
|
||||||
|
app.get('/health', async (req, res) => {
|
||||||
|
return res.send({ status: 'ok' })
|
||||||
|
})
|
||||||
|
|
||||||
|
//Plugin nur auf bestimmten Routes
|
||||||
|
await app.register(queryConfigPlugin, {
|
||||||
|
routes: ['/api/resource/:resource/paginated']
|
||||||
|
})
|
||||||
|
|
||||||
|
app.register(fastifyCookie, {
|
||||||
|
secret: secrets.COOKIE_SECRET,
|
||||||
|
})
|
||||||
|
// Öffentliche Routes
|
||||||
|
await app.register(authRoutes);
|
||||||
|
await app.register(healthRoutes);
|
||||||
|
|
||||||
|
await app.register(helpdeskInboundRoutes);
|
||||||
|
|
||||||
|
await app.register(publiclinksNonAuthenticatedRoutes)
|
||||||
|
|
||||||
|
|
||||||
|
await app.register(async (m2mApp) => {
|
||||||
|
await m2mApp.register(authM2m)
|
||||||
|
await m2mApp.register(helpdeskInboundEmailRoutes)
|
||||||
|
await m2mApp.register(deviceRoutes)
|
||||||
|
await m2mApp.register(tenantRoutesInternal)
|
||||||
|
await m2mApp.register(staffTimeRoutesInternal)
|
||||||
|
},{prefix: "/internal"})
|
||||||
|
|
||||||
|
await app.register(async (devicesApp) => {
|
||||||
|
await devicesApp.register(devicesRFIDRoutes)
|
||||||
|
},{prefix: "/devices"})
|
||||||
|
|
||||||
|
|
||||||
|
//Geschützte Routes
|
||||||
|
|
||||||
|
await app.register(async (subApp) => {
|
||||||
|
await subApp.register(authPlugin);
|
||||||
|
await subApp.register(authRoutesAuthenticated);
|
||||||
|
await subApp.register(meRoutes);
|
||||||
|
await subApp.register(tenantRoutes);
|
||||||
|
await subApp.register(adminRoutes);
|
||||||
|
await subApp.register(resourceRoutesSpecial);
|
||||||
|
await subApp.register(historyRoutes);
|
||||||
|
await subApp.register(fileRoutes);
|
||||||
|
await subApp.register(functionRoutes);
|
||||||
|
await subApp.register(bankingRoutes);
|
||||||
|
await subApp.register(exportRoutes);
|
||||||
|
await subApp.register(emailAsUserRoutes);
|
||||||
|
await subApp.register(authProfilesRoutes);
|
||||||
|
await subApp.register(helpdeskRoutes);
|
||||||
|
await subApp.register(notificationsRoutes);
|
||||||
|
await subApp.register(staffTimeRoutes);
|
||||||
|
await subApp.register(staffTimeConnectRoutes);
|
||||||
|
await subApp.register(userRoutes);
|
||||||
|
await subApp.register(publiclinksAuthenticatedRoutes);
|
||||||
|
await subApp.register(resourceRoutes);
|
||||||
|
|
||||||
|
},{prefix: "/api"})
|
||||||
|
|
||||||
|
app.ready(async () => {
|
||||||
|
try {
|
||||||
|
const result = await app.db.execute("SELECT NOW()");
|
||||||
|
console.log("✓ DB connection OK: " + JSON.stringify(result.rows[0]));
|
||||||
|
} catch (err) {
|
||||||
|
console.log("❌ DB connection failed:", err);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Start
|
||||||
|
try {
|
||||||
|
await app.listen({ port: secrets.PORT, host: secrets.HOST });
|
||||||
|
console.log(`🚀 Server läuft auf http://${secrets.HOST}:${secrets.PORT}`);
|
||||||
|
} catch (err) {
|
||||||
|
app.log.error(err);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
main();
|
||||||
253
backend/src/modules/cron/bankstatementsync.service.ts
Normal file
253
backend/src/modules/cron/bankstatementsync.service.ts
Normal file
@@ -0,0 +1,253 @@
|
|||||||
|
// /services/bankStatementService.ts
|
||||||
|
import axios from "axios"
|
||||||
|
import dayjs from "dayjs"
|
||||||
|
import utc from "dayjs/plugin/utc.js"
|
||||||
|
import {secrets} from "../../utils/secrets"
|
||||||
|
import {FastifyInstance} from "fastify"
|
||||||
|
|
||||||
|
// Drizzle imports
|
||||||
|
import {
|
||||||
|
bankaccounts,
|
||||||
|
bankstatements,
|
||||||
|
} from "../../../db/schema"
|
||||||
|
|
||||||
|
import {
|
||||||
|
eq,
|
||||||
|
and,
|
||||||
|
isNull,
|
||||||
|
} from "drizzle-orm"
|
||||||
|
|
||||||
|
dayjs.extend(utc)
|
||||||
|
|
||||||
|
interface BalanceAmount {
|
||||||
|
amount: string
|
||||||
|
currency: string
|
||||||
|
}
|
||||||
|
|
||||||
|
interface BookedTransaction {
|
||||||
|
bookingDate: string
|
||||||
|
valueDate: string
|
||||||
|
internalTransactionId: string
|
||||||
|
transactionAmount: { amount: string; currency: string }
|
||||||
|
|
||||||
|
creditorAccount?: { iban?: string }
|
||||||
|
creditorName?: string
|
||||||
|
|
||||||
|
debtorAccount?: { iban?: string }
|
||||||
|
debtorName?: string
|
||||||
|
|
||||||
|
remittanceInformationUnstructured?: string
|
||||||
|
remittanceInformationStructured?: string
|
||||||
|
remittanceInformationStructuredArray?: string[]
|
||||||
|
additionalInformation?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
interface TransactionsResponse {
|
||||||
|
transactions: {
|
||||||
|
booked: BookedTransaction[]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const normalizeDate = (val: any) => {
|
||||||
|
if (!val) return null
|
||||||
|
const d = new Date(val)
|
||||||
|
return isNaN(d.getTime()) ? null : d
|
||||||
|
}
|
||||||
|
|
||||||
|
export function bankStatementService(server: FastifyInstance) {
|
||||||
|
|
||||||
|
let accessToken: string | null = null
|
||||||
|
|
||||||
|
// -----------------------------------------------
|
||||||
|
// ✔ TOKEN LADEN
|
||||||
|
// -----------------------------------------------
|
||||||
|
const getToken = async () => {
|
||||||
|
console.log("Fetching GoCardless token…")
|
||||||
|
|
||||||
|
const response = await axios.post(
|
||||||
|
`${secrets.GOCARDLESS_BASE_URL}/token/new/`,
|
||||||
|
{
|
||||||
|
secret_id: secrets.GOCARDLESS_SECRET_ID,
|
||||||
|
secret_key: secrets.GOCARDLESS_SECRET_KEY,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
accessToken = response.data.access
|
||||||
|
}
|
||||||
|
|
||||||
|
// -----------------------------------------------
|
||||||
|
// ✔ Salden laden
|
||||||
|
// -----------------------------------------------
|
||||||
|
const getBalanceData = async (accountId: string): Promise<any | false> => {
|
||||||
|
try {
|
||||||
|
const {data} = await axios.get(
|
||||||
|
`${secrets.GOCARDLESS_BASE_URL}/accounts/${accountId}/balances`,
|
||||||
|
{
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${accessToken}`,
|
||||||
|
Accept: "application/json",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
return data
|
||||||
|
} catch (err: any) {
|
||||||
|
server.log.error(err.response?.data ?? err)
|
||||||
|
|
||||||
|
const expired =
|
||||||
|
err.response?.data?.summary?.includes("expired") ||
|
||||||
|
err.response?.data?.detail?.includes("expired")
|
||||||
|
|
||||||
|
if (expired) {
|
||||||
|
await server.db
|
||||||
|
.update(bankaccounts)
|
||||||
|
.set({expired: true})
|
||||||
|
.where(eq(bankaccounts.accountId, accountId))
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// -----------------------------------------------
|
||||||
|
// ✔ Transaktionen laden
|
||||||
|
// -----------------------------------------------
|
||||||
|
const getTransactionData = async (accountId: string) => {
|
||||||
|
try {
|
||||||
|
const {data} = await axios.get<TransactionsResponse>(
|
||||||
|
`${secrets.GOCARDLESS_BASE_URL}/accounts/${accountId}/transactions`,
|
||||||
|
{
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${accessToken}`,
|
||||||
|
Accept: "application/json",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
return data.transactions.booked
|
||||||
|
} catch (err: any) {
|
||||||
|
server.log.error(err.response?.data ?? err)
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// -----------------------------------------------
|
||||||
|
// ✔ Haupt-Sync-Prozess
|
||||||
|
// -----------------------------------------------
|
||||||
|
const syncAccounts = async (tenantId:number) => {
|
||||||
|
try {
|
||||||
|
console.log("Starting account sync…")
|
||||||
|
|
||||||
|
// 🟦 DB: Aktive Accounts
|
||||||
|
const accounts = await server.db
|
||||||
|
.select()
|
||||||
|
.from(bankaccounts)
|
||||||
|
.where(and(eq(bankaccounts.expired, false),eq(bankaccounts.tenant, tenantId)))
|
||||||
|
|
||||||
|
if (!accounts.length) return
|
||||||
|
|
||||||
|
const allNewTransactions: any[] = []
|
||||||
|
|
||||||
|
for (const account of accounts) {
|
||||||
|
|
||||||
|
// ---------------------------
|
||||||
|
// 1. BALANCE SYNC
|
||||||
|
// ---------------------------
|
||||||
|
const balData = await getBalanceData(account.accountId)
|
||||||
|
|
||||||
|
if (balData === false) break
|
||||||
|
|
||||||
|
if (balData) {
|
||||||
|
const closing = balData.balances.find(
|
||||||
|
(i: any) => i.balanceType === "closingBooked"
|
||||||
|
)
|
||||||
|
|
||||||
|
const bookedBal = Number(closing.balanceAmount.amount)
|
||||||
|
|
||||||
|
await server.db
|
||||||
|
.update(bankaccounts)
|
||||||
|
.set({balance: bookedBal})
|
||||||
|
.where(eq(bankaccounts.id, account.id))
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------
|
||||||
|
// 2. TRANSACTIONS
|
||||||
|
// ---------------------------
|
||||||
|
let transactions = await getTransactionData(account.accountId)
|
||||||
|
if (!transactions) continue
|
||||||
|
|
||||||
|
//@ts-ignore
|
||||||
|
transactions = transactions.map((item) => ({
|
||||||
|
account: account.id,
|
||||||
|
date: normalizeDate(item.bookingDate),
|
||||||
|
credIban: item.creditorAccount?.iban ?? null,
|
||||||
|
credName: item.creditorName ?? null,
|
||||||
|
text: `
|
||||||
|
${item.remittanceInformationUnstructured ?? ""}
|
||||||
|
${item.remittanceInformationStructured ?? ""}
|
||||||
|
${item.additionalInformation ?? ""}
|
||||||
|
${item.remittanceInformationStructuredArray?.join("") ?? ""}
|
||||||
|
`.trim(),
|
||||||
|
amount: Number(item.transactionAmount.amount),
|
||||||
|
tenant: account.tenant,
|
||||||
|
debIban: item.debtorAccount?.iban ?? null,
|
||||||
|
debName: item.debtorName ?? null,
|
||||||
|
gocardlessId: item.internalTransactionId,
|
||||||
|
currency: item.transactionAmount.currency,
|
||||||
|
valueDate: normalizeDate(item.valueDate),
|
||||||
|
}))
|
||||||
|
|
||||||
|
// Existierende Statements laden
|
||||||
|
const existing = await server.db
|
||||||
|
.select({gocardlessId: bankstatements.gocardlessId})
|
||||||
|
.from(bankstatements)
|
||||||
|
.where(eq(bankstatements.tenant, account.tenant))
|
||||||
|
|
||||||
|
const filtered = transactions.filter(
|
||||||
|
//@ts-ignore
|
||||||
|
(tx) => !existing.some((x) => x.gocardlessId === tx.gocardlessId)
|
||||||
|
)
|
||||||
|
|
||||||
|
allNewTransactions.push(...filtered)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------
|
||||||
|
// 3. NEW TRANSACTIONS → DB
|
||||||
|
// ---------------------------
|
||||||
|
if (allNewTransactions.length > 0) {
|
||||||
|
await server.db.insert(bankstatements).values(allNewTransactions)
|
||||||
|
|
||||||
|
const affectedAccounts = [
|
||||||
|
...new Set(allNewTransactions.map((t) => t.account)),
|
||||||
|
]
|
||||||
|
|
||||||
|
const normalizeDate = (val: any) => {
|
||||||
|
if (!val) return null
|
||||||
|
const d = new Date(val)
|
||||||
|
return isNaN(d.getTime()) ? null : d
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const accId of affectedAccounts) {
|
||||||
|
await server.db
|
||||||
|
.update(bankaccounts)
|
||||||
|
//@ts-ignore
|
||||||
|
.set({syncedAt: normalizeDate(dayjs())})
|
||||||
|
.where(eq(bankaccounts.id, accId))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log("Bank statement sync completed.")
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
run: async (tenant) => {
|
||||||
|
await getToken()
|
||||||
|
await syncAccounts(tenant)
|
||||||
|
console.log("Service: Bankstatement sync finished")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
259
backend/src/modules/cron/dokuboximport.service.ts
Normal file
259
backend/src/modules/cron/dokuboximport.service.ts
Normal file
@@ -0,0 +1,259 @@
|
|||||||
|
import axios from "axios"
|
||||||
|
import dayjs from "dayjs"
|
||||||
|
import { ImapFlow } from "imapflow"
|
||||||
|
import { simpleParser } from "mailparser"
|
||||||
|
import { FastifyInstance } from "fastify"
|
||||||
|
|
||||||
|
import {saveFile} from "../../utils/files";
|
||||||
|
import { secrets } from "../../utils/secrets"
|
||||||
|
|
||||||
|
// Drizzle Imports
|
||||||
|
import {
|
||||||
|
tenants,
|
||||||
|
folders,
|
||||||
|
filetags,
|
||||||
|
} from "../../../db/schema"
|
||||||
|
|
||||||
|
import {
|
||||||
|
eq,
|
||||||
|
and,
|
||||||
|
} from "drizzle-orm"
|
||||||
|
|
||||||
|
let badMessageDetected = false
|
||||||
|
let badMessageMessageSent = false
|
||||||
|
|
||||||
|
let client: ImapFlow | null = null
|
||||||
|
|
||||||
|
// -------------------------------------------------------------
|
||||||
|
// IMAP CLIENT INITIALIZEN
|
||||||
|
// -------------------------------------------------------------
|
||||||
|
export async function initDokuboxClient() {
|
||||||
|
client = new ImapFlow({
|
||||||
|
host: secrets.DOKUBOX_IMAP_HOST,
|
||||||
|
port: secrets.DOKUBOX_IMAP_PORT,
|
||||||
|
secure: secrets.DOKUBOX_IMAP_SECURE,
|
||||||
|
auth: {
|
||||||
|
user: secrets.DOKUBOX_IMAP_USER,
|
||||||
|
pass: secrets.DOKUBOX_IMAP_PASSWORD
|
||||||
|
},
|
||||||
|
logger: false
|
||||||
|
})
|
||||||
|
|
||||||
|
console.log("Dokubox E-Mail Client Initialized")
|
||||||
|
|
||||||
|
await client.connect()
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
// -------------------------------------------------------------
|
||||||
|
// MAIN SYNC FUNCTION (DRIZZLE VERSION)
|
||||||
|
// -------------------------------------------------------------
|
||||||
|
export const syncDokubox = (server: FastifyInstance) =>
|
||||||
|
async () => {
|
||||||
|
|
||||||
|
console.log("Perform Dokubox Sync")
|
||||||
|
|
||||||
|
await initDokuboxClient()
|
||||||
|
|
||||||
|
if (!client?.usable) {
|
||||||
|
throw new Error("E-Mail Client not usable")
|
||||||
|
}
|
||||||
|
|
||||||
|
// -------------------------------
|
||||||
|
// TENANTS LADEN (DRIZZLE)
|
||||||
|
// -------------------------------
|
||||||
|
const tenantList = await server.db
|
||||||
|
.select({
|
||||||
|
id: tenants.id,
|
||||||
|
name: tenants.name,
|
||||||
|
emailAddresses: tenants.dokuboxEmailAddresses,
|
||||||
|
key: tenants.dokuboxkey
|
||||||
|
})
|
||||||
|
.from(tenants)
|
||||||
|
|
||||||
|
const lock = await client.getMailboxLock("INBOX")
|
||||||
|
|
||||||
|
try {
|
||||||
|
|
||||||
|
for await (let msg of client.fetch({ seen: false }, { envelope: true, source: true })) {
|
||||||
|
|
||||||
|
const parsed = await simpleParser(msg.source)
|
||||||
|
|
||||||
|
const message = {
|
||||||
|
id: msg.uid,
|
||||||
|
subject: parsed.subject,
|
||||||
|
to: parsed.to?.value || [],
|
||||||
|
cc: parsed.cc?.value || [],
|
||||||
|
attachments: parsed.attachments || []
|
||||||
|
}
|
||||||
|
|
||||||
|
// -------------------------------------------------
|
||||||
|
// MAPPING / FIND TENANT
|
||||||
|
// -------------------------------------------------
|
||||||
|
const config = await getMessageConfigDrizzle(server, message, tenantList)
|
||||||
|
|
||||||
|
if (!config) {
|
||||||
|
badMessageDetected = true
|
||||||
|
|
||||||
|
if (!badMessageMessageSent) {
|
||||||
|
badMessageMessageSent = true
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (message.attachments.length > 0) {
|
||||||
|
for (const attachment of message.attachments) {
|
||||||
|
await saveFile(
|
||||||
|
server,
|
||||||
|
config.tenant,
|
||||||
|
message.id,
|
||||||
|
attachment,
|
||||||
|
config.folder,
|
||||||
|
config.filetype
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!badMessageDetected) {
|
||||||
|
badMessageDetected = false
|
||||||
|
badMessageMessageSent = false
|
||||||
|
}
|
||||||
|
|
||||||
|
await client.messageFlagsAdd({ seen: false }, ["\\Seen"])
|
||||||
|
await client.messageDelete({ seen: true })
|
||||||
|
|
||||||
|
} finally {
|
||||||
|
lock.release()
|
||||||
|
client.close()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
// -------------------------------------------------------------
|
||||||
|
// TENANT ERKENNEN + FOLDER/FILETYPES (DRIZZLE VERSION)
|
||||||
|
// -------------------------------------------------------------
|
||||||
|
const getMessageConfigDrizzle = async (
|
||||||
|
server: FastifyInstance,
|
||||||
|
message,
|
||||||
|
tenantsList: any[]
|
||||||
|
) => {
|
||||||
|
|
||||||
|
let possibleKeys: string[] = []
|
||||||
|
|
||||||
|
if (message.to) {
|
||||||
|
message.to.forEach((item) =>
|
||||||
|
possibleKeys.push(item.address.split("@")[0].toLowerCase())
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (message.cc) {
|
||||||
|
message.cc.forEach((item) =>
|
||||||
|
possibleKeys.push(item.address.split("@")[0].toLowerCase())
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// -------------------------------------------
|
||||||
|
// TENANT IDENTIFY
|
||||||
|
// -------------------------------------------
|
||||||
|
let tenant = tenantsList.find((t) => possibleKeys.includes(t.key))
|
||||||
|
|
||||||
|
if (!tenant && message.to?.length) {
|
||||||
|
const address = message.to[0].address.toLowerCase()
|
||||||
|
|
||||||
|
tenant = tenantsList.find((t) =>
|
||||||
|
(t.emailAddresses || []).map((m) => m.toLowerCase()).includes(address)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!tenant) return null
|
||||||
|
|
||||||
|
// -------------------------------------------
|
||||||
|
// FOLDER + FILETYPE VIA SUBJECT
|
||||||
|
// -------------------------------------------
|
||||||
|
let folderId = null
|
||||||
|
let filetypeId = null
|
||||||
|
|
||||||
|
// -------------------------------------------
|
||||||
|
// Rechnung / Invoice
|
||||||
|
// -------------------------------------------
|
||||||
|
if (message.subject?.match(/(Rechnung|Beleg|Invoice|Quittung)/gi)) {
|
||||||
|
|
||||||
|
const folder = await server.db
|
||||||
|
.select({ id: folders.id })
|
||||||
|
.from(folders)
|
||||||
|
.where(
|
||||||
|
and(
|
||||||
|
eq(folders.tenant, tenant.id),
|
||||||
|
and(
|
||||||
|
eq(folders.function, "incomingInvoices"),
|
||||||
|
//@ts-ignore
|
||||||
|
eq(folders.year, dayjs().format("YYYY"))
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.limit(1)
|
||||||
|
|
||||||
|
folderId = folder[0]?.id ?? null
|
||||||
|
|
||||||
|
const tag = await server.db
|
||||||
|
.select({ id: filetags.id })
|
||||||
|
.from(filetags)
|
||||||
|
.where(
|
||||||
|
and(
|
||||||
|
eq(filetags.tenant, tenant.id),
|
||||||
|
eq(filetags.incomingDocumentType, "invoices")
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.limit(1)
|
||||||
|
|
||||||
|
filetypeId = tag[0]?.id ?? null
|
||||||
|
}
|
||||||
|
|
||||||
|
// -------------------------------------------
|
||||||
|
// Mahnung
|
||||||
|
// -------------------------------------------
|
||||||
|
else if (message.subject?.match(/(Mahnung|Zahlungsaufforderung|Zahlungsverzug)/gi)) {
|
||||||
|
|
||||||
|
const tag = await server.db
|
||||||
|
.select({ id: filetags.id })
|
||||||
|
.from(filetags)
|
||||||
|
.where(
|
||||||
|
and(
|
||||||
|
eq(filetags.tenant, tenant.id),
|
||||||
|
eq(filetags.incomingDocumentType, "reminders")
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.limit(1)
|
||||||
|
|
||||||
|
filetypeId = tag[0]?.id ?? null
|
||||||
|
}
|
||||||
|
|
||||||
|
// -------------------------------------------
|
||||||
|
// Sonstige Dokumente → Deposit Folder
|
||||||
|
// -------------------------------------------
|
||||||
|
else {
|
||||||
|
|
||||||
|
const folder = await server.db
|
||||||
|
.select({ id: folders.id })
|
||||||
|
.from(folders)
|
||||||
|
.where(
|
||||||
|
and(
|
||||||
|
eq(folders.tenant, tenant.id),
|
||||||
|
eq(folders.function, "deposit")
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.limit(1)
|
||||||
|
|
||||||
|
folderId = folder[0]?.id ?? null
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
return {
|
||||||
|
tenant: tenant.id,
|
||||||
|
folder: folderId,
|
||||||
|
filetype: filetypeId
|
||||||
|
}
|
||||||
|
}
|
||||||
175
backend/src/modules/cron/prepareIncomingInvoices.ts
Normal file
175
backend/src/modules/cron/prepareIncomingInvoices.ts
Normal file
@@ -0,0 +1,175 @@
|
|||||||
|
import { FastifyInstance } from "fastify"
|
||||||
|
import dayjs from "dayjs"
|
||||||
|
import { getInvoiceDataFromGPT } from "../../utils/gpt"
|
||||||
|
|
||||||
|
// Drizzle schema
|
||||||
|
import {
|
||||||
|
tenants,
|
||||||
|
files,
|
||||||
|
filetags,
|
||||||
|
incominginvoices,
|
||||||
|
} from "../../../db/schema"
|
||||||
|
|
||||||
|
import { eq, and, isNull, not } from "drizzle-orm"
|
||||||
|
|
||||||
|
export function prepareIncomingInvoices(server: FastifyInstance) {
|
||||||
|
const processInvoices = async (tenantId:number) => {
|
||||||
|
console.log("▶ Starting Incoming Invoice Preparation")
|
||||||
|
|
||||||
|
const tenantsRes = await server.db
|
||||||
|
.select()
|
||||||
|
.from(tenants)
|
||||||
|
.where(eq(tenants.id, tenantId))
|
||||||
|
.orderBy(tenants.id)
|
||||||
|
|
||||||
|
if (!tenantsRes.length) {
|
||||||
|
console.log("No tenants with autoPrepareIncomingInvoices = true")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`Processing tenants: ${tenantsRes.map(t => t.id).join(", ")}`)
|
||||||
|
|
||||||
|
// -------------------------------------------------------------
|
||||||
|
// 2️⃣ Jeden Tenant einzeln verarbeiten
|
||||||
|
// -------------------------------------------------------------
|
||||||
|
for (const tenant of tenantsRes) {
|
||||||
|
const tenantId = tenant.id
|
||||||
|
|
||||||
|
// 2.1 Datei-Tags holen für incoming invoices
|
||||||
|
const tagRes = await server.db
|
||||||
|
.select()
|
||||||
|
.from(filetags)
|
||||||
|
.where(
|
||||||
|
and(
|
||||||
|
eq(filetags.tenant, tenantId),
|
||||||
|
eq(filetags.incomingDocumentType, "invoices")
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.limit(1)
|
||||||
|
|
||||||
|
const invoiceFileTag = tagRes?.[0]?.id
|
||||||
|
if (!invoiceFileTag) {
|
||||||
|
server.log.error(`❌ Missing filetag 'invoices' for tenant ${tenantId}`)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2.2 Alle Dateien laden, die als Invoice markiert sind aber NOCH keine incominginvoice haben
|
||||||
|
const filesRes = await server.db
|
||||||
|
.select()
|
||||||
|
.from(files)
|
||||||
|
.where(
|
||||||
|
and(
|
||||||
|
eq(files.tenant, tenantId),
|
||||||
|
eq(files.type, invoiceFileTag),
|
||||||
|
isNull(files.incominginvoice),
|
||||||
|
eq(files.archived, false),
|
||||||
|
not(isNull(files.path))
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
if (!filesRes.length) {
|
||||||
|
console.log(`No invoice files for tenant ${tenantId}`)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// -------------------------------------------------------------
|
||||||
|
// 3️⃣ Jede Datei einzeln durch GPT jagen & IncomingInvoice erzeugen
|
||||||
|
// -------------------------------------------------------------
|
||||||
|
for (const file of filesRes) {
|
||||||
|
console.log(`Processing file ${file.id} for tenant ${tenantId}`)
|
||||||
|
|
||||||
|
const data = await getInvoiceDataFromGPT(server,file, tenantId)
|
||||||
|
|
||||||
|
if (!data) {
|
||||||
|
server.log.warn(`GPT returned no data for file ${file.id}`)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------
|
||||||
|
// 3.1 IncomingInvoice-Objekt vorbereiten
|
||||||
|
// ---------------------------------------------------------
|
||||||
|
let itemInfo: any = {
|
||||||
|
tenant: tenantId,
|
||||||
|
state: "Vorbereitet"
|
||||||
|
}
|
||||||
|
|
||||||
|
if (data.invoice_number) itemInfo.reference = data.invoice_number
|
||||||
|
if (data.invoice_date) itemInfo.date = dayjs(data.invoice_date).toISOString()
|
||||||
|
if (data.issuer?.id) itemInfo.vendor = data.issuer.id
|
||||||
|
if (data.invoice_duedate) itemInfo.dueDate = dayjs(data.invoice_duedate).toISOString()
|
||||||
|
|
||||||
|
// Payment terms mapping
|
||||||
|
const mapPayment: any = {
|
||||||
|
"Direct Debit": "Einzug",
|
||||||
|
"Transfer": "Überweisung",
|
||||||
|
"Credit Card": "Kreditkarte",
|
||||||
|
"Other": "Sonstiges",
|
||||||
|
}
|
||||||
|
if (data.terms) itemInfo.paymentType = mapPayment[data.terms] ?? data.terms
|
||||||
|
|
||||||
|
// 3.2 Positionszeilen konvertieren
|
||||||
|
if (data.invoice_items?.length > 0) {
|
||||||
|
itemInfo.accounts = data.invoice_items.map(item => ({
|
||||||
|
account: item.account_id,
|
||||||
|
description: item.description,
|
||||||
|
amountNet: item.total_without_tax,
|
||||||
|
amountTax: Number((item.total - item.total_without_tax).toFixed(2)),
|
||||||
|
taxType: String(item.tax_rate),
|
||||||
|
amountGross: item.total,
|
||||||
|
costCentre: null,
|
||||||
|
quantity: item.quantity,
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3.3 Beschreibung generieren
|
||||||
|
let description = ""
|
||||||
|
if (data.delivery_note_number) description += `Lieferschein: ${data.delivery_note_number}\n`
|
||||||
|
if (data.reference) description += `Referenz: ${data.reference}\n`
|
||||||
|
if (data.invoice_items) {
|
||||||
|
for (const item of data.invoice_items) {
|
||||||
|
description += `${item.description} - ${item.quantity} ${item.unit} - ${item.total}\n`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
itemInfo.description = description.trim()
|
||||||
|
|
||||||
|
// ---------------------------------------------------------
|
||||||
|
// 4️⃣ IncomingInvoice erstellen
|
||||||
|
// ---------------------------------------------------------
|
||||||
|
const inserted = await server.db
|
||||||
|
.insert(incominginvoices)
|
||||||
|
.values(itemInfo)
|
||||||
|
.returning()
|
||||||
|
|
||||||
|
const newInvoice = inserted?.[0]
|
||||||
|
|
||||||
|
if (!newInvoice) {
|
||||||
|
server.log.error(`Failed to insert incoming invoice for file ${file.id}`)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------
|
||||||
|
// 5️⃣ Datei mit incominginvoice-ID verbinden
|
||||||
|
// ---------------------------------------------------------
|
||||||
|
await server.db
|
||||||
|
.update(files)
|
||||||
|
.set({ incominginvoice: newInvoice.id })
|
||||||
|
.where(eq(files.id, file.id))
|
||||||
|
|
||||||
|
console.log(`IncomingInvoice ${newInvoice.id} created for file ${file.id}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
run: async (tenant:number) => {
|
||||||
|
await processInvoices(tenant)
|
||||||
|
console.log("Incoming Invoice Preparation Completed.")
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
38
backend/src/modules/helpdesk/helpdesk.contact.service.ts
Normal file
38
backend/src/modules/helpdesk/helpdesk.contact.service.ts
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
// modules/helpdesk/helpdesk.contact.service.ts
|
||||||
|
import { FastifyInstance } from 'fastify'
|
||||||
|
|
||||||
|
export async function getOrCreateContact(
|
||||||
|
server: FastifyInstance,
|
||||||
|
tenant_id: number,
|
||||||
|
{ email, phone, display_name, customer_id, contact_id }: { email?: string; phone?: string; display_name?: string; customer_id?: number; contact_id?: number }
|
||||||
|
) {
|
||||||
|
if (!email && !phone) throw new Error('Contact must have at least an email or phone')
|
||||||
|
|
||||||
|
// Bestehenden Kontakt prüfen
|
||||||
|
const { data: existing, error: findError } = await server.supabase
|
||||||
|
.from('helpdesk_contacts')
|
||||||
|
.select('*')
|
||||||
|
.eq('tenant_id', tenant_id)
|
||||||
|
.or(`email.eq.${email || ''},phone.eq.${phone || ''}`)
|
||||||
|
.maybeSingle()
|
||||||
|
|
||||||
|
if (findError) throw findError
|
||||||
|
if (existing) return existing
|
||||||
|
|
||||||
|
// Anlegen
|
||||||
|
const { data: created, error: insertError } = await server.supabase
|
||||||
|
.from('helpdesk_contacts')
|
||||||
|
.insert({
|
||||||
|
tenant_id,
|
||||||
|
email,
|
||||||
|
phone,
|
||||||
|
display_name,
|
||||||
|
customer_id,
|
||||||
|
contact_id
|
||||||
|
})
|
||||||
|
.select()
|
||||||
|
.single()
|
||||||
|
|
||||||
|
if (insertError) throw insertError
|
||||||
|
return created
|
||||||
|
}
|
||||||
@@ -0,0 +1,90 @@
|
|||||||
|
// modules/helpdesk/helpdesk.conversation.service.ts
|
||||||
|
import { FastifyInstance } from 'fastify'
|
||||||
|
import { getOrCreateContact } from './helpdesk.contact.service.js'
|
||||||
|
import {useNextNumberRangeNumber} from "../../utils/functions";
|
||||||
|
|
||||||
|
export async function createConversation(
|
||||||
|
server: FastifyInstance,
|
||||||
|
{
|
||||||
|
tenant_id,
|
||||||
|
contact,
|
||||||
|
channel_instance_id,
|
||||||
|
subject,
|
||||||
|
customer_id = null,
|
||||||
|
contact_person_id = null,
|
||||||
|
}: {
|
||||||
|
tenant_id: number
|
||||||
|
contact: { email?: string; phone?: string; display_name?: string }
|
||||||
|
channel_instance_id: string
|
||||||
|
subject?: string,
|
||||||
|
customer_id?: number,
|
||||||
|
contact_person_id?: number
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
const contactRecord = await getOrCreateContact(server, tenant_id, contact)
|
||||||
|
|
||||||
|
const {usedNumber } = await useNextNumberRangeNumber(server, tenant_id, "tickets")
|
||||||
|
|
||||||
|
const { data, error } = await server.supabase
|
||||||
|
.from('helpdesk_conversations')
|
||||||
|
.insert({
|
||||||
|
tenant_id,
|
||||||
|
contact_id: contactRecord.id,
|
||||||
|
channel_instance_id,
|
||||||
|
subject: subject || null,
|
||||||
|
status: 'open',
|
||||||
|
created_at: new Date().toISOString(),
|
||||||
|
customer_id,
|
||||||
|
contact_person_id,
|
||||||
|
ticket_number: usedNumber
|
||||||
|
})
|
||||||
|
.select()
|
||||||
|
.single()
|
||||||
|
|
||||||
|
if (error) throw error
|
||||||
|
return data
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getConversations(
|
||||||
|
server: FastifyInstance,
|
||||||
|
tenant_id: number,
|
||||||
|
opts?: { status?: string; limit?: number }
|
||||||
|
) {
|
||||||
|
const { status, limit = 50 } = opts || {}
|
||||||
|
|
||||||
|
let query = server.supabase.from('helpdesk_conversations').select('*, customer_id(*)').eq('tenant_id', tenant_id)
|
||||||
|
|
||||||
|
if (status) query = query.eq('status', status)
|
||||||
|
query = query.order('last_message_at', { ascending: false }).limit(limit)
|
||||||
|
|
||||||
|
const { data, error } = await query
|
||||||
|
if (error) throw error
|
||||||
|
|
||||||
|
const mappedData = data.map(entry => {
|
||||||
|
return {
|
||||||
|
...entry,
|
||||||
|
customer: entry.customer_id
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
return mappedData
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function updateConversationStatus(
|
||||||
|
server: FastifyInstance,
|
||||||
|
conversation_id: string,
|
||||||
|
status: string
|
||||||
|
) {
|
||||||
|
const valid = ['open', 'in_progress', 'waiting_for_customer', 'answered', 'closed']
|
||||||
|
if (!valid.includes(status)) throw new Error('Invalid status')
|
||||||
|
|
||||||
|
const { data, error } = await server.supabase
|
||||||
|
.from('helpdesk_conversations')
|
||||||
|
.update({ status })
|
||||||
|
.eq('id', conversation_id)
|
||||||
|
.select()
|
||||||
|
.single()
|
||||||
|
|
||||||
|
if (error) throw error
|
||||||
|
return data
|
||||||
|
}
|
||||||
60
backend/src/modules/helpdesk/helpdesk.message.service.ts
Normal file
60
backend/src/modules/helpdesk/helpdesk.message.service.ts
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
// modules/helpdesk/helpdesk.message.service.ts
|
||||||
|
import { FastifyInstance } from 'fastify'
|
||||||
|
|
||||||
|
export async function addMessage(
|
||||||
|
server: FastifyInstance,
|
||||||
|
{
|
||||||
|
tenant_id,
|
||||||
|
conversation_id,
|
||||||
|
author_user_id = null,
|
||||||
|
direction = 'incoming',
|
||||||
|
payload,
|
||||||
|
raw_meta = null,
|
||||||
|
external_message_id = null,
|
||||||
|
}: {
|
||||||
|
tenant_id: number
|
||||||
|
conversation_id: string
|
||||||
|
author_user_id?: string | null
|
||||||
|
direction?: 'incoming' | 'outgoing' | 'internal' | 'system'
|
||||||
|
payload: any
|
||||||
|
raw_meta?: any
|
||||||
|
external_message_id?: string
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
if (!payload?.text) throw new Error('Message payload requires text content')
|
||||||
|
|
||||||
|
const { data: message, error } = await server.supabase
|
||||||
|
.from('helpdesk_messages')
|
||||||
|
.insert({
|
||||||
|
tenant_id,
|
||||||
|
conversation_id,
|
||||||
|
author_user_id,
|
||||||
|
direction,
|
||||||
|
payload,
|
||||||
|
raw_meta,
|
||||||
|
created_at: new Date().toISOString(),
|
||||||
|
})
|
||||||
|
.select()
|
||||||
|
.single()
|
||||||
|
|
||||||
|
if (error) throw error
|
||||||
|
|
||||||
|
// Letzte Nachricht aktualisieren
|
||||||
|
await server.supabase
|
||||||
|
.from('helpdesk_conversations')
|
||||||
|
.update({ last_message_at: new Date().toISOString() })
|
||||||
|
.eq('id', conversation_id)
|
||||||
|
|
||||||
|
return message
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getMessages(server: FastifyInstance, conversation_id: string) {
|
||||||
|
const { data, error } = await server.supabase
|
||||||
|
.from('helpdesk_messages')
|
||||||
|
.select('*')
|
||||||
|
.eq('conversation_id', conversation_id)
|
||||||
|
.order('created_at', { ascending: true })
|
||||||
|
|
||||||
|
if (error) throw error
|
||||||
|
return data
|
||||||
|
}
|
||||||
148
backend/src/modules/notification.service.ts
Normal file
148
backend/src/modules/notification.service.ts
Normal file
@@ -0,0 +1,148 @@
|
|||||||
|
// services/notification.service.ts
|
||||||
|
import type { FastifyInstance } from 'fastify';
|
||||||
|
import {secrets} from "../utils/secrets";
|
||||||
|
|
||||||
|
export type NotificationStatus = 'queued' | 'sent' | 'failed';
|
||||||
|
|
||||||
|
export interface TriggerInput {
|
||||||
|
tenantId: number;
|
||||||
|
userId: string; // muss auf public.auth_users.id zeigen
|
||||||
|
eventType: string; // muss in notifications_event_types existieren
|
||||||
|
title: string; // Betreff/Title
|
||||||
|
message: string; // Klartext-Inhalt
|
||||||
|
payload?: Record<string, unknown>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface UserDirectoryInfo {
|
||||||
|
email?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type UserDirectory = (server: FastifyInstance, userId: string, tenantId: number) => Promise<UserDirectoryInfo | null>;
|
||||||
|
|
||||||
|
export class NotificationService {
|
||||||
|
constructor(
|
||||||
|
private server: FastifyInstance,
|
||||||
|
private getUser: UserDirectory
|
||||||
|
) {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Löst eine E-Mail-Benachrichtigung aus:
|
||||||
|
* - Validiert den Event-Typ
|
||||||
|
* - Legt einen Datensatz in notifications_items an (status: queued)
|
||||||
|
* - Versendet E-Mail (FEDEO Branding)
|
||||||
|
* - Aktualisiert status/sent_at bzw. error
|
||||||
|
*/
|
||||||
|
async trigger(input: TriggerInput) {
|
||||||
|
const { tenantId, userId, eventType, title, message, payload } = input;
|
||||||
|
const supabase = this.server.supabase;
|
||||||
|
|
||||||
|
// 1) Event-Typ prüfen (aktiv?)
|
||||||
|
const { data: eventTypeRow, error: etErr } = await supabase
|
||||||
|
.from('notifications_event_types')
|
||||||
|
.select('event_key,is_active')
|
||||||
|
.eq('event_key', eventType)
|
||||||
|
.maybeSingle();
|
||||||
|
|
||||||
|
if (etErr || !eventTypeRow || eventTypeRow.is_active !== true) {
|
||||||
|
throw new Error(`Unbekannter oder inaktiver Event-Typ: ${eventType}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2) Zieladresse beschaffen
|
||||||
|
const user = await this.getUser(this.server, userId, tenantId);
|
||||||
|
if (!user?.email) {
|
||||||
|
throw new Error(`Nutzer ${userId} hat keine E-Mail-Adresse`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3) Notification anlegen (status: queued)
|
||||||
|
const { data: inserted, error: insErr } = await supabase
|
||||||
|
.from('notifications_items')
|
||||||
|
.insert({
|
||||||
|
tenant_id: tenantId,
|
||||||
|
user_id: userId,
|
||||||
|
event_type: eventType,
|
||||||
|
title,
|
||||||
|
message,
|
||||||
|
payload: payload ?? null,
|
||||||
|
channel: 'email',
|
||||||
|
status: 'queued'
|
||||||
|
})
|
||||||
|
.select('id')
|
||||||
|
.single();
|
||||||
|
|
||||||
|
if (insErr || !inserted) {
|
||||||
|
throw new Error(`Fehler beim Einfügen der Notification: ${insErr?.message}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4) E-Mail versenden
|
||||||
|
try {
|
||||||
|
await this.sendEmail(user.email, title, message);
|
||||||
|
|
||||||
|
await supabase
|
||||||
|
.from('notifications_items')
|
||||||
|
.update({ status: 'sent', sent_at: new Date().toISOString() })
|
||||||
|
.eq('id', inserted.id);
|
||||||
|
|
||||||
|
return { success: true, id: inserted.id };
|
||||||
|
} catch (err: any) {
|
||||||
|
await supabase
|
||||||
|
.from('notifications_items')
|
||||||
|
.update({ status: 'failed', error: String(err?.message || err) })
|
||||||
|
.eq('id', inserted.id);
|
||||||
|
|
||||||
|
this.server.log.error({ err, notificationId: inserted.id }, 'E-Mail Versand fehlgeschlagen');
|
||||||
|
return { success: false, error: err?.message || 'E-Mail Versand fehlgeschlagen' };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- private helpers ------------------------------------------------------
|
||||||
|
|
||||||
|
private async sendEmail(to: string, subject: string, message: string) {
|
||||||
|
const nodemailer = await import('nodemailer');
|
||||||
|
|
||||||
|
const transporter = nodemailer.createTransport({
|
||||||
|
host: secrets.MAILER_SMTP_HOST,
|
||||||
|
port: Number(secrets.MAILER_SMTP_PORT),
|
||||||
|
secure: secrets.MAILER_SMTP_SSL === 'true',
|
||||||
|
auth: {
|
||||||
|
user: secrets.MAILER_SMTP_USER,
|
||||||
|
pass: secrets.MAILER_SMTP_PASS
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const html = this.renderFedeoHtml(subject, message);
|
||||||
|
|
||||||
|
await transporter.sendMail({
|
||||||
|
from: secrets.MAILER_FROM,
|
||||||
|
to,
|
||||||
|
subject,
|
||||||
|
text: message,
|
||||||
|
html
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private renderFedeoHtml(title: string, message: string) {
|
||||||
|
return `
|
||||||
|
<html><body style="font-family:sans-serif;color:#222">
|
||||||
|
<div style="border:1px solid #ddd;border-radius:8px;padding:16px;max-width:600px;margin:auto">
|
||||||
|
<h2 style="color:#0f62fe;margin:0 0 12px">FEDEO</h2>
|
||||||
|
<h3 style="margin:0 0 8px">${this.escapeHtml(title)}</h3>
|
||||||
|
<p>${this.nl2br(this.escapeHtml(message))}</p>
|
||||||
|
<hr style="margin:16px 0;border:none;border-top:1px solid #eee"/>
|
||||||
|
<p style="font-size:12px;color:#666">Automatisch generiert von FEDEO</p>
|
||||||
|
</div>
|
||||||
|
</body></html>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// simple escaping (ausreichend für unser Template)
|
||||||
|
private escapeHtml(s: string) {
|
||||||
|
return s
|
||||||
|
.replace(/&/g, '&')
|
||||||
|
.replace(/</g, '<')
|
||||||
|
.replace(/>/g, '>');
|
||||||
|
}
|
||||||
|
|
||||||
|
private nl2br(s: string) {
|
||||||
|
return s.replace(/\n/g, '<br/>');
|
||||||
|
}
|
||||||
|
}
|
||||||
406
backend/src/modules/publiclinks.service.ts
Normal file
406
backend/src/modules/publiclinks.service.ts
Normal file
@@ -0,0 +1,406 @@
|
|||||||
|
import { FastifyInstance } from 'fastify';
|
||||||
|
import bcrypt from 'bcrypt';
|
||||||
|
import crypto from 'crypto';
|
||||||
|
import {and, eq, inArray, not} from 'drizzle-orm';
|
||||||
|
import * as schema from '../../db/schema';
|
||||||
|
import {useNextNumberRangeNumber} from "../utils/functions"; // Pfad anpassen
|
||||||
|
|
||||||
|
|
||||||
|
export const publicLinkService = {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Erstellt einen neuen Public Link
|
||||||
|
*/
|
||||||
|
async createLink(server: FastifyInstance, tenantId: number,
|
||||||
|
name: string,
|
||||||
|
isProtected?: boolean,
|
||||||
|
pin?: string,
|
||||||
|
customToken?: string,
|
||||||
|
config?: Record<string, any>,
|
||||||
|
defaultProfileId?: string) {
|
||||||
|
let pinHash: string | null = null;
|
||||||
|
|
||||||
|
// 1. PIN Hashen, falls Schutz aktiviert ist
|
||||||
|
if (isProtected && pin) {
|
||||||
|
pinHash = await bcrypt.hash(pin, 10);
|
||||||
|
} else if (isProtected && !pin) {
|
||||||
|
throw new Error("Für geschützte Links muss eine PIN angegeben werden.");
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Token generieren oder Custom Token verwenden
|
||||||
|
let token = customToken;
|
||||||
|
|
||||||
|
if (!token) {
|
||||||
|
// Generiere einen zufälligen Token (z.B. hex string)
|
||||||
|
// Alternativ: nanoid nutzen, falls installiert
|
||||||
|
token = crypto.randomBytes(12).toString('hex');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Prüfen, ob Token schon existiert (nur bei Custom Token wichtig)
|
||||||
|
if (customToken) {
|
||||||
|
const existing = await server.db.query.publicLinks.findFirst({
|
||||||
|
where: eq(schema.publicLinks.token, token)
|
||||||
|
});
|
||||||
|
if (existing) {
|
||||||
|
throw new Error(`Der Token '${token}' ist bereits vergeben.`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. DB Insert
|
||||||
|
const [newLink] = await server.db.insert(schema.publicLinks).values({
|
||||||
|
tenant: tenantId,
|
||||||
|
name: name,
|
||||||
|
token: token,
|
||||||
|
isProtected: isProtected || false,
|
||||||
|
pinHash: pinHash,
|
||||||
|
config: config || {},
|
||||||
|
defaultProfile: defaultProfileId || null,
|
||||||
|
active: true
|
||||||
|
}).returning();
|
||||||
|
|
||||||
|
return newLink;
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Listet alle Links für einen Tenant auf (für die Verwaltungs-UI später)
|
||||||
|
*/
|
||||||
|
async getLinksByTenant(server: FastifyInstance, tenantId: number) {
|
||||||
|
return server.db.select()
|
||||||
|
.from(schema.publicLinks)
|
||||||
|
.where(eq(schema.publicLinks.tenant, tenantId));
|
||||||
|
},
|
||||||
|
|
||||||
|
|
||||||
|
async getLinkContext(server: FastifyInstance, token: string, providedPin?: string) {
|
||||||
|
// 1. Link laden & Checks
|
||||||
|
const linkConfig = await server.db.query.publicLinks.findFirst({
|
||||||
|
where: eq(schema.publicLinks.token, token)
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!linkConfig || !linkConfig.active) throw new Error("Link_NotFound");
|
||||||
|
|
||||||
|
// 2. PIN Check
|
||||||
|
if (linkConfig.isProtected) {
|
||||||
|
if (!providedPin) throw new Error("Pin_Required");
|
||||||
|
const isValid = await bcrypt.compare(providedPin, linkConfig.pinHash || "");
|
||||||
|
if (!isValid) throw new Error("Pin_Invalid");
|
||||||
|
}
|
||||||
|
|
||||||
|
const tenantId = linkConfig.tenant;
|
||||||
|
const config = linkConfig.config as any;
|
||||||
|
|
||||||
|
// --- RESSOURCEN & FILTER ---
|
||||||
|
|
||||||
|
// Standardmäßig alles laden, wenn 'resources' nicht definiert ist
|
||||||
|
const requestedResources: string[] = config.resources || ['profiles', 'projects', 'services', 'units'];
|
||||||
|
const filters = config.filters || {}; // Erwartet jetzt: { projects: [1,2], services: [3,4] }
|
||||||
|
|
||||||
|
const queryPromises: Record<string, Promise<any[]>> = {};
|
||||||
|
|
||||||
|
// ---------------------------------------------------------
|
||||||
|
// 1. PROFILES (Mitarbeiter)
|
||||||
|
// ---------------------------------------------------------
|
||||||
|
if (requestedResources.includes('profiles')) {
|
||||||
|
let profileCondition = and(
|
||||||
|
eq(schema.authProfiles.tenant_id, tenantId),
|
||||||
|
eq(schema.authProfiles.active, true)
|
||||||
|
);
|
||||||
|
|
||||||
|
// Sicherheits-Feature: Default Profil erzwingen
|
||||||
|
if (linkConfig.defaultProfile) {
|
||||||
|
profileCondition = and(profileCondition, eq(schema.authProfiles.id, linkConfig.defaultProfile));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Optional: Auch hier Filter ermöglichen (falls man z.B. nur 3 bestimmte MA zur Auswahl geben will)
|
||||||
|
if (filters.profiles && Array.isArray(filters.profiles) && filters.profiles.length > 0) {
|
||||||
|
profileCondition = and(profileCondition, inArray(schema.authProfiles.id, filters.profiles));
|
||||||
|
}
|
||||||
|
|
||||||
|
queryPromises.profiles = server.db.select({
|
||||||
|
id: schema.authProfiles.id,
|
||||||
|
fullName: schema.authProfiles.full_name
|
||||||
|
})
|
||||||
|
.from(schema.authProfiles)
|
||||||
|
.where(profileCondition);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------
|
||||||
|
// 2. PROJECTS (Aufträge)
|
||||||
|
// ---------------------------------------------------------
|
||||||
|
if (requestedResources.includes('projects')) {
|
||||||
|
let projectCondition = and(
|
||||||
|
eq(schema.projects.tenant, tenantId),
|
||||||
|
not(eq(schema.projects.active_phase, 'Abgeschlossen'))
|
||||||
|
);
|
||||||
|
|
||||||
|
// NEU: Zugriff direkt auf filters.projects
|
||||||
|
if (filters.projects && Array.isArray(filters.projects) && filters.projects.length > 0) {
|
||||||
|
projectCondition = and(projectCondition, inArray(schema.projects.id, filters.projects));
|
||||||
|
}
|
||||||
|
|
||||||
|
queryPromises.projects = server.db.select({
|
||||||
|
id: schema.projects.id,
|
||||||
|
name: schema.projects.name
|
||||||
|
})
|
||||||
|
.from(schema.projects)
|
||||||
|
.where(projectCondition);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------
|
||||||
|
// 3. SERVICES (Tätigkeiten)
|
||||||
|
// ---------------------------------------------------------
|
||||||
|
if (requestedResources.includes('services')) {
|
||||||
|
let serviceCondition = eq(schema.services.tenant, tenantId);
|
||||||
|
|
||||||
|
// NEU: Zugriff direkt auf filters.services
|
||||||
|
if (filters.services && Array.isArray(filters.services) && filters.services.length > 0) {
|
||||||
|
serviceCondition = and(serviceCondition, inArray(schema.services.id, filters.services));
|
||||||
|
}
|
||||||
|
|
||||||
|
queryPromises.services = server.db.select({
|
||||||
|
id: schema.services.id,
|
||||||
|
name: schema.services.name,
|
||||||
|
unit: schema.services.unit
|
||||||
|
})
|
||||||
|
.from(schema.services)
|
||||||
|
.where(serviceCondition);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------
|
||||||
|
// 4. UNITS (Einheiten)
|
||||||
|
// ---------------------------------------------------------
|
||||||
|
if (requestedResources.includes('units')) {
|
||||||
|
// Units werden meist global geladen, könnten aber auch gefiltert werden
|
||||||
|
queryPromises.units = server.db.select().from(schema.units);
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- QUERY AUSFÜHRUNG ---
|
||||||
|
const results = await Promise.all(Object.values(queryPromises));
|
||||||
|
const keys = Object.keys(queryPromises);
|
||||||
|
|
||||||
|
const dataResponse: Record<string, any[]> = {
|
||||||
|
profiles: [],
|
||||||
|
projects: [],
|
||||||
|
services: [],
|
||||||
|
units: []
|
||||||
|
};
|
||||||
|
|
||||||
|
keys.forEach((key, index) => {
|
||||||
|
dataResponse[key] = results[index];
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
config: linkConfig.config,
|
||||||
|
meta: {
|
||||||
|
formName: linkConfig.name,
|
||||||
|
defaultProfileId: linkConfig.defaultProfile
|
||||||
|
},
|
||||||
|
data: dataResponse
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
async submitFormData(server: FastifyInstance, token: string, payload: any, providedPin?: string) {
|
||||||
|
// 1. Validierung (Token & PIN)
|
||||||
|
const linkConfig = await server.db.query.publicLinks.findFirst({
|
||||||
|
where: eq(schema.publicLinks.token, token)
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!linkConfig || !linkConfig.active) throw new Error("Link_NotFound");
|
||||||
|
|
||||||
|
if (linkConfig.isProtected) {
|
||||||
|
if (!providedPin) throw new Error("Pin_Required");
|
||||||
|
const isValid = await bcrypt.compare(providedPin, linkConfig.pinHash || "");
|
||||||
|
if (!isValid) throw new Error("Pin_Invalid");
|
||||||
|
}
|
||||||
|
|
||||||
|
const tenantId = linkConfig.tenant;
|
||||||
|
const config = linkConfig.config as any;
|
||||||
|
|
||||||
|
// 2. USER ID AUFLÖSEN
|
||||||
|
// Wir holen die profileId aus dem Link (Default) oder dem Payload (User-Auswahl)
|
||||||
|
const rawProfileId = linkConfig.defaultProfile || payload.profile;
|
||||||
|
if (!rawProfileId) throw new Error("Profile_Missing");
|
||||||
|
|
||||||
|
// Profil laden, um die user_id zu bekommen
|
||||||
|
const authProfile = await server.db.query.authProfiles.findFirst({
|
||||||
|
where: eq(schema.authProfiles.id, rawProfileId)
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!authProfile) throw new Error("Profile_Not_Found");
|
||||||
|
|
||||||
|
// Da du sagtest, es gibt immer einen User, verlassen wir uns darauf.
|
||||||
|
// Falls null, werfen wir einen Fehler, da die DB sonst beim Insert knallt.
|
||||||
|
const userId = authProfile.user_id;
|
||||||
|
if (!userId) throw new Error("Profile_Has_No_User_Assigned");
|
||||||
|
|
||||||
|
|
||||||
|
// Helper für Datum
|
||||||
|
const normalizeDate = (val: any) => {
|
||||||
|
if (!val) return null
|
||||||
|
const d = new Date(val)
|
||||||
|
return isNaN(d.getTime()) ? null : d
|
||||||
|
}
|
||||||
|
|
||||||
|
// =================================================================
|
||||||
|
// SCHRITT A: Stammdaten laden
|
||||||
|
// =================================================================
|
||||||
|
const project = await server.db.query.projects.findFirst({
|
||||||
|
where: eq(schema.projects.id, payload.project)
|
||||||
|
});
|
||||||
|
if (!project) throw new Error("Project not found");
|
||||||
|
|
||||||
|
const customer = await server.db.query.customers.findFirst({
|
||||||
|
where: eq(schema.customers.id, project.customer)
|
||||||
|
});
|
||||||
|
|
||||||
|
const service = await server.db.query.services.findFirst({
|
||||||
|
where: eq(schema.services.id, payload.service)
|
||||||
|
});
|
||||||
|
if (!service) throw new Error("Service not found");
|
||||||
|
|
||||||
|
// Texttemplates & Letterhead laden
|
||||||
|
const texttemplates = await server.db.query.texttemplates.findMany({
|
||||||
|
where: (t, {and, eq}) => and(
|
||||||
|
eq(t.tenant, tenantId),
|
||||||
|
eq(t.documentType, 'deliveryNotes')
|
||||||
|
)
|
||||||
|
});
|
||||||
|
const letterhead = await server.db.query.letterheads.findFirst({
|
||||||
|
where: eq(schema.letterheads.tenant, tenantId)
|
||||||
|
});
|
||||||
|
|
||||||
|
// =================================================================
|
||||||
|
// SCHRITT B: Nummernkreis generieren
|
||||||
|
// =================================================================
|
||||||
|
const {usedNumber} = await useNextNumberRangeNumber(server, tenantId, "deliveryNotes");
|
||||||
|
|
||||||
|
// =================================================================
|
||||||
|
// SCHRITT C: Berechnungen
|
||||||
|
// =================================================================
|
||||||
|
const startDate = normalizeDate(payload.startDate) || new Date();
|
||||||
|
let endDate = normalizeDate(payload.endDate);
|
||||||
|
|
||||||
|
// Fallback Endzeit (+1h)
|
||||||
|
if (!endDate) {
|
||||||
|
endDate = server.dayjs(startDate).add(1, 'hour').toDate();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Menge berechnen
|
||||||
|
let quantity = payload.quantity;
|
||||||
|
if (!quantity && payload.totalHours) quantity = payload.totalHours;
|
||||||
|
if (!quantity) {
|
||||||
|
const diffMin = server.dayjs(endDate).diff(server.dayjs(startDate), 'minute');
|
||||||
|
quantity = Number((diffMin / 60).toFixed(2));
|
||||||
|
}
|
||||||
|
|
||||||
|
// =================================================================
|
||||||
|
// SCHRITT D: Lieferschein erstellen
|
||||||
|
// =================================================================
|
||||||
|
const createDocData = {
|
||||||
|
tenant: tenantId,
|
||||||
|
type: "deliveryNotes",
|
||||||
|
state: "Entwurf",
|
||||||
|
customer: project.customer,
|
||||||
|
//@ts-ignore
|
||||||
|
address: customer ? {zip: customer.infoData.zip, city: customer.infoData.city, street: customer.infoData.street,} : {},
|
||||||
|
project: project.id,
|
||||||
|
documentNumber: usedNumber,
|
||||||
|
documentDate: String(new Date()), // Schema sagt 'text', evtl toISOString() besser?
|
||||||
|
deliveryDate: String(startDate), // Schema sagt 'text'
|
||||||
|
deliveryDateType: "Leistungsdatum",
|
||||||
|
createdBy: userId, // WICHTIG: Hier die User ID
|
||||||
|
created_by: userId, // WICHTIG: Hier die User ID
|
||||||
|
title: "Lieferschein",
|
||||||
|
description: "",
|
||||||
|
startText: texttemplates.find((i: any) => i.default && i.pos === "startText")?.text || "",
|
||||||
|
endText: texttemplates.find((i: any) => i.default && i.pos === "endText")?.text || "",
|
||||||
|
rows: [
|
||||||
|
{
|
||||||
|
pos: "1",
|
||||||
|
mode: "service",
|
||||||
|
service: service.id,
|
||||||
|
text: service.name,
|
||||||
|
unit: service.unit,
|
||||||
|
quantity: quantity,
|
||||||
|
description: service.description || null,
|
||||||
|
descriptionText: service.description || null,
|
||||||
|
agriculture: {
|
||||||
|
dieselUsage: payload.dieselUsage || null,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
contactPerson: userId, // WICHTIG: Hier die User ID
|
||||||
|
letterhead: letterhead?.id,
|
||||||
|
};
|
||||||
|
|
||||||
|
const [createdDoc] = await server.db.insert(schema.createddocuments)
|
||||||
|
//@ts-ignore
|
||||||
|
.values(createDocData)
|
||||||
|
.returning();
|
||||||
|
|
||||||
|
// =================================================================
|
||||||
|
// SCHRITT E: Zeiterfassung Events
|
||||||
|
// =================================================================
|
||||||
|
if (config.features?.timeTracking) {
|
||||||
|
|
||||||
|
// Metadaten für das Event
|
||||||
|
const eventMetadata = {
|
||||||
|
project: project.id,
|
||||||
|
service: service.id,
|
||||||
|
description: payload.description || "",
|
||||||
|
generatedDocumentId: createdDoc.id
|
||||||
|
};
|
||||||
|
|
||||||
|
// 1. START EVENT
|
||||||
|
await server.db.insert(schema.stafftimeevents).values({
|
||||||
|
tenant_id: tenantId,
|
||||||
|
user_id: userId, // WICHTIG: User ID
|
||||||
|
actortype: "user",
|
||||||
|
actoruser_id: userId, // WICHTIG: User ID
|
||||||
|
eventtime: startDate,
|
||||||
|
eventtype: "START",
|
||||||
|
source: "PUBLIC_LINK",
|
||||||
|
metadata: eventMetadata // WICHTIG: Schema heißt 'metadata', nicht 'payload'
|
||||||
|
});
|
||||||
|
|
||||||
|
// 2. STOP EVENT
|
||||||
|
await server.db.insert(schema.stafftimeevents).values({
|
||||||
|
tenant_id: tenantId,
|
||||||
|
user_id: userId,
|
||||||
|
actortype: "user",
|
||||||
|
actoruser_id: userId,
|
||||||
|
eventtime: endDate,
|
||||||
|
eventtype: "STOP",
|
||||||
|
source: "PUBLIC_LINK",
|
||||||
|
metadata: eventMetadata
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// =================================================================
|
||||||
|
// SCHRITT F: History Items
|
||||||
|
// =================================================================
|
||||||
|
const historyItemsToCreate = [];
|
||||||
|
|
||||||
|
if (payload.description) {
|
||||||
|
historyItemsToCreate.push({
|
||||||
|
tenant: tenantId,
|
||||||
|
createdBy: userId, // WICHTIG: User ID
|
||||||
|
text: `Notiz aus Webformular Lieferschein ${createdDoc.documentNumber}: ${payload.description}`,
|
||||||
|
project: project.id,
|
||||||
|
createdDocument: createdDoc.id
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
historyItemsToCreate.push({
|
||||||
|
tenant: tenantId,
|
||||||
|
createdBy: userId, // WICHTIG: User ID
|
||||||
|
text: `Webformular abgeschickt. Lieferschein ${createdDoc.documentNumber} erstellt. Zeit gebucht (Start/Stop).`,
|
||||||
|
project: project.id,
|
||||||
|
createdDocument: createdDoc.id
|
||||||
|
});
|
||||||
|
|
||||||
|
await server.db.insert(schema.historyitems).values(historyItemsToCreate);
|
||||||
|
|
||||||
|
return {success: true, documentNumber: createdDoc.documentNumber};
|
||||||
|
}
|
||||||
|
}
|
||||||
725
backend/src/modules/serialexecution.service.ts
Normal file
725
backend/src/modules/serialexecution.service.ts
Normal file
@@ -0,0 +1,725 @@
|
|||||||
|
import dayjs from "dayjs";
|
||||||
|
import quarterOfYear from "dayjs/plugin/quarterOfYear";
|
||||||
|
import Handlebars from "handlebars";
|
||||||
|
import axios from "axios";
|
||||||
|
import { eq, inArray, and } from "drizzle-orm"; // Drizzle Operatoren
|
||||||
|
|
||||||
|
// DEINE IMPORTS
|
||||||
|
import * as schema from "../../db/schema"; // Importiere dein Drizzle Schema
|
||||||
|
import { saveFile } from "../utils/files";
|
||||||
|
import {FastifyInstance} from "fastify";
|
||||||
|
import {useNextNumberRangeNumber} from "../utils/functions";
|
||||||
|
import {createInvoicePDF} from "../utils/pdf"; // Achtung: Muss Node.js Buffer unterstützen!
|
||||||
|
|
||||||
|
dayjs.extend(quarterOfYear);
|
||||||
|
|
||||||
|
|
||||||
|
export const executeManualGeneration = async (server:FastifyInstance,executionDate,templateIds,tenantId,executedBy) => {
|
||||||
|
try {
|
||||||
|
console.log(executedBy)
|
||||||
|
|
||||||
|
const executionDayjs = dayjs(executionDate);
|
||||||
|
|
||||||
|
console.log(`Starte manuelle Generierung für Tenant ${tenantId} am ${executionDate}`);
|
||||||
|
|
||||||
|
// 1. Tenant laden (Drizzle)
|
||||||
|
// Wir nehmen an, dass 'tenants' im Schema definiert ist
|
||||||
|
const [tenant] = await server.db
|
||||||
|
.select()
|
||||||
|
.from(schema.tenants)
|
||||||
|
.where(eq(schema.tenants.id, tenantId))
|
||||||
|
.limit(1);
|
||||||
|
|
||||||
|
if (!tenant) throw new Error(`Tenant mit ID ${tenantId} nicht gefunden.`);
|
||||||
|
|
||||||
|
// 2. Templates laden
|
||||||
|
const templates = await server.db
|
||||||
|
.select()
|
||||||
|
.from(schema.createddocuments)
|
||||||
|
.where(
|
||||||
|
and(
|
||||||
|
eq(schema.createddocuments.tenant, tenantId),
|
||||||
|
eq(schema.createddocuments.type, "serialInvoices"),
|
||||||
|
inArray(schema.createddocuments.id, templateIds)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
if (templates.length === 0) {
|
||||||
|
console.warn("Keine passenden Vorlagen gefunden.");
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. Folder & FileType IDs holen (Hilfsfunktionen unten)
|
||||||
|
const folderId = await getFolderId(server,tenantId);
|
||||||
|
const fileTypeId = await getFileTypeId(server,tenantId);
|
||||||
|
|
||||||
|
const results = [];
|
||||||
|
|
||||||
|
const [executionRecord] = await server.db
|
||||||
|
.insert(schema.serialExecutions)
|
||||||
|
.values({
|
||||||
|
tenant: tenantId,
|
||||||
|
executionDate: executionDayjs.toDate(),
|
||||||
|
status: "draft",
|
||||||
|
createdBy: executedBy,
|
||||||
|
summary: `${templateIds.length} Vorlagen verarbeitet`
|
||||||
|
})
|
||||||
|
.returning();
|
||||||
|
|
||||||
|
console.log(executionRecord);
|
||||||
|
|
||||||
|
// 4. Loop durch die Templates
|
||||||
|
for (const template of templates) {
|
||||||
|
try {
|
||||||
|
const resultId = await processSingleTemplate(
|
||||||
|
server,
|
||||||
|
template,
|
||||||
|
tenant,
|
||||||
|
executionDayjs,
|
||||||
|
folderId,
|
||||||
|
fileTypeId,
|
||||||
|
executedBy,
|
||||||
|
executionRecord.id
|
||||||
|
);
|
||||||
|
results.push({ id: template.id, status: "success", newDocumentId: resultId });
|
||||||
|
} catch (e: any) {
|
||||||
|
console.error(`Fehler bei Template ${template.id}:`, e);
|
||||||
|
results.push({ id: template.id, status: "error", error: e.message });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return results;
|
||||||
|
} catch (error) {
|
||||||
|
console.log(error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const finishManualGeneration = async (server: FastifyInstance, executionId: number) => {
|
||||||
|
try {
|
||||||
|
console.log(`Beende Ausführung ${executionId}...`);
|
||||||
|
|
||||||
|
// 1. Execution und Tenant laden
|
||||||
|
|
||||||
|
const [executionRecord] = await server.db
|
||||||
|
.select()
|
||||||
|
.from(schema.serialExecutions)// @ts-ignore
|
||||||
|
.where(eq(schema.serialExecutions.id, executionId))
|
||||||
|
.limit(1);
|
||||||
|
|
||||||
|
if (!executionRecord) throw new Error("Execution nicht gefunden");
|
||||||
|
|
||||||
|
console.log(executionRecord);
|
||||||
|
|
||||||
|
const tenantId = executionRecord.tenant;
|
||||||
|
|
||||||
|
console.log(tenantId)
|
||||||
|
|
||||||
|
// Tenant laden (für Settings etc.)
|
||||||
|
const [tenant] = await server.db
|
||||||
|
.select()
|
||||||
|
.from(schema.tenants)
|
||||||
|
.where(eq(schema.tenants.id, tenantId))
|
||||||
|
.limit(1);
|
||||||
|
|
||||||
|
// 2. Status auf "processing" setzen (optional, damit UI feedback hat)
|
||||||
|
|
||||||
|
/*await server.db
|
||||||
|
.update(schema.serialExecutions)
|
||||||
|
.set({ status: "processing" })// @ts-ignore
|
||||||
|
.where(eq(schema.serialExecutions.id, executionId));*/
|
||||||
|
|
||||||
|
// 3. Alle erstellten Dokumente dieser Execution laden
|
||||||
|
const documents = await server.db
|
||||||
|
.select()
|
||||||
|
.from(schema.createddocuments)
|
||||||
|
.where(eq(schema.createddocuments.serialexecution, executionId));
|
||||||
|
|
||||||
|
console.log(`${documents.length} Dokumente werden finalisiert...`);
|
||||||
|
|
||||||
|
// 4. IDs für File-System laden (nur einmalig nötig)
|
||||||
|
const folderId = await getFolderId(server, tenantId);
|
||||||
|
const fileTypeId = await getFileTypeId(server, tenantId);
|
||||||
|
|
||||||
|
// Globale Daten laden, die für alle gleich sind (Optimierung)
|
||||||
|
const [units, products, services] = await Promise.all([
|
||||||
|
server.db.select().from(schema.units),
|
||||||
|
server.db.select().from(schema.products).where(eq(schema.products.tenant, tenantId)),
|
||||||
|
server.db.select().from(schema.services).where(eq(schema.services.tenant, tenantId)),
|
||||||
|
]);
|
||||||
|
|
||||||
|
let successCount = 0;
|
||||||
|
let errorCount = 0;
|
||||||
|
|
||||||
|
// 5. Loop durch Dokumente
|
||||||
|
for (const doc of documents) {
|
||||||
|
try {
|
||||||
|
|
||||||
|
const [letterhead] = await Promise.all([
|
||||||
|
/*fetchById(server, schema.contacts, doc.contact),
|
||||||
|
fetchById(server, schema.customers, doc.customer),
|
||||||
|
fetchById(server, schema.authProfiles, doc.contactPerson), // oder createdBy, je nach Logik
|
||||||
|
fetchById(server, schema.projects, doc.project),
|
||||||
|
fetchById(server, schema.contracts, doc.contract),*/
|
||||||
|
doc.letterhead ? fetchById(server, schema.letterheads, doc.letterhead) : null
|
||||||
|
]);
|
||||||
|
|
||||||
|
const pdfData = await getCloseData(
|
||||||
|
server,
|
||||||
|
doc,
|
||||||
|
tenant,
|
||||||
|
units,
|
||||||
|
products,
|
||||||
|
services,
|
||||||
|
);
|
||||||
|
|
||||||
|
console.log(pdfData);
|
||||||
|
|
||||||
|
// D. PDF Generieren
|
||||||
|
const pdfBase64 = await createInvoicePDF(server,"base64",pdfData, letterhead?.path);
|
||||||
|
|
||||||
|
console.log(pdfBase64);
|
||||||
|
|
||||||
|
// E. Datei speichern
|
||||||
|
// @ts-ignore
|
||||||
|
const fileBuffer = Buffer.from(pdfBase64.base64, "base64");
|
||||||
|
const filename = `${pdfData.documentNumber}.pdf`;
|
||||||
|
|
||||||
|
await saveFile(
|
||||||
|
server,
|
||||||
|
tenantId,
|
||||||
|
null, // User ID (hier ggf. executionRecord.createdBy nutzen wenn verfügbar)
|
||||||
|
fileBuffer,
|
||||||
|
folderId,
|
||||||
|
fileTypeId,
|
||||||
|
{
|
||||||
|
createddocument: doc.id,
|
||||||
|
filename: filename,
|
||||||
|
filesize: fileBuffer.length // Falls saveFile das braucht
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
// F. Dokument in DB final updaten
|
||||||
|
await server.db
|
||||||
|
.update(schema.createddocuments)
|
||||||
|
.set({
|
||||||
|
state: "Gebucht",
|
||||||
|
documentNumber: pdfData.documentNumber,
|
||||||
|
title: pdfData.title,
|
||||||
|
pdf_path: filename // Optional, falls du den Pfad direkt am Doc speicherst
|
||||||
|
})
|
||||||
|
.where(eq(schema.createddocuments.id, doc.id));
|
||||||
|
|
||||||
|
successCount++;
|
||||||
|
|
||||||
|
} catch (innerErr) {
|
||||||
|
console.error(`Fehler beim Finalisieren von Doc ID ${doc.id}:`, innerErr);
|
||||||
|
errorCount++;
|
||||||
|
// Optional: Status des einzelnen Dokuments auf Error setzen
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 6. Execution abschließen
|
||||||
|
const finalStatus = errorCount > 0 ? "error" : "completed"; // Oder 'completed' auch wenn Teilerfolge
|
||||||
|
|
||||||
|
|
||||||
|
await server.db
|
||||||
|
.update(schema.serialExecutions)
|
||||||
|
.set({
|
||||||
|
status: finalStatus,
|
||||||
|
summary: `Abgeschlossen: ${successCount} erfolgreich, ${errorCount} Fehler.`
|
||||||
|
})// @ts-ignore
|
||||||
|
.where(eq(schema.serialExecutions.id, executionId));
|
||||||
|
|
||||||
|
return { success: true, processed: successCount, errors: errorCount };
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Critical Error in finishManualGeneration:", error);
|
||||||
|
// Execution auf Error setzen
|
||||||
|
// @ts-ignore
|
||||||
|
await server.db
|
||||||
|
.update(schema.serialExecutions)
|
||||||
|
.set({ status: "error", summary: "Kritischer Fehler beim Finalisieren." })
|
||||||
|
//@ts-ignore
|
||||||
|
.where(eq(schema.serialExecutions.id, executionId));
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Verarbeitet eine einzelne Vorlage
|
||||||
|
*/
|
||||||
|
async function processSingleTemplate(server: FastifyInstance, template: any, tenant: any,executionDate: dayjs.Dayjs,folderId: string,fileTypeId: string,executedBy: string,executionId: string) {
|
||||||
|
// A. Zugehörige Daten parallel laden
|
||||||
|
const [contact, customer, profile, project, contract, units, products, services, letterhead] = await Promise.all([
|
||||||
|
fetchById(server, schema.contacts, template.contact),
|
||||||
|
fetchById(server, schema.customers, template.customer),
|
||||||
|
fetchById(server, schema.authProfiles, template.contactPerson),
|
||||||
|
fetchById(server, schema.projects, template.project),
|
||||||
|
fetchById(server, schema.contracts, template.contract),
|
||||||
|
server.db.select().from(schema.units),
|
||||||
|
server.db.select().from(schema.products).where(eq(schema.products.tenant, tenant.id)),
|
||||||
|
server.db.select().from(schema.services).where(eq(schema.services.tenant, tenant.id)),
|
||||||
|
template.letterhead ? fetchById(server, schema.letterheads, template.letterhead) : null
|
||||||
|
]);
|
||||||
|
|
||||||
|
// B. Datumsberechnung (Logik aus dem Original)
|
||||||
|
const { firstDate, lastDate } = calculateDateRange(template.serialConfig, executionDate);
|
||||||
|
|
||||||
|
// C. Rechnungsnummer & Save Data
|
||||||
|
const savePayload = await getSaveData(
|
||||||
|
template,
|
||||||
|
tenant,
|
||||||
|
firstDate,
|
||||||
|
lastDate,
|
||||||
|
executionDate.toISOString(),
|
||||||
|
executedBy
|
||||||
|
);
|
||||||
|
|
||||||
|
const payloadWithRelation = {
|
||||||
|
...savePayload,
|
||||||
|
serialexecution: executionId
|
||||||
|
};
|
||||||
|
|
||||||
|
// D. Dokument in DB anlegen (Drizzle Insert)
|
||||||
|
const [createdDoc] = await server.db
|
||||||
|
.insert(schema.createddocuments)
|
||||||
|
.values(payloadWithRelation)
|
||||||
|
.returning(); // Wichtig für Postgres: returning() gibt das erstellte Objekt zurück
|
||||||
|
|
||||||
|
return createdDoc.id;
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Drizzle Helper ---
|
||||||
|
|
||||||
|
async function fetchById(server: FastifyInstance, table: any, id: number | null) {
|
||||||
|
if (!id) return null;
|
||||||
|
const [result] = await server.db.select().from(table).where(eq(table.id, id)).limit(1);
|
||||||
|
return result || null;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getFolderId(server:FastifyInstance, tenantId: number) {
|
||||||
|
const [folder] = await server.db
|
||||||
|
.select({ id: schema.folders.id })
|
||||||
|
.from(schema.folders)
|
||||||
|
.where(
|
||||||
|
and(
|
||||||
|
eq(schema.folders.tenant, tenantId),
|
||||||
|
eq(schema.folders.function, "invoices"), // oder 'invoices', check deine DB
|
||||||
|
eq(schema.folders.year, dayjs().format("YYYY"))
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.limit(1);
|
||||||
|
return folder?.id;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getFileTypeId(server: FastifyInstance,tenantId: number) {
|
||||||
|
const [tag] = await server.db
|
||||||
|
.select({ id: schema.filetags.id })
|
||||||
|
.from(schema.filetags)
|
||||||
|
.where(
|
||||||
|
and(
|
||||||
|
eq(schema.filetags.tenant, tenantId),
|
||||||
|
eq(schema.filetags.createdDocumentType, "invoices")
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.limit(1);
|
||||||
|
return tag?.id;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
// --- Logik Helper (Unverändert zur Business Logik) ---
|
||||||
|
|
||||||
|
function calculateDateRange(config: any, executionDate: dayjs.Dayjs) {
|
||||||
|
// Basis nehmen
|
||||||
|
let baseDate = executionDate;
|
||||||
|
|
||||||
|
let firstDate = baseDate;
|
||||||
|
let lastDate = baseDate;
|
||||||
|
|
||||||
|
if (config.intervall === "monatlich" && config.dateDirection === "Rückwirkend") {
|
||||||
|
// 1. Monat abziehen
|
||||||
|
// 2. Start/Ende des Monats berechnen
|
||||||
|
// 3. WICHTIG: Zeit hart auf 12:00:00 setzen, damit Zeitzonen das Datum nicht kippen
|
||||||
|
firstDate = baseDate.subtract(1, "month").startOf("month").hour(12).minute(0).second(0).millisecond(0);
|
||||||
|
lastDate = baseDate.subtract(1, "month").endOf("month").hour(12).minute(0).second(0).millisecond(0);
|
||||||
|
|
||||||
|
} else if (config.intervall === "vierteljährlich" && config.dateDirection === "Rückwirkend") {
|
||||||
|
|
||||||
|
firstDate = baseDate.subtract(1, "quarter").startOf("quarter").hour(12).minute(0).second(0).millisecond(0);
|
||||||
|
lastDate = baseDate.subtract(1, "quarter").endOf("quarter").hour(12).minute(0).second(0).millisecond(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Das Ergebnis ist nun z.B.:
|
||||||
|
// firstDate: '2025-12-01T12:00:00.000Z' (Eindeutig der 1. Dezember)
|
||||||
|
// lastDate: '2025-12-31T12:00:00.000Z' (Eindeutig der 31. Dezember)
|
||||||
|
|
||||||
|
return {
|
||||||
|
firstDate: firstDate.toISOString(),
|
||||||
|
lastDate: lastDate.toISOString()
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getSaveData(item: any, tenant: any, firstDate: string, lastDate: string, executionDate: string, executedBy: string) {
|
||||||
|
const cleanRows = item.rows.map((row: any) => ({
|
||||||
|
...row,
|
||||||
|
descriptionText: row.description || null,
|
||||||
|
}));
|
||||||
|
|
||||||
|
//const documentNumber = await this.useNextInvoicesNumber(item.tenant);
|
||||||
|
|
||||||
|
return {
|
||||||
|
tenant: item.tenant,
|
||||||
|
type: "invoices",
|
||||||
|
state: "Entwurf",
|
||||||
|
customer: item.customer,
|
||||||
|
contact: item.contact,
|
||||||
|
contract: item.contract,
|
||||||
|
address: item.address,
|
||||||
|
project: item.project,
|
||||||
|
documentDate: executionDate,
|
||||||
|
deliveryDate: firstDate,
|
||||||
|
deliveryDateEnd: lastDate,
|
||||||
|
paymentDays: item.paymentDays,
|
||||||
|
payment_type: item.payment_type,
|
||||||
|
deliveryDateType: item.deliveryDateType,
|
||||||
|
info: {}, // Achtung: Postgres erwartet hier valides JSON Objekt
|
||||||
|
createdBy: item.createdBy,
|
||||||
|
created_by: item.created_by,
|
||||||
|
title: `Rechnung-Nr. XXX`,
|
||||||
|
description: item.description,
|
||||||
|
startText: item.startText,
|
||||||
|
endText: item.endText,
|
||||||
|
rows: cleanRows, // JSON Array
|
||||||
|
contactPerson: item.contactPerson,
|
||||||
|
linkedDocument: item.linkedDocument,
|
||||||
|
letterhead: item.letterhead,
|
||||||
|
taxType: item.taxType,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getCloseData(server:FastifyInstance,item: any, tenant: any, units, products,services) {
|
||||||
|
const documentNumber = await useNextNumberRangeNumber(server,tenant.id,"invoices");
|
||||||
|
|
||||||
|
console.log(item);
|
||||||
|
|
||||||
|
const [contact, customer, project, contract] = await Promise.all([
|
||||||
|
fetchById(server, schema.contacts, item.contact),
|
||||||
|
fetchById(server, schema.customers, item.customer),
|
||||||
|
fetchById(server, schema.projects, item.project),
|
||||||
|
fetchById(server, schema.contracts, item.contract),
|
||||||
|
item.letterhead ? fetchById(server, schema.letterheads, item.letterhead) : null
|
||||||
|
|
||||||
|
]);
|
||||||
|
|
||||||
|
const profile = (await server.db.select().from(schema.authProfiles).where(and(eq(schema.authProfiles.user_id, item.created_by),eq(schema.authProfiles.tenant_id,tenant.id))).limit(1))[0];
|
||||||
|
|
||||||
|
console.log(profile)
|
||||||
|
|
||||||
|
const pdfData = getDocumentDataBackend(
|
||||||
|
{
|
||||||
|
...item,
|
||||||
|
state: "Gebucht",
|
||||||
|
documentNumber: documentNumber.usedNumber,
|
||||||
|
title: `Rechnung-Nr. ${documentNumber.usedNumber}`,
|
||||||
|
}, // Das Dokument (mit neuer Nummer)
|
||||||
|
tenant, // Tenant Object
|
||||||
|
customer, // Customer Object
|
||||||
|
contact, // Contact Object (kann null sein)
|
||||||
|
profile, // User Profile (Contact Person)
|
||||||
|
project, // Project Object
|
||||||
|
contract, // Contract Object
|
||||||
|
units, // Units Array
|
||||||
|
products, // Products Array
|
||||||
|
services // Services Array
|
||||||
|
);
|
||||||
|
|
||||||
|
|
||||||
|
return pdfData;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
// Formatiert Zahlen zu deutscher Währung
|
||||||
|
function renderCurrency(value: any, currency = "€") {
|
||||||
|
if (value === undefined || value === null) return "0,00 " + currency;
|
||||||
|
return Number(value).toFixed(2).replace(".", ",") + " " + currency;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Berechnet den Zeilenpreis (Menge * Preis * Rabatt)
|
||||||
|
function getRowAmount(row: any) {
|
||||||
|
const price = Number(row.price || 0);
|
||||||
|
const quantity = Number(row.quantity || 0);
|
||||||
|
const discount = Number(row.discountPercent || 0);
|
||||||
|
return quantity * price * (1 - discount / 100);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Berechnet alle Summen (Netto, Brutto, Steuern, Titel-Summen)
|
||||||
|
// Dies ersetzt 'documentTotal.value' aus dem Frontend
|
||||||
|
function calculateDocumentTotals(rows: any[], taxType: string) {
|
||||||
|
console.log(rows);
|
||||||
|
|
||||||
|
let totalNet = 0;
|
||||||
|
let totalNet19 = 0;
|
||||||
|
let totalNet7 = 0;
|
||||||
|
let totalNet0 = 0;
|
||||||
|
let titleSums: Record<string, number> = {};
|
||||||
|
|
||||||
|
// Aktueller Titel für Gruppierung
|
||||||
|
let currentTitle = "";
|
||||||
|
|
||||||
|
rows.forEach(row => {
|
||||||
|
if (row.mode === 'title') {
|
||||||
|
currentTitle = row.text || row.description || "Titel";
|
||||||
|
if (!titleSums[currentTitle]) titleSums[currentTitle] = 0;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (['normal', 'service', 'free'].includes(row.mode)) {
|
||||||
|
const amount = getRowAmount(row);
|
||||||
|
totalNet += amount;
|
||||||
|
|
||||||
|
// Summen pro Titel addieren
|
||||||
|
//if (!titleSums[currentTitle]) titleSums[currentTitle] = 0;
|
||||||
|
if(currentTitle.length > 0) titleSums[currentTitle] += amount;
|
||||||
|
|
||||||
|
// Steuer-Logik
|
||||||
|
const tax = taxType === "19 UStG" || taxType === "13b UStG" ? 0 : Number(row.taxPercent);
|
||||||
|
|
||||||
|
if (tax === 19) totalNet19 += amount;
|
||||||
|
else if (tax === 7) totalNet7 += amount;
|
||||||
|
else totalNet0 += amount;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const isTaxFree = ["13b UStG", "19 UStG"].includes(taxType);
|
||||||
|
|
||||||
|
const tax19 = isTaxFree ? 0 : totalNet19 * 0.19;
|
||||||
|
const tax7 = isTaxFree ? 0 : totalNet7 * 0.07;
|
||||||
|
const totalGross = totalNet + tax19 + tax7;
|
||||||
|
|
||||||
|
return {
|
||||||
|
totalNet,
|
||||||
|
totalNet19,
|
||||||
|
totalNet7,
|
||||||
|
totalNet0,
|
||||||
|
total19: tax19,
|
||||||
|
total7: tax7,
|
||||||
|
total0: 0,
|
||||||
|
totalGross,
|
||||||
|
titleSums // Gibt ein Objekt zurück: { "Titel A": 150.00, "Titel B": 200.00 }
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getDocumentDataBackend(
|
||||||
|
itemInfo: any, // Das Dokument objekt (createddocument)
|
||||||
|
tenant: any, // Tenant Infos (auth.activeTenantData)
|
||||||
|
customerData: any, // Geladener Kunde
|
||||||
|
contactData: any, // Geladener Kontakt (optional)
|
||||||
|
contactPerson: any, // Geladenes User-Profil (ersetzt den API Call)
|
||||||
|
projectData: any, // Projekt
|
||||||
|
contractData: any, // Vertrag
|
||||||
|
units: any[], // Array aller Einheiten
|
||||||
|
products: any[], // Array aller Produkte
|
||||||
|
services: any[] // Array aller Services
|
||||||
|
) {
|
||||||
|
const businessInfo = tenant.businessInfo || {}; // Fallback falls leer
|
||||||
|
|
||||||
|
// --- 1. Agriculture Logic ---
|
||||||
|
// Prüfen ob 'extraModules' existiert, sonst leeres Array annehmen
|
||||||
|
const modules = tenant.extraModules || [];
|
||||||
|
if (modules.includes("agriculture")) {
|
||||||
|
itemInfo.rows.forEach((row: any) => {
|
||||||
|
if (row.agriculture && row.agriculture.dieselUsage) {
|
||||||
|
row.agriculture.description = `${row.agriculture.dieselUsage} L Diesel zu ${renderCurrency(row.agriculture.dieselPrice)}/L verbraucht ${row.description ? "\n" + row.description : ""}`;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- 2. Tax Override Logic ---
|
||||||
|
let rows = JSON.parse(JSON.stringify(itemInfo.rows)); // Deep Clone um Original nicht zu mutieren
|
||||||
|
if (itemInfo.taxType === "13b UStG" || itemInfo.taxType === "19 UStG") {
|
||||||
|
rows = rows.map((row: any) => ({ ...row, taxPercent: 0 }));
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- 4. Berechnungen (Ersetzt Vue computed props) ---
|
||||||
|
const totals = calculateDocumentTotals(rows, itemInfo.taxType);
|
||||||
|
|
||||||
|
console.log(totals);
|
||||||
|
|
||||||
|
// --- 3. Rows Mapping & Processing ---
|
||||||
|
rows = rows.map((row: any) => {
|
||||||
|
const unit = units.find(i => i.id === row.unit) || { short: "" };
|
||||||
|
|
||||||
|
// Description Text Logic
|
||||||
|
if (!['pagebreak', 'title'].includes(row.mode)) {
|
||||||
|
if (row.agriculture && row.agriculture.description) {
|
||||||
|
row.descriptionText = row.agriculture.description;
|
||||||
|
} else if (row.description) {
|
||||||
|
row.descriptionText = row.description;
|
||||||
|
} else {
|
||||||
|
delete row.descriptionText;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Product/Service Name Resolution
|
||||||
|
if (!['pagebreak', 'title', 'text'].includes(row.mode)) {
|
||||||
|
if (row.mode === 'normal') {
|
||||||
|
const prod = products.find(i => i.id === row.product);
|
||||||
|
if (prod) row.text = prod.name;
|
||||||
|
}
|
||||||
|
if (row.mode === 'service') {
|
||||||
|
const serv = services.find(i => i.id === row.service);
|
||||||
|
if (serv) row.text = serv.name;
|
||||||
|
}
|
||||||
|
|
||||||
|
const rowAmount = getRowAmount(row);
|
||||||
|
|
||||||
|
return {
|
||||||
|
...row,
|
||||||
|
rowAmount: renderCurrency(rowAmount),
|
||||||
|
quantity: String(row.quantity).replace(".", ","),
|
||||||
|
unit: unit.short,
|
||||||
|
pos: String(row.pos),
|
||||||
|
price: renderCurrency(row.price),
|
||||||
|
discountText: row.discountPercent > 0 ? `(Rabatt: ${row.discountPercent} %)` : ""
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
return row;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
// --- 5. Handlebars Context ---
|
||||||
|
const generateContext = () => {
|
||||||
|
return {
|
||||||
|
// lohnkosten: null, // Backend hat diesen Wert oft nicht direkt, ggf. aus itemInfo holen
|
||||||
|
anrede: (contactData && contactData.salutation) || (customerData && customerData.salutation),
|
||||||
|
titel: (contactData && contactData.title) || (customerData && customerData.title),
|
||||||
|
vorname: (contactData && contactData.firstName) || (customerData && customerData.firstname), // Achte auf casing (firstName vs firstname) in deiner DB
|
||||||
|
nachname: (contactData && contactData.lastName) || (customerData && customerData.lastname),
|
||||||
|
kundenname: customerData && customerData.name,
|
||||||
|
zahlungsziel_in_tagen: itemInfo.paymentDays,
|
||||||
|
zahlungsart: itemInfo.payment_type === "transfer" ? "Überweisung" : "Lastschrift",
|
||||||
|
diesel_gesamtverbrauch: (itemInfo.agriculture && itemInfo.agriculture.dieselUsageTotal) || null
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const templateStartText = Handlebars.compile(itemInfo.startText || "");
|
||||||
|
const templateEndText = Handlebars.compile(itemInfo.endText || "");
|
||||||
|
|
||||||
|
// --- 6. Title Sums Formatting ---
|
||||||
|
let returnTitleSums: Record<string, string> = {};
|
||||||
|
Object.keys(totals.titleSums).forEach(key => {
|
||||||
|
returnTitleSums[key] = renderCurrency(totals.titleSums[key]);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Transfer logic (Falls nötig, hier vereinfacht)
|
||||||
|
let returnTitleSumsTransfer = { ...returnTitleSums };
|
||||||
|
|
||||||
|
// --- 7. Construct Final Object ---
|
||||||
|
|
||||||
|
// Adresse aufbereiten
|
||||||
|
const recipientArray = [
|
||||||
|
customerData.name,
|
||||||
|
...(customerData.nameAddition ? [customerData.nameAddition] : []),
|
||||||
|
...(contactData ? [`${contactData.firstName} ${contactData.lastName}`] : []),
|
||||||
|
itemInfo.address?.street || customerData.street || "",
|
||||||
|
...(itemInfo.address?.special ? [itemInfo.address.special] : []),
|
||||||
|
`${itemInfo.address?.zip || customerData.zip} ${itemInfo.address?.city || customerData.city}`,
|
||||||
|
].filter(Boolean); // Leere Einträge entfernen
|
||||||
|
|
||||||
|
console.log(contactPerson)
|
||||||
|
|
||||||
|
// Info Block aufbereiten
|
||||||
|
const infoBlock = [
|
||||||
|
{
|
||||||
|
label: itemInfo.documentNumberTitle || "Rechnungsnummer",
|
||||||
|
content: itemInfo.documentNumber || "ENTWURF",
|
||||||
|
}, {
|
||||||
|
label: "Kundennummer",
|
||||||
|
content: customerData.customerNumber,
|
||||||
|
}, {
|
||||||
|
label: "Belegdatum",
|
||||||
|
content: itemInfo.documentDate ? dayjs(itemInfo.documentDate).format("DD.MM.YYYY") : dayjs().format("DD.MM.YYYY"),
|
||||||
|
},
|
||||||
|
// Lieferdatum Logik
|
||||||
|
...(itemInfo.deliveryDateType !== "Kein Lieferdatum anzeigen" ? [{
|
||||||
|
label: itemInfo.deliveryDateType || "Lieferdatum",
|
||||||
|
content: !['Lieferzeitraum', 'Leistungszeitraum'].includes(itemInfo.deliveryDateType)
|
||||||
|
? (itemInfo.deliveryDate ? dayjs(itemInfo.deliveryDate).format("DD.MM.YYYY") : "")
|
||||||
|
: `${itemInfo.deliveryDate ? dayjs(itemInfo.deliveryDate).format("DD.MM.YYYY") : ""} - ${itemInfo.deliveryDateEnd ? dayjs(itemInfo.deliveryDateEnd).format("DD.MM.YYYY") : ""}`,
|
||||||
|
}] : []),
|
||||||
|
{
|
||||||
|
label: "Ansprechpartner",
|
||||||
|
content: contactPerson ? (contactPerson.name || contactPerson.full_name || contactPerson.email) : "-",
|
||||||
|
},
|
||||||
|
// Kontakt Infos
|
||||||
|
...((itemInfo.contactTel || contactPerson?.fixed_tel || contactPerson?.mobile_tel) ? [{
|
||||||
|
label: "Telefon",
|
||||||
|
content: itemInfo.contactTel || contactPerson?.fixed_tel || contactPerson?.mobile_tel,
|
||||||
|
}] : []),
|
||||||
|
...(contactPerson?.email ? [{
|
||||||
|
label: "E-Mail",
|
||||||
|
content: contactPerson.email,
|
||||||
|
}] : []),
|
||||||
|
// Objekt / Projekt / Vertrag
|
||||||
|
...(itemInfo.plant ? [{ label: "Objekt", content: "Objekt Name" }] : []), // Hier müsstest du Plant Data übergeben wenn nötig
|
||||||
|
...(projectData ? [{ label: "Projekt", content: projectData.name }] : []),
|
||||||
|
...(contractData ? [{ label: "Vertrag", content: contractData.contractNumber }] : [])
|
||||||
|
];
|
||||||
|
|
||||||
|
// Total Array für PDF Footer
|
||||||
|
const totalArray = [
|
||||||
|
{
|
||||||
|
label: "Nettobetrag",
|
||||||
|
content: renderCurrency(totals.totalNet),
|
||||||
|
},
|
||||||
|
...(totals.totalNet19 > 0 && !["13b UStG"].includes(itemInfo.taxType) ? [{
|
||||||
|
label: `zzgl. 19% USt auf ${renderCurrency(totals.totalNet19)}`,
|
||||||
|
content: renderCurrency(totals.total19),
|
||||||
|
}] : []),
|
||||||
|
...(totals.totalNet7 > 0 && !["13b UStG"].includes(itemInfo.taxType) ? [{
|
||||||
|
label: `zzgl. 7% USt auf ${renderCurrency(totals.totalNet7)}`,
|
||||||
|
content: renderCurrency(totals.total7),
|
||||||
|
}] : []),
|
||||||
|
{
|
||||||
|
label: "Gesamtbetrag",
|
||||||
|
content: renderCurrency(totals.totalGross),
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
return {
|
||||||
|
...itemInfo,
|
||||||
|
type: itemInfo.type,
|
||||||
|
taxType: itemInfo.taxType,
|
||||||
|
adressLine: `${businessInfo.name || ''}, ${businessInfo.street || ''}, ${businessInfo.zip || ''} ${businessInfo.city || ''}`,
|
||||||
|
recipient: recipientArray,
|
||||||
|
info: infoBlock,
|
||||||
|
title: itemInfo.title,
|
||||||
|
description: itemInfo.description,
|
||||||
|
// Handlebars Compilation ausführen
|
||||||
|
endText: templateEndText(generateContext()),
|
||||||
|
startText: templateStartText(generateContext()),
|
||||||
|
rows: rows,
|
||||||
|
totalArray: totalArray,
|
||||||
|
total: {
|
||||||
|
totalNet: renderCurrency(totals.totalNet),
|
||||||
|
total19: renderCurrency(totals.total19),
|
||||||
|
total0: renderCurrency(totals.total0), // 0% USt Zeilen
|
||||||
|
totalGross: renderCurrency(totals.totalGross),
|
||||||
|
// Diese Werte existieren im einfachen Backend-Kontext oft nicht (Zahlungen checken), daher 0 oder Logik bauen
|
||||||
|
totalGrossAlreadyPaid: renderCurrency(0),
|
||||||
|
totalSumToPay: renderCurrency(totals.totalGross),
|
||||||
|
titleSums: returnTitleSums,
|
||||||
|
titleSumsTransfer: returnTitleSumsTransfer
|
||||||
|
},
|
||||||
|
agriculture: itemInfo.agriculture,
|
||||||
|
// Falls du AdvanceInvoices brauchst, musst du die Objekte hier übergeben oder leer lassen
|
||||||
|
usedAdvanceInvoices: []
|
||||||
|
};
|
||||||
|
}
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user