commit working state

Too many changes to split them up. This commit contains roughly two months worth of work.

This commit introduces the first aggregate and rewrites the ticket system to follow DDD patterns.
It adds Spartan NG as the new component library. TanStack Query to replace GraphQL. Oh and Prisma is
almost over as well.
This commit is contained in:
Marcel Arndt
2026-01-19 12:21:48 +01:00
parent 62e663d053
commit ce676f20a4
334 changed files with 64333 additions and 52554 deletions
@@ -0,0 +1,74 @@
,,,Vertrag,Vertrag,Vertrag,Vertrag,Vertrag,Vertrag,Vertrag,Vertrag,"Stunden innerhalb der Soll-Stunden","Stunden innerhalb der Soll-Stunden","Stunden innerhalb der Soll-Stunden","Stunden innerhalb der Soll-Stunden","Stunden innerhalb der Soll-Stunden","Stunden innerhalb der Soll-Stunden",Ist-/Soll-Stunden,Ist-/Soll-Stunden,Ist-/Soll-Stunden,Ist-/Soll-Stunden,Berechnung,Berechnung,Berechnung,Berechnung,Berechnung,Berechnung,Berechnung,Berechnung,Berechnung,Berechnung,Berechnung,Berechnung,Berechnung,Berechnung,Berechnung,Salden,Salden,Salden,Salden,Salden,Salden,Salden,Salden,Auszahlen,Auszahlen,Auszahlen,Auszahlen,Auszahlen,Auszahlen,Auszahlen,Auszahlen,Auszahlen,Auszahlen
,,,,,,,,,,,"Gearbeitete Stunden","Gearbeitete Stunden","Urlaub nehmen","Urlaub nehmen",Krankheitsstunden,Krankheitsstunden,Gesamt,Gesamt,Vertrag,Vertrag,Plus/Min,Mehrstunden,Überstunden,"Zuschlag P-Schein","Krank ohne AU","Krank ohne AU","Nachtzuschlag 25 (25%)","Nachtzuschlag 25 (25%)","Nachtzuschlag 40 (40%)","Nachtzuschlag 40 (40%)","Sonntagszuschlag (50%)","Sonntagszuschlag (50%)","Verpfl.-pauschale (14%)","Samstagzuschlag (15%)","Samstagzuschlag (15%)",Zeitausgleich,Zeitausgleich,Zeitausgleich,Zeitausgleich,Zeitausgleich,Urlaub,Urlaub,Urlaub,Mehrstunden,"Überstunden auszahlen",Fahrtkosten,Verpflegungspauschale,"Zuschlag P-Schein",25%,40%,50%,14%,15%
Persnr.,Nachname,Vorname,Art,P-Schein,"Recht auf Zuschläge?",Start,Tage,Stunden,Max,"Urlaubstage pro Jahr",Tage,Stunden,Tage,Stunden,Tage,Stunden,Tage,Stunden,Tage,Stunden,Stunden,Stunden,Stunden,anzahl,Tage,Stunden,Tage,Stunden,Tage,Stunden,Tage,Stunden,Tage,Tage,Stunden,Start,+/-,Änderungen,Auszahlen,Jetzt,Start,Abbau,Jetzt,Stunden,Stunden,km,Tage,Anzahl,Stunden,Stunden,Stunden,Stunden,Stunden
169,Safari,"Abdul Latif ",Fest,Ja,Ja,2025-09-01,5.00,40.00,0.00,20,21,185.2167,1,8,0,0,22,193.21666666667,21.667,173.333,19.88366666667,0.00033333332999064,19.88333333334,21,0,0,0,0,0,0,0,0,18,2,18.933333333333,0.334,19.88366666667,19.88366666667,19.88366666667,0.334,14,1,13,0.00033333332999064,19.88333333334,0.00,20,21,0,0,0,15.783333333333,18.933333333333
93,"Moshtagh Askari",Adel,Fest,Ja,Ja,2025-01-01,5.00,40.00,0.00,20,23,198.9167,0,0,0,0,23,198.91666666667,21.667,173.333,25.58366666667,0.00033333332999064,25.58333333334,23,0,0,0,0,0,0,0,0,20,2,17.45,0.007,25.58366666667,25.58366666667,25.58366666667,0.0070000000000014,31,0,31,0.00033333332999064,25.58333333334,0.00,22,23,0,0,0,14.966666666667,17.45
18,Mihailescu,"Adrian Lucian",Fest,Ja,Ja,2025-09-01,5.00,40.00,0.00,20,21,178.6,0,0,0,0,21,178.6,21.667,173.333,5.267,0.00033333332999064,5.26666666667,21,0,0,0,0,0,0,0,0,11,3,25.65,0,5.267,5.267,5.267,0,20,0,20,0.00033333332999064,5.26666666667,0.00,13,21,0,0,0,9.6833333333333,25.65
,"Esmaeili Dashtbayaz",Amin,Fest,Nein,Ja,2025-04-01,5.00,40.00,0.00,,21,181.0833,0,0,0,0,21,181.08333333333,21.667,173.333,7.75033333333,0.00033333332999064,7.75,,0,0,0,0,0,0,0,0,18,3,24.033333333333,-2.066,7.75033333333,7.75033333333,5.68433333333,8.8817841970013E-16,12,0,12,0.00033333332999064,5.684,0.00,20,0,0,0,0,13.05,24.033333333333
,Ahmadpour,Amirhossein,Fest,Ja,Ja,2025-09-01,5.00,40.00,0.00,24,22,194.75,0,0,0,0,22,194.75,21.667,173.333,21.417,0.00033333332999064,21.41666666667,22,0,0,1,1.1333333333333,0,0,0,0,20,1,8.75,0,21.417,21.417,21.417,0,9,0,9,0.00033333332999064,21.41666666667,0.00,21,22,1.1333333333333,0,0,18.366666666667,8.75
188,"Malani Karizi",Arasch,Fest,Nein,Ja,2025-09-01,5.00,40.00,0.00,20,24,207.75,0,0,0,0,24,207.75,21.667,173.333,34.417,0.00033333332999064,34.41666666667,,0,0,1,1.5,0,0,1,8.8666666666667,19,3,24.55,-108.997,34.417,34.417,0,-74.58,5,0,5,0,0,0.00,23,0,1.5,0,8.8666666666667,14.333333333333,24.55
113,Ziaudin,Atiq,Fest,Ja,Ja,2025-02-24,5.00,40.00,0.00,20,20,172.6,0,0,0,0,20,172.6,21.667,173.333,-0.733,0,0,20,0,0,0,0,0,0,0,0,19,1,8.0166666666667,14.047,-0.733,-0.733,0,13.314,9,0,9,0,0,0.00,20,20,0,0,0,12.583333333333,8.0166666666667
,Rahmati,Behnam,Fest,Ja,Ja,2025-04-01,5.00,40.00,0.00,,16,143.95,0,0,2,16,18,159.95,21.667,173.333,-13.383,0,0,16,0,0,0,0,0,0,0,0,16,0,0,0,-13.383,-13.383,0,-13.383,13,0,13,0,0,0.00,16,16,0,0,0,15.95,0
,Aba,Cuan,Fest,Nein,Ja,2025-08-01,5.00,40.00,0.00,,10,69.45,0,0,2,16,12,85.45,21.667,173.333,-87.883,0,0,,0,0,0,0,0,0,0,0,0,0,0,-228.333,-87.883,-87.883,0,-316.216,4,0,4,0,0,0.00,1,0,0,0,0,0,0
,Haghighivand,Danial,Fest,Nein,Ja,2025-04-01,5.00,40.00,0.00,,17,152.2333,0,0,1,8,18,160.23333333333,21.667,173.333,-13.09966666667,0,0,,0,0,0,0,0,0,0,0,17,0,0,0,-13.09966666667,-13.09966666667,0,-13.09966666667,3,0,3,0,0,0.00,17,0,0,0,0,16.233333333333,0
,Mohammadi,Danial,Fest,Ja,Ja,2025-01-01,5.00,40.00,0.00,20,19,165.0167,0,0,0,0,19,165.01666666667,21.667,173.333,-8.31633333333,0,0,19,0,0,0,0,0,0,0,0,17,2,14.5,0,-8.31633333333,-8.31633333333,0,-8.31633333333,7,0,7,0,0,0.00,17,19,0,0,0,14.516666666667,14.5
,Shahrasebi,Danial,Fest,,,2021-05-01,0.00,0.00,0.00,,,,0,0,0,0,0,0,0,0,0,0,0,,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,5,0,5,0,0,,,0,0,0,0,0,0
,Emkani,Daniel,Fest,Nein,Ja,2025-07-15,3.00,25.00,0.00,,7,58.9167,3,24,3,24,13,106.91666666667,13,108.333,-1.41633333333,0,0,,0,0,0,0,0,0,0,0,0,2,15.65,0,-1.41633333333,-1.41633333333,0,-1.41633333333,7,3,4,0,0,0.00,6,0,0,0,0,0,15.65
,Lolies,Danny,Fest,Ja,Ja,2025-03-01,5.00,40.00,0.00,,19,161.4833,0,0,1,8,20,169.48333333333,21.667,173.333,-3.84966666667,0,0,19,2,16.5,0,0,0,0,0,0,15,2,16.566666666667,-5.933,-3.84966666667,-3.84966666667,0,-9.78266666667,10,0,10,0,0,0.00,17,19,0,0,0,10.7,16.566666666667
,Hastedt,David,Fest,Nein,Ja,2025-01-27,4.00,32.00,0.00,20,13,110.2,2,16,0,0,15,126.2,17.333,138.667,-12.467,0,0,,1,8,0,0,0,0,0,0,10,0,0,0,-12.467,-12.467,0,-12.467,2,2,0,0,0,0.00,10,0,0,0,0,8.6666666666667,0
2,Shahrasebi,David,Fest,Ja,Ja,2022-10-01,5.00,40.00,0.00,20,,,0,0,0,0,0,0,21.667,173.333,0,0,0,,0,0,0,0,0,0,0,0,0,0,0,151.34,0,0,0,151.34,2,0,2,0,0,,,0,0,0,0,0,0
,Golmohammadi,"Davood ",Fest,Ja,Ja,2025-08-01,5.00,40.00,0.00,20,19,171.6667,1,8,0,0,20,179.66666666667,21.667,173.333,6.33366666667,0.00033333332999064,6.33333333334,19,0,0,0,0,0,0,0,0,19,0,0,0,6.33366666667,6.33366666667,6.33366666667,0,2,1,1,0.00033333332999064,6.33333333334,0.00,19,19,0,0,0,19.666666666667,0
202,Pouzesh,Esmail,Fest,Ja,Ja,2025-09-01,5.00,40.00,0.00,20,21,170.3667,0,0,0,0,21,170.36666666667,21.667,173.333,-2.96633333333,0,0,21,0,0,20,66.283333333333,11,21.333333333333,0,0,0,0,0,0,-2.96633333333,-2.96633333333,0,-2.96633333333,12,0,12,0,0,0.00,20,21,66.283333333333,21.333333333333,0,0,0
187,"Sajaei Khouzestani",Farid,Fest,Ja,Ja,2025-04-07,5.00,40.00,0.00,20,23,203.5,0,0,0,0,23,203.5,21.667,173.333,30.167,0.00033333332999064,30.16666666667,23,0,0,0,0,0,0,0,0,19,3,28.75,0,30.167,30.167,30.167,0,19,0,19,0.00033333332999064,30.16666666667,0.00,22,23,0,0,0,16.25,28.75
191,"Faridani Rad",Farzad,Fest,Ja,Ja,2025-02-01,5.00,40.00,0.00,20,19,157.2,0,0,2,16,21,173.2,21.667,173.333,-0.13300000000001,0,0,19,1,8,19,59.9,5,11.55,0,0,0,0,0,0,-0.13300000000001,-0.13300000000001,0,-0.13300000000001,0,0,0,0,0,0.00,19,19,59.9,11.55,0,0,0
,Movafagh,"Farzad ",Fest,Ja,Ja,2025-02-03,5.00,40.00,0.00,20,21,182.65,0,0,0,0,21,182.65,21.667,173.333,9.317,0.00033333332999064,9.31666666667,21,0,0,0,0,0,0,0,0,19,2,16.016666666667,0,9.317,9.317,9.317,0,17,0,17,0.00033333332999064,9.31666666667,0.00,20,21,0,0,0,14.633333333333,16.016666666667
89,Hashimazada,Firuz,Fest,Ja,Ja,2025-01-01,5.00,40.00,0.00,20,,0,0,0,16,144,16,144,21.667,173.333,-29.333,0,0,,0,0,0,0,0,0,0,0,0,0,0,0,-29.333,-29.333,0,-29.333,8,0,8,0,0,,,0,0,0,0,0,0
,Saeedpour,Fouad,Fest,Ja,Ja,2025-09-01,5.00,40.00,0.00,,21,180.6833,0,0,0,0,21,180.68333333333,21.667,173.333,7.35033333333,0.00033333332999064,7.35,21,0,0,5,17.5,5,10.55,2,18.583333333333,10,4,33.033333333333,0,7.35033333333,7.35033333333,7.35033333333,0,26,0,26,0.00033333332999064,7.35,0.00,21,21,17.5,10.55,18.583333333333,8.5166666666667,33.033333333333
55,Khodayaran,Hossein,Fest,Ja,Ja,2025-01-01,5.00,40.00,0.00,20,19,152.55,0,0,0,0,19,152.55,21.667,173.333,-20.783,0,0,19,0,0,18,56.733333333333,5,11.55,1,3.5166666666667,0,0,0,0,-20.783,-20.783,0,-20.783,6,0,6,0,0,0.00,18,19,56.733333333333,11.55,3.5166666666667,0,0
206,Rasulkhani,Hossein,Fest,Ja,Ja,2025-03-06,5.00,40.00,0.00,20,9,76.4667,10,80,0,0,19,156.46666666667,21.667,173.333,-16.86633333333,0,0,9,0,0,0,0,0,0,1,9,6,2,16.2,0,-16.86633333333,-16.86633333333,0,-16.86633333333,29,10,19,0,0,0.00,9,9,0,0,9,3.2666666666667,16.2
,"Mohamed Salah","Hussin Noureldin",Fest,Ja,Ja,2025-06-06,5.00,40.00,0.00,20,20,173.6667,0,0,0,0,20,173.66666666667,21.667,173.333,0.33366666667001,0.00033333332999064,0.33333333334002,20,0,0,0,0,0,0,0,0,18,2,17.616666666667,0,0.33366666667001,0.33366666667001,0.33366666667001,0,0,0,0,0.00033333332999064,0.33333333334002,0.00,19,20,0,0,0,12.05,17.616666666667
50,"Rohany Oskoee",Iman,Fest,Nein,Ja,2025-01-01,5.00,40.00,0.00,20,23,196.8667,0,0,0,0,23,196.86666666667,21.667,173.333,23.53366666667,0.00033333332999064,23.53333333334,,0,0,0,0,0,0,0,0,20,2,16.883333333333,0,23.53366666667,23.53366666667,23.53366666667,0,36,0,36,0.00033333332999064,23.53333333334,0.00,21,0,0,0,0,13.483333333333,16.883333333333
7,Salehi,Iman,Fest,Ja,Ja,2025-02-01,5.00,40.00,0.00,20,21,176.55,0,0,0,0,21,176.55,21.667,173.333,3.217,0.00033333332999064,3.21666666667,21,0,0,20,62.466666666667,7,10.9,1,10,0,1,8.75,0,3.217,3.217,3.217,0,0,0,0,0.00033333332999064,3.21666666667,0.00,21,21,62.466666666667,10.9,10,0,8.75
,Krüger,Julian,Fest,Ja,Ja,2025-01-15,5.00,40.00,0.00,20,20,171.2833,0,0,0,0,20,171.28333333333,21.667,173.333,-2.04966666667,0,0,20,0,0,2,7,2,4.4333333333333,0,0,0,1,6.05,0,-2.04966666667,-2.04966666667,0,-2.04966666667,8,0,8,0,0,0.00,17,20,7,4.4333333333333,0,0,6.05
,Hazrati,Kamyar,Fest,,Ja,2025-02-24,5.00,40.00,0.00,20,21,182.2333,0,0,0,0,21,182.23333333333,21.667,173.333,8.90033333333,0.00033333332999064,8.9,,0,0,0,0,0,0,0,0,18,3,22.616666666667,0,8.90033333333,8.90033333333,8.90033333333,0,10,0,10,0.00033333332999064,8.9,0.00,19,0,0,0,0,15.616666666667,22.616666666667
,Saei,Kasra,Fest,Nein,Ja,2025-08-01,5.00,40.00,0.00,,8,56.2333,2,16,0,0,10,72.233333333333,21.667,173.333,-101.09966666667,0,0,,2,15,0,0,0,0,0,0,0,0,0,-186.848,-101.09966666667,-101.09966666667,0,-287.94766666667,0,2,-2,0,0,0.00,,0,0,0,0,0,0
182,Kalantari,Kaveh,Fest,Ja,Ja,2025-03-01,5.00,40.00,0.00,20,18,155.1667,6,48,0,0,24,203.16666666667,21.667,173.333,29.83366666667,0.00033333332999064,29.83333333334,18,0,0,1,1.05,0,0,2,17.733333333333,13,3,25.533333333333,0,29.83366666667,29.83366666667,29.83366666667,0,17,6,11,0.00033333332999064,29.83333333334,0.00,18,18,1.05,0,17.733333333333,6.85,25.533333333333
,Sahem,Khalid,Fest,Ja,Ja,2025-06-16,5.00,40.00,0.00,20,20,168.7167,0,0,0,0,20,168.71666666667,21.667,173.333,-4.61633333333,0,0,20,0,0,0,0,0,0,0,0,18,1,8.6833333333333,0,-4.61633333333,-4.61633333333,0,-4.61633333333,15,0,15,0,0,0.00,19,20,0,0,0,9.5333333333333,8.6833333333333
194,Saffarnajib,Majid,Fest,Ja,Ja,2025-04-01,3.00,21.00,0.00,16,17,145.5667,0,0,0,0,17,145.56666666667,13,91,54.56666666667,54.56666666667,0,17,0,0,2,3.85,0,0,0,0,16,0,0,0,54.56666666667,54.56666666667,54.56666666667,0,16,0,16,54.56666666667,0,0.00,17,17,3.85,0,0,8.7166666666667,0
177,Juyande,Masoud,Fest,Nein,Ja,2025-01-01,5.00,40.00,0.00,20,14,113.4333,7,56,0,0,21,169.43333333333,21.667,173.333,-3.89966666667,0,0,,0,0,10,33.116666666667,5,11.116666666667,0,0,3,0,0,0.009,-3.89966666667,-3.89966666667,0,-3.89066666667,-7,7,-14,0,0,0.00,13,0,33.116666666667,11.116666666667,0,0.7,0
53,Eggerstedt,Matthias,Fest,Ja,Ja,2025-01-01,5.00,40.00,0.00,20,20,171.6833,0,0,0,0,20,171.68333333333,21.667,173.333,-1.64966666667,0,0,20,0,0,0,0,0,0,0,0,18,2,16.933333333333,0,-1.64966666667,-1.64966666667,0,-1.64966666667,10,0,10,0,0,0.00,20,20,0,0,0,10.75,16.933333333333
,Arabnia,Mehdi,Fest,Ja,Ja,2025-05-05,5.00,40.00,0.00,,21,185.9333,1,8,0,0,22,193.93333333333,21.667,173.333,20.60033333333,0.00033333332999064,20.6,21,0,0,0,0,0,0,0,0,18,3,24.766666666667,0,20.60033333333,20.60033333333,20.60033333333,0,7,1,6,0.00033333332999064,20.6,0.00,20,21,0,0,0,17.166666666667,24.766666666667
,"Jalali Wagener",Mehdi,Fest,Ja,Ja,2025-02-01,5.00,40.00,0.00,,23,195.7167,0,0,0,0,23,195.71666666667,21.667,173.333,22.38366666667,0.00033333332999064,22.38333333334,23,0,0,0,0,0,0,0,0,20,3,24.033333333333,0,22.38366666667,22.38366666667,22.38366666667,0,-1,0,-1,0.00033333332999064,22.38333333334,0.00,22,23,0,0,0,11.683333333333,24.033333333333
,Taghadosi,Mehdi,Fest,Ja,Ja,2025-08-08,5.00,40.00,0.00,,19,167.25,0,0,0,0,19,167.25,21.667,173.333,-6.083,0,0,19,0,0,0,0,0,0,0,0,17,2,16.266666666667,0,-6.083,-6.083,0,-6.083,10,0,10,0,0,0.00,19,19,0,0,0,14.983333333333,16.266666666667
,Mazidi,Mehrdad,Fest,Ja,Ja,2025-07-01,5.00,40.00,0.00,20,19,166.9167,0,0,0,0,19,166.91666666667,21.667,173.333,-6.41633333333,0,0,19,0,0,0,0,0,0,0,0,17,2,17.616666666667,0,-6.41633333333,-6.41633333333,0,-6.41633333333,10,0,10,0,0,0.00,18,19,0,0,0,13.3,17.616666666667
165,Koell,"Melanie Nadine",Fest,Nein,Ja,2025-01-01,5.00,40.00,0.00,20,1,6.5,0,0,0,0,1,6.5,21.667,173.333,-166.833,0,0,,0,0,0,0,0,0,0,0,0,0,0,-306.596,-166.833,-166.833,0,-473.429,12,0,12,0,0,0.00,,0,0,0,0,0,0
,Zekarias,Michael,Fest,Nein,Ja,2025-11-01,5.00,40.00,0.00,,21,172.8333,0,0,0,0,21,172.83333333333,21.667,173.333,-0.49966666667001,0,0,,0,0,5,14.616666666667,0,0,0,0,14,1,8.0333333333333,0,-0.49966666667001,-0.49966666667001,0,-0.49966666667001,0,0,0,0,0,0.00,20,0,14.616666666667,0,0,6.1833333333333,8.0333333333333
119,Pourhossein,Milad,Fest,Ja,Ja,2025-09-01,5.00,40.00,0.00,20,22,182.6,0,0,0,0,22,182.6,21.667,173.333,9.267,0.00033333332999064,9.26666666667,22,0,0,10,30.9,2,0.35,0,0,9,1,9.25,-49.564,9.267,9.267,0,-40.297,10,0,10,0,0,0.00,20,22,30.9,0.35,0,3.7,9.25
144,Maleki,Mohammad,Fest,Ja,Ja,2025-09-01,3.00,24.00,0.00,20,9,72.6833,0,0,0,0,9,72.683333333333,13,104,-31.316666666667,0,0,9,1,8,0,0,0,0,0,0,6,0,0,-313.845,-31.316666666667,-31.316666666667,0,-345.16166666667,-1,0,-1,0,0,0.00,6,9,0,0,0,3.55,0
217,"Rasul Khani",Mohammad,Fest,Ja,Ja,2025-03-03,5.00,40.00,0.00,20,11,95,10,80,0,0,21,175,21.667,173.333,1.667,0.00033333332999064,1.66666666667,11,0,0,0,0,0,0,0,0,10,1,8.5166666666667,0,1.667,1.667,1.667,0,20,10,10,0.00033333332999064,1.66666666667,0.00,11,11,0,0,0,6.4833333333333,8.5166666666667
96,Sabahi,Mohammad,Fest,Ja,Ja,2025-01-01,5.00,40.00,0.00,20,20,170.15,0,0,0,0,20,170.15,21.667,173.333,-3.183,0,0,20,0,0,0,0,0,0,0,0,19,0,0,0,-3.183,-3.183,0,-3.183,9,0,9,0,0,0.00,19,20,0,0,0,11.65,0
78,Tajikzadeh,Mohammad,Fest,Ja,Ja,2025-01-01,5.00,40.00,0.00,20,20,169.3833,0,0,0,0,20,169.38333333333,21.667,173.333,-3.94966666667,0,0,20,0,0,0,0,0,0,0,0,17,3,24.35,0,-3.94966666667,-3.94966666667,0,-3.94966666667,2,0,2,0,0,0.00,20,20,0,0,0,9.0333333333333,24.35
,Atrafi,"Mohammad Nasir",Fest,Ja,Ja,2025-10-01,5.00,4.00,0.00,20,20,177.8,1,8,0,0,21,185.8,21.667,17.333,168.467,156.00033333333,12.46666666667,20,0,0,0,0,0,0,0,0,19,1,7.85,0,168.467,168.467,168.467,0,5,1,4,156.00033333333,12.46666666667,0.00,19,20,0,0,0,17.95,7.85
,Amiri,"Mohammad Samir",Fest,Ja,Ja,2025-09-01,5.00,40.00,0.00,20,22,197.6667,0,0,0,0,22,197.66666666667,21.667,173.333,24.33366666667,0.00033333332999064,24.33333333334,22,0,0,10,34.75,10,23.166666666667,3,27.833333333333,0,2,19.25,0,24.33366666667,24.33366666667,24.33366666667,0,11,0,11,0.00033333332999064,24.33333333334,0.00,21,22,34.75,23.166666666667,27.833333333333,0,19.25
,"El Moustaiqid",Najlae,Fest,,Ja,2025-09-01,5.00,40.00,0.00,,17,126.5,0,0,1,8,18,134.5,21.667,173.333,-38.833,0,0,,2,16,0,0,0,0,0,0,0,0,0,-1154.263,-38.833,-38.833,0,-1193.096,9,0,9,0,0,0.00,,0,0,0,0,0,0
,Jasarevic,Noel,Fest,Nein,Ja,2025-09-15,5.00,40.00,0.00,,19,161.9333,1,8,0,0,20,169.93333333333,21.667,173.333,-3.39966666667,0,0,,0,0,0,0,0,0,0,0,0,0,0,-0.7,-3.39966666667,-3.39966666667,0,-4.09966666667,-1,1,-2,0,0,0.00,19,0,0,0,0,0,0
,"Sharh Ghaboos",Peyman,Fest,Ja,Ja,2025-03-01,5.00,40.00,0.00,,23,199.0833,0,0,0,0,23,199.08333333333,21.667,173.333,25.75033333333,0.00033333332999064,25.75,23,0,0,1,1.5,0,0,1,8.8666666666667,18,3,24.55,0,25.75033333333,25.75033333333,25.75033333333,0,20,0,20,0.00033333332999064,25.75,0.00,22,23,1.5,0,8.8666666666667,13.666666666667,24.55
227,Feilbach,"Raoul Nikolai ",Fest,Nein,Ja,2025-09-01,5.00,40.00,0.00,20,20,173.3667,0,0,0,0,20,173.36666666667,21.667,173.333,0.033666666670001,0.00033333332999064,0.033333333340011,,0,0,8,28,8,16.766666666667,2,19,0,0,0,0.007,0.033666666670001,0.033666666670001,0.033666666670002,0.006999999999999,6,0,6,0.00033333332999064,0.033333333340011,0.00,19,0,28,16.766666666667,19,0,0
,Hatami,Reza,Fest,Ja,Ja,2025-09-15,5.00,40.00,0.00,20,20,175.3333,0,0,0,0,20,175.33333333333,21.667,173.333,2.00033333333,0.00033333332999064,2,20,0,0,0,0,0,0,0,0,18,2,16.266666666667,0,2.00033333333,2.00033333333,2.00033333333,0,6,0,6,0.00033333332999064,2,0.00,20,20,0,0,0,15.066666666667,16.266666666667
137,Pourhossein,Reza,Fest,Ja,Ja,2025-04-01,5.00,40.00,0.00,20,25,201.4667,0,0,0,0,25,201.46666666667,21.667,173.333,28.13366666667,0.00033333332999064,28.13333333334,25,0,0,15,45.85,5,10.166666666667,3,20.95,5,2,16.266666666667,0,28.13366666667,28.13366666667,28.13366666667,0,29,0,29,0.00033333332999064,28.13333333334,0.00,22,25,45.85,10.166666666667,20.95,3.7333333333333,16.266666666667
,"Rohi Pour",Sadroldin,Fest,Ja,Ja,2025-11-01,5.00,40.00,0.00,20,18,155.4667,0,0,0,0,18,155.46666666667,21.667,173.333,-17.86633333333,0,0,18,0,0,0,0,0,0,0,0,16,2,16.016666666667,0,-17.86633333333,-17.86633333333,0,-17.86633333333,0,0,0,0,0,0.00,17,18,0,0,0,11.45,16.016666666667
225,"Hosseini Jahed",Sajad,Fest,Ja,Ja,2025-03-03,5.00,40.00,0.00,20,21,184.2,0,0,0,0,21,184.2,21.667,173.333,10.867,0.00033333332999064,10.86666666667,21,0,0,0,0,0,0,0,0,19,1,8.5166666666667,0,10.867,10.867,10.867,0,20,0,20,0.00033333332999064,10.86666666667,0.00,20,21,0,0,0,17.183333333333,8.5166666666667
,Saki,Sara,Fest,Ja,Ja,2025-02-01,5.00,40.00,0.00,,4,25.7667,0,0,15,120,19,145.76666666667,21.667,173.333,-27.56633333333,0,0,4,0,0,0,0,0,0,0,0,2,0,0,0,-27.56633333333,-27.56633333333,0,-27.56633333333,11,0,11,0,0,0.00,2,4,0,0,0,1.0166666666667,0
,"Perez Y Waldt",Sebastian,Fest,Nein,Ja,2024-09-23,5.00,40.00,0.00,,6,47.3,0,0,7,56,13,103.3,21.667,173.333,-70.033,0,0,,1,8,0,0,0,0,0,0,3,1,7.7166666666667,-747.879,-70.033,-70.033,0,-817.912,14,0,14,0,0,0.00,3,0,0,0,0,1.35,7.7166666666667
38,Hosseini,"Seyed Hamid",Fest,Ja,Ja,2025-03-10,5.00,40.00,0.00,20,22,189.2667,0,0,0,0,22,189.26666666667,21.667,173.333,15.93366666667,0.00033333332999064,15.93333333334,22,0,0,0,0,0,0,0,0,19,3,24.333333333333,0,15.93366666667,15.93366666667,15.93366666667,0,26,0,26,0.00033333332999064,15.93333333334,0.00,22,22,0,0,0,12.933333333333,24.333333333333
,Jabbari,Shahram,Fest,Nein,Ja,2025-04-01,5.00,40.00,0.00,,19,165.5,0,0,0,0,19,165.5,21.667,173.333,-7.833,0,0,,0,0,0,0,0,0,0,0,17,2,16.816666666667,0,-7.833,-7.833,0,-7.833,-5,0,-5,0,0,0.00,19,0,0,0,0,12.683333333333,16.816666666667
124,Imanian,Shahriyar,Fest,Ja,Ja,2025-03-10,5.00,40.00,0.00,20,23,199.2167,0,0,0,0,23,199.21666666667,21.667,173.333,25.88366666667,0.00033333332999064,25.88333333334,23,0,0,2,0.7,0,0,5,44.716666666667,14,4,33.45,0,25.88366666667,25.88366666667,25.88366666667,0,24,0,24,0.00033333332999064,25.88333333334,0.00,23,23,0.7,0,44.716666666667,8.35,33.45
164,Tokhi,Shokrollah,Fest,Ja,Ja,2025-03-04,5.00,40.00,0.00,20,20,168.0333,0,0,0,0,20,168.03333333333,21.667,173.333,-5.29966666667,0,0,20,0,0,10,33.016666666667,1,0.05,2,17.116666666667,6,2,16.45,0,-5.29966666667,-5.29966666667,0,-5.29966666667,9,0,9,0,0,0.00,19,20,33.016666666667,0.05,17.116666666667,2.9166666666667,16.45
210,Rahimzadeh,Siamak,Fest,Ja,Ja,2025-03-01,5.00,40.00,0.00,20,22,182.9667,0,0,0,0,22,182.96666666667,21.667,173.333,9.63366666667,0.00033333332999064,9.63333333334,22,0,0,0,0,0,0,0,0,18,2,16.433333333333,0.027,9.63366666667,9.63366666667,9.63366666667,0.026999999999999,39,0,39,0.00033333332999064,9.63333333334,0.00,20,22,0,0,0,12.033333333333,16.433333333333
,Florescu,Silviu-Cezar,Fest,Nein,Ja,2025-10-01,5.00,40.00,0.00,,21,182.2833,0,0,0,0,21,182.28333333333,21.667,173.333,8.95033333333,0.00033333332999064,8.95,,0,0,2,0.43333333333333,0,0,0,0,18,2,17.533333333333,0,8.95033333333,8.95033333333,8.95033333333,0,9,0,9,0.00033333332999064,8.95,0.00,20,0,0.43333333333333,0,0,13.816666666667,17.533333333333
,Vafaei,Soheil,Fest,Ja,Ja,2025-04-01,5.00,40.00,0.00,,20,176.0667,0,0,0,0,20,176.06666666667,21.667,173.333,2.73366666667,0.00033333332999064,2.73333333334,20,0,0,0,0,0,0,0,0,18,2,16.033333333333,0,2.73366666667,2.73366666667,2.73366666667,0,-13,0,-13,0.00033333332999064,2.73333333334,0.00,20,20,0,0,0,16.033333333333,16.033333333333
,Toraman,"Tolga Can",Fest,Nein,Ja,2025-09-15,5.00,40.00,0.00,20,21,177.7333,0,0,0,0,21,177.73333333333,21.667,173.333,4.40033333333,0.00033333332999064,4.4,,0,0,6,17.883333333333,0,0,1,9.0833333333333,12,2,15.65,0,4.40033333333,4.40033333333,4.40033333333,0,0,0,0,0.00033333332999064,4.4,0.00,18,0,17.883333333333,0,9.0833333333333,7.6333333333333,15.65
14,Haugg,Uwe,Fest,Ja,Ja,2025-01-01,4.00,32.00,0.00,18,17,142.2667,0,0,0,0,17,142.26666666667,17.333,138.667,3.59966666667,3.59966666667,0,17,0,0,0,0,0,0,0,0,12,5,38.4,0,3.59966666667,3.59966666667,3.59966666667,0,-18,0,-18,3.59966666667,0,0.00,14,17,0,0,0,7.8666666666667,38.4
231,Shokripour,Vahid,Fest,Ja,Ja,2025-01-01,5.00,40.00,0.00,20,20,174.25,0,0,0,0,20,174.25,21.667,173.333,0.917,0.00033333332999064,0.91666666667001,20,0,0,0,0,0,0,0,0,18,2,17.266666666667,0,0.917,0.917,0.917,0,35,0,35,0.00033333332999064,0.91666666667001,0.00,20,20,0,0,0,12.983333333333,17.266666666667
226,Konak,Zidar,Fest,Nein,Ja,2025-05-01,5.00,40.00,0.00,20,21,178.9667,0,0,0,0,21,178.96666666667,21.667,173.333,5.63366666667,0.00033333332999064,5.63333333334,,0,0,2,1.1,0,0,0,0,0,3,26.95,0,5.63366666667,5.63366666667,5.63366666667,0,16,0,16,0.00033333332999064,5.63333333334,0.00,17,0,1.1,0,0,0,26.95
,,,,,,,,,,,1234,10534.0502,45,360,50,416,1329,11310.05,1460.354,11517.979,-34.595999999977,,,,10,79.5,170,519.28333333333,66,131.93333333333,25,215.26666666667,796,106,879.75,,-34.595999999977,-34.595999999977,633.31100000003,-3607.16,,45,662,214.1769999999,419.13400000013,0,1126,,519.28333333333,131.93333333333,215.26666666667,591.26666666667,879.75
1 Vertrag Vertrag Vertrag Vertrag Vertrag Vertrag Vertrag Vertrag Stunden innerhalb der Soll-Stunden Stunden innerhalb der Soll-Stunden Stunden innerhalb der Soll-Stunden Stunden innerhalb der Soll-Stunden Stunden innerhalb der Soll-Stunden Stunden innerhalb der Soll-Stunden Ist-/Soll-Stunden Ist-/Soll-Stunden Ist-/Soll-Stunden Ist-/Soll-Stunden Berechnung Berechnung Berechnung Berechnung Berechnung Berechnung Berechnung Berechnung Berechnung Berechnung Berechnung Berechnung Berechnung Berechnung Berechnung Salden Salden Salden Salden Salden Salden Salden Salden Auszahlen Auszahlen Auszahlen Auszahlen Auszahlen Auszahlen Auszahlen Auszahlen Auszahlen Auszahlen
2 Gearbeitete Stunden Gearbeitete Stunden Urlaub nehmen Urlaub nehmen Krankheitsstunden Krankheitsstunden Gesamt Gesamt Vertrag Vertrag Plus/Min Mehrstunden Überstunden Zuschlag P-Schein Krank ohne AU Krank ohne AU Nachtzuschlag 25 (25%) Nachtzuschlag 25 (25%) Nachtzuschlag 40 (40%) Nachtzuschlag 40 (40%) Sonntagszuschlag (50%) Sonntagszuschlag (50%) Verpfl.-pauschale (14%) Samstagzuschlag (15%) Samstagzuschlag (15%) Zeitausgleich Zeitausgleich Zeitausgleich Zeitausgleich Zeitausgleich Urlaub Urlaub Urlaub Mehrstunden Überstunden auszahlen Fahrtkosten Verpflegungspauschale Zuschlag P-Schein 25% 40% 50% 14% 15%
3 Persnr. Nachname Vorname Art P-Schein Recht auf Zuschläge? Start Tage Stunden Max Urlaubstage pro Jahr Tage Stunden Tage Stunden Tage Stunden Tage Stunden Tage Stunden Stunden Stunden Stunden anzahl Tage Stunden Tage Stunden Tage Stunden Tage Stunden Tage Tage Stunden Start +/- Änderungen Auszahlen Jetzt Start Abbau Jetzt Stunden Stunden km Tage Anzahl Stunden Stunden Stunden Stunden Stunden
4 169 Safari Abdul Latif Fest Ja Ja 2025-09-01 5.00 40.00 0.00 20 21 185.2167 1 8 0 0 22 193.21666666667 21.667 173.333 19.88366666667 0.00033333332999064 19.88333333334 21 0 0 0 0 0 0 0 0 18 2 18.933333333333 0.334 19.88366666667 19.88366666667 19.88366666667 0.334 14 1 13 0.00033333332999064 19.88333333334 0.00 20 21 0 0 0 15.783333333333 18.933333333333
5 93 Moshtagh Askari Adel Fest Ja Ja 2025-01-01 5.00 40.00 0.00 20 23 198.9167 0 0 0 0 23 198.91666666667 21.667 173.333 25.58366666667 0.00033333332999064 25.58333333334 23 0 0 0 0 0 0 0 0 20 2 17.45 0.007 25.58366666667 25.58366666667 25.58366666667 0.0070000000000014 31 0 31 0.00033333332999064 25.58333333334 0.00 22 23 0 0 0 14.966666666667 17.45
6 18 Mihailescu Adrian Lucian Fest Ja Ja 2025-09-01 5.00 40.00 0.00 20 21 178.6 0 0 0 0 21 178.6 21.667 173.333 5.267 0.00033333332999064 5.26666666667 21 0 0 0 0 0 0 0 0 11 3 25.65 0 5.267 5.267 5.267 0 20 0 20 0.00033333332999064 5.26666666667 0.00 13 21 0 0 0 9.6833333333333 25.65
7 Esmaeili Dashtbayaz Amin Fest Nein Ja 2025-04-01 5.00 40.00 0.00 21 181.0833 0 0 0 0 21 181.08333333333 21.667 173.333 7.75033333333 0.00033333332999064 7.75 0 0 0 0 0 0 0 0 18 3 24.033333333333 -2.066 7.75033333333 7.75033333333 5.68433333333 8.8817841970013E-16 12 0 12 0.00033333332999064 5.684 0.00 20 0 0 0 0 13.05 24.033333333333
8 Ahmadpour Amirhossein Fest Ja Ja 2025-09-01 5.00 40.00 0.00 24 22 194.75 0 0 0 0 22 194.75 21.667 173.333 21.417 0.00033333332999064 21.41666666667 22 0 0 1 1.1333333333333 0 0 0 0 20 1 8.75 0 21.417 21.417 21.417 0 9 0 9 0.00033333332999064 21.41666666667 0.00 21 22 1.1333333333333 0 0 18.366666666667 8.75
9 188 Malani Karizi Arasch Fest Nein Ja 2025-09-01 5.00 40.00 0.00 20 24 207.75 0 0 0 0 24 207.75 21.667 173.333 34.417 0.00033333332999064 34.41666666667 0 0 1 1.5 0 0 1 8.8666666666667 19 3 24.55 -108.997 34.417 34.417 0 -74.58 5 0 5 0 0 0.00 23 0 1.5 0 8.8666666666667 14.333333333333 24.55
10 113 Ziaudin Atiq Fest Ja Ja 2025-02-24 5.00 40.00 0.00 20 20 172.6 0 0 0 0 20 172.6 21.667 173.333 -0.733 0 0 20 0 0 0 0 0 0 0 0 19 1 8.0166666666667 14.047 -0.733 -0.733 0 13.314 9 0 9 0 0 0.00 20 20 0 0 0 12.583333333333 8.0166666666667
11 Rahmati Behnam Fest Ja Ja 2025-04-01 5.00 40.00 0.00 16 143.95 0 0 2 16 18 159.95 21.667 173.333 -13.383 0 0 16 0 0 0 0 0 0 0 0 16 0 0 0 -13.383 -13.383 0 -13.383 13 0 13 0 0 0.00 16 16 0 0 0 15.95 0
12 Aba Cuan Fest Nein Ja 2025-08-01 5.00 40.00 0.00 10 69.45 0 0 2 16 12 85.45 21.667 173.333 -87.883 0 0 0 0 0 0 0 0 0 0 0 0 0 -228.333 -87.883 -87.883 0 -316.216 4 0 4 0 0 0.00 1 0 0 0 0 0 0
13 Haghighivand Danial Fest Nein Ja 2025-04-01 5.00 40.00 0.00 17 152.2333 0 0 1 8 18 160.23333333333 21.667 173.333 -13.09966666667 0 0 0 0 0 0 0 0 0 0 17 0 0 0 -13.09966666667 -13.09966666667 0 -13.09966666667 3 0 3 0 0 0.00 17 0 0 0 0 16.233333333333 0
14 Mohammadi Danial Fest Ja Ja 2025-01-01 5.00 40.00 0.00 20 19 165.0167 0 0 0 0 19 165.01666666667 21.667 173.333 -8.31633333333 0 0 19 0 0 0 0 0 0 0 0 17 2 14.5 0 -8.31633333333 -8.31633333333 0 -8.31633333333 7 0 7 0 0 0.00 17 19 0 0 0 14.516666666667 14.5
15 Shahrasebi Danial Fest 2021-05-01 0.00 0.00 0.00 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 5 0 5 0 0 0 0 0 0 0 0
16 Emkani Daniel Fest Nein Ja 2025-07-15 3.00 25.00 0.00 7 58.9167 3 24 3 24 13 106.91666666667 13 108.333 -1.41633333333 0 0 0 0 0 0 0 0 0 0 0 2 15.65 0 -1.41633333333 -1.41633333333 0 -1.41633333333 7 3 4 0 0 0.00 6 0 0 0 0 0 15.65
17 Lolies Danny Fest Ja Ja 2025-03-01 5.00 40.00 0.00 19 161.4833 0 0 1 8 20 169.48333333333 21.667 173.333 -3.84966666667 0 0 19 2 16.5 0 0 0 0 0 0 15 2 16.566666666667 -5.933 -3.84966666667 -3.84966666667 0 -9.78266666667 10 0 10 0 0 0.00 17 19 0 0 0 10.7 16.566666666667
18 Hastedt David Fest Nein Ja 2025-01-27 4.00 32.00 0.00 20 13 110.2 2 16 0 0 15 126.2 17.333 138.667 -12.467 0 0 1 8 0 0 0 0 0 0 10 0 0 0 -12.467 -12.467 0 -12.467 2 2 0 0 0 0.00 10 0 0 0 0 8.6666666666667 0
19 2 Shahrasebi David Fest Ja Ja 2022-10-01 5.00 40.00 0.00 20 0 0 0 0 0 0 21.667 173.333 0 0 0 0 0 0 0 0 0 0 0 0 0 0 151.34 0 0 0 151.34 2 0 2 0 0 0 0 0 0 0 0
20 Golmohammadi Davood Fest Ja Ja 2025-08-01 5.00 40.00 0.00 20 19 171.6667 1 8 0 0 20 179.66666666667 21.667 173.333 6.33366666667 0.00033333332999064 6.33333333334 19 0 0 0 0 0 0 0 0 19 0 0 0 6.33366666667 6.33366666667 6.33366666667 0 2 1 1 0.00033333332999064 6.33333333334 0.00 19 19 0 0 0 19.666666666667 0
21 202 Pouzesh Esmail Fest Ja Ja 2025-09-01 5.00 40.00 0.00 20 21 170.3667 0 0 0 0 21 170.36666666667 21.667 173.333 -2.96633333333 0 0 21 0 0 20 66.283333333333 11 21.333333333333 0 0 0 0 0 0 -2.96633333333 -2.96633333333 0 -2.96633333333 12 0 12 0 0 0.00 20 21 66.283333333333 21.333333333333 0 0 0
22 187 Sajaei Khouzestani Farid Fest Ja Ja 2025-04-07 5.00 40.00 0.00 20 23 203.5 0 0 0 0 23 203.5 21.667 173.333 30.167 0.00033333332999064 30.16666666667 23 0 0 0 0 0 0 0 0 19 3 28.75 0 30.167 30.167 30.167 0 19 0 19 0.00033333332999064 30.16666666667 0.00 22 23 0 0 0 16.25 28.75
23 191 Faridani Rad Farzad Fest Ja Ja 2025-02-01 5.00 40.00 0.00 20 19 157.2 0 0 2 16 21 173.2 21.667 173.333 -0.13300000000001 0 0 19 1 8 19 59.9 5 11.55 0 0 0 0 0 0 -0.13300000000001 -0.13300000000001 0 -0.13300000000001 0 0 0 0 0 0.00 19 19 59.9 11.55 0 0 0
24 Movafagh Farzad Fest Ja Ja 2025-02-03 5.00 40.00 0.00 20 21 182.65 0 0 0 0 21 182.65 21.667 173.333 9.317 0.00033333332999064 9.31666666667 21 0 0 0 0 0 0 0 0 19 2 16.016666666667 0 9.317 9.317 9.317 0 17 0 17 0.00033333332999064 9.31666666667 0.00 20 21 0 0 0 14.633333333333 16.016666666667
25 89 Hashimazada Firuz Fest Ja Ja 2025-01-01 5.00 40.00 0.00 20 0 0 0 16 144 16 144 21.667 173.333 -29.333 0 0 0 0 0 0 0 0 0 0 0 0 0 0 -29.333 -29.333 0 -29.333 8 0 8 0 0 0 0 0 0 0 0
26 Saeedpour Fouad Fest Ja Ja 2025-09-01 5.00 40.00 0.00 21 180.6833 0 0 0 0 21 180.68333333333 21.667 173.333 7.35033333333 0.00033333332999064 7.35 21 0 0 5 17.5 5 10.55 2 18.583333333333 10 4 33.033333333333 0 7.35033333333 7.35033333333 7.35033333333 0 26 0 26 0.00033333332999064 7.35 0.00 21 21 17.5 10.55 18.583333333333 8.5166666666667 33.033333333333
27 55 Khodayaran Hossein Fest Ja Ja 2025-01-01 5.00 40.00 0.00 20 19 152.55 0 0 0 0 19 152.55 21.667 173.333 -20.783 0 0 19 0 0 18 56.733333333333 5 11.55 1 3.5166666666667 0 0 0 0 -20.783 -20.783 0 -20.783 6 0 6 0 0 0.00 18 19 56.733333333333 11.55 3.5166666666667 0 0
28 206 Rasulkhani Hossein Fest Ja Ja 2025-03-06 5.00 40.00 0.00 20 9 76.4667 10 80 0 0 19 156.46666666667 21.667 173.333 -16.86633333333 0 0 9 0 0 0 0 0 0 1 9 6 2 16.2 0 -16.86633333333 -16.86633333333 0 -16.86633333333 29 10 19 0 0 0.00 9 9 0 0 9 3.2666666666667 16.2
29 Mohamed Salah Hussin Noureldin Fest Ja Ja 2025-06-06 5.00 40.00 0.00 20 20 173.6667 0 0 0 0 20 173.66666666667 21.667 173.333 0.33366666667001 0.00033333332999064 0.33333333334002 20 0 0 0 0 0 0 0 0 18 2 17.616666666667 0 0.33366666667001 0.33366666667001 0.33366666667001 0 0 0 0 0.00033333332999064 0.33333333334002 0.00 19 20 0 0 0 12.05 17.616666666667
30 50 Rohany Oskoee Iman Fest Nein Ja 2025-01-01 5.00 40.00 0.00 20 23 196.8667 0 0 0 0 23 196.86666666667 21.667 173.333 23.53366666667 0.00033333332999064 23.53333333334 0 0 0 0 0 0 0 0 20 2 16.883333333333 0 23.53366666667 23.53366666667 23.53366666667 0 36 0 36 0.00033333332999064 23.53333333334 0.00 21 0 0 0 0 13.483333333333 16.883333333333
31 7 Salehi Iman Fest Ja Ja 2025-02-01 5.00 40.00 0.00 20 21 176.55 0 0 0 0 21 176.55 21.667 173.333 3.217 0.00033333332999064 3.21666666667 21 0 0 20 62.466666666667 7 10.9 1 10 0 1 8.75 0 3.217 3.217 3.217 0 0 0 0 0.00033333332999064 3.21666666667 0.00 21 21 62.466666666667 10.9 10 0 8.75
32 Krüger Julian Fest Ja Ja 2025-01-15 5.00 40.00 0.00 20 20 171.2833 0 0 0 0 20 171.28333333333 21.667 173.333 -2.04966666667 0 0 20 0 0 2 7 2 4.4333333333333 0 0 0 1 6.05 0 -2.04966666667 -2.04966666667 0 -2.04966666667 8 0 8 0 0 0.00 17 20 7 4.4333333333333 0 0 6.05
33 Hazrati Kamyar Fest Ja 2025-02-24 5.00 40.00 0.00 20 21 182.2333 0 0 0 0 21 182.23333333333 21.667 173.333 8.90033333333 0.00033333332999064 8.9 0 0 0 0 0 0 0 0 18 3 22.616666666667 0 8.90033333333 8.90033333333 8.90033333333 0 10 0 10 0.00033333332999064 8.9 0.00 19 0 0 0 0 15.616666666667 22.616666666667
34 Saei Kasra Fest Nein Ja 2025-08-01 5.00 40.00 0.00 8 56.2333 2 16 0 0 10 72.233333333333 21.667 173.333 -101.09966666667 0 0 2 15 0 0 0 0 0 0 0 0 0 -186.848 -101.09966666667 -101.09966666667 0 -287.94766666667 0 2 -2 0 0 0.00 0 0 0 0 0 0
35 182 Kalantari Kaveh Fest Ja Ja 2025-03-01 5.00 40.00 0.00 20 18 155.1667 6 48 0 0 24 203.16666666667 21.667 173.333 29.83366666667 0.00033333332999064 29.83333333334 18 0 0 1 1.05 0 0 2 17.733333333333 13 3 25.533333333333 0 29.83366666667 29.83366666667 29.83366666667 0 17 6 11 0.00033333332999064 29.83333333334 0.00 18 18 1.05 0 17.733333333333 6.85 25.533333333333
36 Sahem Khalid Fest Ja Ja 2025-06-16 5.00 40.00 0.00 20 20 168.7167 0 0 0 0 20 168.71666666667 21.667 173.333 -4.61633333333 0 0 20 0 0 0 0 0 0 0 0 18 1 8.6833333333333 0 -4.61633333333 -4.61633333333 0 -4.61633333333 15 0 15 0 0 0.00 19 20 0 0 0 9.5333333333333 8.6833333333333
37 194 Saffarnajib Majid Fest Ja Ja 2025-04-01 3.00 21.00 0.00 16 17 145.5667 0 0 0 0 17 145.56666666667 13 91 54.56666666667 54.56666666667 0 17 0 0 2 3.85 0 0 0 0 16 0 0 0 54.56666666667 54.56666666667 54.56666666667 0 16 0 16 54.56666666667 0 0.00 17 17 3.85 0 0 8.7166666666667 0
38 177 Juyande Masoud Fest Nein Ja 2025-01-01 5.00 40.00 0.00 20 14 113.4333 7 56 0 0 21 169.43333333333 21.667 173.333 -3.89966666667 0 0 0 0 10 33.116666666667 5 11.116666666667 0 0 3 0 0 0.009 -3.89966666667 -3.89966666667 0 -3.89066666667 -7 7 -14 0 0 0.00 13 0 33.116666666667 11.116666666667 0 0.7 0
39 53 Eggerstedt Matthias Fest Ja Ja 2025-01-01 5.00 40.00 0.00 20 20 171.6833 0 0 0 0 20 171.68333333333 21.667 173.333 -1.64966666667 0 0 20 0 0 0 0 0 0 0 0 18 2 16.933333333333 0 -1.64966666667 -1.64966666667 0 -1.64966666667 10 0 10 0 0 0.00 20 20 0 0 0 10.75 16.933333333333
40 Arabnia Mehdi Fest Ja Ja 2025-05-05 5.00 40.00 0.00 21 185.9333 1 8 0 0 22 193.93333333333 21.667 173.333 20.60033333333 0.00033333332999064 20.6 21 0 0 0 0 0 0 0 0 18 3 24.766666666667 0 20.60033333333 20.60033333333 20.60033333333 0 7 1 6 0.00033333332999064 20.6 0.00 20 21 0 0 0 17.166666666667 24.766666666667
41 Jalali Wagener Mehdi Fest Ja Ja 2025-02-01 5.00 40.00 0.00 23 195.7167 0 0 0 0 23 195.71666666667 21.667 173.333 22.38366666667 0.00033333332999064 22.38333333334 23 0 0 0 0 0 0 0 0 20 3 24.033333333333 0 22.38366666667 22.38366666667 22.38366666667 0 -1 0 -1 0.00033333332999064 22.38333333334 0.00 22 23 0 0 0 11.683333333333 24.033333333333
42 Taghadosi Mehdi Fest Ja Ja 2025-08-08 5.00 40.00 0.00 19 167.25 0 0 0 0 19 167.25 21.667 173.333 -6.083 0 0 19 0 0 0 0 0 0 0 0 17 2 16.266666666667 0 -6.083 -6.083 0 -6.083 10 0 10 0 0 0.00 19 19 0 0 0 14.983333333333 16.266666666667
43 Mazidi Mehrdad Fest Ja Ja 2025-07-01 5.00 40.00 0.00 20 19 166.9167 0 0 0 0 19 166.91666666667 21.667 173.333 -6.41633333333 0 0 19 0 0 0 0 0 0 0 0 17 2 17.616666666667 0 -6.41633333333 -6.41633333333 0 -6.41633333333 10 0 10 0 0 0.00 18 19 0 0 0 13.3 17.616666666667
44 165 Koell Melanie Nadine Fest Nein Ja 2025-01-01 5.00 40.00 0.00 20 1 6.5 0 0 0 0 1 6.5 21.667 173.333 -166.833 0 0 0 0 0 0 0 0 0 0 0 0 0 -306.596 -166.833 -166.833 0 -473.429 12 0 12 0 0 0.00 0 0 0 0 0 0
45 Zekarias Michael Fest Nein Ja 2025-11-01 5.00 40.00 0.00 21 172.8333 0 0 0 0 21 172.83333333333 21.667 173.333 -0.49966666667001 0 0 0 0 5 14.616666666667 0 0 0 0 14 1 8.0333333333333 0 -0.49966666667001 -0.49966666667001 0 -0.49966666667001 0 0 0 0 0 0.00 20 0 14.616666666667 0 0 6.1833333333333 8.0333333333333
46 119 Pourhossein Milad Fest Ja Ja 2025-09-01 5.00 40.00 0.00 20 22 182.6 0 0 0 0 22 182.6 21.667 173.333 9.267 0.00033333332999064 9.26666666667 22 0 0 10 30.9 2 0.35 0 0 9 1 9.25 -49.564 9.267 9.267 0 -40.297 10 0 10 0 0 0.00 20 22 30.9 0.35 0 3.7 9.25
47 144 Maleki Mohammad Fest Ja Ja 2025-09-01 3.00 24.00 0.00 20 9 72.6833 0 0 0 0 9 72.683333333333 13 104 -31.316666666667 0 0 9 1 8 0 0 0 0 0 0 6 0 0 -313.845 -31.316666666667 -31.316666666667 0 -345.16166666667 -1 0 -1 0 0 0.00 6 9 0 0 0 3.55 0
48 217 Rasul Khani Mohammad Fest Ja Ja 2025-03-03 5.00 40.00 0.00 20 11 95 10 80 0 0 21 175 21.667 173.333 1.667 0.00033333332999064 1.66666666667 11 0 0 0 0 0 0 0 0 10 1 8.5166666666667 0 1.667 1.667 1.667 0 20 10 10 0.00033333332999064 1.66666666667 0.00 11 11 0 0 0 6.4833333333333 8.5166666666667
49 96 Sabahi Mohammad Fest Ja Ja 2025-01-01 5.00 40.00 0.00 20 20 170.15 0 0 0 0 20 170.15 21.667 173.333 -3.183 0 0 20 0 0 0 0 0 0 0 0 19 0 0 0 -3.183 -3.183 0 -3.183 9 0 9 0 0 0.00 19 20 0 0 0 11.65 0
50 78 Tajikzadeh Mohammad Fest Ja Ja 2025-01-01 5.00 40.00 0.00 20 20 169.3833 0 0 0 0 20 169.38333333333 21.667 173.333 -3.94966666667 0 0 20 0 0 0 0 0 0 0 0 17 3 24.35 0 -3.94966666667 -3.94966666667 0 -3.94966666667 2 0 2 0 0 0.00 20 20 0 0 0 9.0333333333333 24.35
51 Atrafi Mohammad Nasir Fest Ja Ja 2025-10-01 5.00 4.00 0.00 20 20 177.8 1 8 0 0 21 185.8 21.667 17.333 168.467 156.00033333333 12.46666666667 20 0 0 0 0 0 0 0 0 19 1 7.85 0 168.467 168.467 168.467 0 5 1 4 156.00033333333 12.46666666667 0.00 19 20 0 0 0 17.95 7.85
52 Amiri Mohammad Samir Fest Ja Ja 2025-09-01 5.00 40.00 0.00 20 22 197.6667 0 0 0 0 22 197.66666666667 21.667 173.333 24.33366666667 0.00033333332999064 24.33333333334 22 0 0 10 34.75 10 23.166666666667 3 27.833333333333 0 2 19.25 0 24.33366666667 24.33366666667 24.33366666667 0 11 0 11 0.00033333332999064 24.33333333334 0.00 21 22 34.75 23.166666666667 27.833333333333 0 19.25
53 El Moustaiqid Najlae Fest Ja 2025-09-01 5.00 40.00 0.00 17 126.5 0 0 1 8 18 134.5 21.667 173.333 -38.833 0 0 2 16 0 0 0 0 0 0 0 0 0 -1154.263 -38.833 -38.833 0 -1193.096 9 0 9 0 0 0.00 0 0 0 0 0 0
54 Jasarevic Noel Fest Nein Ja 2025-09-15 5.00 40.00 0.00 19 161.9333 1 8 0 0 20 169.93333333333 21.667 173.333 -3.39966666667 0 0 0 0 0 0 0 0 0 0 0 0 0 -0.7 -3.39966666667 -3.39966666667 0 -4.09966666667 -1 1 -2 0 0 0.00 19 0 0 0 0 0 0
55 Sharh Ghaboos Peyman Fest Ja Ja 2025-03-01 5.00 40.00 0.00 23 199.0833 0 0 0 0 23 199.08333333333 21.667 173.333 25.75033333333 0.00033333332999064 25.75 23 0 0 1 1.5 0 0 1 8.8666666666667 18 3 24.55 0 25.75033333333 25.75033333333 25.75033333333 0 20 0 20 0.00033333332999064 25.75 0.00 22 23 1.5 0 8.8666666666667 13.666666666667 24.55
56 227 Feilbach Raoul Nikolai Fest Nein Ja 2025-09-01 5.00 40.00 0.00 20 20 173.3667 0 0 0 0 20 173.36666666667 21.667 173.333 0.033666666670001 0.00033333332999064 0.033333333340011 0 0 8 28 8 16.766666666667 2 19 0 0 0 0.007 0.033666666670001 0.033666666670001 0.033666666670002 0.006999999999999 6 0 6 0.00033333332999064 0.033333333340011 0.00 19 0 28 16.766666666667 19 0 0
57 Hatami Reza Fest Ja Ja 2025-09-15 5.00 40.00 0.00 20 20 175.3333 0 0 0 0 20 175.33333333333 21.667 173.333 2.00033333333 0.00033333332999064 2 20 0 0 0 0 0 0 0 0 18 2 16.266666666667 0 2.00033333333 2.00033333333 2.00033333333 0 6 0 6 0.00033333332999064 2 0.00 20 20 0 0 0 15.066666666667 16.266666666667
58 137 Pourhossein Reza Fest Ja Ja 2025-04-01 5.00 40.00 0.00 20 25 201.4667 0 0 0 0 25 201.46666666667 21.667 173.333 28.13366666667 0.00033333332999064 28.13333333334 25 0 0 15 45.85 5 10.166666666667 3 20.95 5 2 16.266666666667 0 28.13366666667 28.13366666667 28.13366666667 0 29 0 29 0.00033333332999064 28.13333333334 0.00 22 25 45.85 10.166666666667 20.95 3.7333333333333 16.266666666667
59 Rohi Pour Sadroldin Fest Ja Ja 2025-11-01 5.00 40.00 0.00 20 18 155.4667 0 0 0 0 18 155.46666666667 21.667 173.333 -17.86633333333 0 0 18 0 0 0 0 0 0 0 0 16 2 16.016666666667 0 -17.86633333333 -17.86633333333 0 -17.86633333333 0 0 0 0 0 0.00 17 18 0 0 0 11.45 16.016666666667
60 225 Hosseini Jahed Sajad Fest Ja Ja 2025-03-03 5.00 40.00 0.00 20 21 184.2 0 0 0 0 21 184.2 21.667 173.333 10.867 0.00033333332999064 10.86666666667 21 0 0 0 0 0 0 0 0 19 1 8.5166666666667 0 10.867 10.867 10.867 0 20 0 20 0.00033333332999064 10.86666666667 0.00 20 21 0 0 0 17.183333333333 8.5166666666667
61 Saki Sara Fest Ja Ja 2025-02-01 5.00 40.00 0.00 4 25.7667 0 0 15 120 19 145.76666666667 21.667 173.333 -27.56633333333 0 0 4 0 0 0 0 0 0 0 0 2 0 0 0 -27.56633333333 -27.56633333333 0 -27.56633333333 11 0 11 0 0 0.00 2 4 0 0 0 1.0166666666667 0
62 Perez Y Waldt Sebastian Fest Nein Ja 2024-09-23 5.00 40.00 0.00 6 47.3 0 0 7 56 13 103.3 21.667 173.333 -70.033 0 0 1 8 0 0 0 0 0 0 3 1 7.7166666666667 -747.879 -70.033 -70.033 0 -817.912 14 0 14 0 0 0.00 3 0 0 0 0 1.35 7.7166666666667
63 38 Hosseini Seyed Hamid Fest Ja Ja 2025-03-10 5.00 40.00 0.00 20 22 189.2667 0 0 0 0 22 189.26666666667 21.667 173.333 15.93366666667 0.00033333332999064 15.93333333334 22 0 0 0 0 0 0 0 0 19 3 24.333333333333 0 15.93366666667 15.93366666667 15.93366666667 0 26 0 26 0.00033333332999064 15.93333333334 0.00 22 22 0 0 0 12.933333333333 24.333333333333
64 Jabbari Shahram Fest Nein Ja 2025-04-01 5.00 40.00 0.00 19 165.5 0 0 0 0 19 165.5 21.667 173.333 -7.833 0 0 0 0 0 0 0 0 0 0 17 2 16.816666666667 0 -7.833 -7.833 0 -7.833 -5 0 -5 0 0 0.00 19 0 0 0 0 12.683333333333 16.816666666667
65 124 Imanian Shahriyar Fest Ja Ja 2025-03-10 5.00 40.00 0.00 20 23 199.2167 0 0 0 0 23 199.21666666667 21.667 173.333 25.88366666667 0.00033333332999064 25.88333333334 23 0 0 2 0.7 0 0 5 44.716666666667 14 4 33.45 0 25.88366666667 25.88366666667 25.88366666667 0 24 0 24 0.00033333332999064 25.88333333334 0.00 23 23 0.7 0 44.716666666667 8.35 33.45
66 164 Tokhi Shokrollah Fest Ja Ja 2025-03-04 5.00 40.00 0.00 20 20 168.0333 0 0 0 0 20 168.03333333333 21.667 173.333 -5.29966666667 0 0 20 0 0 10 33.016666666667 1 0.05 2 17.116666666667 6 2 16.45 0 -5.29966666667 -5.29966666667 0 -5.29966666667 9 0 9 0 0 0.00 19 20 33.016666666667 0.05 17.116666666667 2.9166666666667 16.45
67 210 Rahimzadeh Siamak Fest Ja Ja 2025-03-01 5.00 40.00 0.00 20 22 182.9667 0 0 0 0 22 182.96666666667 21.667 173.333 9.63366666667 0.00033333332999064 9.63333333334 22 0 0 0 0 0 0 0 0 18 2 16.433333333333 0.027 9.63366666667 9.63366666667 9.63366666667 0.026999999999999 39 0 39 0.00033333332999064 9.63333333334 0.00 20 22 0 0 0 12.033333333333 16.433333333333
68 Florescu Silviu-Cezar Fest Nein Ja 2025-10-01 5.00 40.00 0.00 21 182.2833 0 0 0 0 21 182.28333333333 21.667 173.333 8.95033333333 0.00033333332999064 8.95 0 0 2 0.43333333333333 0 0 0 0 18 2 17.533333333333 0 8.95033333333 8.95033333333 8.95033333333 0 9 0 9 0.00033333332999064 8.95 0.00 20 0 0.43333333333333 0 0 13.816666666667 17.533333333333
69 Vafaei Soheil Fest Ja Ja 2025-04-01 5.00 40.00 0.00 20 176.0667 0 0 0 0 20 176.06666666667 21.667 173.333 2.73366666667 0.00033333332999064 2.73333333334 20 0 0 0 0 0 0 0 0 18 2 16.033333333333 0 2.73366666667 2.73366666667 2.73366666667 0 -13 0 -13 0.00033333332999064 2.73333333334 0.00 20 20 0 0 0 16.033333333333 16.033333333333
70 Toraman Tolga Can Fest Nein Ja 2025-09-15 5.00 40.00 0.00 20 21 177.7333 0 0 0 0 21 177.73333333333 21.667 173.333 4.40033333333 0.00033333332999064 4.4 0 0 6 17.883333333333 0 0 1 9.0833333333333 12 2 15.65 0 4.40033333333 4.40033333333 4.40033333333 0 0 0 0 0.00033333332999064 4.4 0.00 18 0 17.883333333333 0 9.0833333333333 7.6333333333333 15.65
71 14 Haugg Uwe Fest Ja Ja 2025-01-01 4.00 32.00 0.00 18 17 142.2667 0 0 0 0 17 142.26666666667 17.333 138.667 3.59966666667 3.59966666667 0 17 0 0 0 0 0 0 0 0 12 5 38.4 0 3.59966666667 3.59966666667 3.59966666667 0 -18 0 -18 3.59966666667 0 0.00 14 17 0 0 0 7.8666666666667 38.4
72 231 Shokripour Vahid Fest Ja Ja 2025-01-01 5.00 40.00 0.00 20 20 174.25 0 0 0 0 20 174.25 21.667 173.333 0.917 0.00033333332999064 0.91666666667001 20 0 0 0 0 0 0 0 0 18 2 17.266666666667 0 0.917 0.917 0.917 0 35 0 35 0.00033333332999064 0.91666666667001 0.00 20 20 0 0 0 12.983333333333 17.266666666667
73 226 Konak Zidar Fest Nein Ja 2025-05-01 5.00 40.00 0.00 20 21 178.9667 0 0 0 0 21 178.96666666667 21.667 173.333 5.63366666667 0.00033333332999064 5.63333333334 0 0 2 1.1 0 0 0 0 0 3 26.95 0 5.63366666667 5.63366666667 5.63366666667 0 16 0 16 0.00033333332999064 5.63333333334 0.00 17 0 1.1 0 0 0 26.95
74 1234 10534.0502 45 360 50 416 1329 11310.05 1460.354 11517.979 -34.595999999977 10 79.5 170 519.28333333333 66 131.93333333333 25 215.26666666667 796 106 879.75 -34.595999999977 -34.595999999977 633.31100000003 -3607.16 45 662 214.1769999999 419.13400000013 0 1126 519.28333333333 131.93333333333 215.26666666667 591.26666666667 879.75
@@ -117,13 +117,13 @@ CREATE TABLE public."Anomaly" (
description text NOT NULL,
"groupKey" text NOT NULL,
"groupDescription" text NOT NULL,
"associatedEntities" jsonb DEFAULT '{}'::jsonb,
"workedOnBy" text NOT NULL,
"sleepTimer" integer NOT NULL,
"sleepSince" timestamp(3) without time zone,
"resolvedAt" timestamp(3) without time zone,
"manuallyResolved" boolean DEFAULT false NOT NULL,
"createdAt" timestamp(3) without time zone DEFAULT CURRENT_TIMESTAMP NOT NULL,
"associatedEntities" jsonb DEFAULT '{}'::jsonb
"createdAt" timestamp(3) without time zone DEFAULT CURRENT_TIMESTAMP NOT NULL
);
@@ -203,16 +203,16 @@ CREATE TABLE public."CostType" (
--
CREATE TABLE public."DyflexisEmployee" (
"internalId" uuid DEFAULT gen_random_uuid() NOT NULL,
id integer NOT NULL,
firstname text NOT NULL,
surname text NOT NULL,
"contractStart" timestamp(3) without time zone NOT NULL,
"contractEnd" timestamp(3) without time zone,
"contractDaysWeek" integer DEFAULT 0 NOT NULL,
"contractHoursWeek" double precision NOT NULL,
"contractSalaryHour" double precision NOT NULL,
"contractTypeName" text,
"internalId" uuid DEFAULT gen_random_uuid() NOT NULL,
"contractDaysWeek" integer DEFAULT 0 NOT NULL
"contractTypeName" text
);
@@ -435,8 +435,8 @@ CREATE TABLE public."PlannedTimeStaging" (
"endDate" timestamp(3) without time zone NOT NULL,
deleted boolean NOT NULL,
mark text,
duration integer NOT NULL,
pause integer NOT NULL
pause integer NOT NULL,
duration integer NOT NULL
);
@@ -506,15 +506,25 @@ CREATE TABLE public."TariffGroup" (
--
CREATE TABLE public."Ticket" (
id uuid DEFAULT gen_random_uuid() NOT NULL,
"currentState" public."TicketValidationState" NOT NULL,
id uuid NOT NULL,
"patientId" uuid,
stage text NOT NULL,
"approvalRequirement" text NOT NULL,
"approvalStatus" text NOT NULL,
"approvalRequestedAt" timestamp(3) without time zone,
"approvalLastInteractionAt" timestamp(3) without time zone,
"approvalReminderCount" integer DEFAULT 0 NOT NULL,
"hasPhysicalTransportDocument" boolean DEFAULT false NOT NULL,
"hasDigitalTransportDocument" boolean DEFAULT false NOT NULL,
"updatedAt" timestamp(3) without time zone,
"createdAt" timestamp(3) without time zone DEFAULT CURRENT_TIMESTAMP NOT NULL,
"currentState" public."TicketValidationState",
notes text[],
errors text[],
"isUrgent" boolean DEFAULT false NOT NULL,
urgency timestamp(3) without time zone DEFAULT '2099-12-31 23:59:59.999'::timestamp without time zone NOT NULL,
"approvalState" public."ApprovalState",
"documentInfo" jsonb DEFAULT '{"approval": {"isNeeded": null, "answeredFrom": "", "whoRequested": ""}, "tdLocation": ""}'::jsonb,
"createdAt" timestamp(3) without time zone DEFAULT CURRENT_TIMESTAMP NOT NULL
"documentInfo" jsonb DEFAULT '{"approval": {"isNeeded": null, "answeredFrom": "", "whoRequested": ""}, "tdLocation": ""}'::jsonb
);
@@ -582,13 +592,13 @@ CREATE TABLE public."Tour" (
revenue double precision DEFAULT 0 NOT NULL,
"consumptionCosts" double precision DEFAULT 0 NOT NULL,
"revenueDeviation" double precision DEFAULT 0 NOT NULL,
"createdAt" timestamp(3) without time zone,
"deletedAt" timestamp(3) without time zone,
"ticketId" uuid,
"updatedAt" timestamp(3) without time zone,
"companyName" text,
"overRTW" boolean DEFAULT false NOT NULL,
schwerlast boolean DEFAULT false NOT NULL
"companyName" text,
schwerlast boolean DEFAULT false NOT NULL,
"createdAt" timestamp(3) without time zone,
"updatedAt" timestamp(3) without time zone,
"deletedAt" timestamp(3) without time zone,
"ticketId" uuid
);
@@ -620,6 +630,7 @@ CREATE TABLE public."TourSource" (
id text,
"check" text,
"createTime" text,
"lastUpdate" text,
"empfangenVonId" text,
endgen text,
"serienId" text,
@@ -681,9 +692,8 @@ CREATE TABLE public."TourSource" (
"fahrerName" text,
fahrer text,
color text,
"lastUpdate" text,
"companyName" text,
"overRTW" text,
"companyName" text,
schwerlast text
);
@@ -741,11 +751,11 @@ CREATE TABLE public."TourStaging" (
revenue double precision DEFAULT 0 NOT NULL,
"revenueDeviation" double precision DEFAULT 0 NOT NULL,
"consumptionCosts" double precision DEFAULT 0 NOT NULL,
"createdAt" timestamp(3) without time zone,
"updatedAt" timestamp(3) without time zone,
"companyName" text,
"overRTW" boolean DEFAULT false NOT NULL,
schwerlast boolean DEFAULT false NOT NULL
"companyName" text,
schwerlast boolean DEFAULT false NOT NULL,
"createdAt" timestamp(3) without time zone,
"updatedAt" timestamp(3) without time zone
);
@@ -812,8 +822,8 @@ CREATE TABLE public."WorkTimeStaging" (
"departmentName" text,
"startDate" timestamp(3) without time zone NOT NULL,
"endDate" timestamp(3) without time zone NOT NULL,
duration integer NOT NULL,
pause integer NOT NULL
pause integer NOT NULL,
duration integer NOT NULL
);
@@ -827,22 +837,6 @@ CREATE TABLE public."_AvicennaTenantToTariffGroup" (
);
--
-- Name: _prisma_migrations; Type: TABLE; Schema: public; Owner: -
--
CREATE TABLE public._prisma_migrations (
id character varying(36) NOT NULL,
checksum character varying(64) NOT NULL,
finished_at timestamp with time zone,
migration_name character varying(255) NOT NULL,
logs text,
rolled_back_at timestamp with time zone,
started_at timestamp with time zone DEFAULT now() NOT NULL,
applied_steps_count integer DEFAULT 0 NOT NULL
);
--
-- Name: Anomaly Anomaly_pkey; Type: CONSTRAINT; Schema: public; Owner: -
--
@@ -1115,14 +1109,6 @@ ALTER TABLE ONLY public."_AvicennaTenantToTariffGroup"
ADD CONSTRAINT "_AvicennaTenantToTariffGroup_AB_pkey" PRIMARY KEY ("A", "B");
--
-- Name: _prisma_migrations _prisma_migrations_pkey; Type: CONSTRAINT; Schema: public; Owner: -
--
ALTER TABLE ONLY public._prisma_migrations
ADD CONSTRAINT _prisma_migrations_pkey PRIMARY KEY (id);
--
-- Name: CostType_id_key; Type: INDEX; Schema: public; Owner: -
--
@@ -1131,10 +1117,10 @@ CREATE UNIQUE INDEX "CostType_id_key" ON public."CostType" USING btree (id);
--
-- Name: TourFile_tourId_ticketId_idx; Type: INDEX; Schema: public; Owner: -
-- Name: TourFile_tourId_ticketId_key; Type: INDEX; Schema: public; Owner: -
--
CREATE INDEX "TourFile_tourId_ticketId_idx" ON public."TourFile" USING btree ("tourId", "ticketId");
CREATE UNIQUE INDEX "TourFile_tourId_ticketId_key" ON public."TourFile" USING btree ("tourId", "ticketId");
--
@@ -14,7 +14,7 @@ SELECT
FROM "DyflexisEmployee" as de
INNER JOIN "Employee" as e ON de.id::text = e."personnelNumber"
WHERE de."contractStart" < CURRENT_DATE AND de."contractEnd" > (CURRENT_DATE - INTERVAL '2 months')
AND de.surname <> 'Shahrasebhi'
AND de.surname <> 'Shahrasebi'
ORDER BY de.firstname ASC;
-- name: GetWeeklyTimeSummaryForEmployee :many
@@ -205,13 +205,13 @@ type Anomaly struct {
Description string `json:"description"`
GroupKey string `json:"groupKey"`
GroupDescription string `json:"groupDescription"`
AssociatedEntities []byte `json:"associatedEntities"`
WorkedOnBy string `json:"workedOnBy"`
SleepTimer int32 `json:"sleepTimer"`
SleepSince *time.Time `json:"sleepSince"`
ResolvedAt *time.Time `json:"resolvedAt"`
ManuallyResolved bool `json:"manuallyResolved"`
CreatedAt time.Time `json:"createdAt"`
AssociatedEntities []byte `json:"associatedEntities"`
}
type AttendanceRegistration struct {
@@ -266,16 +266,16 @@ type CostType struct {
}
type DyflexisEmployee struct {
InternalId pgtype.UUID `json:"internalId"`
ID int32 `json:"id"`
Firstname string `json:"firstname"`
Surname string `json:"surname"`
ContractStart time.Time `json:"contractStart"`
ContractEnd *time.Time `json:"contractEnd"`
ContractDaysWeek int32 `json:"contractDaysWeek"`
ContractHoursWeek float64 `json:"contractHoursWeek"`
ContractSalaryHour float64 `json:"contractSalaryHour"`
ContractTypeName pgtype.Text `json:"contractTypeName"`
InternalId pgtype.UUID `json:"internalId"`
ContractDaysWeek int32 `json:"contractDaysWeek"`
}
type Employee struct {
@@ -438,19 +438,8 @@ type PlannedTimeStaging struct {
EndDate time.Time `json:"endDate"`
Deleted bool `json:"deleted"`
Mark pgtype.Text `json:"mark"`
Duration int32 `json:"duration"`
Pause int32 `json:"pause"`
}
type PrismaMigration struct {
ID string `json:"id"`
Checksum string `json:"checksum"`
FinishedAt *time.Time `json:"finished_at"`
MigrationName string `json:"migration_name"`
Logs pgtype.Text `json:"logs"`
RolledBackAt *time.Time `json:"rolled_back_at"`
StartedAt time.Time `json:"started_at"`
AppliedStepsCount int32 `json:"applied_steps_count"`
Duration int32 `json:"duration"`
}
type Sickday struct {
@@ -490,15 +479,25 @@ type TariffGroup struct {
}
type Ticket struct {
ID pgtype.UUID `json:"id"`
CurrentState TicketValidationState `json:"currentState"`
Notes []string `json:"notes"`
Errors []string `json:"errors"`
IsUrgent bool `json:"isUrgent"`
Urgency time.Time `json:"urgency"`
ApprovalState NullApprovalState `json:"approvalState"`
DocumentInfo []byte `json:"documentInfo"`
CreatedAt time.Time `json:"createdAt"`
ID pgtype.UUID `json:"id"`
PatientId pgtype.UUID `json:"patientId"`
Stage string `json:"stage"`
ApprovalRequirement string `json:"approvalRequirement"`
ApprovalStatus string `json:"approvalStatus"`
ApprovalRequestedAt *time.Time `json:"approvalRequestedAt"`
ApprovalLastInteractionAt *time.Time `json:"approvalLastInteractionAt"`
ApprovalReminderCount int32 `json:"approvalReminderCount"`
HasPhysicalTransportDocument bool `json:"hasPhysicalTransportDocument"`
HasDigitalTransportDocument bool `json:"hasDigitalTransportDocument"`
UpdatedAt *time.Time `json:"updatedAt"`
CreatedAt time.Time `json:"createdAt"`
CurrentState NullTicketValidationState `json:"currentState"`
Notes []string `json:"notes"`
Errors []string `json:"errors"`
IsUrgent bool `json:"isUrgent"`
Urgency time.Time `json:"urgency"`
ApprovalState NullApprovalState `json:"approvalState"`
DocumentInfo []byte `json:"documentInfo"`
}
type TicketStateHistory struct {
@@ -556,13 +555,13 @@ type Tour struct {
Revenue float64 `json:"revenue"`
ConsumptionCosts float64 `json:"consumptionCosts"`
RevenueDeviation float64 `json:"revenueDeviation"`
OverRTW bool `json:"overRTW"`
CompanyName pgtype.Text `json:"companyName"`
Schwerlast bool `json:"schwerlast"`
CreatedAt *time.Time `json:"createdAt"`
UpdatedAt *time.Time `json:"updatedAt"`
DeletedAt *time.Time `json:"deletedAt"`
TicketId pgtype.UUID `json:"ticketId"`
UpdatedAt *time.Time `json:"updatedAt"`
CompanyName pgtype.Text `json:"companyName"`
OverRTW bool `json:"overRTW"`
Schwerlast bool `json:"schwerlast"`
}
type TourFile struct {
@@ -584,6 +583,7 @@ type TourSource struct {
ID pgtype.Text `json:"id"`
Check pgtype.Text `json:"check"`
CreateTime pgtype.Text `json:"createTime"`
LastUpdate pgtype.Text `json:"lastUpdate"`
EmpfangenVonId pgtype.Text `json:"empfangenVonId"`
Endgen pgtype.Text `json:"endgen"`
SerienId pgtype.Text `json:"serienId"`
@@ -645,9 +645,8 @@ type TourSource struct {
FahrerName pgtype.Text `json:"fahrerName"`
Fahrer pgtype.Text `json:"fahrer"`
Color pgtype.Text `json:"color"`
LastUpdate pgtype.Text `json:"lastUpdate"`
CompanyName pgtype.Text `json:"companyName"`
OverRTW pgtype.Text `json:"overRTW"`
CompanyName pgtype.Text `json:"companyName"`
Schwerlast pgtype.Text `json:"schwerlast"`
}
@@ -700,11 +699,11 @@ type TourStaging struct {
Revenue float64 `json:"revenue"`
RevenueDeviation float64 `json:"revenueDeviation"`
ConsumptionCosts float64 `json:"consumptionCosts"`
OverRTW bool `json:"overRTW"`
CompanyName pgtype.Text `json:"companyName"`
Schwerlast bool `json:"schwerlast"`
CreatedAt *time.Time `json:"createdAt"`
UpdatedAt *time.Time `json:"updatedAt"`
CompanyName pgtype.Text `json:"companyName"`
OverRTW bool `json:"overRTW"`
Schwerlast bool `json:"schwerlast"`
}
type Trace struct {
@@ -751,6 +750,6 @@ type WorkTimeStaging struct {
DepartmentName pgtype.Text `json:"departmentName"`
StartDate time.Time `json:"startDate"`
EndDate time.Time `json:"endDate"`
Duration int32 `json:"duration"`
Pause int32 `json:"pause"`
Duration int32 `json:"duration"`
}
@@ -21,16 +21,7 @@ WITH ToursInPeriod AS (
CASE
WHEN "occupiedKm" <= 30 THEN 1.0
ELSE ROUND("occupiedKm" / 30.0)
END AS tour_value,
CASE
WHEN EXTRACT(ISODOW FROM "startDate") = 6 AND "occupiedKm" <= 30 THEN 0.5
ELSE 0.0
END AS weekend_bonus,
"infectionName",
CASE
WHEN "infectionName" = 'Covid-19' THEN 0.5
ELSE 0.0
END AS infection_bonus
END AS tour_value
FROM
"Tour"
WHERE
@@ -62,7 +53,7 @@ WITH ToursInPeriod AS (
DailyCounts AS (
SELECT
day,
SUM(tour_value) + SUM(weekend_bonus) + SUM(infection_bonus) AS tour_amount
SUM(tour_value) AS tour_amount
FROM
ToursInPeriod
GROUP BY
@@ -358,7 +349,7 @@ SELECT
FROM "DyflexisEmployee" as de
INNER JOIN "Employee" as e ON de.id::text = e."personnelNumber"
WHERE de."contractStart" < CURRENT_DATE AND de."contractEnd" > (CURRENT_DATE - INTERVAL '2 months')
AND de.surname <> 'Shahrasebhi'
AND de.surname <> 'Shahrasebi'
ORDER BY de.firstname ASC
`
@@ -194,6 +194,15 @@ func genLineItemsFromStatement(stmt domain.PayrollStatement) []LineItem {
Faktor: 125,
})
}
if stmt.Feiertagszuschlag150 > 0 {
items = append(items, LineItem{
Lohnart: "0124",
Beschreibung: "Feiertagszuschlag 150%",
Anzahl: stmt.Feiertagszuschlag150,
Betrag: stmt.VertragStundenlohn,
Faktor: 150,
})
}
if stmt.Nachtzuschlag25 > 0 {
items = append(items, LineItem{
Lohnart: "0505",
@@ -25,7 +25,7 @@ func NewEmployeeHandler(service Service, logger *slog.Logger) *EmployeeHandler {
}
func (h *EmployeeHandler) RunPayroll(w http.ResponseWriter, r *http.Request) {
month := time.Date(2025, time.December, 1, 0, 0, 0, 0, time.UTC)
month := time.Date(2026, time.January, 1, 0, 0, 0, 0, time.UTC)
employees, err := h.service.FetchActiveEmployees(r.Context())
if err != nil {
@@ -0,0 +1,85 @@
Nachname;Vorname;Verpflegungspauschale;Vertr. Stundenlohn;Arbeitsstunden lt. Vertrag;Gearbeitet Stunden;Gearbeitet Tage;Pünktlichkeitsprämie anzahl;Krankheitstage x 8 Stunden;davon Überstunden;Tourengeld Anzahl;Tourengeld-Bonus (Platz 1-3;Gesundheitsbonus abzuziehen? (ja/nein);Gesundheitsbonus;Urlaubsgeld (44 EUR pro tag);P-schein Zulage;Wochenendzuschlag 15%;Sonntagszuschlag 50%;Feiertagszuschlag 125%;Feiertagszuschlag 150%;Nachtzuschlag 25%;Nachtzuschlag 40%
Ahmadpour;Amirhossein;21;14.42;173.32;205.68333333333334;24;23;0;32.35133333333332;44;500;;200;0;24;26.833333333333;0;5.75;0;0;0
Hosseini Jahed;Sajad;19;14.42;173.32;187.76666666666665;22;21;0;14.434666666666637;40;400;;200;0;22;26.833333333333;0;6;0;0;0
Salehi;Iman;25;17.31;173.32;223.78333333333333;26;25;0;50.45133333333332;28;300;;200;0;26;16.5;19.45;9.25;8.6;63.45;21.033333333333
Khodayaran;Hossein;22;16.15;173.32;185.89999999999998;22;22;0;12.567999999999962;26;0;;200;0;22;8.2666666666667;8.1666666666667;8.0166666666667;8.1333333333333;49.583333333333;11.633333333333
Saki;Sara;24;15;173.32;220.23333333333332;26;25;0;46.901333333333305;25;0;;200;0;26;0;28.75;9.25;8.6;62.35;21.033333333333
Rohany Oskoee;Iman;23;16.15;173.32;213.84999999999997;25;8;0;40.51799999999995;23;0;;200;0;0;34.566666666667;0;7.7333333333333;0;0;0
Moshtagh Askari;Adel;26;15.58;173.32;226;26;20;0;52.667999999999985;23;0;;200;0;26;34.666666666667;0;9.0333333333333;0;0;0
Faridani Rad;Farzad;21;15;173.32;183.75;22;22;0;10.417999999999985;23;0;;200;0;22;7.5;6.1166666666667;8.0166666666667;8.1333333333333;50.25;11.633333333333
Malani Karizi;Arasch;22;14.42;173.32;199.3;24;23;0;25.967999999999996;17;0;;200;0;0;8.6666666666667;0;16.533333333333;0;0.25;0
Sharh Ghaboos;Peyman;19;14.42;173.32;169.08333333333331;20;19;0;19.7513333333333;16;0;;200;3;20;8.2833333333333;0;16.533333333333;0;0.25;0
Sajaei Khouzestani;Farid;23;14.42;173.32;203.6;24;23;0;30.26799999999998;16;0;;200;0;24;18.25;0;9.0333333333333;0.58333333333333;0;0
Hosseini;Seyed Hamid;23;14.42;173.32;197.3;24;24;0;23.967999999999996;15;0;;200;0;24;8.0833333333333;0;15.25;0.25;0;0
Saeedpour;Fouad;21;14.42;173.32;179.15;21;21;0;5.817999999999991;15;0;;200;0;21;26.033333333333;17.3;0;0;13.733333333333;0
Zekarias;Michael;22;15;173.32;186.54999999999998;23;22;0;13.217999999999968;14;0;;200;0;0;17.466666666667;0;8.2833333333333;0.8;12.166666666667;0
Rahimzadeh;Siamak;24;14.42;173.32;217.91666666666666;26;25;0;44.58466666666664;14;0;;200;0;26;18.25;0;15.25;0.25;0;0
Safari;Abdul Latif ;22;14.42;173.32;203.8;24;23;0;54.467999999999996;14;0;;200;3;24;25.416666666667;0;14.533333333333;0;0;0
Sabahi;Mohammad;22;16.15;173.32;192.00000000000003;23;22;0;18.668000000000013;12;0;;200;0;23;8.2333333333333;0;8.0166666666667;0;0;0
Pourhossein;Milad;17;14.42;173.32;148.56666666666666;18;18;0;-0.7653333333333521;11;0;;200;3;18;16.716666666667;0;8.0333333333333;0;9.7333333333333;0
Juyande;Masoud;23;15;173.32;197.75;24;23;0;24.417999999999985;11;0;;200;0;0;0;0;16.05;6;50.4;21.45
Pourhossein;Reza;13;14.42;173.32;122.65;15;14;0;21.31799999999999;11;0;;200;9;15;8.5833333333333;15.033333333333;0;0;13.733333333333;0
Eggerstedt;Matthias;18;15.58;173.32;156.64999999999998;19;19;0;7.317999999999962;10;0;;200;3;19;8.6666666666667;0;8.4166666666667;0;0;0
Pouzesh;Esmail;24;16.15;173.32;199.58333333333337;24;24;0;26.251333333333356;10;0;;200;0;24;0;8.1666666666667;16.05;6;62.416666666667;21.45
Saffarnajib;Majid;18;15.38;90.99300000000001;150.1;18;18;0;59.10069999999999;10;0;;0;0;18;8.2666666666667;0;8.0333333333333;4.2666666666667;0;0
Tajikzadeh;Mohammad;22;16.15;173.32;180.29999999999998;22;22;0;6.967999999999968;9;0;;200;0;22;16.316666666667;0;8.0333333333333;4.0333333333333;0;0
Rasulkhani;Hossein;12;14.42;173.32;105.26666666666668;13;13;0;27.934666666666665;8;0;;200;12;13;0;0;14.033333333333;0.58333333333333;0;0
Shokripour;Vahid;21;15;173.32;177.85000000000002;22;21;4;36.51800000000001;8;0;ja;0;0;22;17.083333333333;0;16.05;0.8;0;0
Maleki;Mohammad;5;14.44;103.992;66.5;8;2;1;-29.49920000000001;7;0;ja;0;0;8;0;0;0;0;0;0
Imanian;Shahriyar;17;14.42;173.32;150.01666666666668;18;16;0;32.684666666666665;7;0;;200;7;18;25.65;9.2666666666667;13.783333333333;4.1166666666667;0.26666666666667;0
Sahem;Khalid;16;14.42;173.32;137.71666666666667;16;16;0;28.384666666666654;7;0;;200;8;16;17.233333333333;0;0;0;0;0
Toraman;Tolga Can;17;14.42;173.32;152.91666666666666;19;17;4;11.584666666666642;6;0;ja;0;0;0;0;0;14.666666666667;0;0;0
Rasul Khani;Mohammad;14;14.42;173.32;117.41666666666667;14;14;0;8.08466666666667;5;0;;200;8;14;8.75;8.9166666666667;8.0333333333333;4.2666666666667;6.05;0
Kalantari;Kaveh;17;14.42;173.32;143.3;17;17;0;9.967999999999996;5;0;;200;5;17;17.05;16.95;16.05;4.1333333333333;0.43333333333333;0
Hastedt;David;8;14.93;138.656;76.08333333333333;9;3;4;-30.582266666666683;5;0;ja;0;0;0;0;0;0;0;0;0
Florescu;Silviu-Cezar;18;15;173.32;159.36666666666667;19;18;0;18.03466666666666;4;0;;200;4;0;8.7333333333333;0;0;0;0.26666666666667;0
Tokhi;Shokrollah;15;14.42;173.32;136.05;17;16;0;2.7179999999999964;3;0;;200;5;17;0;0;0;4.0333333333333;3.5333333333333;0
Najafgholizadeh;Masoud;11;12.82;103.992;95.9;11;0;0;-8.099200000000003;0;0;ja;0;0;0;8.25;0;0;0;0;0
Heucke;Niklas;0;15;173.32;0;0;0;0;-173.33200000000002;0;0;ja;0;0;0;0;0;0;0;0;0
Latza;Lelia Eleonore;0;6.35;173.32;0;0;0;0;-173.33200000000002;0;0;ja;0;0;0;0;0;0;0;0;0
Hazrati;Kamyar;12;12.82;173.32;112.03333333333335;13;0;1;-28.798666666666655;0;0;ja;0;0;0;15.066666666667;0;0;0;0;0
Haralambous;Martin;0;12.41;173.32;0;0;0;0;-173.33200000000002;0;0;ja;0;0;0;0;0;0;0;0;0
Krüger;Julian;0;22.5;173.32;90.48333333333333;11;0;5;21.151333333333334;0;0;ja;0;0;0;0;0;0;0;0;0
Mohamed Salah;Hussin Noureldin;13;12.82;173.32;125.28333333333333;15;0;1;-40.04866666666668;0;0;ja;0;0;0;15.766666666667;0;0;0;0;0
Tokhi;Haroun;0;14.42;173.32;0;0;0;0;-173.33200000000002;0;0;ja;0;0;0;0;0;0;0;0;0
Arabnia;Mehdi;18;12.82;173.32;181.06666666666666;21;0;0;7.734666666666648;0;0;ja;0;0;0;16;0;7.7833333333333;0;0;0
Taghadosi;Mehdi;16;12.82;173.32;145.41666666666666;17;0;0;-27.915333333333358;0;0;ja;0;0;0;25.033333333333;0;7.6833333333333;0.083333333333333;0;0
Jalali Wagener;Mehdi;17;12.82;173.32;167.61666666666667;20;0;0;-5.715333333333341;0;0;ja;0;0;0;16.016666666667;0;6.2666666666667;0;0;0
Mazidi;Mehrdad;13;12.82;173.32;121.28333333333335;14;0;1;3.951333333333345;0;0;ja;0;0;0;15.816666666667;0;0;0;0;0
Koell;Melanie Nadine;0;15.58;173.32;0;0;0;0;-173.33200000000002;0;0;ja;0;0;0;0;0;0;0;0;0
Karimloo;Hamidreza;0;12.82;173.32;0;0;0;0;-173.33200000000002;0;0;ja;0;0;0;0;0;0;0;0;0
Hashimazada;Firuz;15;12.82;173.32;135.96666666666667;16;0;0;-37.365333333333346;0;0;ja;0;0;0;0;0;8.5;0;0;0
Esmaeili Miandehi;Milad;0;12.82;173.32;0;0;0;0;-173.33200000000002;0;0;ja;0;0;0;0;0;0;0;0;0
Movafagh;Farzad ;21;12.82;173.32;187.56666666666666;22;0;0;14.234666666666648;0;0;ja;0;0;0;16.266666666667;0;7.1833333333333;0.083333333333333;0;0
Esser;Detlef;0;12;121.32400000000001;0;0;0;0;-121.3324;0;0;ja;0;0;0;0;0;0;0;0;0
Abdallah;Delshad;0;12.82;173.32;0;0;0;0;-173.33200000000002;0;0;ja;0;0;0;0;0;0;0;0;0
Golmohammadi;Davood ;18;12.82;173.32;164.38333333333333;19;0;0;-8.94866666666669;0;0;ja;0;0;0;0;0;0;0;0;0
Atrafi;Mohammad Nasir;13;12.82;173.32;124.25;15;0;0;-17.082000000000015;0;0;ja;0;0;0;8.0166666666667;0;0;0;0;0
Sedighian Bidgoli;Mohammadreza;12;12.82;173.32;118.75;14;0;0;-54.582000000000015;0;0;ja;0;0;0;0;0;0;0;0;0
Amiri;Mohammad Samir;0;19.62;173.32;165.4333333333333;19;0;0;8.101333333333294;0;0;ja;0;0;0;14.416666666667;27;17.05;6.5;24.5;19.4
Babaei Dezghi;Mojtaba;0;15;160.321;0;0;0;0;-162.49875;0;0;ja;0;0;0;0;0;0;0;0;0
El Moustaiqid;Najlae;0;12.82;173.32;117.19999999999999;16;0;0;-32.132000000000026;0;0;ja;0;0;0;0;0;0;0;0;0
Saei;Kasra;0;5.65;173.32;15.133333333333333;2;0;0;-158.19866666666667;0;0;ja;0;0;0;0;0;0;0;0;0
Jasarevic;Noel;0;14.42;173.32;128.36666666666667;15;0;4;-10.96533333333334;0;0;ja;0;0;0;0;0;0;0;0;0
Lolies;Danny;11;12.82;173.32;102.41666666666667;12;0;1;-38.41533333333333;0;0;ja;0;0;0;16.016666666667;0;0;0;0;0
Feilbach;Raoul Nikolai ;0;19.62;173.32;191.36666666666667;22;0;0;18.03466666666666;0;0;ja;0;0;0;8.7;9.4166666666667;7.25;0.016666666666667;17.25;14.366666666667
Hatami;Reza;17;12.82;173.32;170.96666666666667;20;0;0;-2.3653333333333464;0;0;ja;0;0;0;16.283333333333;0;8.7;0;0;0
Emkani;Daniel;0;22.48;108.325;106.23333333333333;13;0;0;6.2341666666666535;0;0;ja;0;0;0;6.85;0;8.7;0;0;0
Rohi Pour;Sadroldin;20;12.82;173.32;183.7;21;0;0;10.367999999999974;0;0;ja;0;0;0;0;0;7.7833333333333;0;0;0
Sidiqi;Sahel;0;14.42;173.32;0;0;0;0;-173.33200000000002;0;0;ja;0;0;0;0;0;0;0;0;0
Mohammadi;Danial;13;12.82;173.32;127.31666666666666;15;0;0;-22.015333333333352;0;0;ja;0;0;0;15.066666666667;0;0;0;0;0
Haghighivand;Danial;16;12.82;173.32;148.93333333333334;17;0;0;-24.398666666666678;0;0;ja;0;0;0;0;0;7.65;0;0;0
Perez Y Waldt;Sebastian;0;14.42;173.32;0;0;0;0;-173.33200000000002;0;0;ja;0;0;0;0;0;0;0;0;0
Aba;Cuan;0;5.65;173.32;81.26666666666667;11;0;0;-84.06533333333334;0;0;ja;0;0;0;0;0;0;0;0;0
Askarian Pour;Seyed Hosein;0;15.58;173.32;0;0;0;0;-173.33200000000002;0;0;ja;0;0;0;0;0;0;0;0;0
Jabbari;Shahram;16;12.82;173.32;151.01666666666665;18;0;0;-22.315333333333363;0;0;ja;0;0;0;17.3;0;6.2666666666667;0;0;0
Altner;Christian Van Manh;0;15;173.32;0;0;0;0;-173.33200000000002;0;0;ja;0;0;0;0;0;0;0;0;0
Alami;Shoaib;0;12.82;173.32;0;0;0;0;-173.33200000000002;0;0;ja;0;0;0;0;0;0;0;0;0
Modrau;Cara Marleen;0;14.42;173.32;0;0;0;0;-173.33200000000002;0;0;ja;0;0;0;0;0;0;0;0;0
Rahmati;Behnam;16;12.82;173.32;148.93333333333334;17;0;0;-24.398666666666678;0;0;ja;0;0;0;0;0;7.65;0;0;0
Ziaudin;Atiq;12;12.82;173.32;119.6;14;0;2;-37.23200000000002;0;0;ja;0;0;0;8;0;0;0;0;0
Vafaei;Soheil;13;12.82;173.32;136.26666666666665;16;0;2;-21.065333333333363;0;0;ja;0;0;0;16;0;6.0166666666667;0;0;0
Esmaeili Dashtbayaz;Amin;8;12.82;173.32;70.48333333333333;8;0;0;17.151333333333334;0;0;ja;0;0;0;0;0;0;0;0;0
Haugg;Uwe;11;12.82;138.656;109.53333333333333;13;0;0;-13.13226666666668;0;0;ja;0;0;0;30.45;0;0;0;0;0
Mihailescu;Adrian Lucian;1;16.73;173.32;8.1;1;0;0;-5.232000000000021;0;0;;200;20;1;0;0;0;0;0;0
Konak;Zidar;0;22.5;173.32;176.85;21;0;0;11.51799999999998;0;0;ja;0;0;0;5.65;0;8.5333333333333;3.15;17;12.066666666667
1 Nachname Vorname Verpflegungspauschale Vertr. Stundenlohn Arbeitsstunden lt. Vertrag Gearbeitet Stunden Gearbeitet Tage Pünktlichkeitsprämie anzahl Krankheitstage x 8 Stunden davon Überstunden Tourengeld Anzahl Tourengeld-Bonus (Platz 1-3 Gesundheitsbonus abzuziehen? (ja/nein) Gesundheitsbonus Urlaubsgeld (44 EUR pro tag) P-schein Zulage Wochenendzuschlag 15% Sonntagszuschlag 50% Feiertagszuschlag 125% Feiertagszuschlag 150% Nachtzuschlag 25% Nachtzuschlag 40%
2 Ahmadpour Amirhossein 21 14.42 173.32 205.68333333333334 24 23 0 32.35133333333332 44 500 200 0 24 26.833333333333 0 5.75 0 0 0
3 Hosseini Jahed Sajad 19 14.42 173.32 187.76666666666665 22 21 0 14.434666666666637 40 400 200 0 22 26.833333333333 0 6 0 0 0
4 Salehi Iman 25 17.31 173.32 223.78333333333333 26 25 0 50.45133333333332 28 300 200 0 26 16.5 19.45 9.25 8.6 63.45 21.033333333333
5 Khodayaran Hossein 22 16.15 173.32 185.89999999999998 22 22 0 12.567999999999962 26 0 200 0 22 8.2666666666667 8.1666666666667 8.0166666666667 8.1333333333333 49.583333333333 11.633333333333
6 Saki Sara 24 15 173.32 220.23333333333332 26 25 0 46.901333333333305 25 0 200 0 26 0 28.75 9.25 8.6 62.35 21.033333333333
7 Rohany Oskoee Iman 23 16.15 173.32 213.84999999999997 25 8 0 40.51799999999995 23 0 200 0 0 34.566666666667 0 7.7333333333333 0 0 0
8 Moshtagh Askari Adel 26 15.58 173.32 226 26 20 0 52.667999999999985 23 0 200 0 26 34.666666666667 0 9.0333333333333 0 0 0
9 Faridani Rad Farzad 21 15 173.32 183.75 22 22 0 10.417999999999985 23 0 200 0 22 7.5 6.1166666666667 8.0166666666667 8.1333333333333 50.25 11.633333333333
10 Malani Karizi Arasch 22 14.42 173.32 199.3 24 23 0 25.967999999999996 17 0 200 0 0 8.6666666666667 0 16.533333333333 0 0.25 0
11 Sharh Ghaboos Peyman 19 14.42 173.32 169.08333333333331 20 19 0 19.7513333333333 16 0 200 3 20 8.2833333333333 0 16.533333333333 0 0.25 0
12 Sajaei Khouzestani Farid 23 14.42 173.32 203.6 24 23 0 30.26799999999998 16 0 200 0 24 18.25 0 9.0333333333333 0.58333333333333 0 0
13 Hosseini Seyed Hamid 23 14.42 173.32 197.3 24 24 0 23.967999999999996 15 0 200 0 24 8.0833333333333 0 15.25 0.25 0 0
14 Saeedpour Fouad 21 14.42 173.32 179.15 21 21 0 5.817999999999991 15 0 200 0 21 26.033333333333 17.3 0 0 13.733333333333 0
15 Zekarias Michael 22 15 173.32 186.54999999999998 23 22 0 13.217999999999968 14 0 200 0 0 17.466666666667 0 8.2833333333333 0.8 12.166666666667 0
16 Rahimzadeh Siamak 24 14.42 173.32 217.91666666666666 26 25 0 44.58466666666664 14 0 200 0 26 18.25 0 15.25 0.25 0 0
17 Safari Abdul Latif 22 14.42 173.32 203.8 24 23 0 54.467999999999996 14 0 200 3 24 25.416666666667 0 14.533333333333 0 0 0
18 Sabahi Mohammad 22 16.15 173.32 192.00000000000003 23 22 0 18.668000000000013 12 0 200 0 23 8.2333333333333 0 8.0166666666667 0 0 0
19 Pourhossein Milad 17 14.42 173.32 148.56666666666666 18 18 0 -0.7653333333333521 11 0 200 3 18 16.716666666667 0 8.0333333333333 0 9.7333333333333 0
20 Juyande Masoud 23 15 173.32 197.75 24 23 0 24.417999999999985 11 0 200 0 0 0 0 16.05 6 50.4 21.45
21 Pourhossein Reza 13 14.42 173.32 122.65 15 14 0 21.31799999999999 11 0 200 9 15 8.5833333333333 15.033333333333 0 0 13.733333333333 0
22 Eggerstedt Matthias 18 15.58 173.32 156.64999999999998 19 19 0 7.317999999999962 10 0 200 3 19 8.6666666666667 0 8.4166666666667 0 0 0
23 Pouzesh Esmail 24 16.15 173.32 199.58333333333337 24 24 0 26.251333333333356 10 0 200 0 24 0 8.1666666666667 16.05 6 62.416666666667 21.45
24 Saffarnajib Majid 18 15.38 90.99300000000001 150.1 18 18 0 59.10069999999999 10 0 0 0 18 8.2666666666667 0 8.0333333333333 4.2666666666667 0 0
25 Tajikzadeh Mohammad 22 16.15 173.32 180.29999999999998 22 22 0 6.967999999999968 9 0 200 0 22 16.316666666667 0 8.0333333333333 4.0333333333333 0 0
26 Rasulkhani Hossein 12 14.42 173.32 105.26666666666668 13 13 0 27.934666666666665 8 0 200 12 13 0 0 14.033333333333 0.58333333333333 0 0
27 Shokripour Vahid 21 15 173.32 177.85000000000002 22 21 4 36.51800000000001 8 0 ja 0 0 22 17.083333333333 0 16.05 0.8 0 0
28 Maleki Mohammad 5 14.44 103.992 66.5 8 2 1 -29.49920000000001 7 0 ja 0 0 8 0 0 0 0 0 0
29 Imanian Shahriyar 17 14.42 173.32 150.01666666666668 18 16 0 32.684666666666665 7 0 200 7 18 25.65 9.2666666666667 13.783333333333 4.1166666666667 0.26666666666667 0
30 Sahem Khalid 16 14.42 173.32 137.71666666666667 16 16 0 28.384666666666654 7 0 200 8 16 17.233333333333 0 0 0 0 0
31 Toraman Tolga Can 17 14.42 173.32 152.91666666666666 19 17 4 11.584666666666642 6 0 ja 0 0 0 0 0 14.666666666667 0 0 0
32 Rasul Khani Mohammad 14 14.42 173.32 117.41666666666667 14 14 0 8.08466666666667 5 0 200 8 14 8.75 8.9166666666667 8.0333333333333 4.2666666666667 6.05 0
33 Kalantari Kaveh 17 14.42 173.32 143.3 17 17 0 9.967999999999996 5 0 200 5 17 17.05 16.95 16.05 4.1333333333333 0.43333333333333 0
34 Hastedt David 8 14.93 138.656 76.08333333333333 9 3 4 -30.582266666666683 5 0 ja 0 0 0 0 0 0 0 0 0
35 Florescu Silviu-Cezar 18 15 173.32 159.36666666666667 19 18 0 18.03466666666666 4 0 200 4 0 8.7333333333333 0 0 0 0.26666666666667 0
36 Tokhi Shokrollah 15 14.42 173.32 136.05 17 16 0 2.7179999999999964 3 0 200 5 17 0 0 0 4.0333333333333 3.5333333333333 0
37 Najafgholizadeh Masoud 11 12.82 103.992 95.9 11 0 0 -8.099200000000003 0 0 ja 0 0 0 8.25 0 0 0 0 0
38 Heucke Niklas 0 15 173.32 0 0 0 0 -173.33200000000002 0 0 ja 0 0 0 0 0 0 0 0 0
39 Latza Lelia Eleonore 0 6.35 173.32 0 0 0 0 -173.33200000000002 0 0 ja 0 0 0 0 0 0 0 0 0
40 Hazrati Kamyar 12 12.82 173.32 112.03333333333335 13 0 1 -28.798666666666655 0 0 ja 0 0 0 15.066666666667 0 0 0 0 0
41 Haralambous Martin 0 12.41 173.32 0 0 0 0 -173.33200000000002 0 0 ja 0 0 0 0 0 0 0 0 0
42 Krüger Julian 0 22.5 173.32 90.48333333333333 11 0 5 21.151333333333334 0 0 ja 0 0 0 0 0 0 0 0 0
43 Mohamed Salah Hussin Noureldin 13 12.82 173.32 125.28333333333333 15 0 1 -40.04866666666668 0 0 ja 0 0 0 15.766666666667 0 0 0 0 0
44 Tokhi Haroun 0 14.42 173.32 0 0 0 0 -173.33200000000002 0 0 ja 0 0 0 0 0 0 0 0 0
45 Arabnia Mehdi 18 12.82 173.32 181.06666666666666 21 0 0 7.734666666666648 0 0 ja 0 0 0 16 0 7.7833333333333 0 0 0
46 Taghadosi Mehdi 16 12.82 173.32 145.41666666666666 17 0 0 -27.915333333333358 0 0 ja 0 0 0 25.033333333333 0 7.6833333333333 0.083333333333333 0 0
47 Jalali Wagener Mehdi 17 12.82 173.32 167.61666666666667 20 0 0 -5.715333333333341 0 0 ja 0 0 0 16.016666666667 0 6.2666666666667 0 0 0
48 Mazidi Mehrdad 13 12.82 173.32 121.28333333333335 14 0 1 3.951333333333345 0 0 ja 0 0 0 15.816666666667 0 0 0 0 0
49 Koell Melanie Nadine 0 15.58 173.32 0 0 0 0 -173.33200000000002 0 0 ja 0 0 0 0 0 0 0 0 0
50 Karimloo Hamidreza 0 12.82 173.32 0 0 0 0 -173.33200000000002 0 0 ja 0 0 0 0 0 0 0 0 0
51 Hashimazada Firuz 15 12.82 173.32 135.96666666666667 16 0 0 -37.365333333333346 0 0 ja 0 0 0 0 0 8.5 0 0 0
52 Esmaeili Miandehi Milad 0 12.82 173.32 0 0 0 0 -173.33200000000002 0 0 ja 0 0 0 0 0 0 0 0 0
53 Movafagh Farzad 21 12.82 173.32 187.56666666666666 22 0 0 14.234666666666648 0 0 ja 0 0 0 16.266666666667 0 7.1833333333333 0.083333333333333 0 0
54 Esser Detlef 0 12 121.32400000000001 0 0 0 0 -121.3324 0 0 ja 0 0 0 0 0 0 0 0 0
55 Abdallah Delshad 0 12.82 173.32 0 0 0 0 -173.33200000000002 0 0 ja 0 0 0 0 0 0 0 0 0
56 Golmohammadi Davood 18 12.82 173.32 164.38333333333333 19 0 0 -8.94866666666669 0 0 ja 0 0 0 0 0 0 0 0 0
57 Atrafi Mohammad Nasir 13 12.82 173.32 124.25 15 0 0 -17.082000000000015 0 0 ja 0 0 0 8.0166666666667 0 0 0 0 0
58 Sedighian Bidgoli Mohammadreza 12 12.82 173.32 118.75 14 0 0 -54.582000000000015 0 0 ja 0 0 0 0 0 0 0 0 0
59 Amiri Mohammad Samir 0 19.62 173.32 165.4333333333333 19 0 0 8.101333333333294 0 0 ja 0 0 0 14.416666666667 27 17.05 6.5 24.5 19.4
60 Babaei Dezghi Mojtaba 0 15 160.321 0 0 0 0 -162.49875 0 0 ja 0 0 0 0 0 0 0 0 0
61 El Moustaiqid Najlae 0 12.82 173.32 117.19999999999999 16 0 0 -32.132000000000026 0 0 ja 0 0 0 0 0 0 0 0 0
62 Saei Kasra 0 5.65 173.32 15.133333333333333 2 0 0 -158.19866666666667 0 0 ja 0 0 0 0 0 0 0 0 0
63 Jasarevic Noel 0 14.42 173.32 128.36666666666667 15 0 4 -10.96533333333334 0 0 ja 0 0 0 0 0 0 0 0 0
64 Lolies Danny 11 12.82 173.32 102.41666666666667 12 0 1 -38.41533333333333 0 0 ja 0 0 0 16.016666666667 0 0 0 0 0
65 Feilbach Raoul Nikolai 0 19.62 173.32 191.36666666666667 22 0 0 18.03466666666666 0 0 ja 0 0 0 8.7 9.4166666666667 7.25 0.016666666666667 17.25 14.366666666667
66 Hatami Reza 17 12.82 173.32 170.96666666666667 20 0 0 -2.3653333333333464 0 0 ja 0 0 0 16.283333333333 0 8.7 0 0 0
67 Emkani Daniel 0 22.48 108.325 106.23333333333333 13 0 0 6.2341666666666535 0 0 ja 0 0 0 6.85 0 8.7 0 0 0
68 Rohi Pour Sadroldin 20 12.82 173.32 183.7 21 0 0 10.367999999999974 0 0 ja 0 0 0 0 0 7.7833333333333 0 0 0
69 Sidiqi Sahel 0 14.42 173.32 0 0 0 0 -173.33200000000002 0 0 ja 0 0 0 0 0 0 0 0 0
70 Mohammadi Danial 13 12.82 173.32 127.31666666666666 15 0 0 -22.015333333333352 0 0 ja 0 0 0 15.066666666667 0 0 0 0 0
71 Haghighivand Danial 16 12.82 173.32 148.93333333333334 17 0 0 -24.398666666666678 0 0 ja 0 0 0 0 0 7.65 0 0 0
72 Perez Y Waldt Sebastian 0 14.42 173.32 0 0 0 0 -173.33200000000002 0 0 ja 0 0 0 0 0 0 0 0 0
73 Aba Cuan 0 5.65 173.32 81.26666666666667 11 0 0 -84.06533333333334 0 0 ja 0 0 0 0 0 0 0 0 0
74 Askarian Pour Seyed Hosein 0 15.58 173.32 0 0 0 0 -173.33200000000002 0 0 ja 0 0 0 0 0 0 0 0 0
75 Jabbari Shahram 16 12.82 173.32 151.01666666666665 18 0 0 -22.315333333333363 0 0 ja 0 0 0 17.3 0 6.2666666666667 0 0 0
76 Altner Christian Van Manh 0 15 173.32 0 0 0 0 -173.33200000000002 0 0 ja 0 0 0 0 0 0 0 0 0
77 Alami Shoaib 0 12.82 173.32 0 0 0 0 -173.33200000000002 0 0 ja 0 0 0 0 0 0 0 0 0
78 Modrau Cara Marleen 0 14.42 173.32 0 0 0 0 -173.33200000000002 0 0 ja 0 0 0 0 0 0 0 0 0
79 Rahmati Behnam 16 12.82 173.32 148.93333333333334 17 0 0 -24.398666666666678 0 0 ja 0 0 0 0 0 7.65 0 0 0
80 Ziaudin Atiq 12 12.82 173.32 119.6 14 0 2 -37.23200000000002 0 0 ja 0 0 0 8 0 0 0 0 0
81 Vafaei Soheil 13 12.82 173.32 136.26666666666665 16 0 2 -21.065333333333363 0 0 ja 0 0 0 16 0 6.0166666666667 0 0 0
82 Esmaeili Dashtbayaz Amin 8 12.82 173.32 70.48333333333333 8 0 0 17.151333333333334 0 0 ja 0 0 0 0 0 0 0 0 0
83 Haugg Uwe 11 12.82 138.656 109.53333333333333 13 0 0 -13.13226666666668 0 0 ja 0 0 0 30.45 0 0 0 0 0
84 Mihailescu Adrian Lucian 1 16.73 173.32 8.1 1 0 0 -5.232000000000021 0 0 200 20 1 0 0 0 0 0 0
85 Konak Zidar 0 22.5 173.32 176.85 21 0 0 11.51799999999998 0 0 ja 0 0 0 5.65 0 8.5333333333333 3.15 17 12.066666666667
@@ -0,0 +1,86 @@
Nachname;Vorname;Verpflegungspauschale;Vertr. Stundenlohn;Arbeitsstunden lt. Vertrag;Gearbeitet Stunden;Gearbeitet Tage;Pünktlichkeitsprämie anzahl;Krankheitstage x 8 Stunden;davon Überstunden;Tourengeld Anzahl;Tourengeld-Bonus (Platz 1-3;Gesundheitsbonus abzuziehen? (ja/nein);Gesundheitsbonus;Urlaubsgeld (44 EUR pro tag);P-schein Zulage;Wochenendzuschlag 15%;Sonntagszuschlag 50%;Feiertagszuschlag 125%;Feiertagszuschlag 150%;Nachtzuschlag 25%;Nachtzuschlag 40%
Malani Karizi;Arasch;23;14.42;173.32;207.74999999999997;24;23;0;34.41799999999996;41.5;500;;200;0;0;24.55;8.8666666666667;0;0;1.5;0
Hosseini;Seyed Hamid;22;14.42;173.32;189.26666666666668;22;22;0;15.934666666666665;35;400;;200;0;22;24.333333333333;0;0;0;0;0
Sharh Ghaboos;Peyman;22;14.42;173.32;199.08333333333334;23;22;0;25.751333333333328;34.5;300;;200;0;23;24.55;8.8666666666667;0;0;1.5;0
Moshtagh Askari;Adel;22;15.58;173.32;198.91666666666666;23;20;0;25.584666666666642;31;0;;200;0;23;17.45;0;0;0;0;0
Imanian;Shahriyar;23;14.42;173.32;199.2166666666667;23;23;0;25.884666666666682;30.5;0;;200;0;23;33.45;44.716666666667;0;0;0.7;0
Saeedpour;Fouad;21;14.42;173.32;180.68333333333334;21;21;0;7.351333333333322;30.5;0;;200;0;21;33.033333333333;18.583333333333;0;0;17.5;10.55
Rahimzadeh;Siamak;20;14.42;173.32;182.96666666666664;22;21;0;9.634666666666625;27;0;;200;0;22;16.433333333333;0;0;0;0;0
Rohany Oskoee;Iman;21;16.15;173.32;196.86666666666667;23;10;0;23.53466666666666;26.5;0;;200;0;0;16.883333333333;0;0;0;0;0
Sahem;Khalid;19;14.42;173.32;168.71666666666664;20;19;0;-4.615333333333375;25;0;;200;0;20;8.6833333333333;0;0;0;0;0
Florescu;Silviu-Cezar;20;15;173.32;182.28333333333336;21;20;0;8.951333333333345;25;0;;200;0;0;17.533333333333;0;0;0;0.43333333333333;0
Safari;Abdul Latif ;20;14.42;173.32;185.21666666666667;21;20;0;19.884666666666654;24.5;0;;200;1;21;18.933333333333;0;0;0;0;0
Ahmadpour;Amirhossein;21;14.42;173.32;194.75;22;21;0;21.417999999999985;24.5;0;;200;0;22;8.75;0;0;0;1.1333333333333;0
Sajaei Khouzestani;Farid;22;14.42;173.32;203.5;23;22;0;30.167999999999985;24.5;0;;200;0;23;28.75;0;0;0;0;0
Mihailescu;Adrian Lucian;13;16.73;173.32;178.59999999999997;21;5;0;5.267999999999951;24;0;;200;0;21;25.65;0;0;0;0;0
Hosseini Jahed;Sajad;20;14.42;173.32;184.20000000000002;21;20;0;10.868000000000002;24;0;;200;0;21;8.5166666666667;0;0;0;0;0
Tajikzadeh;Mohammad;20;16.15;173.32;169.38333333333335;20;20;0;-3.9486666666666608;24;0;;200;0;20;24.35;0;0;0;0;0
Shokripour;Vahid;20;15;173.32;174.25;20;20;0;0.917999999999985;21;0;;200;0;20;17.266666666667;0;0;0;0;0
Saffarnajib;Majid;17;15.38;90.99300000000001;145.56666666666666;17;17;0;54.56736666666666;20;0;;0;0;17;0;0;0;0;3.85;0
Kalantari;Kaveh;18;14.42;173.32;155.16666666666666;18;18;0;29.834666666666642;19.5;0;;200;6;18;25.533333333333;17.733333333333;0;0;1.05;0
Pourhossein;Milad;20;14.42;173.32;182.6;22;19;0;9.26799999999998;18.5;0;;200;0;22;9.25;0;0;0;30.9;0.35
Hastedt;David;10;14.93;138.656;110.19999999999999;13;8;1;-7.665600000000012;17;0;ja;0;2;0;0;0;0;0;0;0
Eggerstedt;Matthias;20;15.58;173.32;171.68333333333334;20;20;0;-1.6486666666666778;17;0;;200;0;20;16.933333333333;0;0;0;0;0
Rasulkhani;Hossein;9;14.42;173.32;76.46666666666667;9;9;0;-16.865333333333346;15.5;0;;200;10;9;16.2;9;0;0;0;0
Pourhossein;Reza;22;14.42;173.32;201.4666666666667;25;24;0;28.134666666666682;14;0;;200;0;25;16.266666666667;20.95;0;0;45.85;10.166666666667
Rasul Khani;Mohammad;11;14.42;173.32;95;11;11;0;1.667999999999985;12;0;;200;10;11;8.5166666666667;0;0;0;0;0
Zekarias;Michael;20;15;173.32;172.83333333333334;21;20;0;-0.49866666666667214;12;0;;200;0;0;8.0333333333333;0;0;0;14.616666666667;0
Salehi;Iman;21;17.31;173.32;176.55;21;20;0;3.2179999999999964;10.5;0;;200;0;21;8.75;10;0;0;62.466666666667;10.9
Sabahi;Mohammad;19;16.15;173.32;170.14999999999998;20;19;0;-3.1820000000000377;10;0;;200;0;20;0;0;0;0;0;0
Toraman;Tolga Can;18;14.42;173.32;177.73333333333332;21;18;0;4.401333333333305;10;0;;200;0;0;15.65;9.0833333333333;0;0;17.883333333333;0
Tokhi;Shokrollah;19;14.42;173.32;168.03333333333336;20;19;0;-5.298666666666655;9;0;;200;0;20;16.45;17.116666666667;0;0;33.016666666667;0.05
Juyande;Masoud;13;15;173.32;113.43333333333334;14;13;0;-3.898666666666678;8;0;;200;7;0;0;0;0;0;33.116666666667;11.116666666667
Perez Y Waldt;Sebastian;3;14.42;173.32;47.3;6;2;8;-62.03200000000002;6.5;0;ja;0;0;0;7.7166666666667;0;0;0;0;0
Pouzesh;Esmail;20;16.15;173.32;170.36666666666665;21;20;0;-2.965333333333369;5;0;;200;0;21;0;0;0;0;66.283333333333;21.333333333333
Khodayaran;Hossein;18;16.15;173.32;152.54999999999998;19;19;0;-20.782000000000032;3;0;;200;0;19;0;3.5166666666667;0;0;56.733333333333;11.55
Faridani Rad;Farzad;19;15;173.32;157.2;19;19;3;7.867999999999974;3;0;ja;0;0;19;0;0;0;0;59.9;11.55
Saki;Sara;2;15;173.32;25.766666666666666;4;3;15;-27.565333333333363;2;0;ja;0;0;4;0;0;0;0;0;0
Maleki;Mohammad;6;14.44;103.992;72.68333333333334;9;1;1;-23.315866666666672;2;0;ja;0;0;9;0;0;0;0;0;0
Babaei Dezghi;Mojtaba;0;15;160.321;0;0;0;0;-162.49875;0;0;ja;0;0;0;0;0;0;0;0;0
Mazidi;Mehrdad;18;12.82;173.32;166.91666666666669;19;0;0;-6.415333333333329;0;0;ja;0;0;0;17.616666666667;0;0;0;0;0
Tokhi;Haroun;0;14.42;173.32;0;0;0;0;-173.33200000000002;0;0;ja;0;0;0;0;0;0;0;0;0
Haralambous;Martin;0;12.41;173.32;0;0;0;0;-173.33200000000002;0;0;ja;0;0;0;0;0;0;0;0;0
Karimloo;Hamidreza;0;12.82;173.32;0;0;0;0;-173.33200000000002;0;0;ja;0;0;0;0;0;0;0;0;0
Najafgholizadeh;Masoud;1;12.82;173.32;8.55;1;0;0;-164.782;0;0;ja;0;0;0;0;0;0;0;0;0
Hashimazada;Firuz;0;12.82;173.32;0;0;0;18;-29.332000000000015;0;0;ja;0;0;0;0;0;0;0;0;0
Taghadosi;Mehdi;19;12.82;173.32;167.25;19;0;0;-6.082000000000015;0;0;ja;0;0;0;16.266666666667;0;0;0;0;0
Arabnia;Mehdi;20;12.82;173.32;185.93333333333334;21;0;0;20.601333333333322;0;0;ja;0;0;0;24.766666666667;0;0;0;0;0
Heucke;Niklas;0;15;173.32;0;0;0;0;-173.33200000000002;0;0;ja;0;0;0;0;0;0;0;0;0
El Moustaiqid;Najlae;0;12.82;173.32;126.5;17;0;3;-22.832000000000015;0;0;ja;0;0;0;0;0;0;0;0;0
Koell;Melanie Nadine;0;15.58;173.32;6.5;1;0;0;-166.83200000000002;0;0;ja;0;0;0;0;0;0;0;0;0
Movafagh;Farzad ;20;12.82;173.32;182.65;21;0;0;9.31799999999999;0;0;ja;0;0;0;16.016666666667;0;0;0;0;0
Esmaeili Miandehi;Milad;0;12.82;173.32;0;0;0;0;-173.33200000000002;0;0;ja;0;0;0;0;0;0;0;0;0
Mohamed Salah;Hussin Noureldin;19;12.82;173.32;173.66666666666666;20;0;0;0.33466666666664224;0;0;ja;0;0;0;17.616666666667;0;0;0;0;0
Esser;Detlef;0;12;121.32400000000001;0;0;0;0;-121.3324;0;0;ja;0;0;0;0;0;0;0;0;0
Abdallah;Delshad;0;12.82;173.32;0;0;0;0;-173.33200000000002;0;0;ja;0;0;0;0;0;0;0;0;0
Saei;Kasra;0;5.65;173.32;56.233333333333334;8;0;1;-102.09866666666667;0;0;ja;0;0;0;0;0;0;0;0;0
Golmohammadi;Davood ;19;12.82;173.32;171.66666666666666;19;0;0;6.334666666666642;0;0;ja;0;0;0;0;0;0;0;0;0
Atrafi;Mohammad Nasir;19;12.82;17.332;177.8;20;0;0;161.26680000000002;0;0;ja;0;0;0;7.85;0;0;0;0;0
Sedighian Bidgoli;Mohammadreza;0;12.82;173.32;0;0;0;0;-173.33200000000002;0;0;ja;0;0;0;0;0;0;0;0;0
Amiri;Mohammad Samir;0;19.62;173.32;197.66666666666669;22;0;0;24.33466666666667;0;0;ja;0;0;0;19.25;27.833333333333;0;0;34.75;23.166666666667
Latza;Lelia Eleonore;0;6.35;173.32;0;0;0;0;-173.33200000000002;0;0;ja;0;0;0;0;0;0;0;0;0
Krüger;Julian;0;22.5;173.32;171.28333333333333;20;0;0;-2.0486666666666835;0;0;ja;0;0;0;6.05;0;0;0;7;4.4333333333333
Jalali Wagener;Mehdi;22;12.82;173.32;195.71666666666667;23;0;0;22.384666666666654;0;0;ja;0;0;0;24.033333333333;0;0;0;0;0
Jasarevic;Noel;0;14.42;173.32;161.93333333333334;19;0;0;-3.398666666666678;0;0;ja;0;0;0;0;0;0;0;0;0
Shahrasebi;David;0;0;173.32;0;0;0;0;-173.33200000000002;0;0;ja;0;0;0;0;0;0;0;0;0
Feilbach;Raoul Nikolai ;0;19.62;173.32;173.36666666666665;20;0;0;0.034666666666630874;0;0;ja;0;0;0;0;19;0;0;28;16.766666666667
Lolies;Danny;17;12.82;173.32;161.48333333333332;19;0;3;12.651333333333305;0;0;ja;0;0;0;16.566666666667;0;0;0;0;0
Hatami;Reza;20;12.82;173.32;175.33333333333334;20;0;0;2.001333333333328;0;0;ja;0;0;0;16.266666666667;0;0;0;0;0
Rohi Pour;Sadroldin;17;12.82;173.32;155.46666666666667;18;0;0;-17.865333333333346;0;0;ja;0;0;0;16.016666666667;0;0;0;0;0
Sidiqi;Sahel;0;14.42;173.32;0;0;0;0;-173.33200000000002;0;0;ja;0;0;0;0;0;0;0;0;0
Emkani;Daniel;0;22.48;108.325;58.916666666666664;7;0;3;-10.415833333333353;0;0;ja;0;0;0;15.65;0;0;0;0;0
Hazrati;Kamyar;19;12.82;173.32;182.23333333333335;21;0;0;8.901333333333334;0;0;ja;0;0;0;22.616666666667;0;0;0;0;0
Haghighivand;Danial;17;12.82;173.32;152.23333333333335;17;0;1;-13.098666666666666;0;0;ja;0;0;0;0;0;0;0;0;0
Mohammadi;Danial;17;12.82;173.32;165.01666666666665;19;0;0;-8.315333333333363;0;0;ja;0;0;0;14.5;0;0;0;0;0
Askarian Pour;Seyed Hosein;0;15.58;173.32;0;0;0;0;-173.33200000000002;0;0;ja;0;0;0;0;0;0;0;0;0
Jabbari;Shahram;19;12.82;173.32;165.5;19;0;0;-7.832000000000015;0;0;ja;0;0;0;16.816666666667;0;0;0;0;0
Aba;Cuan;0;5.65;173.32;69.45;10;0;2;-87.882;0;0;ja;0;0;0;0;0;0;0;0;0
Alami;Shoaib;0;12.82;173.32;0;0;0;0;-173.33200000000002;0;0;ja;0;0;0;0;0;0;0;0;0
Altner;Christian Van Manh;0;15;173.32;0;0;0;0;-173.33200000000002;0;0;ja;0;0;0;0;0;0;0;0;0
Modrau;Cara Marleen;0;14.42;173.32;0;0;0;0;-173.33200000000002;0;0;ja;0;0;0;0;0;0;0;0;0
Rahmati;Behnam;16;12.82;173.32;143.95;16;0;2;-13.382000000000026;0;0;ja;0;0;0;0;0;0;0;0;0
Vafaei;Soheil;20;12.82;173.32;176.06666666666666;20;0;0;2.734666666666648;0;0;ja;0;0;0;16.033333333333;0;0;0;0;0
Ziaudin;Atiq;20;12.82;173.32;172.6;20;0;0;-0.7320000000000206;0;0;ja;0;0;0;8.0166666666667;0;0;0;0;0
Haugg;Uwe;14;12.82;138.656;142.26666666666665;17;0;0;3.6010666666666395;0;0;ja;0;0;0;38.4;0;0;0;0;0
Esmaeili Dashtbayaz;Amin;20;12.82;173.32;181.08333333333331;21;0;0;7.751333333333299;0;0;ja;0;0;0;24.033333333333;0;0;0;0;0
Konak;Zidar;0;22.5;173.32;178.96666666666667;21;0;0;5.634666666666654;0;0;ja;0;0;0;26.95;0;0;0;1.1;0
1 Nachname Vorname Verpflegungspauschale Vertr. Stundenlohn Arbeitsstunden lt. Vertrag Gearbeitet Stunden Gearbeitet Tage Pünktlichkeitsprämie anzahl Krankheitstage x 8 Stunden davon Überstunden Tourengeld Anzahl Tourengeld-Bonus (Platz 1-3 Gesundheitsbonus abzuziehen? (ja/nein) Gesundheitsbonus Urlaubsgeld (44 EUR pro tag) P-schein Zulage Wochenendzuschlag 15% Sonntagszuschlag 50% Feiertagszuschlag 125% Feiertagszuschlag 150% Nachtzuschlag 25% Nachtzuschlag 40%
2 Malani Karizi Arasch 23 14.42 173.32 207.74999999999997 24 23 0 34.41799999999996 41.5 500 200 0 0 24.55 8.8666666666667 0 0 1.5 0
3 Hosseini Seyed Hamid 22 14.42 173.32 189.26666666666668 22 22 0 15.934666666666665 35 400 200 0 22 24.333333333333 0 0 0 0 0
4 Sharh Ghaboos Peyman 22 14.42 173.32 199.08333333333334 23 22 0 25.751333333333328 34.5 300 200 0 23 24.55 8.8666666666667 0 0 1.5 0
5 Moshtagh Askari Adel 22 15.58 173.32 198.91666666666666 23 20 0 25.584666666666642 31 0 200 0 23 17.45 0 0 0 0 0
6 Imanian Shahriyar 23 14.42 173.32 199.2166666666667 23 23 0 25.884666666666682 30.5 0 200 0 23 33.45 44.716666666667 0 0 0.7 0
7 Saeedpour Fouad 21 14.42 173.32 180.68333333333334 21 21 0 7.351333333333322 30.5 0 200 0 21 33.033333333333 18.583333333333 0 0 17.5 10.55
8 Rahimzadeh Siamak 20 14.42 173.32 182.96666666666664 22 21 0 9.634666666666625 27 0 200 0 22 16.433333333333 0 0 0 0 0
9 Rohany Oskoee Iman 21 16.15 173.32 196.86666666666667 23 10 0 23.53466666666666 26.5 0 200 0 0 16.883333333333 0 0 0 0 0
10 Sahem Khalid 19 14.42 173.32 168.71666666666664 20 19 0 -4.615333333333375 25 0 200 0 20 8.6833333333333 0 0 0 0 0
11 Florescu Silviu-Cezar 20 15 173.32 182.28333333333336 21 20 0 8.951333333333345 25 0 200 0 0 17.533333333333 0 0 0 0.43333333333333 0
12 Safari Abdul Latif 20 14.42 173.32 185.21666666666667 21 20 0 19.884666666666654 24.5 0 200 1 21 18.933333333333 0 0 0 0 0
13 Ahmadpour Amirhossein 21 14.42 173.32 194.75 22 21 0 21.417999999999985 24.5 0 200 0 22 8.75 0 0 0 1.1333333333333 0
14 Sajaei Khouzestani Farid 22 14.42 173.32 203.5 23 22 0 30.167999999999985 24.5 0 200 0 23 28.75 0 0 0 0 0
15 Mihailescu Adrian Lucian 13 16.73 173.32 178.59999999999997 21 5 0 5.267999999999951 24 0 200 0 21 25.65 0 0 0 0 0
16 Hosseini Jahed Sajad 20 14.42 173.32 184.20000000000002 21 20 0 10.868000000000002 24 0 200 0 21 8.5166666666667 0 0 0 0 0
17 Tajikzadeh Mohammad 20 16.15 173.32 169.38333333333335 20 20 0 -3.9486666666666608 24 0 200 0 20 24.35 0 0 0 0 0
18 Shokripour Vahid 20 15 173.32 174.25 20 20 0 0.917999999999985 21 0 200 0 20 17.266666666667 0 0 0 0 0
19 Saffarnajib Majid 17 15.38 90.99300000000001 145.56666666666666 17 17 0 54.56736666666666 20 0 0 0 17 0 0 0 0 3.85 0
20 Kalantari Kaveh 18 14.42 173.32 155.16666666666666 18 18 0 29.834666666666642 19.5 0 200 6 18 25.533333333333 17.733333333333 0 0 1.05 0
21 Pourhossein Milad 20 14.42 173.32 182.6 22 19 0 9.26799999999998 18.5 0 200 0 22 9.25 0 0 0 30.9 0.35
22 Hastedt David 10 14.93 138.656 110.19999999999999 13 8 1 -7.665600000000012 17 0 ja 0 2 0 0 0 0 0 0 0
23 Eggerstedt Matthias 20 15.58 173.32 171.68333333333334 20 20 0 -1.6486666666666778 17 0 200 0 20 16.933333333333 0 0 0 0 0
24 Rasulkhani Hossein 9 14.42 173.32 76.46666666666667 9 9 0 -16.865333333333346 15.5 0 200 10 9 16.2 9 0 0 0 0
25 Pourhossein Reza 22 14.42 173.32 201.4666666666667 25 24 0 28.134666666666682 14 0 200 0 25 16.266666666667 20.95 0 0 45.85 10.166666666667
26 Rasul Khani Mohammad 11 14.42 173.32 95 11 11 0 1.667999999999985 12 0 200 10 11 8.5166666666667 0 0 0 0 0
27 Zekarias Michael 20 15 173.32 172.83333333333334 21 20 0 -0.49866666666667214 12 0 200 0 0 8.0333333333333 0 0 0 14.616666666667 0
28 Salehi Iman 21 17.31 173.32 176.55 21 20 0 3.2179999999999964 10.5 0 200 0 21 8.75 10 0 0 62.466666666667 10.9
29 Sabahi Mohammad 19 16.15 173.32 170.14999999999998 20 19 0 -3.1820000000000377 10 0 200 0 20 0 0 0 0 0 0
30 Toraman Tolga Can 18 14.42 173.32 177.73333333333332 21 18 0 4.401333333333305 10 0 200 0 0 15.65 9.0833333333333 0 0 17.883333333333 0
31 Tokhi Shokrollah 19 14.42 173.32 168.03333333333336 20 19 0 -5.298666666666655 9 0 200 0 20 16.45 17.116666666667 0 0 33.016666666667 0.05
32 Juyande Masoud 13 15 173.32 113.43333333333334 14 13 0 -3.898666666666678 8 0 200 7 0 0 0 0 0 33.116666666667 11.116666666667
33 Perez Y Waldt Sebastian 3 14.42 173.32 47.3 6 2 8 -62.03200000000002 6.5 0 ja 0 0 0 7.7166666666667 0 0 0 0 0
34 Pouzesh Esmail 20 16.15 173.32 170.36666666666665 21 20 0 -2.965333333333369 5 0 200 0 21 0 0 0 0 66.283333333333 21.333333333333
35 Khodayaran Hossein 18 16.15 173.32 152.54999999999998 19 19 0 -20.782000000000032 3 0 200 0 19 0 3.5166666666667 0 0 56.733333333333 11.55
36 Faridani Rad Farzad 19 15 173.32 157.2 19 19 3 7.867999999999974 3 0 ja 0 0 19 0 0 0 0 59.9 11.55
37 Saki Sara 2 15 173.32 25.766666666666666 4 3 15 -27.565333333333363 2 0 ja 0 0 4 0 0 0 0 0 0
38 Maleki Mohammad 6 14.44 103.992 72.68333333333334 9 1 1 -23.315866666666672 2 0 ja 0 0 9 0 0 0 0 0 0
39 Babaei Dezghi Mojtaba 0 15 160.321 0 0 0 0 -162.49875 0 0 ja 0 0 0 0 0 0 0 0 0
40 Mazidi Mehrdad 18 12.82 173.32 166.91666666666669 19 0 0 -6.415333333333329 0 0 ja 0 0 0 17.616666666667 0 0 0 0 0
41 Tokhi Haroun 0 14.42 173.32 0 0 0 0 -173.33200000000002 0 0 ja 0 0 0 0 0 0 0 0 0
42 Haralambous Martin 0 12.41 173.32 0 0 0 0 -173.33200000000002 0 0 ja 0 0 0 0 0 0 0 0 0
43 Karimloo Hamidreza 0 12.82 173.32 0 0 0 0 -173.33200000000002 0 0 ja 0 0 0 0 0 0 0 0 0
44 Najafgholizadeh Masoud 1 12.82 173.32 8.55 1 0 0 -164.782 0 0 ja 0 0 0 0 0 0 0 0 0
45 Hashimazada Firuz 0 12.82 173.32 0 0 0 18 -29.332000000000015 0 0 ja 0 0 0 0 0 0 0 0 0
46 Taghadosi Mehdi 19 12.82 173.32 167.25 19 0 0 -6.082000000000015 0 0 ja 0 0 0 16.266666666667 0 0 0 0 0
47 Arabnia Mehdi 20 12.82 173.32 185.93333333333334 21 0 0 20.601333333333322 0 0 ja 0 0 0 24.766666666667 0 0 0 0 0
48 Heucke Niklas 0 15 173.32 0 0 0 0 -173.33200000000002 0 0 ja 0 0 0 0 0 0 0 0 0
49 El Moustaiqid Najlae 0 12.82 173.32 126.5 17 0 3 -22.832000000000015 0 0 ja 0 0 0 0 0 0 0 0 0
50 Koell Melanie Nadine 0 15.58 173.32 6.5 1 0 0 -166.83200000000002 0 0 ja 0 0 0 0 0 0 0 0 0
51 Movafagh Farzad 20 12.82 173.32 182.65 21 0 0 9.31799999999999 0 0 ja 0 0 0 16.016666666667 0 0 0 0 0
52 Esmaeili Miandehi Milad 0 12.82 173.32 0 0 0 0 -173.33200000000002 0 0 ja 0 0 0 0 0 0 0 0 0
53 Mohamed Salah Hussin Noureldin 19 12.82 173.32 173.66666666666666 20 0 0 0.33466666666664224 0 0 ja 0 0 0 17.616666666667 0 0 0 0 0
54 Esser Detlef 0 12 121.32400000000001 0 0 0 0 -121.3324 0 0 ja 0 0 0 0 0 0 0 0 0
55 Abdallah Delshad 0 12.82 173.32 0 0 0 0 -173.33200000000002 0 0 ja 0 0 0 0 0 0 0 0 0
56 Saei Kasra 0 5.65 173.32 56.233333333333334 8 0 1 -102.09866666666667 0 0 ja 0 0 0 0 0 0 0 0 0
57 Golmohammadi Davood 19 12.82 173.32 171.66666666666666 19 0 0 6.334666666666642 0 0 ja 0 0 0 0 0 0 0 0 0
58 Atrafi Mohammad Nasir 19 12.82 17.332 177.8 20 0 0 161.26680000000002 0 0 ja 0 0 0 7.85 0 0 0 0 0
59 Sedighian Bidgoli Mohammadreza 0 12.82 173.32 0 0 0 0 -173.33200000000002 0 0 ja 0 0 0 0 0 0 0 0 0
60 Amiri Mohammad Samir 0 19.62 173.32 197.66666666666669 22 0 0 24.33466666666667 0 0 ja 0 0 0 19.25 27.833333333333 0 0 34.75 23.166666666667
61 Latza Lelia Eleonore 0 6.35 173.32 0 0 0 0 -173.33200000000002 0 0 ja 0 0 0 0 0 0 0 0 0
62 Krüger Julian 0 22.5 173.32 171.28333333333333 20 0 0 -2.0486666666666835 0 0 ja 0 0 0 6.05 0 0 0 7 4.4333333333333
63 Jalali Wagener Mehdi 22 12.82 173.32 195.71666666666667 23 0 0 22.384666666666654 0 0 ja 0 0 0 24.033333333333 0 0 0 0 0
64 Jasarevic Noel 0 14.42 173.32 161.93333333333334 19 0 0 -3.398666666666678 0 0 ja 0 0 0 0 0 0 0 0 0
65 Shahrasebi David 0 0 173.32 0 0 0 0 -173.33200000000002 0 0 ja 0 0 0 0 0 0 0 0 0
66 Feilbach Raoul Nikolai 0 19.62 173.32 173.36666666666665 20 0 0 0.034666666666630874 0 0 ja 0 0 0 0 19 0 0 28 16.766666666667
67 Lolies Danny 17 12.82 173.32 161.48333333333332 19 0 3 12.651333333333305 0 0 ja 0 0 0 16.566666666667 0 0 0 0 0
68 Hatami Reza 20 12.82 173.32 175.33333333333334 20 0 0 2.001333333333328 0 0 ja 0 0 0 16.266666666667 0 0 0 0 0
69 Rohi Pour Sadroldin 17 12.82 173.32 155.46666666666667 18 0 0 -17.865333333333346 0 0 ja 0 0 0 16.016666666667 0 0 0 0 0
70 Sidiqi Sahel 0 14.42 173.32 0 0 0 0 -173.33200000000002 0 0 ja 0 0 0 0 0 0 0 0 0
71 Emkani Daniel 0 22.48 108.325 58.916666666666664 7 0 3 -10.415833333333353 0 0 ja 0 0 0 15.65 0 0 0 0 0
72 Hazrati Kamyar 19 12.82 173.32 182.23333333333335 21 0 0 8.901333333333334 0 0 ja 0 0 0 22.616666666667 0 0 0 0 0
73 Haghighivand Danial 17 12.82 173.32 152.23333333333335 17 0 1 -13.098666666666666 0 0 ja 0 0 0 0 0 0 0 0 0
74 Mohammadi Danial 17 12.82 173.32 165.01666666666665 19 0 0 -8.315333333333363 0 0 ja 0 0 0 14.5 0 0 0 0 0
75 Askarian Pour Seyed Hosein 0 15.58 173.32 0 0 0 0 -173.33200000000002 0 0 ja 0 0 0 0 0 0 0 0 0
76 Jabbari Shahram 19 12.82 173.32 165.5 19 0 0 -7.832000000000015 0 0 ja 0 0 0 16.816666666667 0 0 0 0 0
77 Aba Cuan 0 5.65 173.32 69.45 10 0 2 -87.882 0 0 ja 0 0 0 0 0 0 0 0 0
78 Alami Shoaib 0 12.82 173.32 0 0 0 0 -173.33200000000002 0 0 ja 0 0 0 0 0 0 0 0 0
79 Altner Christian Van Manh 0 15 173.32 0 0 0 0 -173.33200000000002 0 0 ja 0 0 0 0 0 0 0 0 0
80 Modrau Cara Marleen 0 14.42 173.32 0 0 0 0 -173.33200000000002 0 0 ja 0 0 0 0 0 0 0 0 0
81 Rahmati Behnam 16 12.82 173.32 143.95 16 0 2 -13.382000000000026 0 0 ja 0 0 0 0 0 0 0 0 0
82 Vafaei Soheil 20 12.82 173.32 176.06666666666666 20 0 0 2.734666666666648 0 0 ja 0 0 0 16.033333333333 0 0 0 0 0
83 Ziaudin Atiq 20 12.82 173.32 172.6 20 0 0 -0.7320000000000206 0 0 ja 0 0 0 8.0166666666667 0 0 0 0 0
84 Haugg Uwe 14 12.82 138.656 142.26666666666665 17 0 0 3.6010666666666395 0 0 ja 0 0 0 38.4 0 0 0 0 0
85 Esmaeili Dashtbayaz Amin 20 12.82 173.32 181.08333333333331 21 0 0 7.751333333333299 0 0 ja 0 0 0 24.033333333333 0 0 0 0 0
86 Konak Zidar 0 22.5 173.32 178.96666666666667 21 0 0 5.634666666666654 0 0 ja 0 0 0 26.95 0 0 0 1.1 0
+15 -8
View File
@@ -1,11 +1,17 @@
FROM node:20.15.1-alpine AS builder
FROM node:20.19.1-slim AS builder
WORKDIR /app/builder
COPY package.json .
COPY package-lock.json .
RUN npm install
COPY . .
COPY package*.json .
COPY packages/shared-dtos/package*.json ./packages/shared-dtos/
COPY data-hub/data-connector/package*.json ./data-hub/data-connector/
RUN npm ci --verbose
COPY packages/shared-dtos ./packages/shared-dtos/
COPY data-hub/data-connector ./data-hub/data-connector/
WORKDIR /app/builder/data-hub/data-connector
RUN npm run build
@@ -27,9 +33,10 @@ FROM node:20.15.1-alpine
WORKDIR /app
COPY --from=builder /app/builder/node_modules ./node_modules
COPY --from=builder /app/builder/prisma ./prisma
COPY --from=builder /app/builder/dist ./dist
COPY --from=builder /app/builder/docker-entrypoint.sh .
COPY --from=builder /app/builder/packages ./packages
COPY --from=builder /app/builder/data-hub/data-connector/prisma ./prisma
COPY --from=builder /app/builder/data-hub/data-connector/dist ./dist
COPY --from=builder /app/builder/data-hub/data-connector/docker-entrypoint.sh .
ENTRYPOINT [ "./docker-entrypoint.sh" ]
File diff suppressed because it is too large Load Diff
+11 -7
View File
@@ -6,12 +6,12 @@
"private": true,
"license": "UNLICENSED",
"scripts": {
"build": "nest build",
"build": "npm run prisma -- generate && nest build",
"format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\"",
"start": "nest start",
"start:dev": "nest start --watch",
"start:debug": "nest start --debug --watch",
"start:prod": "node dist/main",
"start": "TZ=UTC nest start",
"start:dev": "TZ=UTC nest start --watch",
"start:debug": "TZ=UTC nest start --debug --watch",
"start:prod": "TZ=UTC node dist/main",
"prisma": "prisma",
"lint": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix",
"test": "jest",
@@ -24,7 +24,9 @@
"seed": "ts-node prisma/seed/seed.ts"
},
"dependencies": {
"@apollo/server": "^4.12.0",
"@apollo/server": "^5.0.0",
"@as-integrations/express5": "^1.1.2",
"@avicenna/shared-dtos": "^1.0.0",
"@faker-js/faker": "^9.6.0",
"@hyperdx/node-logger": "^0.4.0",
"@hyperdx/node-opentelemetry": "^0.10.2",
@@ -39,12 +41,14 @@
"@nestjs/graphql": "^13.0.4",
"@nestjs/platform-express": "^11.0.15",
"@nestjs/schedule": "^5.0.1",
"@nestjs/swagger": "^11.2.5",
"@opentelemetry/api": "^1.9.0",
"@opentelemetry/auto-instrumentations-node": "^0.60.0",
"@opentelemetry/exporter-trace-otlp-http": "^0.202.0",
"@opentelemetry/instrumentation-runtime-node": "^0.22.0",
"@opentelemetry/sdk-node": "^0.202.0",
"@prisma/client": "^6.6.0",
"axios": "^1.13.2",
"bullmq": "^5.56.0",
"date-fns": "^4.1.0",
"date-fns-tz": "^3.2.0",
@@ -77,7 +81,7 @@
"eslint-config-prettier": "^10.1.2",
"eslint-plugin-prettier": "^5.2.6",
"jest": "^29.7.0",
"prettier": "^3.5.3",
"prettier": "^3.8.0",
"prisma": "^6.6.0",
"source-map-support": "^0.5.21",
"supertest": "^7.1.0",
@@ -0,0 +1,47 @@
/*
Warnings:
- A unique constraint covering the columns `[tourId,ticketId]` on the table `TourFile` will be added. If there are existing duplicate values, this will fail.
*/
-- DropIndex
DROP INDEX "TourFile_tourId_ticketId_idx";
-- AlterTable
ALTER TABLE "Patient" ADD COLUMN "insuranceNo" TEXT;
-- AlterTable
ALTER TABLE "PatientSource" ADD COLUMN "stammVersNr" TEXT;
-- AlterTable
ALTER TABLE "PatientStaging" ADD COLUMN "insuranceNo" TEXT;
-- AlterTable
ALTER TABLE "Ticket" ADD COLUMN "approvalFullRequestMessageId" TEXT,
ADD COLUMN "approvalIsOverriden" BOOLEAN NOT NULL DEFAULT false,
ADD COLUMN "approvalLastInteractionAt" TIMESTAMP(3),
ADD COLUMN "approvalPreInquiryMessageId" TEXT,
ADD COLUMN "approvalRecipient" TEXT,
ADD COLUMN "approvalReminderCount" INTEGER DEFAULT 0,
ADD COLUMN "approvalRequestedAt" TIMESTAMP(3),
ADD COLUMN "approvalRequirement" TEXT DEFAULT 'UNKNOWN',
ADD COLUMN "approvalStatus" TEXT DEFAULT 'NOT_STARTED',
ADD COLUMN "hasDigitalTransportDocument" BOOLEAN NOT NULL DEFAULT false,
ADD COLUMN "hasPhysicalTransportDocument" BOOLEAN NOT NULL DEFAULT false,
ADD COLUMN "patientId" UUID,
ADD COLUMN "stage" TEXT DEFAULT 'DRAFT',
ADD COLUMN "updatedAt" TIMESTAMP(3),
ALTER COLUMN "id" DROP DEFAULT,
ALTER COLUMN "currentState" DROP NOT NULL;
-- AlterTable
ALTER TABLE "Tour" ADD COLUMN "insuranceNo" TEXT;
-- AlterTable
ALTER TABLE "TourSource" ADD COLUMN "stammVersNr" TEXT;
-- AlterTable
ALTER TABLE "TourStaging" ADD COLUMN "insuranceNo" TEXT;
-- CreateIndex
CREATE UNIQUE INDEX "TourFile_tourId_ticketId_key" ON "TourFile"("tourId", "ticketId");
@@ -0,0 +1,2 @@
-- AlterTable
ALTER TABLE "Ticket" ADD COLUMN "lastStageBeforeClosed" TEXT;
@@ -0,0 +1,2 @@
-- AlterTable
ALTER TABLE "Tour" ADD COLUMN "optimizationInMinutes" INTEGER NOT NULL DEFAULT 0;
@@ -59,6 +59,7 @@ model TourSource {
itName String?
kkId String?
krankenkasse String?
stammVersNr String?
patId String?
patName String?
patSurname String?
@@ -196,6 +197,7 @@ model PatientSource {
birthday String?
kkId String?
krankenkasse String?
stammVersNr String?
pflegestufe String?
behinderung1 String?
behinderung2 String?
@@ -223,6 +225,7 @@ model TourStaging {
patientCity String?
healthInsurance String?
healthInsuranceNumber String?
insuranceNo String?
type String?
category String?
@@ -282,6 +285,7 @@ model PatientStaging {
birthday DateTime?
healthinsuranceId String?
healthinsurance String?
insuranceNo String?
careDegree String?
disabilityMark1 String?
disabilityMark2 String?
@@ -371,6 +375,7 @@ model Tour {
patientCity String?
healthInsurance String?
healthInsuranceNumber String?
insuranceNo String?
type String?
category String?
@@ -414,11 +419,12 @@ model Tour {
companyName String?
schwerlast Boolean @default(false)
createdAt DateTime?
updatedAt DateTime?
deletedAt DateTime?
Ticket Ticket? @relation(fields: [ticketId], references: [id])
ticketId String? @db.Uuid
createdAt DateTime?
updatedAt DateTime?
deletedAt DateTime?
optimizationInMinutes Int @default(0)
Ticket Ticket? @relation(fields: [ticketId], references: [id])
ticketId String? @db.Uuid
}
model TourFile {
@@ -435,7 +441,7 @@ model TourFile {
createdAt DateTime? @default(now())
@@index([tourId, ticketId])
@@unique([tourId, ticketId])
}
model Patient {
@@ -449,6 +455,7 @@ model Patient {
birthday DateTime?
healthinsuranceId String?
healthinsurance String?
insuranceNo String?
careDegree String?
disabilityMark1 String?
disabilityMark2 String?
@@ -480,17 +487,36 @@ enum ApprovalState {
}
model Ticket {
id String @id @default(dbgenerated("gen_random_uuid()")) @db.Uuid
id String @id @db.Uuid
patientId String? @db.Uuid
stage String? @default("DRAFT")
approvalRequirement String? @default("UNKNOWN")
approvalStatus String? @default("NOT_STARTED")
approvalRequestedAt DateTime?
approvalLastInteractionAt DateTime?
approvalReminderCount Int? @default(0)
approvalRecipient String?
approvalPreInquiryMessageId String?
approvalFullRequestMessageId String?
approvalIsOverriden Boolean @default(false)
hasPhysicalTransportDocument Boolean @default(false)
hasDigitalTransportDocument Boolean @default(false)
lastStageBeforeClosed String?
updatedAt DateTime? @updatedAt
createdAt DateTime @default(now())
tours Tour[]
state TicketStateHistory[]
currentState TicketValidationState
currentState TicketValidationState?
notes String[]
errors String[]
isUrgent Boolean @default(false)
urgency DateTime @default("2099-12-31T23:59:59.999Z")
isUrgent Boolean @default(false)
urgency DateTime @default("2099-12-31T23:59:59.999Z")
approvalState ApprovalState?
documentInfo Json? @default("{\"approval\": {\"isNeeded\": null, \"whoRequested\": \"\",\"answeredFrom\": \"\"},\"tdLocation\": \"\"}")
createdAt DateTime @default(now())
documentInfo Json? @default("{\"approval\": {\"isNeeded\": null, \"whoRequested\": \"\",\"answeredFrom\": \"\"},\"tdLocation\": \"\"}")
}
model TicketStateHistory {
@@ -25,7 +25,7 @@ import { FeatCalculationModule } from './modules/feat-calculation/feat-calculati
import { FeatureEtlModule } from './modules/feat-etl/feat-etl.module';
import { FeatReportsModule } from './modules/feat-reports/feat-reports.module';
import { FeatTicketSystemModule } from './modules/feat-tickets/ticket-system.module';
import { OtelMetricsService } from './core/service/otel-metrics.service';
import { AppPlaygroundModule } from './modules/app-playground/app-playground.module';
@Module({
imports: [
@@ -60,7 +60,7 @@ import { OtelMetricsService } from './core/service/otel-metrics.service';
EventEmitterModule.forRoot(),
GraphQLModule.forRoot<ApolloDriverConfig>({
driver: ApolloDriver,
playground: true,
graphiql: true,
autoSchemaFile: join(process.cwd(), 'src/core/database/schema.gql'),
sortSchema: true,
subscriptions: {
@@ -78,9 +78,10 @@ import { OtelMetricsService } from './core/service/otel-metrics.service';
ApplicationControlCenterModule,
ApplicationTicketSystemModule,
AppHomeModule,
AppPlaygroundModule,
],
controllers: [AppController],
providers: [AppService, Logger, OtelMetricsService],
providers: [AppService, Logger],
})
export class AppModule {
configure(consumer: MiddlewareConsumer) {
@@ -24,13 +24,6 @@ export interface PrismaServiceOptions {
* See: https://www.prisma.io/docs/concepts/components/prisma-client/working-with-prismaclient/connection-management
*/
explicitConnect?: boolean;
/**
* Apply Prisma middlewares to perform actions before or after db queries.
*
* See: https://www.prisma.io/docs/concepts/components/prisma-client/middleware
*/
middlewares?: Array<Prisma.Middleware>;
}
export interface PrismaOptionsFactory {
@@ -31,12 +31,6 @@ export class PrismaService extends PrismaClient implements OnModuleInit {
],
});
if (this.prismaServiceOptions.middlewares) {
this.prismaServiceOptions.middlewares.forEach((middleware) =>
this.$use(middleware),
);
}
/*
Sadly typescript does not recognize the Prisma Client configuration passed in the super call.
As Prisma defines the events generically, based on the configuration, the event type, typescript inferred, is wrong.
@@ -191,7 +191,7 @@ type PlannedTime {
type Query {
accountingStateMeta(state: AccountingState!): TourStateMeta!
accountingTours(cursor: String, filters: AccountingFilterArgs, state: AccountingState!, take: Int! = 10): PaginatedTour!
accountingTours(cursor: String, filters: AccountingFilterArgs, state: AccountingState!, take: Int): PaginatedTour!
anomalies(cursor: String, take: Int! = 10): PaginatedAnomaly!
anomaliesProto: [Anomaly!]!
anomaly(id: String!): Anomaly!
@@ -187,6 +187,7 @@ export type Patient = {
birthday: Timestamp | null;
healthinsuranceId: string | null;
healthinsurance: string | null;
insuranceNo: string | null;
careDegree: string | null;
disabilityMark1: string | null;
disabilityMark2: string | null;
@@ -204,6 +205,7 @@ export type PatientSource = {
birthday: string | null;
kkId: string | null;
krankenkasse: string | null;
stammVersNr: string | null;
pflegestufe: string | null;
behinderung1: string | null;
behinderung2: string | null;
@@ -219,6 +221,7 @@ export type PatientStaging = {
birthday: Timestamp | null;
healthinsuranceId: string | null;
healthinsurance: string | null;
insuranceNo: string | null;
careDegree: string | null;
disabilityMark1: string | null;
disabilityMark2: string | null;
@@ -298,15 +301,30 @@ export type TariffGroup = {
ordinanceType: string;
};
export type Ticket = {
id: Generated<string>;
currentState: TicketValidationState;
id: string;
patientId: string | null;
stage: Generated<string | null>;
approvalRequirement: Generated<string | null>;
approvalStatus: Generated<string | null>;
approvalRequestedAt: Timestamp | null;
approvalLastInteractionAt: Timestamp | null;
approvalReminderCount: Generated<number | null>;
approvalRecipient: string | null;
approvalPreInquiryMessageId: string | null;
approvalFullRequestMessageId: string | null;
approvalIsOverriden: Generated<boolean>;
hasPhysicalTransportDocument: Generated<boolean>;
hasDigitalTransportDocument: Generated<boolean>;
lastStageBeforeClosed: string | null;
updatedAt: Timestamp | null;
createdAt: Generated<Timestamp>;
currentState: TicketValidationState | null;
notes: string[];
errors: string[];
isUrgent: Generated<boolean>;
urgency: Generated<Timestamp>;
approvalState: ApprovalState | null;
documentInfo: Generated<unknown | null>;
createdAt: Generated<Timestamp>;
};
export type TicketStateHistory = {
id: Generated<string>;
@@ -332,6 +350,7 @@ export type Tour = {
patientCity: string | null;
healthInsurance: string | null;
healthInsuranceNumber: string | null;
insuranceNo: string | null;
type: string | null;
category: string | null;
transportType: string | null;
@@ -368,6 +387,7 @@ export type Tour = {
createdAt: Timestamp | null;
updatedAt: Timestamp | null;
deletedAt: Timestamp | null;
optimizationInMinutes: Generated<number>;
ticketId: string | null;
};
export type TourFile = {
@@ -415,6 +435,7 @@ export type TourSource = {
itName: string | null;
kkId: string | null;
krankenkasse: string | null;
stammVersNr: string | null;
patId: string | null;
patName: string | null;
patSurname: string | null;
@@ -473,6 +494,7 @@ export type TourStaging = {
patientCity: string | null;
healthInsurance: string | null;
healthInsuranceNumber: string | null;
insuranceNo: string | null;
type: string | null;
category: string | null;
transportType: string | null;
@@ -1,35 +1,97 @@
import { MailerService } from '@nestjs-modules/mailer';
import { Injectable, Logger } from '@nestjs/common';
import { TourFile } from '@prisma/client';
import { SentMessageInfo } from 'nodemailer';
import { ConfigService } from '../config/config.service';
export interface VorabGenehmigungsanfrageParams {
patientName: string;
birthday: string;
insuranceNo: string;
healthInsurance: string;
startAddressLine: string;
targetAddressLine: string;
transportDate: string;
}
export interface MailAttachment {
originalFilename: string;
storagePath: string;
mimeType: string;
}
@Injectable()
export class MailService {
private readonly logger = new Logger(MailService.name);
constructor(private readonly mailerService: MailerService, private readonly config: ConfigService) {}
constructor(
private readonly mailerService: MailerService,
private readonly config: ConfigService,
) {}
async sendGenehmigungsanfrage(recipient: string, transportschein: TourFile) {
async sendGenehmigungsanfrage(
recipient: string,
attachment: MailAttachment,
replyToId?: string,
): Promise<SentMessageInfo> {
this.logger.log(
`sending Genehmigungsanfrage mail to ${recipient} with Transportschein ${transportschein.originalFilename} for Tour ${transportschein.tourId}`,
`sending Genehmigungsanfrage mail to ${recipient} with Transportschein ${attachment.originalFilename}`,
);
try {
await this.mailerService.sendMail({
to: recipient,
bcc: 'info@avicenna.hamburg',
subject: `Antrag auf Kostenübernahme`,
template: 'genehmigungsanfrage',
attachments: [
{
filename: transportschein.originalFilename,
path: transportschein.storagePath,
contentType: transportschein.mimeType,
},
],
});
} catch (error) {
this.logger.error('failed to send mail', error)
}
return await this.mailerService.sendMail({
to: recipient,
bcc: 'info@avicenna.hamburg',
subject: `Antrag auf Kostenübernahme`,
template: 'genehmigungsanfrage',
headers: this.getThreadingHeaders(replyToId),
attachments: [
{
filename: attachment.originalFilename,
path: attachment.storagePath,
contentType: attachment.mimeType,
},
],
});
}
async sendVorabGenehmigungsanfrage(
recipient: string,
params: VorabGenehmigungsanfrageParams,
replyToId?: string,
): Promise<SentMessageInfo> {
this.logger.log(
`sending Vorab-Genehmigungsanfrage mail to ${recipient} for Patient ${params.patientName} and transport at ${params.transportDate}`,
);
return await this.mailerService.sendMail({
to: recipient,
bcc: 'info@avicenna.hamburg',
headers: this.getThreadingHeaders(replyToId),
subject: `Antrag auf Kostenübernahme Transport am ${params.transportDate} für ${params.patientName}`,
template: 'genehmigung-vorab-anfrage',
context: params,
});
}
async sendReminder(
recipient: string,
params: { patientName: string; transportDate: Date; reminderCount: number },
replyToId?: string,
): Promise<SentMessageInfo> {
this.logger.log(`sending Reminder ${params.reminderCount} to ${recipient}`);
return await this.mailerService.sendMail({
to: recipient,
bcc: 'info@avicenna.hamburg',
subject: `Erinnerung: Antrag für ${params.patientName}`, // Ggf. gleicher Betreff wie Vorher für Threading
template: 'genehmigung-reminder',
headers: this.getThreadingHeaders(replyToId),
context: params,
});
}
private getThreadingHeaders(replyToMessageId?: string) {
if (!replyToMessageId) return {};
return {
'In-Reply-To': replyToMessageId,
References: replyToMessageId,
};
}
}
@@ -1,18 +0,0 @@
<div style="font-family: sans-serif; white-space: pre-wrap;">
Sehr geehrte Damen und Herren,
für den betreffenden Transport wurde bereits eine Kostenübernahme gemäß § 6 Abs. 3 der Krankentransport-Richtlinie beantragt. Leider liegt uns bislang keine Genehmigung vor.
Da uns ohne Genehmigung keine Abrechnung mit der Krankenkasse möglich ist, verzögert sich der gesamte Abrechnungsprozess. Um eine private Rechnungsstellung an den Patienten zu vermeiden, bitten wir Sie um zeitnahe Übermittlung der Genehmigung.
Bitte senden Sie die Unterlagen an eine der folgenden Stellen:
• E-Mail: info@avicenna.hamburg
• Fax: 040 460006820
• Postanschrift: Thomas-Mann-Straße 21, 22175 Hamburg
• Institutionskennzeichen: 600224841
Für Rückfragen stehen wir Ihnen selbstverständlich gerne zur Verfügung.
Vielen Dank vorab.
Mit freundlichen Grüßen
Ihr Team von Avicenna
</div>
@@ -0,0 +1,18 @@
<div style="font-family: sans-serif; white-space: pre-wrap;">
Sehr geehrte Damen und Herren,<br>
<br>
für den betreffenden Transport wurde bereits eine Kostenübernahme gemäß § 6 Abs. 3 der Krankentransport-Richtlinie beantragt. Leider liegt uns bislang keine Genehmigung vor.<br>
<br>
Da uns ohne Genehmigung keine Abrechnung mit der Krankenkasse möglich ist, verzögert sich der gesamte Abrechnungsprozess. Um eine private Rechnungsstellung an den Patienten zu vermeiden, bitten wir Sie um zeitnahe Übermittlung der Genehmigung.<br>
<br>
Bitte senden Sie die Unterlagen an eine der folgenden Stellen:<br>
• E-Mail: info@avicenna.hamburg<br>
• Fax: 040 460006820<br>
• Postanschrift: Thomas-Mann-Straße 21, 22175 Hamburg<br>
• Institutionskennzeichen: 600224841<br>
<br>
Für Rückfragen stehen wir Ihnen selbstverständlich gerne zur Verfügung.<br>
Vielen Dank vorab.<br>
Mit freundlichen Grüßen<br>
Ihr Team von Avicenna<br>
</div>
@@ -0,0 +1,27 @@
<div style="font-family: sans-serif; white-space: pre-wrap">
Sehr geehrte Damen und Herren,<br>
<br>
hiermit beantragen wir im Auftrag des Versicherten {{patientName}} die Kostenübernahme gemäß § 6 Abs. 3 der Krankentransport-Richtlinie für den geplanten Transport am {{transportDate}}.<br>
<br>
Eine ärztliche Verordnung liegt uns zum jetzigen Zeitpunkt noch nicht vor. Diese wird nachgereicht, sobald sie uns übermittelt wurde.<br>
<br>
Für die Vorabprüfung übermitteln wir Ihnen die relevanten Eckdaten:<br>
<br>
Patient: {{patientName}}<br>
Geburtsdatum: {{birthday}}<br>
Versicherungsnummer: {{insuranceNo}}<br>
Krankenkasse: {{healthInsurance}}<br>
Abholadresse: {{startAddressLine}}<br>
Zieladresse: {{targetAddressLine}}<br>
Datum & Uhrzeit: {{transportDate}}<br>
<br>
Bitte senden Sie uns die Bestätigung an:<br>
• E-Mail: info@avicenna.hamburg<br>
• Fax: 040 460006820<br>
• Postanschrift: Thomas-Mann-Straße 21, 22175 Hamburg<br>
• Institutionskennzeichen: 600224841<br>
<br>
Vielen Dank für Ihre Unterstützung.<br>
Mit freundlichen Grüßen<br>
Ihr Team der Avicenna Ambulance<br>
</div>
+10 -3
View File
@@ -1,13 +1,11 @@
// const tracer = require('./tracer');
// import tracer from './tracer'
import { HyperDXNestLoggerModule } from '@hyperdx/node-logger';
import * as HyperDX from '@hyperdx/node-opentelemetry';
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
import './instrumentation';
import { ValidationPipe } from '@nestjs/common';
async function bootstrap() {
const logger = HyperDXNestLoggerModule.createLogger({
apiKey: '71addf09-bcd2-4e83-8158-97458402137b',
service: 'avicenna-data-connector',
@@ -16,6 +14,15 @@ async function bootstrap() {
const app = await NestFactory.create(AppModule, {
logger: logger,
});
app.useGlobalPipes(
new ValidationPipe({
whitelist: true,
transform: true,
transformOptions: {
enableImplicitConversion: false,
},
}),
);
app.enableCors();
HyperDX.setupExpressErrorHandler(app);
@@ -1,14 +1,7 @@
import {
ArgsType,
Field,
Float,
InputType,
Int,
ObjectType,
} from '@nestjs/graphql';
import { ArgsType, Field, InputType } from '@nestjs/graphql';
import { PaginationArgs } from 'src/core/base/pagination.input';
import { AccountingState } from './accounting-state';
import { TourAccountingFilterDto } from '../feat-business-objects/entities/tour/tours.filter';
import { AccountingState } from './accounting-state';
@InputType()
export class AccountingFilterArgs implements TourAccountingFilterDto {
@@ -27,15 +20,3 @@ export class AccountingTourArgs extends PaginationArgs {
@Field(() => AccountingFilterArgs, { nullable: true })
filters!: AccountingFilterArgs;
}
@ObjectType('TourStateMeta')
export class TourStateMetaObjectType {
@Field(() => Int)
count!: number;
@Field(() => Float, { nullable: true })
revenueSum?: number;
@Field(() => Float, { nullable: true })
positiveRevenueDeviation?: number;
@Field(() => Float, { nullable: true })
negativeRevenueDeviation?: number;
}
@@ -0,0 +1,64 @@
import { Inject, Injectable } from '@nestjs/common';
import { Kysely } from 'kysely';
import { KYSELY_CLIENT_PROVIDER_TOKEN } from 'src/core/database/database.module';
import { DB } from 'src/core/database/types';
import { AccountingState } from './accounting-state';
import {
AccountingListDto,
PaginatedResult,
TicketStage,
} from '@avicenna/shared-dtos';
import { TourService } from '../feat-business-objects/entities/tour/tour.service';
import { Tour } from '@prisma/client';
@Injectable()
export class AccountingQueryService {
constructor(private readonly tourService: TourService) {}
async toursByState(
state: AccountingState,
page: number,
limit: number = 10,
): Promise<PaginatedResult<AccountingListDto>> {
const offset = (page - 1) * limit;
const { tours, total } = await this.tourService.findByAccountingState(
state,
limit,
offset,
);
return {
data: tours.map((tour) => this.toAccountingListDto(tour)),
meta: {
total,
page,
limit,
totalPages: Math.ceil(total / limit),
},
};
}
private toAccountingListDto(
tour: Tour & { stage: string },
): AccountingListDto {
return {
tourId: tour.id,
operationId: tour.operationId,
ordinanceType: tour.ordinanceType,
startDate: tour.startDate,
revenue: tour.revenue,
isTicketOpen: [
TicketStage.DRAFT,
TicketStage.DATA_INCOMPLETE,
TicketStage.ASSESSMENT_PENDING,
TicketStage.APPROVAL_IN_PROGRESS,
TicketStage.DOCUMENTS_PENDING,
].includes(tour.stage as TicketStage),
hasRevenueDeviation:
Math.abs(tour.revenue - tour.revenueDispoLive) > 0.01,
hasOptimizationPotential: tour.optimizationInMinutes !== 0,
};
}
}
@@ -1,12 +1,22 @@
import { Args, Parent, Query, ResolveField, Resolver } from '@nestjs/graphql';
import {
Args,
Int,
Parent,
Query,
ResolveField,
Resolver,
} from '@nestjs/graphql';
import { TicketObjectType } from '../app-ticket-system/ticket.object-type';
import {
AccountingTourArgs,
TourStateMetaObjectType,
AccountingFilterArgs
} from './accounting-arg.types';
import { AccountingState } from './accounting-state';
import { AccountingValidationService } from './accounting-validation.service';
import { PaginatedTourObjectType, TourCostBreakdownObjectType } from './accounting.object-type';
import {
PaginatedTourObjectType,
TourCostBreakdownObjectType,
TourStateMetaObjectType,
} from './accounting.object-type';
import { TourObjectType } from './tour.object-type';
@Resolver(() => TourObjectType)
@@ -15,9 +25,13 @@ export class AccountingValidationResolver {
@Query(() => PaginatedTourObjectType)
accountingTours(
@Args() { state, filters, cursor, take }: AccountingTourArgs,
@Args('state', { type: () => AccountingState }) state: AccountingState,
@Args('filters', { type: () => AccountingFilterArgs, nullable: true })
filters: AccountingFilterArgs,
@Args('cursor', { nullable: true }) cursor: string,
@Args('take', { type: () => Int, nullable: true }) take: number = 10,
) {
return this.service.paginateToursByState(state, filters, take, cursor);
return this.service.paginateToursByState(state, take, cursor);
}
@Query(() => TourStateMetaObjectType)
@@ -30,7 +44,7 @@ export class AccountingValidationResolver {
@Query(() => TourCostBreakdownObjectType)
tourCostBreakdown(@Args('operationId') operationId: string) {
return this.service.getTourCostBreakdown(operationId)
return this.service.getTourCostBreakdown(operationId);
}
@ResolveField(() => TicketObjectType, { nullable: true })
@@ -1,15 +1,18 @@
import {
PaginatedResult,
PaginatedTourDto,
TourDto,
TourStateMetaDto,
} from '@avicenna/shared-dtos';
import { Injectable } from '@nestjs/common';
import {
decodeOffsetCursor,
encodeCursor,
} from 'src/core/base/pagination.util';
import { TourStateMetaObjectType } from './accounting-arg.types';
import { AccountingState } from './accounting-state';
import { PaginatedTourObjectType } from './accounting.object-type';
import { AccountingService } from './accounting.service';
import { TicketService } from '../feat-business-objects/entities/ticket/ticket.service';
import { TourService } from '../feat-business-objects/entities/tour/tour.service';
import { TourAccountingFilterDto } from '../feat-business-objects/entities/tour/tours.filter';
import { AccountingState } from './accounting-state';
import { AccountingService } from './accounting.service';
@Injectable()
export class AccountingValidationService {
@@ -19,16 +22,39 @@ export class AccountingValidationService {
private readonly tourService: TourService,
) {}
async paginateToursByState(
async toursByState(
state: AccountingState,
filter: TourAccountingFilterDto,
take: number = 10,
cursor?: string,
): Promise<PaginatedTourObjectType> {
const skip = cursor ? decodeOffsetCursor(cursor) : 0;
page: number,
limit: number = 10,
): Promise<PaginatedResult<TourDto>> {
const offset = (page - 1) * limit;
const { tours, total } = await this.tourService.findByAccountingState(
state,
limit,
offset,
);
return {
data: tours,
meta: {
total,
page,
limit,
totalPages: Math.ceil(total / limit),
},
};
}
async paginateToursByState(
state: AccountingState,
take: number = 10,
cursor?: string,
): Promise<PaginatedTourDto> {
const skip = cursor ? decodeOffsetCursor(cursor) : 0;
const { tours, total } = await this.tourService.findByAccountingState(
state,
filter,
take,
skip,
);
@@ -47,7 +73,7 @@ export class AccountingValidationService {
async getAccountingStateMeta(
accountingState: AccountingState,
): Promise<TourStateMetaObjectType> {
): Promise<TourStateMetaDto> {
return this.tourService.calculateAccountingStateMeta(accountingState);
}
@@ -0,0 +1,96 @@
import {
OptimizationCarDto,
PaginatedTourDto,
TourCostBreakdownDto,
TourDto,
TourStateMetaDto,
} from '@avicenna/shared-dtos';
import { Controller, Get, Param, Query } from '@nestjs/common';
import { ApiOkResponse, ApiTags } from '@nestjs/swagger';
import { resolveDateFromTime } from 'src/utils/resolve-timestamp';
import { TourService } from '../feat-business-objects/entities/tour/tour.service';
import { AccountingQueryService } from './accounting-query.service';
import { AccountingState } from './accounting-state';
import { AccountingValidationService } from './accounting-validation.service';
import { RevenueOptimizationService } from './revenue-optimization.service';
@ApiTags('Accounting')
@Controller('api/accounting')
export class AccountingController {
constructor(
private readonly service: AccountingValidationService,
private readonly queryService: AccountingQueryService,
private readonly optimizationService: RevenueOptimizationService,
private readonly tourService: TourService,
) {}
@Get('cars/:name')
async getCarForOptimization(
@Param('name') name: string,
@Query('operation-day') operationDay: string,
): Promise<OptimizationCarDto> {
const date = new Date(operationDay);
const tours = await this.tourService.getOptimizationToursForCar(name, date);
return {
name,
tours: tours.map((tour) => ({
id: tour.id,
operationId: tour.operationId,
ordinanceType: tour.ordinanceType,
startDate: tour.startDate.toISOString(),
optimizationInMinutes: tour.optimizationInMinutes ?? 0,
departedForPickup: tour.startBegin
? resolveDateFromTime(tour.startDate, tour.startBegin).toISOString()
: null,
arrivedAtPickup: tour.startEnd
? resolveDateFromTime(tour.startDate, tour.startEnd).toISOString()
: null,
departedPickup: tour.target
? resolveDateFromTime(tour.startDate, tour.target).toISOString()
: null,
arrivedAtTarget: tour.targetBegin
? resolveDateFromTime(tour.startDate, tour.targetBegin).toISOString()
: null,
completed: tour.empty
? resolveDateFromTime(tour.startDate, tour.empty).toISOString()
: null,
})),
};
}
@Get('tours')
@ApiOkResponse({ type: PaginatedTourDto })
async getAccountingTours(
@Query('state') state: AccountingState,
@Query('page') page: number,
@Query('take') take: number = 10,
) {
return this.queryService.toursByState(state, page, take);
}
@Get('tours/:id')
@ApiOkResponse({ type: TourDto })
getTourDetail(@Param('id') tourId: string): Promise<TourDto> {
return this.tourService.findById(tourId);
}
@Get('state-meta')
@ApiOkResponse({ type: TourStateMetaDto })
async getAccountingStateMeta(@Query('state') state: AccountingState) {
return this.service.getAccountingStateMeta(state);
}
@Get('cost-breakdown/:operationId')
@ApiOkResponse({ type: TourCostBreakdownDto })
async getTourCostBreakdown(@Param('operationId') operationId: string) {
return this.service.getTourCostBreakdown(operationId);
}
@Get('optimizations')
async getOptimizations(@Query('day') day: Date) {
return this.optimizationService.findOptimizationsForOperationDay(day);
}
}
@@ -0,0 +1,53 @@
import { Inject, Injectable, Logger } from '@nestjs/common';
import { OnEvent } from '@nestjs/event-emitter';
import { eachDayOfInterval, startOfDay, subWeeks } from 'date-fns';
import { Kysely } from 'kysely';
import { KYSELY_CLIENT_PROVIDER_TOKEN } from 'src/core/database/database.module';
import { DB } from 'src/core/database/types';
import { LoadingDoneEvent } from '../feat-etl/feat-importers/events/loading-done.event';
import {
RevenueOptimizationDto,
RevenueOptimizationService,
} from './revenue-optimization.service';
import { TourService } from '../feat-business-objects/entities/tour/tour.service';
import { ETLEventTypes } from '../feat-etl/feat-importers/events/event-types.enum';
@Injectable()
export class AccountingListener {
private readonly logger = new Logger(AccountingListener.name);
constructor(
private readonly service: RevenueOptimizationService,
@Inject(KYSELY_CLIENT_PROVIDER_TOKEN) private readonly db: Kysely<DB>,
private readonly tourService: TourService,
) {}
@OnEvent(ETLEventTypes.LoadingDone)
async handleRevenueOptimizationCheck() {
this.logger.log(`looking for revenue optimizations`);
const batchSize = 10;
const days = eachDayOfInterval({
start: subWeeks(startOfDay(new Date()), 4),
end: startOfDay(new Date()),
});
const optimizationsInPeriod: RevenueOptimizationDto[] = [];
for (let i = 0; i < days.length; i += batchSize) {
const batch = days.slice(i, i + batchSize);
const optimizations = await Promise.all(
batch.map((day) => this.service.findOptimizationsForOperationDay(day)),
);
optimizationsInPeriod.push(...optimizations.flat(2));
}
await Promise.all(
optimizationsInPeriod.map((optimization) =>
this.tourService.updateTour(optimization.tourId, {
optimizationInMinutes: optimization.timeModificationInMinutes,
}),
),
);
return optimizationsInPeriod;
}
}
@@ -1,8 +1,14 @@
import { Field, Float, ObjectType, registerEnumType } from '@nestjs/graphql';
import { AccountingState } from './accounting-state';
import { Paginated } from 'src/core/base/pagination.input';
import { TourObjectType } from './tour.object-type';
import {
Field,
Float,
Int,
ObjectType,
registerEnumType,
} from '@nestjs/graphql';
import { CostTypeVariant } from '@prisma/client';
import { Paginated } from 'src/core/base/pagination.input';
import { AccountingState } from './accounting-state';
import { TourObjectType } from './tour.object-type';
registerEnumType(AccountingState, {
name: 'AccountingState',
@@ -13,7 +19,7 @@ export class PaginatedTourObjectType extends Paginated(TourObjectType) {}
@ObjectType('ServicePosition')
export class ServicePositionObjectType {
@Field()
@Field(() => String)
variant: CostTypeVariant;
@Field()
description: string;
@@ -31,3 +37,15 @@ export class TourCostBreakdownObjectType {
@Field(() => [ServicePositionObjectType])
servicePositions: ServicePositionObjectType[];
}
@ObjectType('TourStateMeta')
export class TourStateMetaObjectType {
@Field(() => Int)
count!: number;
@Field(() => Float, { nullable: true })
revenueSum?: number;
@Field(() => Float, { nullable: true })
positiveRevenueDeviation?: number;
@Field(() => Float, { nullable: true })
negativeRevenueDeviation?: number;
}
@@ -4,14 +4,22 @@ import { FeatBusinessObjectsModule } from '../feat-business-objects/feat-busines
import { FeatCalculationModule } from '../feat-calculation/feat-calculation.module';
import { AccountingValidationResolver } from './accounting-validation.resolver';
import { AccountingValidationService } from './accounting-validation.service';
import { AccountingController } from './accounting.controller';
import { AccountingListener } from './accounting.listener';
import { AccountingService } from './accounting.service';
import { RevenueOptimizationService } from './revenue-optimization.service';
import { AccountingQueryService } from './accounting-query.service';
@Module({
imports: [PrismaModule, FeatCalculationModule, FeatBusinessObjectsModule],
controllers: [AccountingController],
providers: [
AccountingValidationResolver,
AccountingValidationService,
AccountingService,
AccountingListener,
RevenueOptimizationService,
AccountingQueryService
],
exports: [],
})
@@ -0,0 +1,208 @@
import { Inject, Injectable, Logger } from '@nestjs/common';
import { CostTypeVariant, Tour } from '@prisma/client';
import {
differenceInMinutes,
isAfter,
isBefore,
isWithinInterval,
} from 'date-fns';
import { Kysely } from 'kysely';
import { KYSELY_CLIENT_PROVIDER_TOKEN } from 'src/core/database/database.module';
import { DB } from 'src/core/database/types';
import { generateIntervalForTimeRange } from 'src/utils';
import { resolveDateFromTime } from 'src/utils/resolve-timestamp';
import { TariffService } from '../feat-business-objects/entities/tariff/tariff.service';
export interface RevenueOptimizationDto {
tourId: string;
timeModificationInMinutes: number;
costDifference: number;
}
@Injectable()
export class RevenueOptimizationService {
private readonly logger = new Logger(RevenueOptimizationService.name);
private healthInsurancesWithNoTenant = [];
constructor(
@Inject(KYSELY_CLIENT_PROVIDER_TOKEN) private readonly db: Kysely<DB>,
private readonly tariffService: TariffService,
) {}
async findOptimizationsForOperationDay(
day: Date = new Date('2026-01-15'),
): Promise<RevenueOptimizationDto[]> {
const tours = await this.loadToursForOperationDay(day);
const groupedByCar = tours.reduce(
(acc, tour) => {
(acc[tour.carName] ??= []).push(tour);
return acc;
},
{} as Record<string, typeof tours>,
);
const result = [];
for (const groupName of Object.keys(groupedByCar)) {
const optimizations = await this.findOptimizationPotential(
groupedByCar[groupName],
);
if (optimizations.length > 0) {
result.push(optimizations);
}
}
return result;
}
async findOptimizationPotential(
tours: Tour[],
): Promise<RevenueOptimizationDto[]> {
const result: RevenueOptimizationDto[] = [];
for (let i = 0; i < tours.length; i++) {
const currentTour = tours[i];
const bonusPeriods = await this.getBonusPeriods(currentTour);
// TODO: Make sure startEnd is the correct timestamp
const tourInvoiceTime = resolveDateFromTime(
currentTour.startDate,
currentTour.startEnd,
);
const bonusPeriod = bonusPeriods
.filter(({ interval }) => isWithinInterval(tourInvoiceTime, interval))
.at(0);
const possiblePeriod = bonusPeriods
.filter(
({ interval }) =>
(Math.abs(differenceInMinutes(tourInvoiceTime, interval.start)) <=
15 ||
Math.abs(differenceInMinutes(tourInvoiceTime, interval.end)) <
15) &&
!isWithinInterval(tourInvoiceTime, interval),
)
.at(0);
if (
(!bonusPeriod && possiblePeriod) ||
bonusPeriod?.costAmount < possiblePeriod?.costAmount
) {
let timeModification = 0;
if (isBefore(tourInvoiceTime, possiblePeriod.interval.start)) {
timeModification = differenceInMinutes(
possiblePeriod.interval.start,
tourInvoiceTime,
);
} else if (isAfter(tourInvoiceTime, possiblePeriod.interval.end)) {
timeModification = differenceInMinutes(
possiblePeriod.interval.end,
tourInvoiceTime,
);
}
let costDifference = 0;
if (bonusPeriod) {
costDifference = possiblePeriod.costAmount - bonusPeriod.costAmount;
} else {
costDifference = possiblePeriod.costAmount;
}
result.push({
tourId: currentTour.id,
timeModificationInMinutes: timeModification,
costDifference,
});
}
}
return result;
}
private async getBonusPeriods(tour: Tour) {
if (!tour.ordinanceType) {
this.logger.warn(`Tour ${tour.operationId} has no ordinanceType`, {
operationId: tour.operationId,
healthInsurance: tour.healthInsurance,
ordinanceType: tour.ordinanceType,
startDate: tour.startDate,
});
return [];
}
const tariff = await this.findTariff(
tour.healthInsurance,
tour.ordinanceType.split(' ').at(1),
tour.startDate,
);
if (!tariff) {
this.logger.warn(`no Tariff found for Tour ${tour.operationId}`, {
operationId: tour.operationId,
healthInsurance: tour.healthInsurance,
ordinanceType: tour.ordinanceType,
startDate: tour.startDate,
});
return [];
}
const bonusPeriods = tariff.costTypes
.filter(
({ variant }) =>
variant === CostTypeVariant.NACHTZUSCHLAG ||
variant === CostTypeVariant.GRUNDPREIS_ZEIT,
)
.map(({ timeRange, costAmount, description }) => ({
description,
interval: generateIntervalForTimeRange(
timeRange,
'Europe/Berlin',
tour.startDate,
),
costAmount,
}));
return bonusPeriods;
}
private async loadToursForOperationDay(day: Date) {
const operationDayInterval = generateIntervalForTimeRange(
'05:00-04:59',
'Europe/Berlin',
day,
);
const rows = await this.db
.selectFrom('Tour')
.selectAll()
.where('Tour.startDate', '>=', new Date(operationDayInterval.start))
.where('Tour.startDate', '<=', new Date(operationDayInterval.end))
.where('Tour.healthInsurance', 'is not', null)
.execute();
return rows;
}
private async findTariff(
healthInsurance: string,
ordinanceType: string,
tourDate: Date,
) {
if (!healthInsurance || healthInsurance === '') {
return;
}
const tenant = await this.tariffService.findTenant(
healthInsurance,
ordinanceType,
);
if (!tenant) {
this.healthInsurancesWithNoTenant.push({
healthInsurance,
ordinanceType,
});
return;
}
return this.tariffService.findByTenantId(
tenant.id,
ordinanceType,
tourDate,
);
}
}
@@ -3,6 +3,8 @@ import { Tour } from '@prisma/client';
@ObjectType('Tour')
export class TourObjectType implements Tour {
optimizationInMinutes: number;
insuranceNo: string;
overRTW: boolean;
companyName: string;
schwerlast: boolean;
@@ -139,6 +139,7 @@ export class ControlCenterResolver {
AnomalyTypes.EmployeesLoggedIn,
AnomalyTypes.OneTourAtATime,
AnomalyTypes.IsGermanZip,
AnomalyTypes.KtwTourNotWithKtwCar
],
);
@@ -2,6 +2,7 @@ import { Module } from '@nestjs/common';
import { PrismaModule } from 'src/core/database/prisma.module';
import { FeatBusinessObjectsModule } from '../feat-business-objects/feat-business-objects.module';
import { FeatCalculationModule } from '../feat-calculation/feat-calculation.module';
import { ManagerController } from './manager.controller';
import { ManagerResolver } from './manager.resolver';
import { TimePerTourKpiService } from './time-per-tour-kpi.service';
import { TimePerTourMetricsService } from './time-per-tour-metrics.service';
@@ -9,6 +10,7 @@ import { TourAmountForecastService } from './tour-amount-forecast.service';
@Module({
imports: [PrismaModule, FeatCalculationModule, FeatBusinessObjectsModule],
controllers: [ManagerController],
providers: [
TourAmountForecastService,
TimePerTourKpiService,
@@ -0,0 +1,59 @@
import {
TimePerTourKPISummaryDto,
TimePerTourMetricDto,
} from '@avicenna/shared-dtos';
import {
BadRequestException,
Controller,
Get,
Logger,
Query,
} from '@nestjs/common';
import { ApiOkResponse } from '@nestjs/swagger';
import { TimePerTourKpiService } from './time-per-tour-kpi.service';
import { TimePerTourMetricsService } from './time-per-tour-metrics.service';
@Controller('api/manager')
export class ManagerController {
private readonly logger = new Logger(ManagerController.name);
constructor(
private readonly timePerTourKpiService: TimePerTourKpiService,
private readonly timePerTourMetricService: TimePerTourMetricsService,
) {}
@Get('overview')
@ApiOkResponse({ type: [TimePerTourKPISummaryDto] })
async overview(
@Query('ordinanceType') ordinanceType: string,
): Promise<TimePerTourKPISummaryDto[]> {
if (!ordinanceType) {
throw new BadRequestException(`no ordinanceType provided`);
}
return this.timePerTourKpiService.toursPerTimeKPI(ordinanceType);
}
@Get('dayview')
@ApiOkResponse({ type: [TimePerTourMetricDto] })
async dayview(
@Query('day') day: Date,
@Query('ordinanceType') ordinanceType: string,
): Promise<TimePerTourMetricDto[]> {
if (!day || !ordinanceType) {
throw new BadRequestException(`no day or ordinanceType provided`);
}
return this.timePerTourMetricService.forDay(day, ordinanceType);
}
@Get('monthview')
@ApiOkResponse({ type: [TimePerTourMetricDto] })
async monthview(
@Query('day') day: Date,
@Query('ordinanceType') ordinanceType: string,
): Promise<TimePerTourMetricDto[]> {
if (!day || !ordinanceType) {
throw new BadRequestException(`no day or ordinanceType provided`);
}
return this.timePerTourMetricService.forMonth(day, ordinanceType);
}
}
@@ -1,6 +1,6 @@
import { Injectable } from '@nestjs/common';
import { OnEvent } from '@nestjs/event-emitter';
import { addMilliseconds } from 'date-fns';
import { addMilliseconds, Interval } from 'date-fns';
import { PubSub } from 'graphql-subscriptions';
import { generateIntervalForTimeRange } from 'src/utils';
import { CalculationService } from '../feat-calculation/calculation.service';
@@ -115,7 +115,10 @@ export class TimePerTourKpiService {
tourAmount,
);
return {
interval,
interval: {
start: new Date(interval.start),
end: new Date(interval.end)
},
kpi: {
value: baseValue,
meta: {
@@ -1,3 +1,4 @@
import { TimePerTourMetricDto } from '@avicenna/shared-dtos';
import { Injectable } from '@nestjs/common';
import {
addDays,
@@ -15,11 +16,10 @@ import {
startOfDay,
startOfMonth,
} from 'date-fns';
import { TourAmountForecastService } from './tour-amount-forecast.service';
import { TimePerTourMetricObjectType } from './manager.object-type';
import { CalculationService } from '../feat-calculation/calculation.service';
import { generateIntervalForTimeRange, groupBy } from 'src/utils';
import { CarEntityService } from '../feat-business-objects/car-entity/car-entity.service';
import { CalculationService } from '../feat-calculation/calculation.service';
import { TourAmountForecastService } from './tour-amount-forecast.service';
@Injectable()
export class TimePerTourMetricsService {
@@ -84,7 +84,7 @@ export class TimePerTourMetricsService {
intervalClamp: Interval,
subIntervals: Interval[],
ordinanceType: string,
): Promise<TimePerTourMetricObjectType[]> {
): Promise<TimePerTourMetricDto[]> {
const carEntities = await this.carEntityService.getCarsInInterval(
intervalClamp,
ordinanceType,
@@ -180,7 +180,10 @@ export class TimePerTourMetricsService {
}
return {
interval,
interval: {
start: new Date(interval.start),
end: new Date(interval.end),
},
tourAmount: Math.round(tourAmount * 100) / 100,
availableTime: Math.round(availableTime * 100) / 100,
breakEven:
@@ -0,0 +1,11 @@
import { Module } from '@nestjs/common';
import { FeatBusinessObjectsModule } from '../feat-business-objects/feat-business-objects.module';
import { PlaygroundController } from './playground.controller';
import { PlaygroundService } from './playground.service';
@Module({
imports: [FeatBusinessObjectsModule],
controllers: [PlaygroundController],
providers: [PlaygroundService],
})
export class AppPlaygroundModule {}
@@ -0,0 +1,12 @@
import { Controller, Get, Query } from '@nestjs/common';
import { PlaygroundService } from './playground.service';
@Controller('api/playground')
export class PlaygroundController {
constructor(private readonly service: PlaygroundService) {}
@Get('operation-day')
async getOperationDay(@Query('day') day: Date) {
return this.service.getCarsByOperationDay(new Date(day));
}
}
@@ -0,0 +1,57 @@
import { Inject, Injectable } from '@nestjs/common';
import { Kysely } from 'kysely';
import { KYSELY_CLIENT_PROVIDER_TOKEN } from 'src/core/database/database.module';
import { DB } from 'src/core/database/types';
import { CarEntityService } from '../feat-business-objects/car-entity/car-entity.service';
import { CarEntity } from '../feat-business-objects/car-entity/car.entity';
@Injectable()
export class PlaygroundService {
constructor(
@Inject(KYSELY_CLIENT_PROVIDER_TOKEN) private readonly db: Kysely<DB>,
private readonly carService: CarEntityService,
) {}
async getCarsByOperationDay(day: Date) {
const ordinanceTypes = ['KTW', 'TSW', 'BTW'];
const cars = await Promise.all(
ordinanceTypes.map((ot) =>
this.carService.getCarsForOperationDay(day, ot),
),
);
return cars
.flat(2)
.filter(({ carName }) => !!carName)
.map((car) => this.carMapper(car))
.filter(
({ tours }) =>
tours.filter(
({ optimizationInMinutes }) => optimizationInMinutes !== 0,
).length > 0,
);
}
private carMapper(car: CarEntity) {
return {
name: car.carName,
drivers: car['plannedTimes'].map((pt) => ({
name: `${pt.firstname} ${pt.surname}`,
})),
tours: car['tours'].map((tour) => ({
id: tour.id,
operationId: tour.operationId,
ordinanceType: tour.ordinanceType,
startDate: tour.startDate,
optimizationInMinutes: tour.optimizationInMinutes,
departedForPickup: tour.startBegin,
arrivedAtPickup: tour.startEnd,
departedPickup: tour.target,
arrivedAtTarget: tour.targetBegin,
completed: tour.empty,
})),
};
}
}
@@ -1,18 +1,19 @@
import { BullModule } from '@nestjs/bullmq';
import { Module } from '@nestjs/common';
import { PrismaModule } from 'src/core/database/prisma.module';
import { FeatBusinessObjectsModule } from '../feat-business-objects/feat-business-objects.module';
import { FeatTicketSystemModule } from '../feat-tickets/ticket-system.module';
import { TicketRepository } from './ticket.repository';
import { TicketsResolver } from './tickets.resolver';
import { TicketSystemService } from './ticket-system.service';
import { TicketsController } from './tickets.controller';
import { MulterModule } from '@nestjs/platform-express';
import { ConfigModule } from 'src/core/config/config.module';
import { ConfigService } from 'src/core/config/config.service';
import { createMulterOptions } from 'src/core/storage/multer.config';
import { BullModule } from '@nestjs/bullmq';
import { TOUR_FILE_UPLOAD_QUEUE } from '../feat-business-objects/entities/tour/tour.tokens';
import { PrismaModule } from 'src/core/database/prisma.module';
import { MAILING_QUEUE } from 'src/core/mail/mail.tokens';
import { createMulterOptions } from 'src/core/storage/multer.config';
import { FeatBusinessObjectsModule } from '../feat-business-objects/feat-business-objects.module';
import { FeatTicketSystemModule } from '../feat-tickets/ticket-system.module';
import { TicketQueryService } from './queries/ticket-query.service';
import { TicketSystemService } from './ticket-system.service';
import { TicketRepository } from './ticket.repository';
import { TicketsController } from './tickets.controller';
import { TicketsResolver } from './tickets.resolver';
import { TicketReadMapper } from './mappers/ticket-read.mapper';
@Module({
imports: [
@@ -24,14 +25,17 @@ import { MAILING_QUEUE } from 'src/core/mail/mail.tokens';
useFactory: (config: ConfigService) => createMulterOptions(config),
inject: [ConfigService],
}),
BullModule.registerQueue({
name: TOUR_FILE_UPLOAD_QUEUE,
}),
BullModule.registerQueue({
name: MAILING_QUEUE,
}),
],
providers: [TicketRepository, TicketSystemService, TicketsResolver],
providers: [
TicketRepository,
TicketSystemService,
TicketReadMapper,
TicketsResolver,
TicketQueryService,
],
controllers: [TicketsController],
exports: [TicketSystemService],
})
@@ -0,0 +1,217 @@
import {
ApprovalRequestStatus,
ApprovalRequirement,
TicketApprovalViewDto,
TicketDetailDto,
TicketTransportDocumentView,
} from '@avicenna/shared-dtos';
import { Injectable } from '@nestjs/common';
import { isDate } from 'date-fns';
import { Ticket } from 'src/modules/feat-tickets/domain/ticket.entity';
@Injectable()
export class TicketReadMapper {
toDetailDto(ticket: Ticket): TicketDetailDto {
const tour = ticket.tours
.sort((a, b) => b.startDate.getTime() - a.startDate.getTime())
.at(0);
return {
id: ticket.id.value,
stage: ticket.currentStage,
general: {
ordinanceType: tour.ordinanceType,
transportType: tour.transportType,
category: tour.category,
hasInfection: tour.hasInfection,
infectionName: tour.infectionName,
isSeries: isDate(tour.seriesEndDate),
seriesEndDate: tour.seriesEndDate,
},
validationErrors: ticket.validationErrors,
patient: {
id: ticket.patientId.value,
name: `${ticket.patient.firstName} ${ticket.patient.lastName}`,
healthInsurance: ticket.patient.healthInsurance,
address: {
street: ticket.patient.street,
city: ticket.patient.city,
zip: ticket.patient.zip,
},
},
approval: this.toApprovalView(ticket),
transportDocument: this.toTransportDocumentView(ticket),
tours: ticket.tours
.sort((a, b) => b.startDate.getTime() - a.startDate.getTime())
.map((tour) => ({
id: tour.id.value,
operationId: tour.operationId,
date: tour.startDate.toISOString(),
startAddress: {
street: tour.startStreet,
city: tour.startCity,
zip: tour.startZip,
},
targetAddress: {
street: tour.targetStreet,
city: tour.targetCity,
zip: tour.targetZip,
},
})),
createdAt: ticket.createdAt,
updatedAt: ticket.updatedAt,
};
}
toApprovalView(ticket: Ticket): TicketApprovalViewDto {
const approval = ticket.approval;
return {
requirement: approval.requirement,
status: approval.status,
statusLabel: this.translateApprovalStatus(approval.status),
actions: {
override: {
isPossible: ApprovalRequestStatus.NOT_STARTED === approval.status,
label:
ApprovalRequirement.REQUIRED === approval.requirement
? 'Genehmigung wird nicht benötigt'
: 'Genehmigung wird benötigt',
confirmationMessage:
'Sind Sie sicher, dass sie die Genehmigungsanforderung des Tickets überschreiben wollen?',
},
registerExternalRequest: {
isPossible:
approval.status !== ApprovalRequestStatus.FULL_REQUEST_SENT &&
approval.status !== ApprovalRequestStatus.EXTERNAL_REQUEST &&
approval.status !== ApprovalRequestStatus.GRANTED &&
approval.requirement === ApprovalRequirement.REQUIRED,
label: 'Bereits außerhalb angefragt',
},
sendRequest: this.canSendApprovalRequest(ticket),
sendReminder: this.canSendApprovalReminder(ticket),
approve: {
isPossible:
approval.requirement !== ApprovalRequirement.NOT_REQUIRED &&
approval.status !== ApprovalRequestStatus.GRANTED,
},
},
};
}
toTransportDocumentView(ticket: Ticket): TicketTransportDocumentView {
const td = ticket.transportDocument;
const isPresent =
ticket.hasDigitalTransportDocument || ticket.hasPhysicalTransportDocument;
return {
isDocumentPresent: isPresent,
label: isPresent ? 'Transportschein liegt vor' : 'Dokument erforderlich',
digitalDownloadLink: td?.storagePath,
actions: {
upload: {
isPossible: !ticket.hasDigitalTransportDocument,
},
receivePhysical: {
isPossible: !ticket.hasPhysicalTransportDocument,
label: 'Transportschein liegt vor (Erfassen)',
},
},
};
}
private translateApprovalStatus(status: ApprovalRequestStatus): string {
switch (status) {
case ApprovalRequestStatus.NOT_STARTED:
return 'Noch keine Anfrage gesendet';
case ApprovalRequestStatus.PRE_INQUIRY_SENT:
return 'Vorab-Anfrage versendet';
case ApprovalRequestStatus.FULL_REQUEST_SENT:
return 'Anfrage versendet';
case ApprovalRequestStatus.EXTERNAL_REQUEST:
return 'Anfrage von Extern versendet';
case ApprovalRequestStatus.GRANTED:
return 'Genehmigung erteilt';
}
}
private canSendApprovalRequest(ticket: Ticket): {
isPossible: boolean;
label: string;
description: string;
} {
const approval = ticket.approval;
let isPossible = false;
if (approval.requirement === ApprovalRequirement.REQUIRED) {
if (approval.status === ApprovalRequestStatus.NOT_STARTED) {
isPossible = true;
} else if (
approval.status === ApprovalRequestStatus.PRE_INQUIRY_SENT &&
ticket.hasDigitalTransportDocument
) {
isPossible = true;
}
}
let label = 'Anfrage senden';
if (
ticket.approval.status === ApprovalRequestStatus.NOT_STARTED &&
!ticket.hasDigitalTransportDocument
) {
label = 'Vorab-Anfrage senden';
}
let description = undefined;
if (
approval.status === ApprovalRequestStatus.PRE_INQUIRY_SENT &&
!ticket.hasDigitalTransportDocument
) {
description = 'Transportschein fehlt';
}
return {
isPossible: isPossible,
label: label,
description: description,
};
}
private canSendApprovalReminder(ticket: Ticket): {
isPossible: boolean;
label: string;
description: string;
} {
const approval = ticket.approval;
let isPossible = false;
if (
approval.status === ApprovalRequestStatus.FULL_REQUEST_SENT &&
approval.isOverdue() &&
approval.hasRemindersLeft()
) {
isPossible = true;
}
const label = `${approval.reminderCount + 1}. Erinnerung senden`;
return {
isPossible: isPossible,
label: label,
description: undefined,
};
}
}
@@ -0,0 +1,199 @@
import {
PaginatedResult,
TicketListItemDto,
TicketStage,
} from '@avicenna/shared-dtos';
import { Inject, Injectable, Logger } from '@nestjs/common';
import { differenceInDays } from 'date-fns';
import { Kysely, sql } from 'kysely';
import { KYSELY_CLIENT_PROVIDER_TOKEN } from 'src/core/database/database.module';
import { DB } from 'src/core/database/types';
@Injectable()
export class TicketQueryService {
private readonly logger = new Logger(TicketQueryService.name);
constructor(
@Inject(KYSELY_CLIENT_PROVIDER_TOKEN) private readonly db: Kysely<DB>,
) {}
async searchTicket(searchTerm: string): Promise<TicketListItemDto[]> {
const searchTerms = searchTerm
.split(' ')
.filter(Boolean)
.map((str) => `%${str}%`);
const rows = await this.db
.selectFrom('Ticket')
.distinctOn('Ticket.id')
.where('stage', 'not in', [TicketStage.ARCHIVED, TicketStage.CLOSED])
.leftJoin('Patient', (join) =>
join.onRef(sql`"Patient"."id"::uuid`, '=', 'Ticket.patientId'),
)
.innerJoin('Tour', 'Ticket.id', 'Tour.ticketId')
.where((eb) =>
eb.and(
searchTerms.map((term) =>
eb.or([
eb('Patient.name', 'ilike', term),
eb('Patient.surname', 'ilike', term),
eb('Tour.patientName', 'ilike', term),
eb('Tour.patientSurname', 'ilike', term),
eb('Tour.operationId', 'ilike', term),
]),
),
),
)
.select((eb) => [
'Ticket.id',
'Ticket.stage',
'Ticket.approvalRequirement',
'Ticket.approvalStatus',
'Ticket.approvalRequestedAt',
'Ticket.createdAt',
'Ticket.updatedAt',
sql<string>`concat("Tour"."patientName", ' ', "Tour"."patientSurname")`.as(
'tourPatientName',
),
sql<string>`concat("Patient".name, ' ', "Patient".surname)`.as(
'patientName',
),
eb
.selectFrom('Tour')
.whereRef('Tour.ticketId', '=', 'Ticket.id')
.select(eb.fn.countAll<number>().as('count'))
.as('tourCount'),
eb
.selectFrom('Tour')
.whereRef('Tour.ticketId', '=', 'Ticket.id')
.select('ordinanceType')
.limit(1)
.as('ordinanceType'),
eb
.selectFrom('Tour')
.whereRef('Tour.ticketId', '=', 'Ticket.id')
.select('Tour.startDate')
.where('Tour.startDate', '>=', sql<Date>`now()`)
.orderBy('Tour.startDate', 'asc')
.limit(1)
.as('nextStartDate'),
])
.limit(5)
.execute();
return rows.map(
(row): TicketListItemDto => ({
id: row.id,
ordinanceType: row.ordinanceType,
patientName:
`${row.patientName}`.length > `${row.tourPatientName}`.length
? `${row.patientName}`
: row.tourPatientName,
stage: row.stage,
nextStart: row.nextStartDate,
tourCount: row.tourCount,
approvalRequirement: row.approvalRequirement,
approvalStatus: row.approvalStatus,
isOverdue: row.approvalRequestedAt
? differenceInDays(new Date(), row.approvalRequestedAt) >= 3
: false,
errorCount: 0,
createdAt: row.createdAt,
updatedAt: row.updatedAt,
}),
);
}
async getTicketsForLane(
stage: string,
page: number,
limit: number = 10,
): Promise<PaginatedResult<TicketListItemDto>> {
const offset = (page - 1) * limit;
const baseQuery = this.db
.selectFrom('Ticket')
.where('Ticket.stage', '=', stage);
const countResult = await baseQuery
.select((eb) => eb.fn.countAll<string>().as('total'))
.executeTakeFirst();
const total = Number(countResult?.total ?? 0);
const rows = await baseQuery
.select((eb) => [
'Ticket.id',
'Ticket.stage',
'Ticket.approvalRequirement',
'Ticket.approvalStatus',
'Ticket.approvalRequestedAt',
'Ticket.createdAt',
'Ticket.updatedAt',
eb
.selectFrom('Tour')
.whereRef('Tour.ticketId', '=', 'Ticket.id')
.select(eb.fn.countAll<number>().as('count'))
.as('tourCount'),
eb
.selectFrom('Tour')
.whereRef('Tour.ticketId', '=', 'Ticket.id')
.select('patientName')
.limit(1)
.as('patientName'),
eb
.selectFrom('Tour')
.whereRef('Tour.ticketId', '=', 'Ticket.id')
.select('patientSurname')
.limit(1)
.as('patientSurname'),
eb
.selectFrom('Tour')
.whereRef('Tour.ticketId', '=', 'Ticket.id')
.select('ordinanceType')
.limit(1)
.as('ordinanceType'),
eb
.selectFrom('Tour')
.select('Tour.startDate')
.whereRef('Tour.ticketId', '=', 'Ticket.id')
.where('Tour.startDate', '>=', sql<Date>`now()`)
.orderBy('Tour.startDate', 'asc')
.limit(1)
.as('nextStartDate'),
])
.limit(limit)
.offset(offset)
.orderBy('createdAt', 'desc')
.execute();
const data = rows.map((row) => ({
id: row.id,
ordinanceType: row.ordinanceType,
patientName: `${row.patientName} ${row.patientSurname}`,
stage: row.stage,
nextStart: row.nextStartDate,
tourCount: row.tourCount,
approvalRequirement: row.approvalRequirement,
approvalStatus: row.approvalStatus,
isOverdue: row.approvalRequestedAt
? differenceInDays(new Date(), row.approvalRequestedAt) >= 3
: false,
errorCount: 0,
createdAt: row.createdAt,
updatedAt: row.updatedAt,
}));
return {
data,
meta: {
total,
page,
limit,
totalPages: Math.ceil(total / limit),
},
};
}
}
@@ -1,5 +1,5 @@
import { InjectQueue } from '@nestjs/bullmq';
import { Injectable, NotFoundException } from '@nestjs/common';
import { Inject, Injectable, NotFoundException } from '@nestjs/common';
import { OnEvent } from '@nestjs/event-emitter';
import { Ticket, TicketValidationState, Tour } from '@prisma/client';
import { Queue } from 'bullmq';
@@ -19,12 +19,14 @@ import {
import { DocumentInfoArgs, TicketFilterArgs } from './ticket-arg.types';
import {
PaginatedTicketObjectType,
TicketObjectType,
TicketStateKeyFiguresObjectType,
TicketStateMetaObjectType,
} from './ticket.object-type';
import { TicketRepository } from './ticket.repository';
import { TicketRepository as LegacyTicketRepository } from './ticket.repository';
import { TourFilesService } from '../feat-business-objects/entities/tour-files/tour-files.service';
import { TICKET_REPOSITORY, TicketRepository } from '../feat-tickets/domain/ticket.repository.interface';
import { TicketId } from '../feat-tickets/domain/ticket.entity';
@Injectable()
export class TicketSystemService {
public readonly pubSub = new PubSub();
@@ -37,7 +39,8 @@ export class TicketSystemService {
}
constructor(
private readonly repository: TicketRepository,
private readonly repository: LegacyTicketRepository,
@Inject(TICKET_REPOSITORY) private readonly dddRepo: TicketRepository,
private readonly ticketService: TicketService,
private readonly tourService: TourService,
private readonly tourFileService: TourFilesService,
@@ -67,10 +70,6 @@ export class TicketSystemService {
return true;
}
findById(id: string): Promise<Ticket> {
return this.ticketService.findById(id);
}
findAll(): Promise<Ticket[]> {
return this.ticketService.findAll() as Promise<Ticket[]>;
}
@@ -97,7 +96,7 @@ export class TicketSystemService {
const nextPage = skip + take < total ? skip + take : skip;
return {
nodes: tickets,
nodes: [], // tickets,
prevCursor: encodeCursor(prevPage),
endCursor: encodeCursor(nextPage),
hasNextPage: nextPage !== skip,
@@ -29,6 +29,24 @@ export class DocumentInfoObjectType {
@ObjectType('Ticket')
export class TicketObjectType implements Ticket {
lastStageBeforeClosed: string;
approvalPreInquiryMessageId: string;
approvalFullRequestMessageId: string;
approvalRecipient: string;
approvalIsOverriden: boolean;
approvalStatus: string;
approvalLastInteractionAt: Date;
approvalReminderCount: number;
hasPhysicalTransportDocument: boolean;
hasDigitalTransportDocument: boolean;
patientId: string;
updatedAt: Date;
stage: string;
approvalRequirement: string;
approvalRequestedAt: Date;
approvalIsGranted: boolean;
hasTransportDocument: boolean;
@Field()
id: string;
@Field(() => TicketValidationState)
@@ -1,31 +1,179 @@
import { InjectQueue } from '@nestjs/bullmq';
import {
GetBoardQueryDto,
PaginatedResult,
TicketApprovalViewDto,
TicketDetailDto,
TicketListItemDto,
} from '@avicenna/shared-dtos';
import {
BadRequestException,
Body,
Controller,
Get,
HttpCode,
HttpStatus,
Inject,
Logger,
NotFoundException,
Param,
Post,
Query,
UploadedFile,
UseInterceptors,
} from '@nestjs/common';
import { FileInterceptor } from '@nestjs/platform-express';
import { Queue } from 'bullmq';
import { TourService } from '../feat-business-objects/entities/tour/tour.service';
import { TOUR_FILE_UPLOAD_QUEUE } from '../feat-business-objects/entities/tour/tour.tokens';
import { TourFilesService } from '../feat-business-objects/entities/tour-files/tour-files.service';
import { MarkAsApprovedUseCase } from '../feat-tickets/application/mark-as-approved.usecase';
import { OverrideApprovalRequirementUseCase } from '../feat-tickets/application/override-approval-requirement.usecase';
import { ReceivePhysicalTransportDocumentUseCase } from '../feat-tickets/application/receive-physical-transport-document.usecase';
import { RequestTicketApprovalUseCase } from '../feat-tickets/application/request-ticket-approval.usecase';
import { SendApprovalReminderUseCase } from '../feat-tickets/application/send-approval-reminder.usecase';
import { UploadTransportDocumentUseCase } from '../feat-tickets/application/upload-transport-document.usecase';
import { TicketId } from '../feat-tickets/domain/ticket.entity';
import {
TICKET_REPOSITORY,
TicketRepository,
} from '../feat-tickets/domain/ticket.repository.interface';
import { TicketReadMapper } from './mappers/ticket-read.mapper';
import { TicketQueryService } from './queries/ticket-query.service';
import { RegisterExternalApprovalRequestUseCase } from '../feat-tickets/application/register-external-approval-request.usecase';
@Controller('api/tickets')
export class TicketsController {
private readonly logger = new Logger(TicketsController.name);
constructor(
private readonly tourService: TourService,
private readonly tourFilesService: TourFilesService,
@InjectQueue(TOUR_FILE_UPLOAD_QUEUE)
private readonly transportscheinQueue: Queue,
private readonly queryService: TicketQueryService,
private readonly ticketReadMapper: TicketReadMapper,
@Inject(TICKET_REPOSITORY) private readonly ticketRepo: TicketRepository,
private readonly uploadUseCase: UploadTransportDocumentUseCase,
private readonly overrideUseCase: OverrideApprovalRequirementUseCase,
private readonly requestApprovalUseCase: RequestTicketApprovalUseCase,
private readonly sendReminderUseCase: SendApprovalReminderUseCase,
private readonly approveUseCase: MarkAsApprovedUseCase,
private readonly receiveUseCase: ReceivePhysicalTransportDocumentUseCase,
private readonly registerUseCase: RegisterExternalApprovalRequestUseCase,
) {}
@Get('search')
async searchTickets(
@Query('q') searchTerm: string,
): Promise<TicketListItemDto[]> {
if (!searchTerm) {
throw new BadRequestException("no 'q' provided");
}
return this.queryService.searchTicket(searchTerm);
}
@Get()
async getBoard(
@Query() query: GetBoardQueryDto,
): Promise<PaginatedResult<TicketListItemDto>> {
this.logger.log(
`fetching tickets board for stage ${query.stage} at page ${query.page}`,
);
return this.queryService.getTicketsForLane(
query.stage,
query.page,
query.limit,
);
}
@Get(':id')
async getTicket(@Param('id') id: string): Promise<TicketDetailDto> {
this.logger.log(`fetch ticket ${id}`);
const ticketId = new TicketId(id);
const ticket = await this.ticketRepo.findById(ticketId);
if (!ticket) {
throw new NotFoundException(`Ticket ${id} not found`);
}
return this.ticketReadMapper.toDetailDto(ticket);
}
@Get(':id/approval')
async getTicketApprovalView(
@Param('id') id: string,
): Promise<TicketApprovalViewDto> {
this.logger.log(`fetch ticket ${id}`);
const ticketId = new TicketId(id);
const ticket = await this.ticketRepo.findById(ticketId);
if (!ticket) {
throw new NotFoundException(`Ticket ${id} not found`);
}
return this.ticketReadMapper.toApprovalView(ticket);
}
@Post(':id/approval/request')
@HttpCode(HttpStatus.NO_CONTENT)
async requestApproval(
@Param('id') id: string,
@Body('recipient') recipient: string,
) {
if (!id) {
throw new BadRequestException(`no id provided`);
}
if (!recipient) {
throw new BadRequestException(`no recipient provided`);
}
await this.requestApprovalUseCase.execute(id, recipient);
}
@Post(':id/approval/register-request')
@HttpCode(HttpStatus.NO_CONTENT)
async registerExternalApprovalRequest(@Param('id') id: string) {
if (!id) {
throw new BadRequestException(`no id provided`);
}
await this.registerUseCase.execute(id);
}
@Post(':id/approval/remind')
@HttpCode(HttpStatus.NO_CONTENT)
async sendReminder(@Param('id') id: string) {
if (!id) {
throw new BadRequestException(`no id provided`);
}
await this.sendReminderUseCase.execute(id);
}
@Post(':id/approval/approve')
@HttpCode(HttpStatus.NO_CONTENT)
async approve(@Param('id') id: string) {
if (!id) {
throw new BadRequestException(`no id provided`);
}
await this.approveUseCase.execute(id);
}
@Post(':id/approval/override')
@HttpCode(HttpStatus.NO_CONTENT)
async overrideApproval(@Param('id') id: string) {
if (!id) {
throw new BadRequestException(`no id provided`);
}
await this.overrideUseCase.execute(id);
}
@Post(':id/transport-document/receive')
@HttpCode(HttpStatus.NO_CONTENT)
async receiveTD(@Param('id') id: string) {
if (!id) {
throw new BadRequestException(`no id provided`);
}
await this.receiveUseCase.execute(id);
}
@Post(':ticketId/files')
@UseInterceptors(FileInterceptor('file'))
@HttpCode(HttpStatus.ACCEPTED)
@@ -33,30 +181,12 @@ export class TicketsController {
@Param('ticketId') ticketId: string,
@UploadedFile() file: Express.Multer.File,
) {
this.logger.log(
`file received for ticket ${ticketId ?? ''}: ${file.filename}`,
);
const existingFile = await this.tourFilesService.findByTicketId(ticketId);
if (existingFile) {
return {message: 'File already exists!'}
if (!file) {
throw new BadRequestException('No file provided');
}
const storage = file.path;
const tours = await this.tourService.findByTicketId(ticketId);
await this.uploadUseCase.execute(ticketId, file);
for (const tour of tours) {
const tourFile = await this.tourFilesService.createTourFile({
ticketId,
tourId: tour.id,
storagePath: storage,
file: file,
});
await this.transportscheinQueue.add('Transportschein', {
tourFileId: tourFile.id,
});
}
return { message: 'File accepted and processing started' };
return { message: 'File accepted and processing' };
}
}
@@ -25,7 +25,7 @@ export class TicketsResolver {
@Query(() => TicketObjectType)
ticket(@Args('id') id: string) {
return this.ticketsService.findById(id);
// return this.ticketsService.findById(id);
}
@Query(() => PaginatedTicketObjectType)
@@ -14,6 +14,7 @@ import { IsGermanZipDetector } from './anomaly-detectors/is-german-zip.detector'
import { EmployeeStillWorkingDetector } from './anomaly-detectors/employee-still-working.detector';
import { IsAttendanceBeforeWorktimeDetector } from './anomaly-detectors/is-attendance-before-worktime.detector';
import { IsWorkTimePlannedDetector } from './anomaly-detectors/is-work-time-planned.detector';
import { KtwTourNotWithKtwCarDetector } from './anomaly-detectors/ktw-tour-with-ktw-car.detector';
@Injectable()
export class AnomaliesService {
@@ -35,6 +36,7 @@ export class AnomaliesService {
new EmployeeStillWorkingDetector(this.anomalyDetectorService),
new IsAttendanceBeforeWorktimeDetector(this.anomalyDetectorService),
// new IsWorkTimePlannedDetector(this.anomalyDetectorService)
new KtwTourNotWithKtwCarDetector(this.anomalyDetectorService)
];
}
@@ -34,6 +34,22 @@ export class AnomalyDetectorRepository {
@Inject(KYSELY_CLIENT_PROVIDER_TOKEN) private readonly db: Kysely<DB>,
) {}
async ktwToursNotOnKtwCars(interval: Interval): Promise<Tour[]> {
return await this.db
.selectFrom('Tour')
.selectAll()
.where((eb) =>
eb.and([
eb('Tour.ordinanceType', 'ilike', '%ktw%'),
eb('Tour.carName', 'not ilike', '%ktw%'),
eb('Tour.startDate', '>=', new Date(interval.start)),
eb('Tour.startDate', '<=', new Date(interval.end)),
onlyRealTours(eb),
]),
)
.execute();
}
async toursWithoutGermanZipInInterval(interval: Interval): Promise<Tour[]> {
return await this.db
.selectFrom('Tour')
@@ -37,6 +37,10 @@ export class AnomalyDetectorService {
private readonly anomalyRepository: AnomalyDetectorRepository,
) {}
public async findKtwToursNotOnKtwCars(interval: Interval): Promise<Tour[]> {
return this.anomalyRepository.ktwToursNotOnKtwCars(interval)
}
public findToursWithoutGermanZipInInterval(
interval: Interval,
): Promise<Tour[]> {
@@ -16,56 +16,54 @@ export class DriverCertificateDetector extends AnomalyDetector {
);
const toursInRange = await this.dataService.findToursInInterval(inInterval);
const anomalies = await Promise.all(
toursInRange
.filter(({ driverId }) => !!driverId)
.map(
async ({
id: tourId,
driverId,
startDate,
driverName,
carName,
operationId,
}) => {
try {
const driverEmployee =
await this.dataService.findEmployeeById(driverId);
if (!driverEmployee) {
try {
const anomalies = await Promise.all(
toursInRange
.filter((tour) => !!tour?.driverId && !!tour?.startDate)
.map(
async ({
id: tourId,
driverId,
startDate,
driverName,
carName,
operationId,
}) => {
try {
const driverEmployee =
await this.dataService.findEmployeeById(driverId);
if (!driverEmployee) {
return;
}
if (
!this.isPersonTransportCertificateValidAtStartOfTour(
driverEmployee.personTransportCertificate,
startDate,
)
) {
this.addAssociatedEntity('employee', driverEmployee.id);
this.addAssociatedEntity('tour', tourId);
return this.createAnomaly(
`${operationId}-${driverId}-${this.type}`,
operationId,
carName,
driverName,
new Date(startDate),
);
}
} catch (error) {
this.logger.error(error);
return;
return this.createAnomaly(
`${operationId}-${driverId}-${this.type}`,
operationId,
carName,
driverName ?? '<Fahrer gelöscht>',
new Date(startDate),
);
}
if (
!this.isPersonTransportCertificateValidAtStartOfTour(
driverEmployee.personTransportCertificate,
startDate,
)
) {
this.addAssociatedEntity('employee', driverEmployee.id);
this.addAssociatedEntity('tour', tourId);
return this.createAnomaly(
`${operationId}-${driverId}-${this.type}`,
operationId,
carName,
driverName,
new Date(startDate),
);
}
} catch (error) {
this.logger.error(error);
return;
}
},
),
);
return anomalies.filter((anomaly) => !!anomaly);
},
),
);
return anomalies.filter((anomaly) => !!anomaly);
} catch (error) {
this.logger.error(error)
return
}
}
async validate(anomaly: Anomaly): Promise<Anomaly> {
@@ -45,7 +45,7 @@ export class EmployeesLoggedInDetector extends AnomalyDetector {
const tour = await this.dataService.findTourByOperationId(operationId);
const employee = await this.dataService.findEmployeeById(employeeId);
if(!tour?.startDate) {
this.logger.error(`tour ${tour?.operationId} has no startDate`)
this.logger.error(`tour ${tour?.operationId ?? operationId} has no startDate`)
}
const startOfTour = this.getStartOfTour(
new Date(tour?.startDate),
@@ -0,0 +1,102 @@
import { Logger } from '@nestjs/common';
import { AnomalyDetector } from './anomaly-detector.class';
import { Prisma, Anomaly } from '@prisma/client';
import { format, Interval } from 'date-fns';
import { AnomalyTypes } from 'src/modules/feat-business-objects/entities/anomaly/anomaly-types.enum';
import { toZonedTime } from 'date-fns-tz';
export class KtwTourNotWithKtwCarDetector extends AnomalyDetector {
private readonly logger = new Logger(KtwTourNotWithKtwCarDetector.name);
readonly type = AnomalyTypes.KtwTourNotWithKtwCar;
async detect(interval: Interval): Promise<Prisma.AnomalyCreateInput[]> {
this.logger.log(
`start '${KtwTourNotWithKtwCarDetector.name}' with interval ${format(interval.start, 'yyyy-MM-dd HH:mm')} - ${format(interval.end, 'yyyy-MM-dd HH:mm')}`,
);
const tours = await this.dataService.findKtwToursNotOnKtwCars(interval);
return await Promise.all(
tours.map((tour) =>
this.createAnomaly(
`${tour.operationId}-${this.type}`,
tour.id,
tour.operationId,
tour.startDate,
),
),
);
}
async validate(anomaly: Anomaly): Promise<Anomaly> {
const [operationId] = anomaly.id.split('-');
try {
const tour = await this.dataService.findTourByOperationId(operationId);
const isKtwTour = tour?.ordinanceType.toLowerCase().indexOf('ktw') > -1;
const isKtwResource = tour?.carName.toLowerCase().indexOf('ktw') > -1;
if (!isKtwTour || isKtwResource) {
return {
...anomaly,
resolvedAt: new Date(),
};
} else {
return {
...anomaly,
};
}
} catch (error) {
this.logger.error(error, { anomalyId: anomaly.id });
// Im Fehlerfall geben wir die Anomalie unverändert zurück, um Datenverlust zu vermeiden
return anomaly;
}
}
createDescription({ operationId }: { operationId: string }): string {
return `${operationId}: KTW-Tour auf falschem Fahrzeugtyp`;
}
createSolution({
tourId,
}: {
tourId: string;
}): Prisma.SolutionTargetCreateInput[] {
return [
{
name: 'DispoLive',
steps: {
create: [
{
description: `Öffnen Sie den <a class="dispolive-btn" href="https://avicenna.dispolive.de/fahrbericht/${tourId}" target="_blank">Fahrbericht</a> und weisen Sie der Tour ein Fahrzeug des Typs <strong>KTW</strong> zu.`,
},
],
},
},
];
}
private createAnomaly(
anomalyId: string,
tourId: string,
operationId: string,
startDate: Date,
): Prisma.AnomalyCreateInput {
return {
...this.getAnomalySkeleton(),
id: anomalyId,
type: this.type,
groupKey: `${format(
new Date(startDate),
'yyyy-MM-dd-HH-mm',
)}-${operationId}`,
groupDescription: `${operationId} - ${format(
toZonedTime(new Date(startDate), 'Europe/Berlin'),
'HH:mm dd.MM.yy',
)}`,
solution: {
create: this.createSolution({ tourId }),
},
description: this.createDescription({ operationId }),
};
}
}
@@ -6,6 +6,7 @@ export enum AnomalyTypes {
IsAttendanceBeforeWorktime = 'IsAttendanceBeforeWorktime',
IsGermanZip = 'IsGermanZip',
IsWorktimePlanned = 'IsWorktimePlanned',
KtwTourNotWithKtwCar = 'KtwTourWithKtwCar',
OneTourAtATime = 'OneTourAtATime',
WorkingEmployeeNotInDistributionSystem = 'WorkingEmployeeNotInDistributionSystem',
}
@@ -1,7 +1,7 @@
import { Inject, Injectable } from '@nestjs/common';
import { Prisma, TicketValidationState, Tour } from '@prisma/client';
import { endOfDay, Interval, startOfDay } from 'date-fns';
import { ExpressionBuilder, ExpressionWrapper, Kysely, SqlBool } from 'kysely';
import { ExpressionBuilder, ExpressionWrapper, Kysely, sql, SqlBool } from 'kysely';
import { KYSELY_CLIENT_PROVIDER_TOKEN } from 'src/core/database/database.module';
import { PrismaService } from 'src/core/database/prisma.service';
import { DB } from 'src/core/database/types';
@@ -26,8 +26,8 @@ function onlyRealToursKysely(eb: TourEb) {
eb('deletedAt', 'is', null),
eb.or([
eb('patientId', 'is', null),
eb('patientId', 'not in', [PAUSE_PATIENT_ID, FEIERABEND_PATIENT_ID]),
eb('Tour.patientId', 'is', null),
eb('Tour.patientId', 'not in', [PAUSE_PATIENT_ID, FEIERABEND_PATIENT_ID]),
]),
]);
}
@@ -77,6 +77,36 @@ export class TourRepository {
.orderBy('Tour.startDate asc')
.execute();
}
// tour.repository.ts
async findForCarInInterval(
carName: string,
interval: Interval,
): Promise<Tour[]> {
return await this.db
.selectFrom('Tour')
.selectAll('Tour')
.where((eb) =>
eb.and([
eb('Tour.carName', 'ilike', `%${carName}%`),
eb('Tour.startDate', '>=', new Date(interval.start)),
eb('Tour.startDate', '<', new Date(interval.end)),
onlyRealToursKysely(eb),
]),
)
.orderBy('Tour.startDate', 'asc')
.execute();
}
async findWithoutTicket(): Promise<Tour[]> {
return this.db
.selectFrom('Tour')
.selectAll()
.where('ticketId', 'is', null)
.where((eb) => onlyRealToursKysely(eb))
.limit(1000)
.execute();
}
async findUpdatedAtBetween(
firstId: string,
@@ -297,26 +327,39 @@ export class TourRepository {
// --------------------- Accounting --------------------- //
async findByAccountingState(
state: AccountingState,
filter: TourAccountingFilterDto,
take?: number,
skip?: number,
): Promise<{ tours: Tour[]; total: number }> {
): Promise<{ tours: (Tour & { stage: string })[]; total: number }> {
return await this.db.transaction().execute(async (trx) => {
const tours = await trx
const tours = (await trx
.selectFrom('Tour')
.selectAll()
// .selectAll('Tour')
.select([
'Tour.id',
'Tour.operationId',
'Tour.startDate',
'Tour.revenue',
'Tour.revenueDispoLive',
'Tour.optimizationInMinutes',
'Tour.ordinanceType',
])
.innerJoin('Ticket', 'Ticket.id', 'Tour.ticketId')
.select(['Ticket.stage', 'Ticket.patientId as ticketPatientId'])
.where((eb) =>
eb.and([
this.getFilterForAccountingState(eb, state),
onlyRealToursKysely(eb),
]),
)
.orderBy(sql`abs("Tour"."optimizationInMinutes")`, 'desc')
.orderBy('Tour.revenueDeviation desc')
.offset(skip)
.limit(take)
.execute();
.execute()) as unknown as (Tour & { stage: string })[];
const total = await trx
.selectFrom('Tour')
.innerJoin('Ticket', 'Ticket.id', 'Tour.ticketId')
.where((eb) =>
eb.and([
this.getFilterForAccountingState(eb, state),
@@ -393,33 +436,11 @@ export class TourRepository {
return eb.and([
eb('Tour.check', '=', 0),
eb('Tour.revenueDeviation', '<>', 0),
eb.exists(
eb
.selectFrom('Ticket')
.select('Ticket.id')
.whereRef('Ticket.id', '=', 'Tour.ticketId')
.where(
'Ticket.currentState',
'<>',
TicketValidationState.ARCHIVED,
),
),
]);
case AccountingState.Billable:
return eb.and([
eb('Tour.check', '=', 0),
eb('Tour.revenueDeviation', '=', 0),
eb.exists(
eb
.selectFrom('Ticket')
.select('Ticket.id')
.whereRef('Ticket.id', '=', 'Tour.ticketId')
.where(
'Ticket.currentState',
'=',
TicketValidationState.ARCHIVED,
),
),
]);
case AccountingState.PreparedForBilling:
return eb.and([eb('Tour.check', '=', 1)]);
@@ -4,6 +4,8 @@ import { Interval } from 'date-fns';
import { AccountingState } from 'src/modules/app-accounting/accounting-state';
import { TourRepository } from './tour.repository';
import { TourAccountingFilterDto } from './tours.filter';
import { OptimizationCarDto } from '@avicenna/shared-dtos';
import { generateIntervalForTimeRange } from 'src/utils';
@Injectable()
export class TourService {
@@ -16,6 +18,10 @@ export class TourService {
return this.repository.findInInterval(interval, ordinanceTypes);
}
async findWithoutTicket(): Promise<Tour[]> {
return this.repository.findWithoutTicket();
}
async findUpdatedAtBetween(
firstId: string,
lastId: string,
@@ -35,6 +41,22 @@ export class TourService {
);
}
async getOptimizationToursForCar(
carName: string,
day: Date,
): Promise<Tour[]> {
const operationDayInterval = generateIntervalForTimeRange(
'05:00-04:59',
'Europe/Berlin',
day,
);
return await this.repository.findForCarInInterval(
carName,
operationDayInterval,
);
}
async findById(tourId: string): Promise<Tour> {
return this.repository.findById(tourId);
}
@@ -56,7 +78,7 @@ export class TourService {
}
async findTragehilfeTour(tour: Tour): Promise<Tour | null> {
return this.repository.findTragehilfeTour(tour)
return this.repository.findTragehilfeTour(tour);
}
async findToursWithTicket(interval: Interval): Promise<Tour[]> {
@@ -71,7 +93,6 @@ export class TourService {
return this.repository.createTour(tourInput);
}
async updateTour(
id: string,
tourInput: Prisma.TourUpdateInput,
@@ -85,11 +106,10 @@ export class TourService {
async findByAccountingState(
state: AccountingState,
filter: TourAccountingFilterDto,
take?: number,
skip?: number,
): Promise<{ tours: Tour[]; total: number }> {
return this.repository.findByAccountingState(state, filter, take, skip);
): Promise<{ tours: (Tour & { stage: string })[]; total: number }> {
return this.repository.findByAccountingState(state, take, skip);
}
async calculateAccountingStateMeta(state: AccountingState): Promise<{
@@ -269,6 +269,7 @@ export class DispoLiveExtractorsService {
importPersId,
qualifikation,
} = employee;
const quali = qualifikation as string | { _id: string };
return {
id: _id,
pScheinAblaufDatum,
@@ -277,11 +278,11 @@ export class DispoLiveExtractorsService {
persId,
importPersId,
qualifikation:
!!qualifikation && typeof qualifikation === 'object'
? //@ts-expect-error ts is wrong here
qualifikation?._id
: (qualifikation ?? undefined),
};
(quali && typeof quali === 'object' && '_id' in quali)
?
quali?._id
: (quali ?? undefined),
} as EmployeeDispoLiveRaw;
}),
),
);
@@ -378,6 +379,7 @@ export class DispoLiveExtractorsService {
birthday,
kkId,
krankenkasse,
stammVersNr,
pflegestufe,
behinderung1,
behinderung2,
@@ -392,6 +394,7 @@ export class DispoLiveExtractorsService {
birthday,
kkId,
krankenkasse,
stammVersNr,
pflegestufe: careDegrees.find(({ id }) => id === pflegestufe)
?.careDegree,
behinderung1: disabilityMarks.find(
@@ -45,12 +45,12 @@ export class DyflexisUserAccessExtractorsService {
loadData(): Observable<any> {
return combineLatest([
// this.extractWorkdays(),
this.extractWorkdays(),
// this.extractSicknessDays({
// start: new Date('2025-01-01T00:00:00.000Z'),
// end: new Date('2025-12-31T23:59:59.999Z'),
// }),
this.extractHolidays(new Date('2025-01-01T00:00:00.000Z')),
// this.extractHolidays(new Date('2025-01-01T00:00:00.000Z')),
]);
}
@@ -34,6 +34,7 @@ export class TourExtractor {
itName,
kkId,
krankenkasse,
stammVersNr,
patId,
patName,
patSurname,
@@ -104,6 +105,7 @@ export class TourExtractor {
itName: itName ? `${itName}` : undefined,
kkId: kkId ? `${kkId}` : undefined,
krankenkasse: krankenkasse ? `${krankenkasse}` : undefined,
stammVersNr: stammVersNr ? `${stammVersNr}` : undefined,
patId: patId ? `${patId}` : undefined,
patName: patName ? `${patName}` : undefined,
patSurname: patSurname ? `${patSurname}` : undefined,
@@ -31,6 +31,7 @@ export interface TourRaw {
itName?: string;
kkId?: string;
krankenkasse?: string;
stammVersNr?: string;
patId?: string;
patName?: string;
patSurname?: string;
@@ -95,6 +96,7 @@ export interface PatientRaw {
city?: string;
kkId?: string;
krankenkasse?: string;
stammVersNr?: string;
pflegestufe?: string;
behinderung1?: string;
behinderung2?: string;
@@ -1,7 +1,7 @@
import { Processor, WorkerHost } from '@nestjs/bullmq';
import { Logger } from '@nestjs/common';
import { Job } from 'bullmq';
import { lastValueFrom } from 'rxjs';
import { lastValueFrom, of } from 'rxjs';
import { TOUR_FILE_UPLOAD_QUEUE } from 'src/modules/feat-business-objects/entities/tour/tour.tokens';
import { DispoLiveExtractorsService } from '../../feat-extractors/dispo-live-extractors.service';
import { TourFilesService } from 'src/modules/feat-business-objects/entities/tour-files/tour-files.service';
@@ -54,6 +54,7 @@ export class PatientLoader {
birthday,
healthinsurance,
healthinsuranceId,
insuranceNo,
careDegree,
disabilityMark1,
disabilityMark2,
@@ -69,6 +70,7 @@ export class PatientLoader {
birthday,
healthinsurance,
healthinsuranceId,
insuranceNo,
careDegree,
disabilityMark1,
disabilityMark2,
@@ -30,7 +30,10 @@ export class TourLoader {
);
if (finalTour) {
updatedTours.push(
this.tourStagingToFinal(stagingTour, finalTour.ticketId),
this.tourStagingToFinal(stagingTour, {
ticketId: finalTour?.ticketId,
optimizationInMinutes: finalTour?.optimizationInMinutes,
}),
);
} else {
newTours.push(this.tourStagingToFinal(stagingTour));
@@ -44,7 +47,10 @@ export class TourLoader {
};
}
private static tourStagingToFinal(tour: TourStaging, ticketId?: string) {
private static tourStagingToFinal(
tour: TourStaging,
keepData?: { ticketId: string; optimizationInMinutes: number },
) {
const {
id,
done,
@@ -63,6 +69,7 @@ export class TourLoader {
patientCity,
healthInsurance,
healthInsuranceNumber,
insuranceNo,
type,
category,
transportType,
@@ -118,6 +125,7 @@ export class TourLoader {
patientCity,
healthInsurance,
healthInsuranceNumber,
insuranceNo,
type,
category,
transportType,
@@ -153,7 +161,10 @@ export class TourLoader {
billNumber,
createdAt,
updatedAt,
...(ticketId ? { ticketId } : {}),
...(keepData?.ticketId ? { ticketId: keepData.ticketId } : {}),
...(keepData?.optimizationInMinutes
? { optimizationInMinutes: keepData.optimizationInMinutes }
: {}),
};
}
}
@@ -18,6 +18,7 @@ export interface TourPreStaging {
patientCity?: string;
healthInsurance?: string;
healthInsuranceNumber?: string;
insuranceNo?: string;
type?: string;
category?: string;
@@ -75,6 +76,7 @@ export interface PatientPreStaging {
birthday?: Date;
healthinsuranceId?: string;
healthinsurance?: string;
insuranceNo?: string;
careDegree?: string;
disabilityMark1?: string;
disabilityMark2?: string;
@@ -31,6 +31,7 @@ export class PatientTransformer {
birthday,
kkId,
krankenkasse,
stammVersNr,
pflegestufe,
behinderung1,
behinderung2,
@@ -46,6 +47,7 @@ export class PatientTransformer {
birthday: isValidDate(new Date(birthday)) ? birthday : undefined,
healthinsurance: krankenkasse,
healthinsuranceId: kkId,
insuranceNo: stammVersNr,
careDegree: pflegestufe,
disabilityMark1: behinderung1,
disabilityMark2: behinderung2,
@@ -85,6 +85,7 @@ export class TourTransformer {
patCity,
krankenkasse,
kkId,
stammVersNr,
type,
serienKategorName,
transportKrz,
@@ -146,6 +147,7 @@ export class TourTransformer {
patientCity: patCity,
healthInsurance: krankenkasse,
healthInsuranceNumber: kkId,
insuranceNo: stammVersNr,
category: serienKategorName,
transportType: transportKrz,
ordinanceType: verordnungsName,
@@ -0,0 +1,18 @@
import { KpiInfoDto } from '@avicenna/shared-dtos';
import { BadRequestException, Controller, Get, Query } from '@nestjs/common';
import { KpiService } from './kpi.service';
@Controller('api/kpis')
export class KpiController {
constructor(private readonly kpiService: KpiService) {}
@Get('manager')
managerKpi(
@Query('ordinanceType') ordinanceType: string,
): Promise<KpiInfoDto> {
if(!ordinanceType) {
throw new BadRequestException('no ordinanceType provided')
}
return this.kpiService.managerKpi(ordinanceType);
}
}
@@ -4,9 +4,11 @@ import { FeatBusinessObjectsModule } from '../feat-business-objects/feat-busines
import { FeatCalculationModule } from '../feat-calculation/feat-calculation.module';
import { KpiResolver } from './kpi.resolver';
import { KpiService } from './kpi.service';
import { KpiController } from './ kpi.controller';
@Module({
imports: [PrismaModule, FeatBusinessObjectsModule, FeatCalculationModule],
controllers: [KpiController],
providers: [KpiService, KpiResolver],
exports: [KpiService],
})
@@ -0,0 +1,51 @@
import { BadRequestException, Inject, Injectable, Logger, NotFoundException } from '@nestjs/common';
import { TICKET_REPOSITORY, TicketRepository } from '../domain/ticket.repository.interface';
import { TicketId } from '../domain/ticket.entity';
import { TOUR_PROVIDER, TourProvider } from '../domain/ports/tour-provider.interface';
import { PATIENT_PROVIDER, PatientProvider } from '../domain/ports/patient-provider.interface';
@Injectable()
export class AssignTourToTicketUseCase {
private readonly logger = new Logger(AssignTourToTicketUseCase.name)
constructor(
@Inject(TICKET_REPOSITORY) private ticketRepo: TicketRepository,
@Inject(TOUR_PROVIDER) private tourAdapter: TourProvider,
@Inject(PATIENT_PROVIDER) private readonly patientProvider: PatientProvider
) {}
async execute(ticketIdRaw: string, tourIdRaw: string): Promise<void> {
this.logger.log(`assigning Tour ${tourIdRaw} to Ticket ${ticketIdRaw}`)
const ticketId = new TicketId(ticketIdRaw)
const ticket = await this.ticketRepo.findById(ticketId);
if (!ticket) {
throw new NotFoundException(`Ticket with ID ${ticketIdRaw} not found`)
}
const tour = await this.tourAdapter.findTourById(tourIdRaw)
if(!tour) {
throw new NotFoundException(`Tour with ID ${tourIdRaw} not found`)
}
if(tour.patientId !== ticket.patientId.value && tour.patientId) {
const patient = await this.patientProvider.findPatientById(tour.patientId);
if(patient) {
ticket.updatePatientData(patient)
}
}
// if(ticket.patientId.value !== tour.patientId) {
// this.logger.warn('patient mismatch', {ticketPatientId: ticket.patientId.value, tourPatientId: tour.patientId})
// throw new BadRequestException(`Patient mismatch between Ticket and Tour`)
// }
try {
ticket.assignTour(tour)
} catch (error) {
throw new BadRequestException(error.message)
}
await this.ticketRepo.save(ticket)
}
}
@@ -0,0 +1,20 @@
import { Inject, Injectable, Logger } from '@nestjs/common';
import {
TICKET_REPOSITORY,
TicketRepository,
} from '../domain/ticket.repository.interface';
@Injectable()
export class CloseInvoicedTickets {
private readonly logger = new Logger(CloseInvoicedTickets.name);
constructor(
@Inject(TICKET_REPOSITORY) private readonly ticketRepo: TicketRepository,
) {}
async execute(): Promise<void> {
this.logger.log(`archiving invoiced tickets`);
await this.ticketRepo.closeSettledTickets();
}
}
@@ -0,0 +1,70 @@
import {
BadRequestException,
Inject,
Injectable,
Logger,
NotFoundException,
} from '@nestjs/common';
import { v4 as uuidv4 } from 'uuid';
import {
PATIENT_PROVIDER,
PatientProvider,
} from '../domain/ports/patient-provider.interface';
import {
TOUR_PROVIDER,
TourProvider,
} from '../domain/ports/tour-provider.interface';
import { Ticket, TicketId } from '../domain/ticket.entity';
import {
TICKET_REPOSITORY,
TicketRepository,
} from '../domain/ticket.repository.interface';
export interface CreateTicketCommand {
patientId: string;
initialTourId?: string;
}
@Injectable()
export class CreateTicketUseCase {
private readonly logger = new Logger(CreateTicketUseCase.name);
constructor(
@Inject(TICKET_REPOSITORY) private readonly ticketRepo: TicketRepository,
@Inject(TOUR_PROVIDER) private readonly tourProvider: TourProvider,
@Inject(PATIENT_PROVIDER) private readonly patientProvider: PatientProvider,
) {}
async execute(command: CreateTicketCommand): Promise<string> {
const newTicketId = new TicketId(uuidv4());
const ticket = Ticket.create(newTicketId, command.patientId);
if (command.patientId) {
const patient = await this.patientProvider.findPatientById(
command.patientId,
);
if (patient) {
ticket.updatePatientData(patient);
}
}
if (command.initialTourId) {
const tour = await this.tourProvider.findTourById(command.initialTourId);
if (!tour)
throw new NotFoundException(
`initial tour ${command.initialTourId} not found`,
);
if (tour.patientId !== command.patientId) {
throw new BadRequestException(`Patient mismatch`);
}
ticket.assignTour(tour);
}
await this.ticketRepo.save(ticket);
return newTicketId.value;
}
}
@@ -0,0 +1,24 @@
import { Inject, Injectable, Logger, NotFoundException, NotImplementedException } from "@nestjs/common";
import { TicketId } from "../domain/ticket.entity";
import { TICKET_REPOSITORY, TicketRepository } from "../domain/ticket.repository.interface";
@Injectable()
export class MarkAsApprovedUseCase {
private readonly logger = new Logger(MarkAsApprovedUseCase.name)
constructor(@Inject(TICKET_REPOSITORY) private ticketRepo: TicketRepository) {}
async execute(ticketIdRaw: string): Promise<void> {
this.logger.log(`approving Ticket ${ticketIdRaw}`)
const ticketId = new TicketId(ticketIdRaw)
const ticket = await this.ticketRepo.findById(ticketId);
if(!ticket) {
throw new NotFoundException(`Ticket ${ticketIdRaw} not found`)
}
ticket.grantApproval()
await this.ticketRepo.save(ticket)
}
}
@@ -0,0 +1,56 @@
import {
Inject,
Injectable,
Logger,
NotFoundException,
NotImplementedException,
} from '@nestjs/common';
import {
TICKET_REPOSITORY,
TicketRepository,
} from '../domain/ticket.repository.interface';
import { ApprovalRequirement } from '@avicenna/shared-dtos';
import { TicketId } from '../domain/ticket.entity';
@Injectable()
export class OverrideApprovalRequirementUseCase {
private readonly logger = new Logger(OverrideApprovalRequirementUseCase.name);
constructor(
@Inject(TICKET_REPOSITORY) private readonly ticketRepo: TicketRepository,
) {}
async execute(ticketIdRaw: string) {
this.logger.log(
`overriding approval requirement for Ticket ${ticketIdRaw}`,
);
const ticketId = new TicketId(ticketIdRaw);
const ticket = await this.ticketRepo.findById(ticketId);
if (!ticket) {
throw new NotFoundException(`Ticket ${ticketIdRaw} not found`);
}
const newRequirement = this.determineNewRequirement(
ticket.approval.requirement,
);
ticket.overrideApprovalRequirement(newRequirement);
console.log(ticket.approval.requirement, newRequirement)
await this.ticketRepo.save(ticket)
}
private determineNewRequirement(
requirement: ApprovalRequirement,
): ApprovalRequirement {
if (
requirement === ApprovalRequirement.UNKOWN ||
requirement === ApprovalRequirement.NOT_REQUIRED
) {
return ApprovalRequirement.REQUIRED;
}
return ApprovalRequirement.NOT_REQUIRED;
}
}
@@ -0,0 +1,55 @@
import { Inject, Injectable, Logger } from '@nestjs/common';
import { LegacyTourAdapter } from '../infrastructure/legacy-tour.adapter';
import { TicketMatchingService } from '../domain/services/ticket-matching.service';
import { AssignTourToTicketUseCase } from './assign-tour.usecase';
import { TourCandidate } from '../domain/interfaces/tour-candidate.interface';
import { CreateTicketUseCase } from './create-ticket.usecase';
import { TOUR_PROVIDER } from '../domain/ports/tour-provider.interface';
@Injectable()
export class ProcessUnticketedToursUseCase {
private readonly logger = new Logger(ProcessUnticketedToursUseCase.name);
constructor(
@Inject(TOUR_PROVIDER) private readonly tourAdapter: LegacyTourAdapter,
private readonly matcher: TicketMatchingService,
private createTicketUseCase: CreateTicketUseCase,
private assignUseCase: AssignTourToTicketUseCase,
) {}
async execute(): Promise<void> {
const orphanedTours = await this.tourAdapter.findUnticketedTours();
this.logger.log(`processing ${orphanedTours.length} orphaned tours...`);
for (const tour of orphanedTours) {
try {
await this.processSingleTour(tour);
} catch (error) {
this.logger.error(`error processing tour ${tour.id.value}:`, error);
this.logger.error(error)
}
}
this.logger.log(`count patientids`, {
id_count: orphanedTours.filter(({patientId}) => !!patientId).length,
tours: orphanedTours.length
})
}
private async processSingleTour(tour: TourCandidate): Promise<void> {
const match = await this.matcher.findMatchingTicket(tour);
if (match) {
await this.assignUseCase.execute(match.id.value, tour.id.value);
this.logger.log(
`Tour ${tour.id.value} assigned to existing Ticket ${match.id.value}`,
);
} else {
await this.createTicketUseCase.execute({
initialTourId: tour.id.value,
patientId: tour.patientId,
});
this.logger.log(`created new Ticket for Tour ${tour.id.value}`);
}
}
}
@@ -0,0 +1,30 @@
import { Inject, Injectable, Logger, NotFoundException } from '@nestjs/common';
import {
TICKET_REPOSITORY,
TicketRepository,
} from '../domain/ticket.repository.interface';
import { TicketId } from '../domain/ticket.entity';
@Injectable()
export class ReceivePhysicalTransportDocumentUseCase {
private readonly logger = new Logger(
ReceivePhysicalTransportDocumentUseCase.name,
);
constructor(
@Inject(TICKET_REPOSITORY) private readonly ticketRepo: TicketRepository,
) {}
async execute(ticketIdRaw: string): Promise<void> {
this.logger.log(
`receiving physical transport document for Ticket ${ticketIdRaw}`,
);
const ticket = await this.ticketRepo.findById(new TicketId(ticketIdRaw));
if (!ticket) throw new NotFoundException(`Ticket ${ticketIdRaw} not found`);
ticket.receivePhysicalDocument();
await this.ticketRepo.save(ticket);
}
}
@@ -0,0 +1,52 @@
import { Inject, Injectable, Logger, NotFoundException } from '@nestjs/common';
import { Kysely } from 'kysely';
import { KYSELY_CLIENT_PROVIDER_TOKEN } from 'src/core/database/database.module';
import { DB } from 'src/core/database/types';
import { Ticket } from '../domain/ticket.entity';
import {
TICKET_REPOSITORY,
TicketRepository,
} from '../domain/ticket.repository.interface';
@Injectable()
export class RefreshTicketDataUseCase {
private readonly logger = new Logger(RefreshTicketDataUseCase.name);
constructor(
@Inject(TICKET_REPOSITORY) private readonly repo: TicketRepository,
@Inject(KYSELY_CLIENT_PROVIDER_TOKEN) private readonly db: Kysely<DB>,
) {}
async execute() {
this.logger.log(`starting Ticket refresh cycle ...`);
for await (const batch of this.repo.streamAllActive(50)) {
await Promise.all(batch.map((ticket) => this.processTicket(ticket)));
}
}
private async processTicket(ticket: Ticket) {
const savedStage = await this.getCurrentStage(ticket.id.value);
if (savedStage !== ticket.currentStage) {
this.logger.log(
`Ticket ${ticket.id.value} stage changed from ${savedStage} to ${ticket.currentStage}`,
);
await this.repo.save(ticket);
}
}
private async getCurrentStage(ticketId: string): Promise<string> {
const currentStage = await this.db
.selectFrom('Ticket')
.select('Ticket.stage')
.where('Ticket.id', '=', ticketId)
.executeTakeFirst();
if (!currentStage?.stage) {
throw new NotFoundException(`Ticket ${ticketId} not found`);
}
return currentStage.stage;
}
}
@@ -0,0 +1,34 @@
import { Inject, Injectable, Logger, NotFoundException } from '@nestjs/common';
import { TicketId } from '../domain/ticket.entity';
import {
TICKET_REPOSITORY,
TicketRepository,
} from '../domain/ticket.repository.interface';
@Injectable()
export class RegisterExternalApprovalRequestUseCase {
private readonly logger = new Logger(
RegisterExternalApprovalRequestUseCase.name,
);
constructor(
@Inject(TICKET_REPOSITORY) private readonly ticketRepo: TicketRepository,
) {}
async execute(ticketIdRaw: string) {
this.logger.log(
`Registering external approval request for Ticket ${ticketIdRaw}`,
);
const ticketId = new TicketId(ticketIdRaw);
const ticket = await this.ticketRepo.findById(ticketId);
if (!ticket) {
throw new NotFoundException(`Ticket ${ticketIdRaw} not found`);
}
ticket.registerExternalRequest();
await this.ticketRepo.save(ticket);
}
}
@@ -0,0 +1,21 @@
import { Injectable, Logger, Inject, NotFoundException } from "@nestjs/common";
import { TicketId } from "../domain/ticket.entity";
import { TICKET_REPOSITORY, TicketRepository } from "../domain/ticket.repository.interface";
@Injectable()
export class RegisterSentEmailUseCase {
private readonly logger = new Logger(RegisterSentEmailUseCase.name);
constructor(@Inject(TICKET_REPOSITORY) private readonly ticketRepo: TicketRepository) {}
async execute(ticketIdRaw: string, type: 'PRE' | 'FULL', messageId: string): Promise<void> {
this.logger.log(`Registering Message-ID for Ticket ${ticketIdRaw} (${type}): ${messageId}`);
const ticket = await this.ticketRepo.findById(new TicketId(ticketIdRaw));
if (!ticket) throw new NotFoundException(`Ticket ${ticketIdRaw} not found`);
ticket.registerSentEmailInfo(type, messageId);
await this.ticketRepo.save(ticket);
}
}
@@ -0,0 +1,29 @@
import {
Inject,
Injectable,
Logger,
NotFoundException
} from '@nestjs/common';
import { TicketId } from '../domain/ticket.entity';
import { TICKET_REPOSITORY, TicketRepository } from '../domain/ticket.repository.interface';
@Injectable()
export class RequestTicketApprovalUseCase {
private readonly logger = new Logger(RequestTicketApprovalUseCase.name);
constructor(@Inject(TICKET_REPOSITORY) private readonly ticketRepo: TicketRepository) {}
async execute(ticketIdRaw: string, recipient: string) {
this.logger.log(`requesting ticket approval`);
const ticketId = new TicketId(ticketIdRaw);
const ticket = await this.ticketRepo.findById(ticketId);
if (!ticket) {
throw new NotFoundException(`Ticket ${ticketIdRaw} not found`);
}
ticket.requestApproval(recipient);
await this.ticketRepo.save(ticket);
}
}
@@ -0,0 +1,27 @@
import { Inject, Injectable, Logger, NotFoundException } from "@nestjs/common";
import { TicketId } from "../domain/ticket.entity";
import { TICKET_REPOSITORY, TicketRepository } from "../domain/ticket.repository.interface";
@Injectable()
export class SendApprovalReminderUseCase {
private readonly logger = new Logger(SendApprovalReminderUseCase.name);
constructor(
@Inject(TICKET_REPOSITORY) private readonly ticketRepo: TicketRepository,
) {}
async execute(ticketIdRaw: string) {
this.logger.log(`sending approval reminder for ticket ${ticketIdRaw}`);
const ticketId = new TicketId(ticketIdRaw);
const ticket = await this.ticketRepo.findById(ticketId);
if (!ticket) {
throw new NotFoundException(`Ticket ${ticketIdRaw} not found`);
}
ticket.sendApprovalReminder();
await this.ticketRepo.save(ticket);
}
}
@@ -0,0 +1,39 @@
import { Inject, Injectable, Logger, NotFoundException } from '@nestjs/common';
import { TicketId } from '../domain/ticket.entity';
import {
TICKET_REPOSITORY,
TicketRepository,
} from '../domain/ticket.repository.interface';
import { TransportDocument } from '../domain/value-objects/transport-document.vo';
@Injectable()
export class UploadTransportDocumentUseCase {
private readonly logger = new Logger(UploadTransportDocumentUseCase.name);
constructor(
@Inject(TICKET_REPOSITORY) private readonly ticketRepo: TicketRepository,
) {}
async execute(ticketIdRaw: string, file: Express.Multer.File) {
this.logger.log(
`adding uploaded TransportDocument to Ticket ${ticketIdRaw}`,
);
const ticketId = new TicketId(ticketIdRaw);
const ticket = await this.ticketRepo.findById(ticketId);
if (!ticket) {
throw new NotFoundException(`Ticket ${ticketIdRaw} not found`);
}
const doc = new TransportDocument(
file.path,
file.originalname,
file.mimetype,
file.size,
);
ticket.registerDigitalDocumentUpload(doc);
await this.ticketRepo.save(ticket);
}
}
@@ -0,0 +1,15 @@
export abstract class AggregateRoot {
private _domainEvents: any[] = [];
protected addDomainEvent(event: any): void {
this._domainEvents.push(event);
}
public get domainEvents(): any[] {
return [...this._domainEvents];
}
public clearDomainEvents(): void {
this._domainEvents = [];
}
}
@@ -0,0 +1,16 @@
export class PatientId {
constructor(public readonly value: string) {}
}
export interface PatientCandidate {
id: PatientId;
firstName: string;
lastName: string;
birthday: Date | null;
healthInsurance: string | null;
insuranceNo: string | null;
careDegree: string | null;
street: string | null,
city: string | null,
zip: string | null,
}
@@ -0,0 +1,39 @@
import { StringifyOptions } from 'querystring';
import { TicketId } from '../ticket.entity';
export class TourId {
constructor(public readonly value: string) {}
}
export interface TourCandidate {
id: TourId;
operationId: string;
category: string;
ordinanceType: string;
transportType: string;
patientId: string;
patientName: string;
patientSurname: string;
patientStreet: string;
patientCity: string;
patientZip: string;
healthInsurance: string;
insuranceNo: string;
hasInfection: boolean;
infectionName: string;
seriesEndDate: Date | null;
direction: string;
occupiedKm: number;
targetStreet: string;
targetCity: string;
targetZip: string;
startStreet: string;
startCity: string;
startZip: string;
startDate: Date;
rangeEndDate: Date;
ticketId: TicketId;
}
@@ -0,0 +1,7 @@
import { PatientCandidate } from '../interfaces/patient-candidate.interface';
export const PATIENT_PROVIDER = 'PatientProvider';
export interface PatientProvider {
findPatientById(id: string): Promise<PatientCandidate | null>;
}
@@ -0,0 +1,9 @@
import { TourCandidate } from "../interfaces/tour-candidate.interface";
export const TOUR_PROVIDER = 'TourProvider';
export interface TourProvider {
findUnticketedTours(): Promise<TourCandidate[]>;
findTourById(id: string): Promise<TourCandidate | null>
}
@@ -0,0 +1,20 @@
import { Inject, Injectable } from "@nestjs/common";
import { TICKET_REPOSITORY, TicketRepository } from "../ticket.repository.interface";
import { TourCandidate } from "../interfaces/tour-candidate.interface";
import { Ticket } from "../ticket.entity";
@Injectable()
export class TicketMatchingService {
constructor(@Inject(TICKET_REPOSITORY) private readonly ticketRepo: TicketRepository) {}
async findMatchingTicket(tour: TourCandidate): Promise<Ticket | null> {
const returnTripTicket = await this.ticketRepo.findTicketForTripInOppositeDirection(tour);
if(returnTripTicket) return returnTripTicket;
const seriesTicket = await this.ticketRepo.findTicketForSeries(tour);
if (seriesTicket) return seriesTicket;
return null;
}
}
@@ -0,0 +1,242 @@
import 'reflect-metadata';
import {
ApprovalRequestStatus,
ApprovalRequirement,
TicketStage,
} from '@avicenna/shared-dtos';
import { v4 as uuidv4 } from 'uuid';
import { Ticket, TicketId } from '../ticket.entity';
import { TicketPatientData } from '../value-objects/ticket-patient-data.vo';
import { TicketTourData } from '../value-objects/ticket-tour-data.vo';
import { TransportDocument } from '../value-objects/transport-document.vo';
import { PatientId } from '../interfaces/patient-candidate.interface';
import { TourId } from '../interfaces/tour-candidate.interface';
export class TicketBuilder {
private props: any;
constructor() {
// STANDARD: Ein valides Ticket, das "READY" sein sollte.
// Wir setzen hier Werte, die validation checks bestehen.
this.props = {
id: uuidv4(),
stage: TicketStage.DRAFT,
patient: this.createValidPatientData(),
approval: {
req: ApprovalRequirement.NOT_REQUIRED,
status: ApprovalRequestStatus.NOT_STARTED,
requestedAt: null,
lastInteractionAt: null,
reminderCount: 0,
recipient: null,
preInquiryMessageId: null,
fullRequestMessageId: null,
isOverriden: false,
},
hasPhysicalTD: false,
hasDigitalTD: false,
tours: [this.createValidTourData()], // Mindestens eine Tour für Validität
transportDocument: null,
createdAt: new Date(),
updatedAt: new Date(),
};
}
// --- MODIFIERS ---
public withId(id: string): this {
this.props.id = id;
return this;
}
public withStage(stage: TicketStage): this {
this.props.stage = stage;
return this;
}
// --- PATIENT ---
public withPatient(data: Partial<TicketPatientData>): this {
// Merge mit validen Daten, um nicht alles angeben zu müssen
const base = this.createValidPatientData();
// Achtung: Da TicketPatientData keine Setter hat, bauen wir ein Objekt,
// das der Struktur entspricht oder nutzen Reflection/Mocking.
// Sauberer Weg: Ein neues TicketPatientData instanziieren.
this.props.patient = new TicketPatientData(
// @ts-ignore - Zugriff auf private props simulation
data['sourceId'] || base['sourceId'],
data['sourceType'] || base['sourceType'],
data.firstName !== undefined ? data.firstName : base.firstName,
data.lastName !== undefined ? data.lastName : base.lastName,
data.birthday || base.birthday,
data.healthInsurance !== undefined
? data.healthInsurance
: base.healthInsurance,
data.insuranceNo || base.insuranceNo,
data.street || base.street,
data.city || base.city,
data.zip || base.zip,
);
return this;
}
public withMissingPatientData(): this {
// Erzeugt invalides PatientData VO
this.props.patient = new TicketPatientData(
new PatientId('p-1'),
'PATIENT',
null, // Invalid
null, // Invalid
new Date(),
null, // Invalid
null,
'Musterstr.',
'Musterstadt',
'12345',
);
return this;
}
public withoutPatient(): this {
this.props.patient = null;
return this;
}
// --- TOURS ---
public withTours(tours: TicketTourData[]): this {
this.props.tours = tours;
return this;
}
public withInvalidTour(): this {
// Erzeugt eine Tour mit fehlenden Pflichtfeldern
const invalidTour = new TicketTourData(
new TourId('t-invalid'),
'op-1',
'', // Category missing
'', // Ordinance missing
'', // Transport missing
'p-1',
'Max',
'Mustermann',
'Str',
'City',
'12345',
'AOK',
'123',
'Hinfahrt',
10,
false,
null,
null,
'',
'',
'', // Target missing
'',
'',
'', // Start missing
new Date(),
);
this.props.tours = [invalidTour];
return this;
}
public withoutTours(): this {
this.props.tours = [];
return this;
}
// --- APPROVAL ---
public withApprovalRequirement(req: ApprovalRequirement): this {
this.props.approval.req = req;
return this;
}
public withApprovalStatus(status: ApprovalRequestStatus): this {
this.props.approval.status = status;
return this;
}
public withApprovalOverride(isOverriden: boolean): this {
this.props.approval.isOverriden = isOverriden;
return this;
}
// --- DOCUMENTS ---
public withDigitalTransportDocument(): this {
this.props.hasDigitalTD = true;
this.props.transportDocument = new TransportDocument(
'path/to/file',
'file.pdf',
'application/pdf',
1024,
);
return this;
}
public withPhysicalTransportDocument(): this {
this.props.hasPhysicalTD = true;
return this;
}
// --- BUILD ---
public build(): Ticket {
// Wir nutzen fromStorage, um den internen State exakt so zu setzen, wie wir wollen.
// WICHTIG: fromStorage ruft am Ende `reassessState()` auf.
// Das bedeutet, der Stage, den wir oben im Konstruktor setzen, wird sofort
// durch die echte Logik überschrieben. Das ist genau das, was wir testen wollen!
return Ticket.fromStorage(this.props);
}
// --- HELPERS ---
private createValidPatientData(): TicketPatientData {
return new TicketPatientData(
new PatientId('p-1'),
'PATIENT',
'Max',
'Mustermann',
new Date('1990-01-01'),
'AOK',
'V123456',
'Musterstraße 1',
'Musterstadt',
'12345',
);
}
private createValidTourData(): TicketTourData {
return new TicketTourData(
new TourId('t-1'),
'OP-123',
'Dialyse', // Category
'Verordnung', // Ordinance
'KTW', // TransportType
'p-1',
'Max',
'Mustermann',
'Str 1',
'City',
'12345',
'AOK',
'123',
'Hinfahrt',
15, // km
false,
null,
null,
'Zielstr 1',
'Zielstadt',
'54321', // Target
'Startstr 1',
'Startstadt',
'12345', // Start
new Date(),
);
}
}
@@ -0,0 +1,134 @@
import 'reflect-metadata';
import {
TicketStage,
ApprovalRequirement,
ApprovalRequestStatus,
} from '@avicenna/shared-dtos';
import { TicketBuilder } from './testing/ticket.builder';
describe('Ticket Entity - Validation & Lifecycle', () => {
describe('Stage: DATA_INCOMPLETE', () => {
it('should be DATA_INCOMPLETE if patient data is missing', () => {
const ticket = new TicketBuilder()
.withMissingPatientData() // Vorname, Nachname, Kasse fehlen
.build();
expect(ticket.currentStage).toBe(TicketStage.DATA_INCOMPLETE);
expect(ticket.validationErrors.length).toBeGreaterThan(0);
expect(ticket.validationErrors).toEqual(
expect.arrayContaining([
expect.objectContaining({ field: 'firstName' }),
expect.objectContaining({ field: 'lastName' }),
]),
);
});
it('should be DATA_INCOMPLETE if tour data is invalid (e.g. missing addresses)', () => {
const ticket = new TicketBuilder().withInvalidTour().build();
expect(ticket.currentStage).toBe(TicketStage.DATA_INCOMPLETE);
expect(ticket.validationErrors).toEqual(
expect.arrayContaining([
expect.objectContaining({ field: 'startAddress' }),
expect.objectContaining({ field: 'category' }),
]),
);
});
});
describe('Stage: DRAFT', () => {
it('should be DRAFT if no tours are assigned', () => {
const ticket = new TicketBuilder().withoutTours().build();
expect(ticket.currentStage).toBe(TicketStage.DRAFT);
});
});
describe('Stage: ASSESSMENT_PENDING (Approval Logic)', () => {
it('should be ASSESSMENT_PENDING if approval is REQUIRED but not requested yet', () => {
const ticket = new TicketBuilder()
.withApprovalRequirement(ApprovalRequirement.REQUIRED)
.withApprovalStatus(ApprovalRequestStatus.NOT_STARTED)
.build();
expect(ticket.validationErrors).toHaveLength(0); // Daten sind valide
expect(ticket.currentStage).toBe(TicketStage.ASSESSMENT_PENDING);
});
it('should transition to APPROVAL_IN_PROGRESS after full request sent', () => {
// Wir simulieren hier den State NACHDEM angefragt wurde
const ticket = new TicketBuilder()
.withApprovalRequirement(ApprovalRequirement.REQUIRED)
.withApprovalStatus(ApprovalRequestStatus.FULL_REQUEST_SENT)
.build();
expect(ticket.currentStage).toBe(TicketStage.APPROVAL_IN_PROGRESS);
});
it('should NOT be pending if approval is NOT_REQUIRED', () => {
const ticket = new TicketBuilder()
.withApprovalRequirement(ApprovalRequirement.NOT_REQUIRED)
.build();
// Sollte weiter zum nächsten Check (Documents) gehen
// Da im Builder default keine Docs da sind -> DOCUMENTS_PENDING
expect(ticket.currentStage).toBe(TicketStage.DOCUMENTS_PENDING);
});
it('should NOT be pending if approval is GRANTED', () => {
const ticket = new TicketBuilder()
.withApprovalRequirement(ApprovalRequirement.REQUIRED)
.withApprovalStatus(ApprovalRequestStatus.GRANTED)
.build();
// Genehmigt -> Weiter zu Documents
expect(ticket.currentStage).toBe(TicketStage.DOCUMENTS_PENDING);
});
});
describe('Stage: DOCUMENTS_PENDING vs READY', () => {
it('should be DOCUMENTS_PENDING if valid, approval clear, but no doc uploaded', () => {
const ticket = new TicketBuilder()
.withApprovalRequirement(ApprovalRequirement.NOT_REQUIRED)
// Kein Dokument gesetzt
.build();
expect(ticket.currentStage).toBe(TicketStage.DOCUMENTS_PENDING);
});
it('should be READY if digital document is present', () => {
const ticket = new TicketBuilder()
.withApprovalRequirement(ApprovalRequirement.NOT_REQUIRED)
.withDigitalTransportDocument()
.build();
expect(ticket.currentStage).toBe(TicketStage.READY);
});
it('should be READY if physical document is present', () => {
const ticket = new TicketBuilder()
.withApprovalRequirement(ApprovalRequirement.NOT_REQUIRED)
.withPhysicalTransportDocument()
.build();
expect(ticket.currentStage).toBe(TicketStage.READY);
});
});
describe('Approval Override Logic', () => {
it('should respect manual override to NOT_REQUIRED even if logic says REQUIRED', () => {
// Zuerst ein Ticket, das theoretisch Genehmigung bräuchte (wird durch reassessState ermittelt)
// Aber: Wir setzen isOverridden = true
const ticket = new TicketBuilder()
.withApprovalRequirement(ApprovalRequirement.NOT_REQUIRED) // Manuell gesetzt
.withApprovalOverride(true)
.build();
// Da Override true ist, darf determineApprovalRequirement() im Entity NICHT laufen
// und den Wert überschreiben.
expect(ticket.approval.requirement).toBe(
ApprovalRequirement.NOT_REQUIRED,
);
});
});
});
@@ -0,0 +1,640 @@
import {
ApprovalRequestStatus,
ApprovalRequirement,
TicketStage,
ValidationError,
} from '@avicenna/shared-dtos';
import { differenceInDays } from 'date-fns';
import { AggregateRoot } from './aggregate-root';
import {
PatientCandidate,
PatientId,
} from './interfaces/patient-candidate.interface';
import { TourCandidate } from './interfaces/tour-candidate.interface';
import {
TicketApprovalGrantedEvent,
TicketApprovalReminderSentEvent,
TicketApprovalRequestedEvent,
TicketApprovalRequirementOverriddenEvent,
TicketCreatedEvent,
TicketPhysicalTransportDocumentReceivedEvent,
TicketTransportDocumentUploadedEvent,
TourAssignedToTicketEvent,
} from './ticket.events';
import { TicketPatientData } from './value-objects/ticket-patient-data.vo';
import { TicketTourData } from './value-objects/ticket-tour-data.vo';
import { TransportDocument } from './value-objects/transport-document.vo';
class DomainError {
constructor(public readonly message: string) {}
}
export class ApprovalWorkflow {
constructor(
public readonly requirement: ApprovalRequirement,
public readonly status: ApprovalRequestStatus,
public readonly requestedAt: Date | null,
public readonly lastInteractionAt: Date | null,
public readonly reminderCount: number,
public readonly recipientEmail: string | null,
public readonly preInquiryMessageId: string | null = null,
public readonly fullRequestMessageId: string | null = null,
public readonly isOverridden: boolean = false,
) {}
static initial(): ApprovalWorkflow {
return new ApprovalWorkflow(
ApprovalRequirement.UNKOWN,
ApprovalRequestStatus.NOT_STARTED,
null,
null,
0,
null,
null,
null,
false,
);
}
isOverdue(): boolean {
if (!this.lastInteractionAt) return false;
return differenceInDays(new Date(), this.lastInteractionAt) >= 3;
}
hasRemindersLeft(): boolean {
return this.reminderCount < 3;
}
setRequirement(req: ApprovalRequirement): ApprovalWorkflow {
if (this.status === ApprovalRequestStatus.GRANTED) return this;
return new ApprovalWorkflow(
req,
this.status,
this.requestedAt,
new Date(),
this.reminderCount,
this.recipientEmail,
this.preInquiryMessageId,
this.fullRequestMessageId,
this.isOverridden,
);
}
overrideRequirement(newReq: ApprovalRequirement): ApprovalWorkflow {
return new ApprovalWorkflow(
newReq,
this.status,
this.requestedAt,
this.lastInteractionAt,
this.reminderCount,
this.recipientEmail,
this.preInquiryMessageId,
this.fullRequestMessageId,
true,
);
}
markAsRequested(
hasDigitalDoc: boolean,
recipient: string | undefined,
isExternal = false,
): ApprovalWorkflow {
if (this.status === ApprovalRequestStatus.FULL_REQUEST_SENT) return this;
if (this.status === ApprovalRequestStatus.GRANTED) return this;
let newStatus: ApprovalRequestStatus;
if (isExternal) {
newStatus = ApprovalRequestStatus.EXTERNAL_REQUEST;
} else if (hasDigitalDoc) {
newStatus = ApprovalRequestStatus.FULL_REQUEST_SENT;
} else {
newStatus = ApprovalRequestStatus.PRE_INQUIRY_SENT;
}
return new ApprovalWorkflow(
this.requirement,
newStatus,
this.requestedAt ?? new Date(),
new Date(),
this.reminderCount,
recipient,
this.preInquiryMessageId,
this.fullRequestMessageId,
this.isOverridden,
);
}
grantApproval(): ApprovalWorkflow {
return new ApprovalWorkflow(
this.requirement,
ApprovalRequestStatus.GRANTED,
this.requestedAt,
new Date(),
this.reminderCount,
this.recipientEmail,
this.preInquiryMessageId,
this.fullRequestMessageId,
this.isOverridden,
);
}
recordReminder(): ApprovalWorkflow {
return new ApprovalWorkflow(
this.requirement,
this.status,
this.requestedAt,
new Date(),
this.reminderCount + 1,
this.recipientEmail,
this.preInquiryMessageId,
this.fullRequestMessageId,
this.isOverridden,
);
}
registerSentEmail(type: 'PRE' | 'FULL', messageId: string): ApprovalWorkflow {
return new ApprovalWorkflow(
this.requirement,
this.status,
this.requestedAt,
this.lastInteractionAt,
this.reminderCount,
this.recipientEmail,
type === 'PRE' ? messageId : this.preInquiryMessageId,
type === 'FULL' ? messageId : this.fullRequestMessageId,
this.isOverridden,
);
}
}
export class TicketId {
constructor(public readonly value: string) {}
}
export class Ticket extends AggregateRoot {
private _stage: TicketStage;
private _tours: Map<string, TicketTourData> = new Map();
private _patientId: PatientId | null;
private _patientData: TicketPatientData | null = null;
private _validationErrors: ValidationError[] = [];
private _approval: ApprovalWorkflow;
private _hasDigitalTransportDocument: boolean = false;
private _hasPhysicalTransportDocument: boolean = false;
private _createdAt: Date;
private _updatedAt: Date;
private _transportDocument: TransportDocument | null = null;
get patientId(): PatientId | null {
return this._patientId;
}
get transportDocument(): TransportDocument | null {
return this._transportDocument;
}
get hasDigitalTransportDocument(): boolean {
return this._hasDigitalTransportDocument;
}
get hasPhysicalTransportDocument(): boolean {
return this._hasPhysicalTransportDocument;
}
get validationErrors(): ValidationError[] {
return [...this._validationErrors];
}
get assignedTourIds(): string[] {
return [...this._tours.keys()];
}
get currentStage(): TicketStage {
return this._stage;
}
get createdAt(): Date {
return this._createdAt;
}
get updatedAt(): Date {
return this._updatedAt;
}
get approval(): ApprovalWorkflow {
return new ApprovalWorkflow(
this._approval.requirement,
this._approval.status,
this._approval.requestedAt,
this._approval.lastInteractionAt,
this._approval.reminderCount,
this._approval.recipientEmail,
this._approval.preInquiryMessageId,
this._approval.fullRequestMessageId,
this._approval.isOverridden,
);
}
get patient(): TicketPatientData {
return this._patientData;
}
get tours(): TicketTourData[] {
return Array.from(this._tours.values()).sort(
(a, b) => b.startDate.getTime() - a.startDate.getTime(),
);
}
private constructor(
public readonly id: TicketId,
patientId: PatientId,
) {
super();
this._stage = TicketStage.DRAFT;
this._approval = ApprovalWorkflow.initial();
this._patientId = patientId;
this._createdAt = new Date();
this._updatedAt = new Date();
}
public updatePatientData(masterData?: PatientCandidate): void {
if (masterData) {
// if (masterData.id.value !== this.patientId.value)
// throw new DomainError('Patient ID mismatch');
this._patientData = TicketPatientData.fromMasterData(masterData);
this._patientId = masterData.id;
} else if (this._tours.size > 0) {
const firstTour = this._tours.values().next().value;
this._patientData = TicketPatientData.fromTourSnapshot(firstTour);
}
this.reassessState();
}
public assignTour(tour: TourCandidate): void {
if (this._tours.has(tour.id.value)) {
return;
}
this.ensureTicketIsOpen();
// this.ensurePatientMatches(tour.patientId);
const tourData = TicketTourData.fromCandidate(tour);
if (!this._tours.has(tourData.id.value)) {
this._tours.set(tourData.id.value, tourData);
this.addDomainEvent(
new TourAssignedToTicketEvent(this.id.value, tour.id.value),
);
} else {
this._tours.set(tourData.id.value, tourData);
}
this.reassessState();
}
public registerDigitalDocumentUpload(doc: TransportDocument): void {
this._transportDocument = doc;
if (!this._hasDigitalTransportDocument) {
this._hasDigitalTransportDocument = true;
this.addDomainEvent(
new TicketTransportDocumentUploadedEvent(this.id.value),
);
this.reassessState();
}
}
public registerSentEmailInfo(type: 'PRE' | 'FULL', messageId: string): void {
this._approval = this._approval.registerSentEmail(type, messageId);
this.reassessState();
}
public receivePhysicalDocument(): void {
const oldState = this._hasPhysicalTransportDocument;
if (!this._hasPhysicalTransportDocument) {
this._hasPhysicalTransportDocument = true;
if (oldState != true) {
this.addDomainEvent(
new TicketPhysicalTransportDocumentReceivedEvent(this.id.value),
);
}
this.reassessState();
}
}
public overrideApprovalRequirement(newReq: ApprovalRequirement): void {
this.ensureTicketIsOpen();
this._approval = this._approval.overrideRequirement(newReq);
this.addDomainEvent(
new TicketApprovalRequirementOverriddenEvent(this.id.value, newReq),
);
this.reassessState();
}
public requestApproval(recipient: string): void {
if (this._approval.requirement !== ApprovalRequirement.REQUIRED) {
throw new DomainError(
`Genehmigung wird nicht benötigt, Anfrage abgebrochen`,
);
}
const prevStatus = this._approval.status;
this._approval = this._approval.markAsRequested(
this._hasDigitalTransportDocument,
recipient,
);
if (this._approval.status !== prevStatus) {
this.addDomainEvent(
new TicketApprovalRequestedEvent(
this.id.value,
this._approval.status,
recipient,
),
);
}
this.reassessState();
}
public registerExternalRequest(): void {
if (this._approval.requirement !== ApprovalRequirement.REQUIRED) {
throw new DomainError(
`Genehmigung wird nicht benötigt, Anfrage abgebrochen`,
);
}
const prevStatus = this._approval.status;
this._approval = this._approval.markAsRequested(
this._hasDigitalTransportDocument,
undefined,
true,
);
if (this._approval.status !== prevStatus) {
this.addDomainEvent(
new TicketApprovalRequestedEvent(
this.id.value,
this._approval.status,
this._approval.recipientEmail,
),
);
}
this.reassessState();
}
public sendApprovalReminder(): void {
this.ensureTicketIsOpen();
if (
this._approval.status !== ApprovalRequestStatus.PRE_INQUIRY_SENT &&
this._approval.status !== ApprovalRequestStatus.FULL_REQUEST_SENT
) {
throw new DomainError(
'cannot send reminder: Approval is not in a requested state.',
);
}
if (!this._approval.hasRemindersLeft()) {
throw new DomainError(
'cannot send reminder: Max reminder count reached.',
);
}
this._approval = this._approval.recordReminder();
this.addDomainEvent(
new TicketApprovalReminderSentEvent(
this.id.value,
this._approval.reminderCount,
this._approval.recipientEmail,
),
);
this.reassessState();
}
public grantApproval(): void {
this.ensureTicketIsOpen();
if (this._approval.status === ApprovalRequestStatus.GRANTED) return;
this._approval = this._approval.grantApproval();
this.addDomainEvent(new TicketApprovalGrantedEvent(this.id.value));
this.reassessState();
}
public toPrimitives() {
return {
id: this.id.value,
patientId: this._patientId.value,
stage: this._stage,
approvalRequirement: this._approval.requirement,
approvalStatus: this._approval.status,
approvalRequestedAt: this._approval.requestedAt,
approvalLastInteractionAt: this._approval.lastInteractionAt,
approvalReminderCount: this._approval.reminderCount,
approvalRecipient: this._approval.recipientEmail,
approvalPreInquiryMessageId: this._approval.preInquiryMessageId,
approvalFullRequestMessageId: this._approval.fullRequestMessageId,
approvalIsOverriden: this._approval.isOverridden,
hasDigitalTransportDocument: this._hasDigitalTransportDocument,
hasPhysicalTransportDocument: this._hasPhysicalTransportDocument,
transportDocument: this._transportDocument
? {
...this.transportDocument,
}
: null,
assignedTourIds: [...this._tours.keys()],
createdAt: this._createdAt,
updateAt: this._updatedAt,
};
}
static create(id: TicketId, patientId: string): Ticket {
const ticket = new Ticket(id, new PatientId(patientId));
ticket.addDomainEvent(new TicketCreatedEvent(id.value, patientId));
return ticket;
}
static fromStorage(props: {
id: string;
patient: TicketPatientData | null;
stage: string;
approval: {
req: string;
status: string;
requestedAt: Date | null;
lastInteractionAt: Date | null;
reminderCount: number;
recipient: string | null;
preInquiryMessageId: string | null;
fullRequestMessageId: string | null;
isOverriden: boolean;
};
hasPhysicalTD: boolean;
hasDigitalTD: boolean;
tours: TicketTourData[];
transportDocument: TransportDocument | null;
createdAt: Date;
updatedAt: Date;
// validationErrors: ValidationError[] // TODO: In DB persistieren, damit Frontend guenstig laden kann
}): Ticket {
const ticket = new Ticket(
new TicketId(props.id),
new PatientId(props.patient.patientId),
);
ticket._stage = props.stage as TicketStage;
ticket._approval = new ApprovalWorkflow(
props.approval.req as ApprovalRequirement,
props.approval.status as ApprovalRequestStatus,
props.approval.requestedAt,
props.approval.lastInteractionAt,
props.approval.reminderCount,
props.approval.recipient,
props.approval.preInquiryMessageId,
props.approval.fullRequestMessageId,
props.approval.isOverriden,
);
ticket._hasDigitalTransportDocument = props.hasDigitalTD;
ticket._hasPhysicalTransportDocument = props.hasPhysicalTD;
ticket._transportDocument = props.transportDocument;
ticket._patientData = props.patient;
for (const tourData of props.tours) {
ticket._tours.set(tourData.id.value, tourData);
}
ticket._createdAt = props.createdAt;
ticket._updatedAt = props.updatedAt;
ticket.reassessState();
return ticket;
}
private reassessState(): void {
this._updatedAt = new Date();
const errors: ValidationError[] = [];
// Ticket Validierung
for (const tour of this._tours.values()) {
errors.push(...tour.validate());
}
if (this._patientData) {
errors.push(...this._patientData.validate());
} else {
// if (this._tours.size > 0) {
// errors.push({
// field: 'patient',
// message: 'Keine Patientendaten verfügbar.',
// source,
// });
// }
}
this._validationErrors = errors;
if (this._tours.size === 0) {
this._stage = TicketStage.DRAFT;
return;
}
if (this._validationErrors.length > 0) {
this._stage = TicketStage.DATA_INCOMPLETE;
return;
}
// Genehmigungsverfahren
if (!this._approval.isOverridden) {
this.determineApprovalRequirement();
}
if (
this._approval.requirement === ApprovalRequirement.REQUIRED &&
this._approval.status !== ApprovalRequestStatus.GRANTED
) {
if (
this._approval.status === ApprovalRequestStatus.FULL_REQUEST_SENT ||
this._approval.status === ApprovalRequestStatus.EXTERNAL_REQUEST
) {
this._stage = TicketStage.APPROVAL_IN_PROGRESS;
} else {
this._stage = TicketStage.ASSESSMENT_PENDING;
}
return;
}
// Dokumente fehlen
if (
!this._hasPhysicalTransportDocument &&
!this._hasDigitalTransportDocument
) {
if (this._approval.requirement === ApprovalRequirement.REQUIRED) {
this._stage = TicketStage.APPROVAL_IN_PROGRESS;
}
this._stage = TicketStage.DOCUMENTS_PENDING;
return;
}
// Alles ready
this._stage = TicketStage.READY;
}
private ensureTicketIsOpen(): void {
if (this._stage === TicketStage.ARCHIVED) {
throw new DomainError('Cannot modify archived ticket.');
}
}
private ensurePatientMatches(tourPatientId: string): void {
if (this.patientId && this.patientId.value !== tourPatientId) {
throw new DomainError('Patient mismatch.');
}
}
private determineApprovalRequirement(): void {
const tour = this._tours.values().next().value;
let req = ApprovalRequirement.UNKOWN;
if (tour) {
if (
tour.ordinanceType.indexOf('BTW') > -1 ||
tour.ordinanceType.indexOf('TSW') > -1
) {
req = ApprovalRequirement.NOT_REQUIRED;
} else if (
[
'1. Entlassung',
'2. Entlassung Vorbestellung',
'3. Einweisung',
'5. Verlegung',
'6. Barzahlung',
'Privatfahrt Rechnung',
'Reha',
'Tagesklinik - AKW',
'Tagesklinik - ASK',
'Tagesklinik - MKH',
'Transportschein abholen',
].includes(tour.category) ||
tour.healthInsurance === 'KOSTENTRÄGER PATIENT'
) {
req = ApprovalRequirement.NOT_REQUIRED;
} else {
req = ApprovalRequirement.REQUIRED;
}
}
this._approval = this._approval.setRequirement(req);
}
}
@@ -0,0 +1,61 @@
import {
ApprovalRequestStatus,
ApprovalRequirement,
} from '@avicenna/shared-dtos';
export class TicketCreatedEvent {
constructor(
public readonly ticketId: string,
public readonly patientId: string,
) {}
}
export class TourAssignedToTicketEvent {
constructor(
public readonly ticketId: string,
public readonly tourId: string,
) {}
}
export class TicketStatusChangedEvent {
constructor(
public readonly ticketId: string,
public readonly oldStage: string,
public readonly newStage: string,
) {}
}
export class TicketTransportDocumentUploadedEvent {
constructor(public readonly ticketId: string) {}
}
export class TicketPhysicalTransportDocumentReceivedEvent {
constructor(public readonly ticketId: string) {}
}
export class TicketApprovalRequirementOverriddenEvent {
constructor(
public readonly ticketId: string,
public readonly newRequirement: ApprovalRequirement,
) {}
}
export class TicketApprovalRequestedEvent {
constructor(
public readonly ticketId: string,
public readonly approvalStatus: ApprovalRequestStatus,
public readonly recipientEmail: string,
) {}
}
export class TicketApprovalReminderSentEvent {
constructor(
public readonly ticketId: string,
public readonly reminderCount: number,
public readonly recipientEmail: string,
) {}
}
export class TicketApprovalGrantedEvent {
constructor(public readonly ticketId: string) {}
}
@@ -0,0 +1,22 @@
import { TourFile } from '@prisma/client';
import { TourCandidate } from './interfaces/tour-candidate.interface';
import { Ticket, TicketId } from './ticket.entity';
export const TICKET_REPOSITORY = 'TicketRepository';
export interface TicketRepository {
findById(id: TicketId): Promise<Ticket | null>;
save(ticket: Ticket): Promise<void>;
findTicketForTripInOppositeDirection(
tour: TourCandidate,
): Promise<Ticket | null>;
findTicketForSeries(tour: TourCandidate): Promise<Ticket | null>;
findPendingFilesForTicket(ticketId: TicketId): Promise<string[]>;
streamAllActive(batchSize: number): AsyncGenerator<Ticket[]>;
/**
* BATCH OPERATION: Bypasses Ticket aggregate for performance.
* Does NOT trigger Domain Events.
* Use only for background maintenance.
*/
closeSettledTickets(): Promise<void>;
}
@@ -0,0 +1,92 @@
import { ValidationError } from '@avicenna/shared-dtos';
import {
PatientCandidate,
PatientId,
} from '../interfaces/patient-candidate.interface';
import { TourCandidate, TourId } from '../interfaces/tour-candidate.interface';
import { TicketTourData } from './ticket-tour-data.vo';
export class TicketPatientData {
get patientId(): string | null {
return this.sourceType === 'PATIENT' ? this.sourceId.value : null;
}
constructor(
private readonly sourceId: PatientId | TourId,
private readonly sourceType: 'PATIENT' | 'TOUR',
public readonly firstName: string | null,
public readonly lastName: string | null,
public readonly birthday: Date | null,
public readonly healthInsurance: string | null,
public readonly insuranceNo: string | null,
public readonly street: string | null,
public readonly city: string | null,
public readonly zip: string | null,
) {}
static fromMasterData(patient: PatientCandidate): TicketPatientData {
return new TicketPatientData(
patient.id,
'PATIENT',
patient.firstName,
patient.lastName,
patient.birthday,
patient.healthInsurance,
patient.insuranceNo,
patient.street,
patient.city,
patient.zip,
);
}
static fromTour(tour: TourCandidate): TicketPatientData {
return new TicketPatientData(
tour.id,
'TOUR',
tour.patientName,
tour.patientSurname,
null,
tour.healthInsurance,
tour.insuranceNo,
tour.patientStreet,
tour.patientCity,
tour.patientZip,
);
}
static fromTourSnapshot(tour: TicketTourData): TicketPatientData {
return new TicketPatientData(
tour.id,
'TOUR',
tour.patientName,
tour.patientSurname,
null,
tour.healthInsurance,
tour.insuranceNo,
tour.patientStreet,
tour.patientCity,
tour.patientZip,
);
}
public validate(): ValidationError[] {
const errors: ValidationError[] = [];
const source = { type: this.sourceType, id: this.sourceId.value };
if (!this.firstName) {
errors.push({ field: 'firstName', message: 'Vorname fehlt', source });
}
if (!this.lastName) {
errors.push({ field: 'lastName', message: 'Nachname fehlt', source });
}
if (!this.healthInsurance) {
errors.push({
field: 'healthInsurance',
message: 'Krankenkasse fehlt',
source,
});
}
return errors;
}
}
@@ -0,0 +1,120 @@
import { ValidationError } from '@avicenna/shared-dtos';
import { TourCandidate, TourId } from '../interfaces/tour-candidate.interface';
export class TicketTourData {
constructor(
public readonly id: TourId,
public readonly operationId: string,
public readonly category: string,
public readonly ordinanceType: string,
public readonly transportType: string,
public readonly patientId: string,
public readonly patientName: string,
public readonly patientSurname: string,
public readonly patientStreet: string,
public readonly patientCity: string,
public readonly patientZip: string,
public readonly healthInsurance: string,
public readonly insuranceNo: string,
public readonly direction: string,
public readonly occupiedKm: number,
public readonly hasInfection: boolean,
public readonly infectionName: string | null,
public readonly seriesEndDate: Date | null,
public readonly targetStreet: string,
public readonly targetCity: string,
public readonly targetZip: string,
public readonly startStreet: string,
public readonly startCity: string,
public readonly startZip: string,
public readonly startDate: Date,
) {}
static fromCandidate(candidate: TourCandidate): TicketTourData {
return new TicketTourData(
candidate.id,
candidate.operationId,
candidate.category,
candidate.ordinanceType,
candidate.transportType,
candidate.patientId,
candidate.patientName,
candidate.patientSurname,
candidate.patientStreet,
candidate.patientCity,
candidate.patientZip,
candidate.healthInsurance,
candidate.insuranceNo,
candidate.direction,
candidate.occupiedKm,
candidate.hasInfection,
candidate.infectionName,
candidate.seriesEndDate,
candidate.targetStreet,
candidate.targetCity,
candidate.targetZip,
candidate.startStreet,
candidate.startCity,
candidate.startZip,
candidate.startDate,
);
}
public validate(): ValidationError[] {
const errors: ValidationError[] = [];
const source = { type: 'TOUR' as const, id: this.id.value };
if (!this.ordinanceType) {
errors.push({
field: 'ordinanceType',
message: `Verordnungsart ist nicht gefüllt`,
source,
});
}
if (!this.transportType) {
errors.push({
field: 'transportType',
message: `Transportart ist nicht gefüllt`,
source,
});
}
// REASON: Dispolive struggles to populate this field with it's value.
// if (!this.category) {
// errors.push({
// field: 'category',
// message: `Kategorie ist nicht gefüllt`,
// source,
// });
// }
if (!this.startDate) {
errors.push({
field: 'startDate',
message: `Startzeit ist nicht gefüllt`,
source,
});
}
if (!this.startStreet || !this.startZip || !this.startCity) {
errors.push({
field: 'startAddress',
message: `Start Adresse ist nicht gefüllt`,
source,
});
}
if (!this.targetStreet || !this.targetZip || !this.targetCity) {
errors.push({
field: 'targetAddress',
message: `Ziel Adresse ist nicht gefüllt`,
source,
});
}
if (this.occupiedKm > 9990) {
errors.push({
field: 'occupiedKm',
message: `Besetzte Kilometer sind zu hoch`,
source,
});
}
return errors;
}
}
@@ -0,0 +1,8 @@
export class TransportDocument {
constructor(
public readonly storagePath: string,
public readonly originalFilename: string,
public readonly mimeType: string,
public readonly sizeBytes: number,
) {}
}
@@ -0,0 +1,41 @@
export const TICKET_EMAIL_QUEUE = 'ticket-email-queue';
export type TicketEmailJob =
| { type: 'send-pre-inquiry'; data: PreInquiryJobData }
| { type: 'send-full-request'; data: FullRequestJobData }
| { type: 'send-reminder'; data: ReminderJobData };
export interface BaseEmailJobData {
ticketId: string;
recipientEmail: string;
// Für Threading: Auf welche ID soll geantwortet werden?
replyToMessageId?: string;
}
export interface PreInquiryJobData extends BaseEmailJobData {
params: {
patientName: string;
birthday: string;
insuranceNo: string;
healthInsurance: string;
startAddressLine: string;
targetAddressLine: string;
transportDate: string;
};
}
export interface FullRequestJobData extends BaseEmailJobData {
// Wir mappen das Ticket-VO auf eine flache Struktur für den Worker
attachment: {
storagePath: string;
originalFilename: string;
mimeType: string;
};
patientName: string; // Für Betreff/Body
}
export interface ReminderJobData extends BaseEmailJobData {
patientName: string;
transportDate: Date;
reminderCount: number;
}

Some files were not shown because too many files have changed in this diff Show More