Compare commits
1272 Commits
ddc9258dfe
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| e7554fa2cc | |||
| 7c1fabf58a | |||
| 1203b6cbd1 | |||
| 525f2906fb | |||
| b105382abf | |||
| b1cdec7d17 | |||
| 6b9de04d83 | |||
| f1d512b2e5 | |||
| 529ec0c77d | |||
| 246677b750 | |||
| c839714945 | |||
| 8614917a05 | |||
| 2de80ea6ca | |||
| 6f5fed0ffb | |||
| 767152c535 | |||
| 3128893ba2 | |||
| bcf460cfd5 | |||
| da704be925 | |||
| c049730599 | |||
| 0194345ed8 | |||
| 82d8cd36b9 | |||
| 66110da6c4 | |||
| 267648074c | |||
| 32b4c40e11 | |||
| f044195d86 | |||
| 202e20ddd5 | |||
| 905f2e7bf4 | |||
| b39a52fb20 | |||
| 098bd02808 | |||
| b35c991634 | |||
| dc376894be | |||
| 90788f22da | |||
| d901ebe365 | |||
| 7f6ba99328 | |||
| 8afdf06c8e | |||
| e1205a8de5 | |||
| 08da93b6c3 | |||
| b0ace924d4 | |||
| 593118c181 | |||
| db21b43120 | |||
| 67d2a05ac4 | |||
| b95d539907 | |||
| 76b363fdaf | |||
| 2cb0d9b607 | |||
| 037a10e93e | |||
| c36b9aa872 | |||
| 14a9435a5a | |||
| 8126c2d3f4 | |||
| a892b7a6e4 | |||
| be7b219569 | |||
| 998d725528 | |||
| 17cd3dc3a3 | |||
| 1d9488b64d | |||
| 9fb520d8c3 | |||
| 3bcfacdf84 | |||
| 8ea41802dd | |||
| 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 |
19
.gitea/ISSUE_TEMPLATE/bug_report.md
Normal file
19
.gitea/ISSUE_TEMPLATE/bug_report.md
Normal file
@@ -0,0 +1,19 @@
|
||||
---
|
||||
name: 🐛 Bug Report
|
||||
about: Erstelle einen Bericht, um uns zu helfen, das Projekt zu verbessern.
|
||||
title: '[BUG] '
|
||||
labels: Problem
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
**Beschreibung**
|
||||
|
||||
|
||||
**Reproduktion**
|
||||
|
||||
|
||||
**Screenshots**
|
||||
|
||||
|
||||
**Achtung: Achte bitte auf Datenschutz deiner Daten sowie der Daten deiner Kunden. Sollten ein Screenshot nur mit Daten möglich sein, schwärze diese bitte vor dem Upload.**
|
||||
17
.gitea/ISSUE_TEMPLATE/feature_request.md
Normal file
17
.gitea/ISSUE_TEMPLATE/feature_request.md
Normal file
@@ -0,0 +1,17 @@
|
||||
---
|
||||
name: ✨ Feature Request
|
||||
about: Schlage eine Idee für dieses Projekt vor.
|
||||
title: '[FEATURE] '
|
||||
labels: Funktionswunsch
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
**Ist dein Feature-Wunsch mit einem Problem verbunden?**
|
||||
|
||||
|
||||
**Lösungsvorschlag**
|
||||
|
||||
|
||||
**Alternativen**
|
||||
|
||||
77
.gitea/workflows/build-push.yaml
Normal file
77
.gitea/workflows/build-push.yaml
Normal file
@@ -0,0 +1,77 @@
|
||||
name: Build and Push Docker Images
|
||||
run-name: Build Backend & Frontend by @${{ github.actor }}
|
||||
|
||||
on: [push]
|
||||
|
||||
env:
|
||||
# Passe dies an deine Registry an.
|
||||
# Wenn du die Gitea-interne Registry nutzt, ist es meist einfach der Hostname deiner Gitea-Instanz.
|
||||
# Beispiel: gitea.deine-domain.de
|
||||
REGISTRY_HOST: git.federspiel.tech
|
||||
# Der Name des Repos (z.B. user/repo)
|
||||
IMAGE_NAME: ${{ github.repository }}
|
||||
ACTOR: flfeders
|
||||
|
||||
jobs:
|
||||
build-backend:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Check out repository code
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Log in to Docker Registry
|
||||
uses: docker/login-action@v2
|
||||
with:
|
||||
registry: ${{ env.REGISTRY_HOST }}
|
||||
# Gitea stellt secrets.GITHUB_TOKEN automatisch bereit (wie GitLab CI_JOB_TOKEN)
|
||||
username: ${{ env.ACTOR }}
|
||||
password: ${{ vars.CI_TOKEN }}
|
||||
|
||||
- name: Extract metadata (tags, labels) for Backend
|
||||
id: meta-backend
|
||||
uses: docker/metadata-action@v4
|
||||
with:
|
||||
images: ${{ env.REGISTRY_HOST }}/${{ env.IMAGE_NAME }}/backend
|
||||
tags: |
|
||||
type=ref,event=branch
|
||||
type=ref,event=tag
|
||||
type=raw,value=latest,enable=${{ github.ref == 'refs/heads/main' }}
|
||||
|
||||
- name: Build and push Backend
|
||||
uses: docker/build-push-action@v4
|
||||
with:
|
||||
context: ./backend # Hier wird der Ordner gewechselt (wie 'cd backend')
|
||||
push: true
|
||||
tags: ${{ steps.meta-backend.outputs.tags }}
|
||||
labels: ${{ steps.meta-backend.outputs.labels }}
|
||||
|
||||
build-frontend:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Check out repository code
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Log in to Docker Registry
|
||||
uses: docker/login-action@v2
|
||||
with:
|
||||
registry: ${{ env.REGISTRY_HOST }}
|
||||
username: ${{ env.ACTOR }}
|
||||
password: ${{ vars.CI_TOKEN }}
|
||||
|
||||
- name: Extract metadata (tags, labels) for Frontend
|
||||
id: meta-frontend
|
||||
uses: docker/metadata-action@v4
|
||||
with:
|
||||
images: ${{ env.REGISTRY_HOST }}/${{ env.IMAGE_NAME }}/frontend
|
||||
tags: |
|
||||
type=ref,event=branch
|
||||
type=ref,event=tag
|
||||
type=raw,value=latest,enable=${{ github.ref == 'refs/heads/main' }}
|
||||
|
||||
- name: Build and push Frontend
|
||||
uses: docker/build-push-action@v4
|
||||
with:
|
||||
context: ./frontend # Hier wird der Ordner gewechselt (wie 'cd frontend')
|
||||
push: true
|
||||
tags: ${{ steps.meta-frontend.outputs.tags }}
|
||||
labels: ${{ steps.meta-frontend.outputs.labels }}
|
||||
110
README.md
110
README.md
@@ -1 +1,109 @@
|
||||
TEST
|
||||
|
||||
|
||||
|
||||
# Docker Compose Setup
|
||||
|
||||
## ENV Vars
|
||||
|
||||
- DOMAIN
|
||||
- PDF_LICENSE
|
||||
- DB_PASS
|
||||
- DB_USER
|
||||
- CONTACT_EMAIL
|
||||
|
||||
## Docker Compose File
|
||||
~~~
|
||||
services:
|
||||
frontend:
|
||||
image: git.federspiel.tech/flfeders/fedeo/frontend:main
|
||||
restart: always
|
||||
environment:
|
||||
- NUXT_PUBLIC_API_BASE=https://${DOMAIN}/backend
|
||||
- NUXT_PUBLIC_PDF_LICENSE=${PDF_LICENSE}
|
||||
networks:
|
||||
- traefik
|
||||
labels:
|
||||
- "traefik.enable=true"
|
||||
- "traefik.docker.network=traefik"
|
||||
- "traefik.port=3000"
|
||||
# Middlewares
|
||||
- "traefik.http.middlewares.fedeo-frontend-redirect-web-secure.redirectscheme.scheme=https"
|
||||
# Web Entrypoint
|
||||
- "traefik.http.routers.fedeo-frontend.middlewares=fedeo-frontend-redirect-web-secure"
|
||||
- "traefik.http.routers.fedeo-frontend.rule=Host(`${DOMAIN}`) && PathPrefix(`/`)"
|
||||
- "traefik.http.routers.fedeo-frontend.entrypoints=web"
|
||||
# Web Secure Entrypoint
|
||||
- "traefik.http.routers.fedeo-frontend-secure.rule=Host(`${DOMAIN}`) && PathPrefix(`/`)"
|
||||
- "traefik.http.routers.fedeo-frontend-secure.entrypoints=web-secured" #
|
||||
- "traefik.http.routers.fedeo-frontend-secure.tls.certresolver=mytlschallenge"
|
||||
backend:
|
||||
image: git.federspiel.tech/flfeders/fedeo/backend:main
|
||||
restart: always
|
||||
environment:
|
||||
- INFISICAL_CLIENT_ID=
|
||||
- INFISICAL_CLIENT_SECRET=
|
||||
- NODE_ENV=production
|
||||
networks:
|
||||
- traefik
|
||||
labels:
|
||||
- "traefik.enable=true"
|
||||
- "traefik.docker.network=traefik"
|
||||
- "traefik.port=3100"
|
||||
# Middlewares
|
||||
- "traefik.http.middlewares.fedeo-backend-redirect-web-secure.redirectscheme.scheme=https"
|
||||
- "traefik.http.middlewares.fedeo-backend-strip.stripprefix.prefixes=/backend"
|
||||
# Web Entrypoint
|
||||
- "traefik.http.routers.fedeo-backend.middlewares=fedeo-backend-redirect-web-secure"
|
||||
- "traefik.http.routers.fedeo-backend.rule=Host(`${DOMAIN}`) && PathPrefix(`/backend`)"
|
||||
- "traefik.http.routers.fedeo-backend.entrypoints=web"
|
||||
# Web Secure Entrypoint
|
||||
- "traefik.http.routers.fedeo-backend-secure.rule=Host(`${DOMAIN}`) && PathPrefix(`/backend`)"
|
||||
- "traefik.http.routers.fedeo-backend-secure.entrypoints=web-secured" #
|
||||
- "traefik.http.routers.fedeo-backend-secure.tls.certresolver=mytlschallenge"
|
||||
- "traefik.http.routers.fedeo-backend-secure.middlewares=fedeo-backend-strip"
|
||||
# db:
|
||||
# image: postgres
|
||||
# restart: always
|
||||
# shm_size: 128mb
|
||||
# environment:
|
||||
# POSTGRES_PASSWORD:
|
||||
# POSTGRES_USER:
|
||||
# POSTGRES_DB:
|
||||
# volumes:
|
||||
# - ./pg-data:/var/lib/postgresql/data
|
||||
# ports:
|
||||
# - "5432:5432"
|
||||
traefik:
|
||||
image: traefik:v2.11
|
||||
restart: unless-stopped
|
||||
container_name: traefik
|
||||
command:
|
||||
- "--api.insecure=false"
|
||||
- "--api.dashboard=false"
|
||||
- "--api.debug=false"
|
||||
- "--providers.docker=true"
|
||||
- "--providers.docker.exposedbydefault=false"
|
||||
- "--providers.docker.network=traefik"
|
||||
- "--entrypoints.web.address=:80"
|
||||
- "--entrypoints.web-secured.address=:443"
|
||||
- "--accesslog=true"
|
||||
- "--accesslog.filepath=/logs/access.log"
|
||||
- "--accesslog.bufferingsize=5000"
|
||||
- "--accesslog.fields.defaultMode=keep"
|
||||
- "--accesslog.fields.headers.defaultMode=keep"
|
||||
- "--certificatesresolvers.mytlschallenge.acme.tlschallenge=true" #
|
||||
- "--certificatesresolvers.mytlschallenge.acme.email=${CONTACT_EMAIL}"
|
||||
- "--certificatesresolvers.mytlschallenge.acme.storage=/letsencrypt/acme.json"
|
||||
ports:
|
||||
- 80:80
|
||||
- 443:443
|
||||
volumes:
|
||||
- "./traefik/letsencrypt:/letsencrypt" # <== Volume for certs (TLS)
|
||||
- "/var/run/docker.sock:/var/run/docker.sock:ro"
|
||||
- "./traefik/logs:/logs"
|
||||
networks:
|
||||
- traefik
|
||||
networks:
|
||||
traefik:
|
||||
external: false
|
||||
~~~
|
||||
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`)
|
||||
|
||||
---
|
||||
13
backend/db/index.ts
Normal file
13
backend/db/index.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
import { drizzle } from "drizzle-orm/node-postgres"
|
||||
import { Pool } from "pg"
|
||||
import {secrets} from "../src/utils/secrets";
|
||||
import * as schema from "./schema"
|
||||
|
||||
|
||||
|
||||
export const pool = new Pool({
|
||||
connectionString: secrets.DATABASE_URL,
|
||||
max: 10, // je nach Last
|
||||
})
|
||||
|
||||
export const db = drizzle(pool , {schema})
|
||||
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").default("Erstkontakt"),
|
||||
})
|
||||
|
||||
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
|
||||
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,
|
||||
},
|
||||
})
|
||||
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")
|
||||
167
backend/src/index.ts
Normal file
167
backend/src/index.ts
Normal file
@@ -0,0 +1,167 @@
|
||||
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 {
|
||||
console.log("Testing DB Connection:")
|
||||
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
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user