From 1de7352fb42c110819e0d5658a2654e209883626 Mon Sep 17 00:00:00 2001 From: Luca Haid Date: Tue, 26 Aug 2025 09:56:54 +0200 Subject: [PATCH 001/103] added new features --- ...undenliste Leibnitz Markierung Zentrum.csv | 371 ++++++++++++++++++ .../leibnitz/import-leibnitz-interests.php | 244 ++++++++++++ 2 files changed, 615 insertions(+) create mode 100644 scripts/preorder/leibnitz/Kundenliste Leibnitz Markierung Zentrum.csv create mode 100644 scripts/preorder/leibnitz/import-leibnitz-interests.php diff --git a/scripts/preorder/leibnitz/Kundenliste Leibnitz Markierung Zentrum.csv b/scripts/preorder/leibnitz/Kundenliste Leibnitz Markierung Zentrum.csv new file mode 100644 index 000000000..f6dddb36c --- /dev/null +++ b/scripts/preorder/leibnitz/Kundenliste Leibnitz Markierung Zentrum.csv @@ -0,0 +1,371 @@ +Name/Firma,Titel,Vorname,KundenNr.,Straße,PLZ,Ort,Kostenstelle,Bemerkung,UID,Zahlungsart,Objekt,Kundenkategorie,Zielkonto,id,,,Interesse Glasfaser,Interesse Telefonie,Breitbandabdeckung,Kontakt +Lembach,,Brigitte,20263,Adalbert Stifter-Weg 3,8430,Leibnitz,"BLB III - Leibnitz, 8030","brigitte_lembach@yahoo.com + +mayer@gmail.com +",,Bankeinzug,"Adalbert-Stifter-Weg 3, 8430 Leibnitz",Wärmekunde,Bioenergie Leibnitzerfeld GmbH,5261,,,,,, +Meßtechnik EKV GmbH,,,20301,Alfred-Feierfeil-Straße 3,2380,Perchtoldsdorf,"BLB I - Leibnitz, 8010",,ATU69508428,Bankeinzug,,Wärmekunde,Bioenergie Leibnitzerfeld GmbH,6425,,,,,, +Ully,Mag.,Ingrid,20078,Am Kögel 15,8430,Leibnitz,"BLB I - Leibnitz, 8010",,,Bankeinzug,"Am Kögel 15, 8430 Leibnitz",Wärmekunde,Bioenergie Leibnitzerfeld GmbH,1823,,,,,, +Slonek,DI,Martin und Karin,20265,Am Kögel 24,8430,Leibnitz,"BLB III - Leibnitz, 8030",,,Bankeinzug,"Am Kögel 24, 8435 Wagna",Wärmekunde,Bioenergie Leibnitzerfeld GmbH,5263,,,,,, +Habisch,,Josef,20076,Am Kögel 26,8430,Leibnitz,"BLB I - Leibnitz, 8010"," +",,Bankeinzug,"Am Kögel 26, 8430 Leibnitz",Wärmekunde,Bioenergie Leibnitzerfeld GmbH,1816,,,,,, +Mayer,,Harald,20109,Am Kögel 33,8430,Leibnitz,"BLB I - Leibnitz, 8010",,,Bankeinzug,"Am Kögel 33, 8430 Leibnitz",Wärmekunde,Bioenergie Leibnitzerfeld GmbH,1991,,,,,, +Köpp,,Gerhard,20077,Am Kögel 35,8435,Wagna,"BLB I - Leibnitz, 8010",,,Bankeinzug,"Am Kögel 35, 8435 Wagna",Wärmekunde,Bioenergie Leibnitzerfeld GmbH,1822,,,,,, +GFSG Gesellschaft zur Förderung seelischer Gesundheit GmbH,,,20285,Annenstraße 24 / 5. Stock,8020,Graz,"BLB I - Leibnitz, 8010",,ATU74646369,Zahlschein,"Bahnhofstraße 19, GFSG Leibnitz, 8430 Leibnitz",Wärmekunde / Mieter,Bioenergie Leibnitzerfeld GmbH,5841,,,,,, +Überbacher & Überbacher Obstverwertung GmbH & Co KG,,,20075,Bahnhofstraße 19,8430,Leibnitz,"BLB I - Leibnitz, 8010",,ATU41719501,Bankeinzug,"Bahnhofstraße 19, Sport Überbacher, 8430 Leibnitz",Wärmekunde,Bioenergie Leibnitzerfeld GmbH,1815,,,ja,ja,Nissl FTTH 1000, +"Lampl +raDL Sport",,Dietmar,20286,Bahnhofstraße 19,8430,Leibnitz,"BLB I - Leibnitz, 8010",,ATU68316878,Zahlschein,"Bahnhofstraße 19, 8430 Leibnitz",Wärmekunde / Mieter,Bioenergie Leibnitzerfeld GmbH,5842,,,ja,ja,Nissl FTTH 1000, +Krüger,,Beate,20170,Bahnhofstraße 26,8430,Leibnitz,"BLB I - Leibnitz, 8010","06.05.2020 Michaela: ZE VS 02-05/2020 gesamt EUR 700,00 +12.05.2020 Michaela: Storno VS 02-05/2020 Kd meldet sich Juli/August um auf FW umstellen zu lassen.",ATU52445402,Bankeinzug,"Bahnhofstraße 26, 8430 Leibnitz",Wärmekunde,Bioenergie Leibnitzerfeld GmbH,2225,,,ja,ja,A1 xDSL 173, +Pfeifer Bekleidung GesmbH,,,20108,Bahnhofstraße 32,8430,Leibnitz,"BLB I - Leibnitz, 8010",,ATU29590500,Bankeinzug,"Bahnhofstraße 32, 8430 Leibnitz",Wärmekunde,Bioenergie Leibnitzerfeld GmbH,1969,,,ja,ja,,0664/5050316 +"Franz-Kerkletz-Weg 3 +8435 Wagna +pA Meßtechnik DVE GmbH& Co KG",,,20149,Bahnhofstraße 8-10,8073,Feldkirchen bei Graz,"BLB I - Leibnitz, 8010",,,Bankeinzug,"Franz-Kerkletz-Weg 3, 8435 Wagna",Wärmekunde,Bioenergie Leibnitzerfeld GmbH,2165,,,,,, +"Franz-Kerkletz-Weg 3a +8435 Wagna +pA Meßtechnik DVE GmbH& Co KG",,,20150,Bahnhofstraße 8-10,8073,Feldkirchen bei Graz,"BLB I - Leibnitz, 8010",,,Bankeinzug,"Franz-Kerkletz-Weg 3a +, 8435 Wagna",Wärmekunde,Bioenergie Leibnitzerfeld GmbH,2167,,,,,, +Höller,,Birgit,20211,Bauhofstraße 11,8435,Wagna,"BLB III - Leibnitz, 8030",,,Zahlschein,"Bauhofstraße 11, 8435 Wagna",Wärmekunde,Bioenergie Leibnitzerfeld GmbH,4154,,,,,, +"FA SD +Straßenmeisterei Leibnitz Nord",,,20019,Bauhofstraße 6,8435,Wagna,"BLB I - Leibnitz, 8010",,,Zahlschein,"Bauhofstraße 6, Wohnhaus, 8435 Wagna",Wärmekunde,Bioenergie Leibnitzerfeld GmbH,1537,,,,,, +"FA SD +Straßenmeisterei Leibnitz Nord",,,20020,Bauhofstraße 6,8435,Wagna,"BLB I - Leibnitz, 8010","smlb1@stmk.gv.at +",,Zahlschein,"Bauhofstraße 6, 8435 Wagna, Straßenmeisterei",Wärmekunde,Bioenergie Leibnitzerfeld GmbH,1538,,,,,, +Sailer-Kronlachner ,Dr.,Maximilian,20326,Beim Färberkreuz 1,8430,Leibnitz,"BLB - ALL - Leibnitz, 8000",,,Zahlschein,"Beim Färberkreuz 1, 8430 Leibnitz",WLV / Anschluss offen,Bioenergie Leibnitzerfeld GmbH,7961,,,,,, +Liebenwein,,Doris,20162,Beim Färberkreuz 22,8430,Leibnitz,"BLB I - Leibnitz, 8010",,,Bankeinzug,"Beim Färberkreuz 22, 8430 Leibnitz",Wärmekunde,Bioenergie Leibnitzerfeld GmbH,2209,,,,,, +"Eigentümer der Wohnanlage Beim Johanniskreuz 16+16a, 18+18a +Lenauweg 7+7a, Lenauweg 9+9a, Lenauweg 11",,,20327,Beim Johanniskreuz 16+16a,8430,Leibnitz,"BLB - ALL - Leibnitz, 8000",,,Zahlschein,"Beim Johanniskreuz + Lenauweg 7, 9, 11",WLV / Anschluss offen,Bioenergie Leibnitzerfeld GmbH,7962,,,,,, +Steiner,,Dr. Ernst und Ulrike,20333,Brahms-Gasse 11,8430,Leibnitz,"BLB - ALL - Leibnitz, 8000",dr.ernst.steiner@aon.at,,Zahlschein,"Brahms-Gasse 11, 8430 Leibnitz",WLV / Anschluss offen,Bioenergie Leibnitzerfeld GmbH,7968,,,,,, +Brodatsch,,Helmut,20325,Brahms-Gasse 6,8430,Leibnitz,"BLB - ALL - Leibnitz, 8000",,,Zahlschein,"Brahms-Gasse 6, 8430 Leibnitz",WLV / Anschluss offen,Bioenergie Leibnitzerfeld GmbH,7960,,,,,, +Kappaun,Mag.,Karl,20332,Brahms-Gasse 8,8430,Leibnitz,"BLB - ALL - Leibnitz, 8000",,,Zahlschein,"Brahms-Gasse 8, 8430 Leibnitz",WLV / Anschluss offen,Bioenergie Leibnitzerfeld GmbH,7967,,,,,, +Aparaschivei,,Constantin u. Rela,20232,Burghartweg 2,4844,Regau,"BLB III - Leibnitz, 8030",,,,"Wohnhaus Neubau 2021, 4844 Regau",WLV / Anschluss offen,Bioenergie Regau,4463,,,,,, +Gogl,,Karin,20204,Dr. Leo Klein-Gasse 5,8430,Leibnitz,"BLB III - Leibnitz, 8030"," +Gogl sen. 0664 / 163 2889 +Gogl jun. 0664 / 122 0626",,Zahlschein,"Dr. Leo Klein-Gasse 5, 8430 Leibnitz",Wärmekunde,Bioenergie Leibnitzerfeld GmbH,4144,,,,,, +Geymayer,DI,Oliver,20160,Dr. Leo-Klein-Gasse 10,8430,Leibnitz,"BLB I - Leibnitz, 8010",,,Bankeinzug,"Karl Morre Gasse 11, 8430 Leibnitz",Wärmekunde,Bioenergie Leibnitzerfeld GmbH,2207,,,,,, +Geymayer,DI,Oliver,20143,Dr. Leo-Klein-Gasse 10,8430,Leibnitz,"BLB I - Leibnitz, 8010",,,Bankeinzug,"Dr. Leo-Klein-Gasse 10, 8430 Leibnitz",Wärmekunde,Bioenergie Leibnitzerfeld GmbH,2124,,,,,, +"HE Marburgerstraße 65/65a +pA Dr. Ulrich Objektverwaltung GmbH",,,20222,Dürergasse 8,8010,Graz,"BLB III - Leibnitz, 8030",,,Bankeinzug,"Marburger Straße 65/65a, 8435 Wagna",Wärmekunde,Bioenergie Leibnitzerfeld GmbH,4342,,,,,, +AIM Immobilien GmbH,,,20292,Eggenberger Gürtel 36,8020,Graz,"BLB III - Leibnitz, 8030",,,Zahlschein,"Fettinger Gasse 5, 8430 Leibnitz",Wärmekunde,Bioenergie Leibnitzerfeld GmbH,5950,,,,,, +Rumpler,,Andrea,20148,Eichfeld 13,8480,Mureck,"BLB I - Leibnitz, 8010","Kontodaten von Frau Ploder, das ist die Mutter +",,Bankeinzug,"Fuxweg 10, 8430 Leibnitz",Wärmekunde,Bioenergie Leibnitzerfeld GmbH,2161,,,,,, +Videcnik,,Horst,20177,Eisenbahnerstraße 1,8435,Wagna,"BLB I - Leibnitz, 8010",,,Bankeinzug,"Eisenbaherstraße 1, 8435 Wagna",Wärmekunde,Bioenergie Leibnitzerfeld GmbH,2255,,,,,, +Evangelische Pfarrgemeinde A.B. Leibnitz,,,20300,Emmerich Assmann-Gasse 1,8430,Leibnitz,"BLB III - Leibnitz, 8030",,,Bankeinzug,"Emmerich Assmann-Gasse 1, 8430 Leibnitz",Wärmekunde,Bioenergie Leibnitzerfeld GmbH,6418,,,,,, +Zupanec,,Herbert,20152,Feldgasse 3,8435,Wagna,"BLB I - Leibnitz, 8010","11.01.2021 Michaela: Tel Hrd. Zupanec, Dauerauftrag Euro 100,00 ab 16.03.2021 für AnschlusskostenRe 2020014",,Bankeinzug,"Feldgasse 3, 8435 Wagna",Wärmekunde,Bioenergie Leibnitzerfeld GmbH,2184,,,,,, +Leber,,Alois und Edda,20126,Feldgasse 5,8435,Wagna,"BLB I - Leibnitz, 8010",IBAN ALT: AT97 3800 0000 0084 9679 ,,Bankeinzug,"Feldgasse 5, 8435 Wagna",Wärmekunde,Bioenergie Leibnitzerfeld GmbH,2048,,,,,, +Schlagbauer,,Mag. Gabriele u. Mag. Jürgen,20288,Fettinger Gasse 16,8430,Leibnitz,"BLB III - Leibnitz, 8030","j.schlagbauer@apomedica.com +juergen.schlagbauer@inode.at",,Bankeinzug,"Fettinger Gasse 16, 8430 Leibnitz",Wärmekunde,Bioenergie Leibnitzerfeld GmbH,5910,,,,,, +Anne Margarethe,,Abel,20275,Fettinger-Gasse 3,8430,Leibnitz,"BLB III - Leibnitz, 8030",,,Zahlschein,Fettinger Gasse 3. 8430 Leibnitz,Wärmekunde,Bioenergie Leibnitzerfeld GmbH,5586,,,,,, +Projektentwicklungs KG,,,20163,Florianerstraße 46,8522,Groß Sankt Florian,"BLB I - Leibnitz, 8010",,,,"Ottokar Kernstock Gasse 2, 8430 Leibnitz",WLV / Anschluss offen,Bioenergie Leibnitzerfeld GmbH,2210,,,,,, +Bordbar,,Renate,20334,Flurweg 20,8055,Graz,"BLB - ALL - Leibnitz, 8000",,,Zahlschein,"Südbahnstraße 33, 8430 Leibnitz",WLV / Anschluss offen,Bioenergie Leibnitzerfeld GmbH,7969,,,,,, +Karl Fink Gesellschaft m.b.H.,,,20164,Frauengasse 10,8430,Kaindorf,"BLB I - Leibnitz, 8010",,ATU29602506,Zahlschein,"Südbahnstraße 10, 8430 Leibnitz",Wärmekunde,Bioenergie Leibnitzerfeld GmbH,2211,,,,,, +Reifenhaus Thomas Plankenauer Gesellschaft m.b.H.,,,20073,Friesacher Straße 19,9300,St. Veit an der Glan,"BLB I - Leibnitz, 8010","24.05.19 Marlene: ZE der VS 02-05/19 versendet EUR 2.800,- - bezahlt am 06.06.19 +03.09.19 Marlene: ZE der VS 06-08/19 versendet EUR 2.100,- +15.10.19 Marlene: 1. Mahnung über VS 06-10/19 versendet, EUR 3.500,- - bezahlt am 22.10.19 +",ATU26125305,Zahlschein,"Marburger Straße 98, 8435 Wagna",Wärmekunde,Bioenergie Leibnitzerfeld GmbH,1813,,,,,, +Lampel,,Josef,20260,Fuxweg 4,8430,Leibnitz,"BLB III - Leibnitz, 8030",,,Bankeinzug,"Fuxweg 4, 8430 Leibnitz",Wärmekunde,Bioenergie Leibnitzerfeld GmbH,4843,,,,,, +Kaufmann,,Maria,20257,Fuxweg 9,8430,Leibnitz,"BLB III - Leibnitz, 8030",,,Bankeinzug,"Fuxweg 9, 8430 Leibnitz",Wärmekunde,Bioenergie Leibnitzerfeld GmbH,4836,,,,,, +Luttenberger,,Franz,20052,Gabersdorf 64,8424,Gabersdorf,"BLB I - Leibnitz, 8010",,,Die Überweisung erfolgt....,,Gestattungen/Leitungsrechte,Bioenergie Leibnitzerfeld GmbH,1723,,,,,, +Nahwärme Tillmitsch GmbH & Co KG,,,20228,Gemeindestraße 10,8434,Tillmitsch,"BLB III - Leibnitz, 8030",,,Zahlschein,Othmar Russheim Straße,"Abre. monatl.,Wärmekunde",Bioenergie Leibnitzerfeld GmbH,4422,,,,,, +Nahwärme Tillmitsch GmbH & Co KG,,,20169,Gemeindestraße 10,8434,Tillmitsch,"BLB I - Leibnitz, 8010","06.05.2020 Michaela: ZE 04-05/2020 Abrechng. 2020004 EUR gesamt 6.766,34 +Zahlung am 13.05.20 +",,Zahlschein,"Emmerich Assmann-Gasse 4 - 6a, 8430 Leibnitz",Wärmekunde,Bioenergie Leibnitzerfeld GmbH,2224,,,,,, +Friess,,Sandra,20281,Gojach 76,8421,St. Stefan im Rosenthal,"BLB III - Leibnitz, 8030",,,Zahlschein,"Marburger Straße 121a, 8435 Wagna",WLV / Anschluss offen,Bioenergie Leibnitzerfeld GmbH,5613,,,,,, +Round,,Doris,20329,Grabenstraße 115/5,8010,Graz,"BLB - ALL - Leibnitz, 8000",,,Zahlschein,"Wilhelm Kienzl-Gasse 13, 8430 Leibnitz",WLV / Anschluss offen,Bioenergie Leibnitzerfeld GmbH,7964,,,,,, +Stein,,Erwin,20105,Grazerstraße 48,8434,Neutillmitsch,"BLB I - Leibnitz, 8010",,,Zahlschein,"Marburger Straße 122, 8435 Wagna",Vertragsende,Bioenergie Leibnitzerfeld GmbH,1966,,,,,, +"WEG Schubertstraße 21, 8430 Leibnitz +z. H. LSH Facility GmbH",,,20118,Gürtlweg 1,8431,Gralla,"BLB I - Leibnitz, 8010",,,Zahlschein,"Schubertstraße 21, 8430 Leibnitz",Wärmekunde,Bioenergie Leibnitzerfeld GmbH,2000,,,,,, +"Am Kögel Projektentwicklungs GmbH +z. H. LSH Facility GmbH",,,20309,Gürtlweg 1,8431,Gralla,"BLB III - Leibnitz, 8030",,,Bankeinzug,"Am Kögel 1, 8430 Leibnitz",Wärmekunde,Bioenergie Leibnitzerfeld GmbH,7873,,,,,, +Kickel,,Alfred und Eva,20062,Haltackerried 4,8430,Leibnitz,"BLB I - Leibnitz, 8010","30.03.20 Marlene: ZE über Abr.2019 EUR 648,74 Euro gesendet +06.05.2020 Michaela: 1. Mahnung Abre 2019 EUR 648,74 gesendet +14.07.2020 Marlene: 2. Mahnung Abr. 2019 EUR 648,74 gesendet - +Michalea:Mahnung retourgekommen - 31.07.2020 +13.08.2020 Michaela 2. Mahnung nochmal versendet mit Datum 13.08.2020 + +",,Zahlschein,"Hauptstraße 15, 8435 Wagna",Wärmekunde,Bioenergie Leibnitzerfeld GmbH,1764,,,,,, +Kammer für Land- und Forstwirtschaft Steiermark,,,20219,Hammerlinggasse 3,8010,Graz,"BLB III - Leibnitz, 8030"," + +hausverwaltung@lk-stmk.at + +ralf.gregory@lk-stmk.at",ATU28606906,Zahlschein,"Julius Strauß Weg 1, Niederlassung Leibnitz, 8430 Leibnitz",Wärmekunde,Bioenergie Leibnitzerfeld GmbH,4296,,,,,, +"Kammer für Arbeiter und Angestellte für Steiermark, AK Steiermark",,,20220,Hans-Resel-Gasse 8-14,8020,Graz,"BLB III - Leibnitz, 8030",,,Bankeinzug,"Karl-Morre-Gasse 6, Bezirksstelle Leibnitz, 8430 Leibnitz",Wärmekunde,Bioenergie Leibnitzerfeld GmbH,4324,,,,,, +Stadtgemeinde Leibnitz,,,20099,Hauptplatz 24,8430,Leibnitz,"BLB I - Leibnitz, 8010",,ATU69172708,Bankeinzug,"Bahnhofstraße 16, Altes Kino Marenzi, 8430 Leibnitz",Wärmekunde,Bioenergie Leibnitzerfeld GmbH,1960,,,,,, +Stadtgemeinde Leibnitz,,,20097,Hauptplatz 24,8430,Leibnitz,"BLB I - Leibnitz, 8010",06.05.2020 Michaela: ZE 01-05/2020 VZ - bezahlt am 08.06.2020,ATU69172708,Bankeinzug,"Karl Morre Gasse 14, NMS 2, 8430 Leibnitz",Wärmekunde,Bioenergie Leibnitzerfeld GmbH,1880,,,,,, +"Stadtgemeinde Leibnitz +Kindergarten Sumsi",,,20100,Hauptplatz 24,8430,Leibnitz,"BLB I - Leibnitz, 8010",,ATU69172708,Bankeinzug,"Dr. Leo-Klein-Gasse 1, KiGa Sumsi, 8430 Leibnitz",Wärmekunde,Bioenergie Leibnitzerfeld GmbH,1961,,,,,, +"Stadtgemeinde Leibnitz +NMS1 und Sporthalle",,,20098,Hauptplatz 24,8430,Leibnitz,"BLB I - Leibnitz, 8010",,ATU69172708,Bankeinzug,"Wagna Straße 7, NMS 1 und Sporthalle, 8430 Leibnitz",Wärmekunde,Bioenergie Leibnitzerfeld GmbH,1959,,,,,, +"Stadtgemeinde Leibnitz +Haus der Musik",,,20094,Hauptplatz 24,8430,Leibnitz,"BLB I - Leibnitz, 8010",,ATU69172708,Bankeinzug,"Bahnhofstraße 14a, Haus d.Musik, 8430 Leibnitz",Wärmekunde,Bioenergie Leibnitzerfeld GmbH,1867,,,,,, +"Stadtgemeinde Leibnitz +Weinwochengelände",,,20103,Hauptplatz 24,8430,Leibnitz,"BLB I - Leibnitz, 8010",,ATU69172708,Bankeinzug,"Bahnhofstraße 14, Weinwochengelände, 8430 Leibnitz",Wärmekunde,Bioenergie Leibnitzerfeld GmbH,1964,,,,,, +"Stadtgemeinde Leibnitz +Wirtschaftshof",,,20102,Hauptplatz 24,8430,Leibnitz,"BLB I - Leibnitz, 8010",,,,"Südbahnstraße 11, Wirtschaftshof, 8430 Leibnitz",WLV / Anschluss offen,Bioenergie Leibnitzerfeld GmbH,1963,,,,,, +Stadtgemeinde Leibnitz,,,20101,Hauptplatz 24,8430,Leibnitz,"BLB I - Leibnitz, 8010",,ATU69172708,Bankeinzug,"Bahnhofstraße 12, Marenzihaus, 8430 Leibnitz",Wärmekunde,Bioenergie Leibnitzerfeld GmbH,1962,,,,,, +Pokes,,Tanja,20060,Hauptstraße 12,8435,Wagna,"BLB I - Leibnitz, 8010",,ATU68085868,Bankeinzug,"Hauptstraße 12, Bäckerei, 8435 Wagna",Wärmekunde,Bioenergie Leibnitzerfeld GmbH,1744,,,,,, +Projekt Marburgerstr. 125+127 der Syn-Ro Immobilienentwicklung u. -verwaltung GmbH & Co KG,,,20063,Hauptstraße 19,8074,Raaba-Grambach,"BLB I - Leibnitz, 8010",,,,"US Marburgerstraße 125, 8435 Wagna",WLV / Anschluss offen,Bioenergie Leibnitzerfeld GmbH,1765,,,,,, +Bilanovic,,Zoran,20227,Hauptstraße 22,8435,Wagna,"BLB III - Leibnitz, 8030",,,Bankeinzug,"Hauptstraße 22, 8435 Wagna",Wärmekunde,Bioenergie Leibnitzerfeld GmbH,4421,,,,,, +Stangl,BM DI (FH),Markus und Rosina,20202,Hauptstraße 25,8435,Wagna,"BLB III - Leibnitz, 8030",,,Bankeinzug,"Hauptstraße 25, 8435 Wagna",Wärmekunde,Bioenergie Leibnitzerfeld GmbH,4142,,,,,, +Bernhard,,Josef und Ingeborg,20324,Hauptstraße 40 b,8435,Wagna,"BLB - ALL - Leibnitz, 8000",,,Zahlschein,"Hauptstraße 40 b, 8435 Wagna",WLV / Anschluss offen,Bioenergie Leibnitzerfeld GmbH,7959,,,,,, +Frühwirth,,Helga,20230,Im Mitterfeld 14,8430,Leibnitz,"BLB III - Leibnitz, 8030",,,Zahlschein,"Im Mitterfeld 14, 8430 Leibnitz",Wärmekunde,Bioenergie Leibnitzerfeld GmbH,4457,,,,,, +Sperger,Dr.,Hanno und Maria,20159,Im Mitterfeld 3,8430,Leibnitz,"BLB I - Leibnitz, 8010",,,Zahlschein,"Im Mitterfeld 3, 8430 Leibnitz",Wärmekunde,Bioenergie Leibnitzerfeld GmbH,2199,,,,,, +Kaucic,,Ilse,20336,Josef Haydn-Weg 1,8430,Leibnitz,"BLB - ALL - Leibnitz, 8000",,,Zahlschein,"Josef Haydn-Weg 1, 8430 Leibnitz",WLV / Anschluss offen,Bioenergie Leibnitzerfeld GmbH,7971,,,,,, +Konrad,,Martin,20251,Josef Haydn-Weg 2,8430,Leibnitz,"BLB III - Leibnitz, 8030",,,Bankeinzug,"Josef Haydn-Weg 2, 8430 Leibnitz",Wärmekunde,Bioenergie Leibnitzerfeld GmbH,4726,,,,,, +Eiletz,,Johanna,20337,Josef Haydn-Weg 3,8430,Leibnitz,"BLB - ALL - Leibnitz, 8000",,,Zahlschein,"Josef Haydn-Weg 3, 8430 Leibnitz",WLV / Anschluss offen,Bioenergie Leibnitzerfeld GmbH,7972,,,,,, +Moser,,Michaela,20330,Josef Haydn-Weg 7,8430,Leibnitz,"BLB - ALL - Leibnitz, 8000",,,Zahlschein,"Josef Haydn-Weg 7, 8430 Leibnitz",WLV / Anschluss offen,Bioenergie Leibnitzerfeld GmbH,7965,,,,,, +Kortschak,DI,Gerwin,20331,Josef Haydn-Weg 8,8430,Leibnitz,"BLB - ALL - Leibnitz, 8000",,,Zahlschein,"Josef Haydn-Weg 8, 8430 Leibnitz",WLV / Anschluss offen,Bioenergie Leibnitzerfeld GmbH,7966,,,,,, +Riedl,,Peter,20335,Josef Haydn-Weg 9,8430,Leibnitz,"BLB - ALL - Leibnitz, 8000",,,Zahlschein,"Josef Haydn-Weg 9, 8430 Leibnitz",WLV / Anschluss offen,Bioenergie Leibnitzerfeld GmbH,7970,,,,,, +Feichtinger,,Walter,20069,Josef-Maier-Straße 1,8435,Wagna,"BLB I - Leibnitz, 8010",,,Bankeinzug,"Josef-Maier-Straße 1, 8435 Wagna, Wohnhaus",Wärmekunde,Bioenergie Leibnitzerfeld GmbH,1782,,,,,, +"WEG Sonnenweg 3 +c/o IMS Immobilien GmbH",,,20313,Kalvarienbergstraße 76-78,8020,Graz,"BLB III - Leibnitz, 8030",,,Zahlschein,"Sonnenweg 3, 8430 Leibnitz",WLV / Anschluss offen,Bioenergie Leibnitzerfeld GmbH,7880,,,,,, +"WEG Sonnenweg 1-2 +c/o IMS Immobilien GmbH",,,20312,Kalvarienbergstraße 76-78,8020,Graz,"BLB III - Leibnitz, 8030",,,Zahlschein,"Sonnenweg 1-2, WEG, 8430 Leibnitz",WLV / Anschluss offen,Bioenergie Leibnitzerfeld GmbH,7879,,,,,, +"WEG Sonnenweg 4-5 +c/o IMS Immobilien GmbH",,,20311,Kalvarienbergstraße 76-78,8020,Graz,"BLB III - Leibnitz, 8030",,,Zahlschein,"Sonnenweg 4-5, WEG, 8430 Leibnitz",WLV / Anschluss offen,Bioenergie Leibnitzerfeld GmbH,7878,,,,,, +Auto Marko Kapellenweg GmbH,,,20120,Kapellenweg 8,8430,Leibnitz,"BLB I - Leibnitz, 8010",,ATU57317102,,"Kapellenweg 8, 8430 Leibnitz",Wärmekunde,Bioenergie Leibnitzerfeld GmbH,2042,,,,,, +KOMPETENZ - Berufliches und soziales Kompetenzzentrum Südsteiermark GmbH,,,20167,Karl Morre Gasse 11,8430,Leibnitz,"BLB I - Leibnitz, 8010"," +AT49 5600 0209 5302 0018",ATU62169156,Bankeinzug,"Karl Morre Gasse 11a, 8430 Leibnitz",Wärmekunde,Bioenergie Leibnitzerfeld GmbH,2214,,,,,, +KOMPETENZ - Berufliches und soziales Kompetenzzentrum Südsteiermark GmbH,,,20166,Karl Morre Gasse 11,8430,Leibnitz,"BLB I - Leibnitz, 8010"," +IBAN alt: AT49 5600 0209 5302 0018",ATU62169156,Bankeinzug,"Karl Morre Gasse 11, 8430 Leibnitz",Wärmekunde,Bioenergie Leibnitzerfeld GmbH,2213,,,,,, +Kalaitzis,Dr.,Dimitrios,20243,Karl Morre-Gasse 10,8430,Leibnitz,"BLB III - Leibnitz, 8030",,,Bankeinzug,"Karl Morre-Gasse 10, 8430 Leibnitz",Wärmekunde,Bioenergie Leibnitzerfeld GmbH,4635,,,,,, +"Guss +Objekt: Kirchengasse 14",Mag.,Helmuth,20129,Kirchengasse 14,8435,Wagna,"BLB I - Leibnitz, 8010",,,Bankeinzug,"Kirchengasse 14, 8435 Wagna",Wärmekunde,Bioenergie Leibnitzerfeld GmbH,2060,,,,,, +"Guss +Objekt: Kirchengasse 12",Mag.,Helmuth,20132,Kirchengasse 14,8435,Wagna,"BLB I - Leibnitz, 8010",,,,"Kirchengasse 12, 8435 Wagna",WLV / Anschluss offen,Bioenergie Leibnitzerfeld GmbH,2088,,,,,, +"Guss +Objekt: Kirchengasse 16",Mag.,Helmuth,20053,Kirchengasse 14,8435,Wagna,"BLB I - Leibnitz, 8010",,,Bankeinzug,"Kirchengasse 16, 8435 Wagna",Wärmekunde,Bioenergie Leibnitzerfeld GmbH,1725,,,,,, +Lambauer,,Ute und Kurt,20038,Kirchengasse 18,8435,Wagna,"BLB I - Leibnitz, 8010","25.06.19 Marlene: Abr.2018 und VS01-06/19 abzüglich Wärmebonus gemahnt: EUR 310,78 - Zahlung erhalten +24.03.20 Marlene. ZE für Abrechnung 2019 versendet, EUR 1.235,19 +30.03.20 Marlene: MAHNSTOPP- Kunde hat mit Silvia bereits gesprochen. 0664/2421769 , habe den Sachverhalt Silvia zur Klärung übergeben +07.04.20 Silvia: Mit Kunden die ZE durchgegangen und die Abrechnung / VS erklärt, der Rückstand wird in Teilzlg. bezahlt",,Zahlschein,"Kirchengasse 18, 8435 Wagna",Wärmekunde,Bioenergie Leibnitzerfeld GmbH,1617,,,,,, +Ferk,,Gerta,20122,Kirchengasse 22,8435,Wagna,"BLB I - Leibnitz, 8010",,,Bankeinzug,"Kirchengasse 22, 8435 Wagna",Wärmekunde,Bioenergie Leibnitzerfeld GmbH,2044,,,,,, +Hechtner,,Anna und Oskar,20201,Kirchengasse 30,8435,Wagna,"BLB III - Leibnitz, 8030",,,Bankeinzug,"Kirchengasse 30, 8435 Wagna",Wärmekunde,Bioenergie Leibnitzerfeld GmbH,4141,,,,,, +Talundzic,,Almedina,20057,Kirchengasse 4,8435,Wagna,"BLB I - Leibnitz, 8010","30.03.2020 Marlene: 1.800,- Euro Restzahlung für RE 2019085 (Invest- und Wüst) +06.04.20 Marlene: Laut Jürgen muss der Kunde diese 1.800,- Euro nicht zahlen - Silvia weitergeleitet für GS-Erstellung + +02.03.2021 Michaela: lt. mail vom 22.02.2021 zahlt Frau Talundzic Almedina die Heizkosten - mail an Herrn Buchgraber wegen neuen Vertrag hat sich bis 02.03.2021 noch nicht bei Frau Talundzic gemeldet +Ab 03/2021 Einzug von Frau Talundzic + +17.03.2021 Talundzic Almedina: Tel: 0660/613 6511 - Tochter von Herrn Hajrudin Sivac",,Bankeinzug,"Kirchengasse 4, 8435 Wagna",Wärmekunde,Bioenergie Leibnitzerfeld GmbH,1740,,,,,, +Ruckenstuhl,,Alois,20068,Kirchengasse 4a,8435,Wagna,"BLB I - Leibnitz, 8010","11.09.2020: Michaela: Tel Jürgen : wir bekommen vom Land Euro 600,00 Förderung, der Kunde hat noch €600,00 zu bezahlen - er ruft den Kunden an. - Rückmeldung: Hr. Ruckenstuhl zahlt Euro 600,00 ein! + +IBAN alt: AT366000080310164856 gültig bis 03 2022",,Bankeinzug,"Kirchengasse 4a, 8435 Wagna",Wärmekunde,Bioenergie Leibnitzerfeld GmbH,1781,,,,,, +Agro Power Düngemittel GmbH,,,20250,Landscha 15,8424,Gabersdorf,"BLB I - Leibnitz, 8010",,,Zahlschein,"Landscha 15, 8424 Gabersdorf",Wärmekunde,Bioenergie Leibnitzerfeld GmbH,4722,,,,,, +Höller,,Josef und Christine,20131,Landscha 15,8424,Gabersdorf,"BLB I - Leibnitz, 8010",,,Zahlschein,"Ottokar Kernstock Gasse 6, 8430 Leibnitz",Wärmekunde,Bioenergie Leibnitzerfeld GmbH,2083,,,,,, +Feldbacher,,Johannes und Waltraud,20054,Landscha 79,8435,Wagna,"BLB I - Leibnitz, 8010",,,Zahlschein,,Gestattungen/Leitungsrechte,Bioenergie Leibnitzerfeld GmbH,1727,,,,,, +PUREA Austria GmbH,,,20092,Landscha 8,8424,Gabersdorf,"BLB I - Leibnitz, 8010","rechnungen@sttkv.at + +cc wolfgang.wiesauer@sttkv.at +cc andrea.swaschnig@bioenergie.at + + +rechnungen@sttkv.at",ATU66534477,,Landscha 8,Wärmekunde,Bioenergie Leibnitzerfeld GmbH,1863,,,,,, +UIDL Parts GmbH,,,20134,Landscha an der Mur 115,8424,Gabersdorf,"BLB I - Leibnitz, 8010",,,Zahlschein,"Landscha an der Mur 115, 8424 Gabersdorf",Einspeisung,Bioenergie Leibnitzerfeld GmbH,2105,,,,,, +Krasser,,Michael,20154,Landscha an der Mur 72,8424,Gabersdorf,"BLB I - Leibnitz, 8010",,,Zahlschein,,Gestattungen/Leitungsrechte,Bioenergie Leibnitzerfeld GmbH,2186,,,,,, +Auerbach,Dr.,Berthold Ralph,20278,Lastenstraße 15,8430,Leibnitz,"BLB III - Leibnitz, 8030",,,Bankeinzug,"Lastenstraße 15, 8430 Leibnitz",Wärmekunde,Bioenergie Leibnitzerfeld GmbH,5608,,,,,, +Heindl,,Annemarie,20242,Lastenstraße 16,8430,Leibnitz,"BLB III - Leibnitz, 8030",,,,"Lastenstraße 16, 8430 Leibnitz",WLV / Anschluss offen,Bioenergie Leibnitzerfeld GmbH,4634,,,,,, +Thomann,Mag.,Ursula,20165,Lastenstraße 23,8430,Leibnitz,"BLB I - Leibnitz, 8010",,,,"Karl Morre Gasse 11a, 8430 Leibnitz",WLV / Anschluss offen,Bioenergie Leibnitzerfeld GmbH,2212,,,,,, +Puntigam,,Sabine,20145,Lastenstraße 24,8430,Leibnitz,"BLB I - Leibnitz, 8010",,,Bankeinzug,"Lastenstraße 24, 8430 Leibnitz",Wärmekunde,Bioenergie Leibnitzerfeld GmbH,2147,,,,,, +GSL Gemeinnützige Bauvereinigungs GmbH,,,20119,Leechgasse 29,8010,Graz,"BLB I - Leibnitz, 8010"," +office@gsl-wohnen.at + + +30.03.2020 Marlene: ZE für die INVEST und WÜST versendet, per Mail: EUR 9.804,- bezahlt am 28.04.2020",ATU59456109,Zahlschein,"Siedlungsstraße 13 -15a, 8435 Wagna",Wärmekunde / Eigentümer,Bioenergie Leibnitzerfeld GmbH,2008,,,,,, +Gady Liegenschaftsverwaltungs GmbH,,,20338,Leibnitzerstraße 76,8403,Lebring,"BLB III - Leibnitz, 8030",,ATU73361612,Zahlschein,"Lastenstraße 26, 8430 Leibnitz",WLV / Anschluss offen,Bioenergie Leibnitzerfeld GmbH,8323,,,,,, +"Krampl Wilhelm & Miteigentümer +Hauptstraße 20 +z.H. IPG-ImmobilienPartnerGmbH",,,20064,Leitringer Hauptstraße 19,8435,Wagna,"BLB I - Leibnitz, 8010",,ATU72265906,Bankeinzug,"Hauptstraße 20, 8435 Wagna",Wärmekunde,Bioenergie Leibnitzerfeld GmbH,1766,,,,,, +"MP Vermögensverwaltungs GmbH +zH IPG-Leitringer Hauptstraße 19, 8435 Wagna",,,20088,Leitringer Hauptstraße 19,8435,Wagna,"BLB I - Leibnitz, 8010"," +elena.pein@ipg-immo.at +markus@agentur-pein.at",,Zahlschein,"Marburger Straße 81, 8435 Wagna, Betriebsobjekt",Wärmekunde,Bioenergie Leibnitzerfeld GmbH,1847,,,,,, +"EG Lastenstraße 22 +zH Gebäudeverwaltung Kienzl GmbH",,,20176,Leonhardstraße 5/2/10,8010,Graz,"BLB I - Leibnitz, 8010","20.05.2020 Michaela: Invest bw WÜST nicht verrechnen, Sekundärseite wird unentgeltlich angeschlossen lt. WLV + +petra.kienzl@gebaeudeverwaltung.cc + +20.01.2021 Michaela: Tel Hr. Kienzl Abrechnung per mail an rechnung@gebaeudeverwaltung.cc senden Einzug bereits am 20.05.2020 an Herrn Reinweber gesendet! + +08.02.2021 Michaela: Einzug von AB 2020 und ab 01/2021 Vorschreibungen +",ATU59468007,Bankeinzug,"Lastenstraße 22, 8430 Leibnitz",Wärmekunde,Bioenergie Leibnitzerfeld GmbH,2243,,,,,, +"Eigentümer d. Wohnhauses Türkengasse 1-3, Leibnitz +c/o Gebäudeverwaltung Kienzl GmbH",,,20188,Leonhardstraße 5/2/10,8010,Graz,"BLB I - Leibnitz, 8010",,ATU59468007,Zahlschein,"Türkengasse 1-3, 8430 Leibnitz",Wärmekunde,Bioenergie Leibnitzerfeld GmbH,3969,,,,,, +Fuchs,,Claudia,20086,Lichendorf 144,8473,Straß in Steiermark,"BLB I - Leibnitz, 8010",,,Bankeinzug,"Marburger Straße 112, 8435 Wagna",Wärmekunde,Bioenergie Leibnitzerfeld GmbH,1845,,,,,, +Mesgetz,,Monika,20061,Lippitzstrasse 5/4,8430,Leibnitz,"BLB I - Leibnitz, 8010",,,,"Kirchengasse 10, 8435 Wagna",WLV / Anschluss offen,Bioenergie Leibnitzerfeld GmbH,1751,,,,,, +Porsche Konstruktionen GmbH & Co KG,,,20225,Louise-Piech-Straße 2,5020,Salzburg,"BLB III - Leibnitz, 8030",,ATU34241503,Zahlschein,"Südbahnstraße 27, 8430 Leibnitz",Wärmekunde,Bioenergie Leibnitzerfeld GmbH,4381,,,,,, +"Harald Kager, Steuerberatung GmbH & Co KG ",Mag.,,20280,Marburger Straße 13,8430,Leibnitz,"BLB III - Leibnitz, 8030","look@aon.at ; Stelzl Hannelore, 0664 443 1077 Eigentümer",ATU43840000,Zahlschein,"Marburger Straße 13, 8430 Leibnitz",Wärmekunde,Bioenergie Leibnitzerfeld GmbH,5612,,,,,, +Neuhold,,Otto und Margit,20279,Marburger Straße 132a,8430,Leibnitz,"BLB III - Leibnitz, 8030",,,Zahlschein,"Marburger Straße 132a, 8430 Leibnitz",WLV / Anschluss offen,Bioenergie Leibnitzerfeld GmbH,5611,,,,,, +Albrecher,,Philip,20226,Marburger Straße 58,8435,Wagna,"BLB III - Leibnitz, 8030",,,Bankeinzug,"Marburger Straße 58, 8435 Wagna",Wärmekunde,Bioenergie Leibnitzerfeld GmbH,4412,,,,,, +Franz Krainer Fleisch u. Wurstwaren GmbH,,,20253,Marburger Straße 91,8435,Wagna,"BLB III - Leibnitz, 8030",AT52 2081 5000 0059 8078,ATU29679504,Zahlschein,"Marburger Straße 91, 8435 Wagna",WLV / Anschluss offen,Bioenergie Leibnitzerfeld GmbH,4758,,,,,, +Franz Krainer Fleisch u. Wurstwaren GmbH,,,20241,Marburger Straße 91,8435,Wagna,"BLB III - Leibnitz, 8030",,ATU29679504,Bankeinzug,"Marburger Straße 91, 8435 Wagna",Wärmekunde,Bioenergie Leibnitzerfeld GmbH,4539,,,,,, +"Autohaus Ornig & Co KG +Albert u. Robert Ornig",,,20067,Marburgerstraße 107,8435,Wagna,"BLB I - Leibnitz, 8010",,ATU55265509,Zahlschein,"Marburger Straße 107, 8435 Wagna",Wärmekunde,Bioenergie Leibnitzerfeld GmbH,1780,,,,,, +"W+M Projektentwicklungs-GmbH +z.H. HV Holler & Höfler Rechtsanwälte OG +",,,20133,Marburgerstraße 11,8430,Leibnitz,"BLB I - Leibnitz, 8010","30.03.2020 Marlene: ZE über Invest u. Wüst, Abr.2019 und Akontos 01-03/2020 versendet. In Summe 11.500,99 Euro +08.04.2020 Silvia: Abrechnung 2019 + INVEST + Akonto nochmals verschickt +05.05.2020 Marlene: Abrechnung 2019 bezahlt - 3.850,99 Euro, VS 01-03/2020 wurde gestundet",,Bankeinzug,"Marburger Straße 12-18, 8430 Leibnitz",Wärmekunde,Bioenergie Leibnitzerfeld GmbH,2103,,,,,, +Killigan,,Franz und Eva,20284,Marburgerstraße 121,8435,Wagna,"BLB III - Leibnitz, 8030",,,Bankeinzug,"Marburger Straße 121, 8435 Wagna",Wärmekunde,Bioenergie Leibnitzerfeld GmbH,5616,,,,,, +Nepel,,Karl,20283,Marburgerstraße 121a,8435,Wagna,"BLB III - Leibnitz, 8030",,,Zahlschein,"Marburger Straße 121a, 8435 Wagna",WLV / Anschluss offen,Bioenergie Leibnitzerfeld GmbH,5615,,,,,, +Neubauer,,Isabella,20254,Marburgerstraße 122,8435,Wagna,"BLB I - Leibnitz, 8010",,,Zahlschein,"Marburger Straße 122, 8435 Wagna",Wärmekunde,Bioenergie Leibnitzerfeld GmbH,4759,,,,,, +Fötsch,,Christine,20282,Marburgerstraße 123,8435,Wagna,"BLB III - Leibnitz, 8030",,,Bankeinzug,"Marburger Straße 123, 8435 Wagna",Wärmekunde,Bioenergie Leibnitzerfeld GmbH,5614,,,,,, +"Hotel - Restaurant Neuhold e.U. +Otto Neuhold",,,20106,Marburgerstraße 132,8435,Wagna,"BLB I - Leibnitz, 8010",,ATU29672903,Bankeinzug,"Marburger Straße 132, Hotel-Restaurant, 8435 Wagna",Wärmekunde,Bioenergie Leibnitzerfeld GmbH,1967,,,,,, +"Hotel - Restaurant Neuhold e.U. +Nebenhaus",,,20107,Marburgerstraße 132,8435,Wagna,"BLB I - Leibnitz, 8010",,ATU29672903,Bankeinzug,"Marburger Straße 132, Nebenhaus Hotel-Rest., 8435 Wagna",Wärmekunde,Bioenergie Leibnitzerfeld GmbH,1968,,,,,, +Binder,,Heinz,20146,Marburgerstraße 7,8430,Leibnitz,"BLB I - Leibnitz, 8010",,,,"Marburgerstraße 7, 8430 Leibnitz",Vorsorgeanschluss,Bioenergie Leibnitzerfeld GmbH,2148,,,,,, +Binder,,Gottfried,20082,Marburgerstraße 85,8435,Wagna,"BLB I - Leibnitz, 8010",,,Zahlschein,Marburger Straße 85,WLV / Anschluss offen,Bioenergie Leibnitzerfeld GmbH,1841,,,,,, +"Krampl Franz Ing. +Hartl Irene DI",,,20123,Marburgerstraße 85 b,8430,Leibnitz,"BLB I - Leibnitz, 8010",,,,"Marburger Straße 85 b, 8430 Leibnitz",WLV / Anschluss offen,Bioenergie Leibnitzerfeld GmbH,2045,,,,,, +Schwarzbauer sen. und jun.,,Franz,20124,Marburgerstraße 85 d,8435,Wagna,"BLB I - Leibnitz, 8010",,,Bankeinzug,"Marburger Straße 85 d, 8430 Leibnitz",Wärmekunde,Bioenergie Leibnitzerfeld GmbH,2046,,,,,, +Jarz,,Rosina,20083,Marburgerstraße 88,8435,Wagna,"BLB I - Leibnitz, 8010",,,,"Marburger Straße 90, Wohnhaus, 8435 Wagna",WLV / Anschluss offen,Bioenergie Leibnitzerfeld GmbH,1842,,,,,, +"Gottfried Binder +Gärtnerei Glashaus +Rosina Jarz",,,20084,Marburgerstraße 88,8435,Wagna,"BLB I - Leibnitz, 8010","14.07.20 Marlene: ZE versendet, gemeinsam mit Kundennr. 20085 + +10.08.2020 Michaela: Kunde Binder Gottfried zahlt für Kd 20084 und 20085 , ist Pächter dieser Gärtnerei (Fr. Hochstrasser ist Schwester von Herrn Binder und erledigt Zahlungen für ihn Tel: 0664/9689872)",,Zahlschein,"Marburger Straße 88, 8435 Wagna, Glashaus",Wärmekunde,Bioenergie Leibnitzerfeld GmbH,1843,,,,,, +"Gärtnerei Rosina Jarz +Gottfried Binder +",,,20085,Marburgerstraße 88,8435,Wagna,"BLB I - Leibnitz, 8010","25.06.19 Marlene: ZE über VS04-06/19 versendet EUR 900,- +06.08.19 Marlene: 1.Mahnung über VS04-09/19 versendet EUR 1.500,- , bezahlt + + +14.07.20 Marlene: ZE versendet, gemeinsam mit Kundennr. 20084 + +10.08.2020 Michaela: Kunde Binder Gottfried zahlt für Kd 20084 und 20085 , ist Pächter dieser Gärtnerei (Fr. Hochstrasser ist Schwester von Herrn Binder und erledigt Zahlungen für ihn Tel: 0664/9689872)",,Zahlschein,"Marburger Straße 88, Büro, 8435 Wagna",Wärmekunde,Bioenergie Leibnitzerfeld GmbH,1844,,,,,, +Krainer,,Berta,20081,Marburgerstraße 91 A,8435,Wagna,"BLB II - Leibnitz, 8020",,,Bankeinzug,"Marburger Straße 91 A, 8435 Wagna",Wärmekunde,Bioenergie Leibnitzerfeld GmbH,1833,,,,,, +Strauß Gesellschaft m.b.H. & Co KG,,,20072,Marburgerstraße 94,8435,Wagna,"BLB I - Leibnitz, 8010",,ATU29678603,Bankeinzug,"Marburger Straße 94, 8435 Wagna",Wärmekunde,Bioenergie Leibnitzerfeld GmbH,1795,,,,,, +Car Service A. W. Wallner,,,20058,Marburgerstraße 96,8435,Wagna,"BLB I - Leibnitz, 8010","24.05.19 Marlene: ZE über VS03-05/19 versendet EUR 1.110,-",ATU57744445,Bankeinzug,"Marburger Straße 96, 8435 Wagna",Wärmekunde,Bioenergie Leibnitzerfeld GmbH,1742,,,,,, +WBG - Wohnen u. Bauen GmbH,,,20056,Margaretengürtel 36-40,1050,Wien,"BLB I - Leibnitz, 8010","03.07.19 Marlene: Telefonat mit Jürgen - er muss sich hier noch reinlesen, er hat bis dato keine Infos + + +19.08.19 VERRECHNUNG ERFOLGT ÜBER ENERGIE CONTRACTING KROBATH GMBH KuNr 20026__sj + +",,Zahlschein,"Kirchengasse 20 a-c, 8435 Wagna",WLV / Eigentümer,Bioenergie Leibnitzerfeld GmbH,1739,,,,,, +"MEG Leibnitz, Wagnastraße 13-15 +p. A. Frieda Rustler GV GmbH & Co KG",,,20135,Mariahilfer Straße 196,1150,Wien,"BLB I - Leibnitz, 8010","HV alt: +HAUSVERWALTUNG: +hauverwaltung@silver-living.at",ATU58552836,Zahlschein,"Wagna Straße 13+15, Kinderkrippe + Betreutes Wohnen, 8430 Leibnitz",Wärmekunde,Bioenergie Leibnitzerfeld GmbH,2114,,,,,, +"WEG Bahnhofstraße 9a +p.A. Frieda Rustler GV GmbH & Co KG",,,20339,Mariahilfer Straße 196,1150,Wien,"BLB - ALL - Leibnitz, 8000",,ATU58552836,Zahlschein,"Bahnhofstraße 9a, WEG, 8430 Leibnitz",WLV / Anschluss offen,Bioenergie Leibnitzerfeld GmbH,8374,,,,,, +Röm.-kath. Pfarrkirche Wagna,,,20187,Marktplatz 1,8435,Wagna,"BLB I - Leibnitz, 8010",,,Bankeinzug,"Marktplatz 1, 8435 Wagna",Wärmekunde,Bioenergie Leibnitzerfeld GmbH,3960,,,,,, +KS Bauentwicklung GmbH,,,20171,Matzelsdorf 70,8411,Hengsberg,"BLB I - Leibnitz, 8010",,ATU56165866,Zahlschein,"Bahnhofstraße 21, 8430 Leibnitz",Wärmekunde,Bioenergie Leibnitzerfeld GmbH,2226,,,,,, +hallo business park gmbh,,,20151,Mitterhoferweg 26,8430,Leibnitz,"BLB I - Leibnitz, 8010",,ATU70371812,Zahlschein,"Landscha an der Mur, 8424 Gabersdorf",Vertragsende,Bioenergie Leibnitzerfeld GmbH,2175,,,,,, +Österr. Wohnbaugen. gemeinn. reg. GenmbH,,,20114,Moserhofgasse 14,8010,Graz,"BLB I - Leibnitz, 8010",,ATU44311605,Bankeinzug,"Föhrenbaumstraße 20 / 20a, 8435 Wagna",Wärmekunde,Bioenergie Leibnitzerfeld GmbH,1996,,,,,, +Österr. Wohnbaugen. gemeinn. reg. GenmbH,,,20113,Moserhofgasse 14,8010,Graz,"BLB I - Leibnitz, 8010",,ATU44311605,Bankeinzug,"Föhrenbaumstraße 14/14a, 8435 Wagna",Wärmekunde,Bioenergie Leibnitzerfeld GmbH,1995,,,,,, +Österr. Wohnbaugen. gemeinn. reg. GenmbH,,,20115,Moserhofgasse 14,8010,Graz,"BLB I - Leibnitz, 8010",,ATU44311703,Bankeinzug,"Föhrenbaumstraße 22 / 22a, 8435 Wagna",Wärmekunde,Bioenergie Leibnitzerfeld GmbH,1997,,,,,, +Österr. Wohnbaugen. gemeinn. reg. GenmbH,,,20112,Moserhofgasse 14,8010,Graz,"BLB I - Leibnitz, 8010",,ATU44311605,Bankeinzug,"Föhrenbaumstraße 5/5a/5b, 8435 Wagna",Wärmekunde,Bioenergie Leibnitzerfeld GmbH,1994,,,,,, +"Österr. Wohnbaugen. gemein. reg. GenmbH +z.H. Hausmanagement-Technik",,,20110,Moserhofgasse 14,8010,Graz,"BLB I - Leibnitz, 8010",,ATU44311605,Bankeinzug,"Eisenbahnerstraße 2, 8435 Wagna, Auftragsnr.: 20/22497",Wärmekunde,Bioenergie Leibnitzerfeld GmbH,1992,,,,,, +Österr. Wohnbaugen. gemeinn. reg. GenmbH,,,20111,Moserhofgasse 14,8010,Graz,"BLB I - Leibnitz, 8010",,ATU44311605,Bankeinzug,"Föhrenbaumstraße 3/3a/3b/, 8435 Wagna",Wärmekunde,Bioenergie Leibnitzerfeld GmbH,1993,,,,,, +ÖWGES gemeinnützige Wohnbaugesellschaft m.b.H.,,,20117,Moserhofgasse 14,8010,Graz,"BLB I - Leibnitz, 8010",,ATU44311703,,"Föhrenbaumstraße 7 / 7a / 7 b, 8435 Wagna",WLV / Anschluss offen,Bioenergie Leibnitzerfeld GmbH,1999,,,,,, +ÖWGES gemeinnützige Wohnbaugesellschaft m.b.H.,,,20116,Moserhofgasse 14,8010,Graz,"BLB I - Leibnitz, 8010",,ATU44311703,,"Föhrenbaumstraße 9a / 9b, 8435 Wagna",WLV / Anschluss offen,Bioenergie Leibnitzerfeld GmbH,1998,,,,,, +Energie Contracting Krobath GmbH,,,20026,Mühlgasse 1,8330,Feldbach,"BLB I - Leibnitz, 8010"," + +",ATU56952827,Zahlschein,"Kirchengasse 20 a-c, 8435 Wagna","Abre. monatl.,Wärmekunde",Bioenergie Leibnitzerfeld GmbH,1573,,,,,, +Roru Immobilien GmbH,,,20209,Nestelberg 57,8452,Großklein,"BLB III - Leibnitz, 8030",,ATU71144059,,"Hauptstraße 23, 8435 Wagna",WLV / Anschluss offen,Bioenergie Leibnitzerfeld GmbH,4149,,,,,, +Url ,,Elfriede,20155,Obere Dorfstraße 17,8461,Obervogau,"BLB I - Leibnitz, 8010",,,Die Überweisung erfolgt....,,Gestattungen/Leitungsrechte,Bioenergie Leibnitzerfeld GmbH,2187,,,,,, +Umdasch Store Makers Leibnitz GmbH,,,20050,Ottokar Kernstock Gasse 16,8430,Leibnitz,"BLB I - Leibnitz, 8010","franz.kleindienst@umdasch.com + +eingangsrechnungen@umdasch.com",ATU29607002,Zahlschein,"Ottokar Kernstock Gasse 16, 8430 Leibnitz","Abre. monatl.,Wärmekunde",Bioenergie Leibnitzerfeld GmbH,1717,,,,,, +Leodolter,,Roswitha,20142,Ottokar Kernstock Gasse 8,8430,Leibnitz,"BLB I - Leibnitz, 8010",,,Bankeinzug,"Ottokar Kernstock Gasse 8, 8430 Leibnitz",Wärmekunde,Bioenergie Leibnitzerfeld GmbH,2123,,,,,, +"Brau Union Österreich AG +p.A. Schauersberg Immob. Ges.m.b.H.",,,20197,Plüddemanngasse 104,8042,Graz,"BLB III - Leibnitz, 8030",,ATU2323106,,"Retzhoferstraße 2, 8430 Leibnitz",WLV / Anschluss offen,Bioenergie Leibnitzerfeld GmbH,4137,,,,,, +"HG Kapellenweg 10,12,14 und Wagnastr. 27,29,31 +z.H. GWS Gemein.Alpenländische Gesellschaft für Wohnungsbau und Siedlungswesen m.b.H.",,,20104,Plüddemanngasse 107,8042,Graz,"BLB I - Leibnitz, 8010",02.03.2021 Michaela: Bankeinzug ab 03/2021 GWS,,Bankeinzug,"Kapellenweg 10-12-14, Wagnastr. 27-29-31, 8430 Leibnitz",Wärmekunde,Bioenergie Leibnitzerfeld GmbH,1965,,,,,, +"WEG Siedlungsstraße 13-15a +z.H. GWS Gemeinn. Alpenl. Ges. für Wohnbau u. Siedlungswesen m.b.H.",,,20153,Plüddemanngasse 107,8042,Graz,"BLB I - Leibnitz, 8010"," +Herr Löwig wegen Schlüssel: 0664 8054 221 + +",ATU55169105,Bankeinzug,"Siedlungsstraße 13 -15a, 8435 Wagna",Wärmekunde,Bioenergie Leibnitzerfeld GmbH,2185,,,,,, +"Steiermärkische Krankenanstaltengesellschaft m.b.H. +Zentraler Rechnungseingang",,,20012,Postfach 1500,8000,Graz,"BLB I - Leibnitz, 8010","kerstin.payer@kages.at +Abrechnung 2019 + Gutschriften geschickt an kerstin.payer@kages.at 19.2.2020 + +21.02.2023 Silvia: ReAdresse per Post richtiggestellt und E-Mail ",ATU28619206,Zahlschein,"Pelzmannstraße 18, 8435 Wagna",Wärmekunde,Bioenergie Leibnitzerfeld GmbH,1523,,,,,, +Immobilientreuhand Färber-Lang e.U.,,,20303,Prebuch 18,8211,Albersdorf-Prebuch,"BLB III - Leibnitz, 8030",,ATU69957825,Zahlschein,"Marburger Straße 67/67a, Wohnhaus, 8435 Wagna",Wärmekunde,Bioenergie Leibnitzerfeld GmbH,7039,,,,,, +Rannacher,Dr.,Peter,20261,Retzhofer Straße 3,8430,Leibnitz,"BLB III - Leibnitz, 8030",,,Zahlschein,"Retzhoferstraße 3, 8430 Leibnitz",WLV / Anschluss offen,Bioenergie Leibnitzerfeld GmbH,4844,,,,,, +Knipitsch,,Susanne,20262,Sackgasse 13,8435,Leitring,"BLB III - Leibnitz, 8030",,,Bankeinzug,"Adalbert Stifter-Weg 1, 8430 Leibnitz",Wärmekunde,Bioenergie Leibnitzerfeld GmbH,5089,,,,,, +"Volkshilfe Steiermark +gemeinnützige Betriebs-GmbH",,,20007,Sackstraße 20/1,8010,Graz,"BLB I - Leibnitz, 8010",,,Zahlschein,"Metilka Straße 9, 8435 Wagna, Seniorenzentrum Wagna",Wärmekunde,Bioenergie Leibnitzerfeld GmbH,1482,,,,,, +DI Dr. Wolfgang Ringer,,,20096,Schillerstraße 9,4800,Attnang-Puchheim,"BLB I - Leibnitz, 8010",,,Die Überweisung erfolgt....,,Gestattungen/Leitungsrechte,,1878,,,,,, +Königshofer,,Herwig,20244,Schubert-Straße 21,8430,Leibnitz,"BLB III - Leibnitz, 8030",,,Zahlschein,,WLV / Anschluss offen,Bioenergie Leibnitzerfeld GmbH,4636,,,,,, +Gupper,,Marina,20245,Schubert-Straße 21,8430,Leibnitz,"BLB III - Leibnitz, 8030",,,Zahlschein,,WLV / Anschluss offen,Bioenergie Leibnitzerfeld GmbH,4640,,,,,, +Sandt,,Martin,20246,Schubert-Straße 21,8430,Leibnitz,"BLB III - Leibnitz, 8030",,,Zahlschein,,WLV / Anschluss offen,Bioenergie Leibnitzerfeld GmbH,4641,,,,,, +Jakomini,,Sandra,20161,Schubertstraße 10,8430,Leibnitz,"BLB I - Leibnitz, 8010",,,Zahlschein,"Schubertstraße 10, 8430 Leibnitz",Wärmekunde,Bioenergie Leibnitzerfeld GmbH,2208,,,,,, +Glatzhofer Heribert,,,20306,Schubertstraße 15,8430,Leibnitz,"BLB III - Leibnitz, 8030",Glatzhofer Armin Mag. ist Miteigentümer,,Zahlschein,"Schubertstraße 15, 8430 Leibnitz",Wärmekunde,Bioenergie Leibnitzerfeld GmbH,7804,,,,,, +Schumacher,Dr.,Alfred und Michaela ,20198,Schubertstraße 8,8430,Leibnitz,"BLB III - Leibnitz, 8030",,,Zahlschein,"Schubertstraße 8, 8430 Leibnitz",Wärmekunde,Bioenergie Leibnitzerfeld GmbH,4138,,,,,, +"Eigentumsgem. Metilka Straße 15, 8435 Wagna +c/o Gemn. Wohn- u. Siedlungsgen. Ennstal reg. Gen.m.b.H. ",,,20180,Siedlungsstraße 2,8940,Liezen,"BLB I - Leibnitz, 8010"," +Ettlmayr Patrick ",ATU64628166,Zahlschein,"Metilka Straße 15, 8435 Wagna",Wärmekunde,Bioenergie Leibnitzerfeld GmbH,3014,,,,,, +"Eigentumsgem. Richard Wagner-Weg 18E/F +c/o Siedl. gen. Ennstal",,,20271,Siedlungsstraße 2,8940,Liezen,"BLB III - Leibnitz, 8030",,,Zahlschein,"Richard Wagner-Weg 18E/F, 8430 Leibnitz",WLV / Anschluss offen,Bioenergie Leibnitzerfeld GmbH,5270,,,,,, +"Eigentumsgem. Richard Wagner-Weg 6/8/10/12/14 +c/o Siedl. gen. Ennstal",,,20266,Siedlungsstraße 2,8940,Liezen,"BLB III - Leibnitz, 8030",,,Zahlschein,"Richard Wagner-Weg 6/8/10/12/14, 8430 Leibnitz",WLV / Anschluss offen,Bioenergie Leibnitzerfeld GmbH,5264,,,,,, +"Eigentumsgem. Richard Wagner-Weg 18/18A +c/o Siedl. gen. Ennstal",,,20269,Siedlungsstraße 2,8940,Liezen,"BLB III - Leibnitz, 8030",,,Zahlschein,"Richard Wagner-Weg 18/18A, 8430 Leibnitz",WLV / Anschluss offen,Bioenergie Leibnitzerfeld GmbH,5268,,,,,, +"Eigentumsgem. Richard Wagner-Weg 18B/C/D/E +c/o Siedl. gen. Ennstal",,,20270,Siedlungsstraße 2,8940,Liezen,"BLB III - Leibnitz, 8030",,,Zahlschein,"Richard Wagner-Weg 18B/C/D/E, 8430 Leibnitz",WLV / Anschluss offen,Bioenergie Leibnitzerfeld GmbH,5269,,,,,, +"Eigentumsgem. Dr. Schachner Weg 4 +c/o Siedl. gen. Ennstal",,,20267,Siedlungsstraße 2,8940,Liezen,"BLB III - Leibnitz, 8030",,,Zahlschein,"Dr. Schachner Weg 4, 8430 Leibnitz",WLV / Anschluss offen,Bioenergie Leibnitzerfeld GmbH,5266,,,,,, +"Eigentumsgem. Dr. Schachner Weg 3 +c/o Siedl. gen. Ennstal",,,20268,Siedlungsstraße 2,8940,Liezen,"BLB III - Leibnitz, 8030",,,Zahlschein,"Dr. Schachner Weg 3, 8430 Leibnitz",WLV / Anschluss offen,Bioenergie Leibnitzerfeld GmbH,5267,,,,,, +"Eigentumsgem. Eisenbahnerstraße 7, 8435 Wagna +c/o Gemn. Wohn- u. Siedlungsgen. Ennstal reg. Gen.m.b.H. ",,,20185,Siedlungsstraße 2,8940,Liezen,"BLB I - Leibnitz, 8010"," +Ettlmayr Patrick ",ATU59479237,Zahlschein,"Eisenbahnerstraße 7, 8435 Wagna",Wärmekunde,Bioenergie Leibnitzerfeld GmbH,3518,,,,,, +"13 Familienwohnhaus Wagna +z. H. Gem. Wohn- u. Siedlungsgen. Ennstal reg. Gen.m.b.H.",,,20206,Siedlungsstraße 2,8940,Liezen,"BLB III - Leibnitz, 8030",,,Zahlschein,"Kirchengasse 15, 15a, 15b, 8435 Wagna",Wärmekunde,Bioenergie Leibnitzerfeld GmbH,4146,,,,,, +"EG Mitterfeldweg 1, Wagna +c/o Siedl. gen. Ennstal",,,20186,Siedlungsstraße 2,8940,Liezen,"BLB I - Leibnitz, 8010",,,Zahlschein,"Mitterfeldweg 1, 8435 Wagna",Wärmekunde,Bioenergie Leibnitzerfeld GmbH,3899,,,,,, +"Eigentumsgem. Hauptstraße Wagna 6, 8435 Wagna +c/o Gemn. Wohn- u. Siedlungsgen. Ennstal reg. Gen.m.b.H. ",,,20184,Siedlungsstraße 2,8940,Liezen,"BLB I - Leibnitz, 8010",,ATU72987636,Zahlschein,"Hauptstraße Wagna 6, 8435 Wagna",Wärmekunde,Bioenergie Leibnitzerfeld GmbH,3501,,,,,, +"Eigentumsgem. Hauptstraße Wagna 8, 8a, 8435 Wagna +c/o Gemn. Wohn- u. Siedlungsgen. Ennstal reg. Gen.m.b.H. ",,,20181,Siedlungsstraße 2,8940,Liezen,"BLB I - Leibnitz, 8010",,ATU64627149,Zahlschein,"Hauptstraße Wagna 8, 8a, 8435 Wagna",Wärmekunde,Bioenergie Leibnitzerfeld GmbH,3476,,,,,, +"Eigentumsgem. Hauptstraße Wagna 4, 8435 Wagna +c/o Gemn. Wohn- u. Siedlungsgen. Ennstal reg. Gen.m.b.H. ",,,20183,Siedlungsstraße 2,8940,Liezen,"BLB I - Leibnitz, 8010",,ATU67716288,Zahlschein,"Hauptstraße Wagna 4, 8435 Wagna",Wärmekunde,Bioenergie Leibnitzerfeld GmbH,3498,,,,,, +"Eigentumsgem. Hauptstraße Wagna 4a, 6b, 6c, 8435 Wagna +c/o Gemn. Wohn- u. Siedlungsgen. Ennstal reg. Gen.m.b.H. ",,,20182,Siedlungsstraße 2,8940,Liezen,"BLB I - Leibnitz, 8010",,ATU66967446,Zahlschein,"Hauptstraße Wagna 4a, 6b, 6c, 8435 Wagna",Wärmekunde,Bioenergie Leibnitzerfeld GmbH,3494,,,,,, +"Gemn. Wohn- u. Siedlungsgen. Ennstal +f. WH Josef-Maier-Straße 16b, 16c, Wagna",,,20136,Siedlungsstraße 2,8940,Liezen,"BLB I - Leibnitz, 8010",,ATU38296802,Zahlschein,"Josef-Maier-Straße 16b, 16c, 8435 Wagna; Auftr.-Nr.8301/190944/606/1",Wärmekunde,Bioenergie Leibnitzerfeld GmbH,2115,,,,,, +"Gemn. Wohn- u. Siedlungsgen. Ennstal reg. Gen.m.b.H. +f. Wohnhaus Gemeindehausstraße 11, 8435 Wagna",,,20137,Siedlungsstraße 2,8940,Liezen,"BLB I - Leibnitz, 8010",,ATU38296802,Zahlschein,"Gemeindehausstraße 11, 8435 Wagna; Auftr.-Nr.123201/190944/606/1",Wärmekunde,Bioenergie Leibnitzerfeld GmbH,2116,,,,,, +"Eigentumsgemeinschaft Josef Maier Straße 2,2a, 8435 Wagna / 876 02 +z.H. Gemn. Wohn- u. Siedlungsgen. Ennstal",,,20070,Siedlungsstraße 2,8940,Liezen,"BLB I - Leibnitz, 8010","30.03.20 Marlene: ZE für Abr. 2019, abzüglich WB und VS 01-03/2020 gesendet: in Summe EUR 3.329,62 - Abrechnung 2019 in Höhe von 4.229,62 bezahlt, OP per 16.04.20: 300 Euro Guthaben, da Wärmebonus bei Zahlung nicht abgezogen wurde!",ATU59479200,Zahlschein,"Josef-Maier-Straße 2,2a +, 8435 Wagna",Wärmekunde,Bioenergie Leibnitzerfeld GmbH,1783,,,,,, +"Marktgemeinde Wagna +für Wohnobjekt Flavia Solva Str. 15, 8435 Wagna +z.H. Gemn. Wohn- u. Siedlungsgen. Ennstal",,,20065,Siedlungsstraße 2,8940,Liezen,"BLB I - Leibnitz, 8010",,ATU38296802,,"Flavia Solva Straße 15, 8435 Wagna",Stornierung WLV,Bioenergie Leibnitzerfeld GmbH,1767,,,,,, +"Gemn. Wohn- u. Siedlungsgen. Ennstal reg. Gen.m.b.H. +f. Wohnhaus Hauptstraße Wagna 38, 8435 Wagna",,,20138,Siedlungsstraße 2,8940,Liezen,"BLB I - Leibnitz, 8010",,ATU38296802,Zahlschein,"Hauptstraße Wagna 38, 8435 Wagna; Auftr.-Nr.148301/190944/606/1",Wärmekunde,Bioenergie Leibnitzerfeld GmbH,2117,,,,,, +Seiser,,Andreas,20125,Siedlungsstraße 25,8435,Wagna,"BLB I - Leibnitz, 8010",9.4.2020 Silvia: Ortner Carmen zahlt für Seiser,,Bankeinzug,"Siedlungsstraße 25, 8435 Wagna",Wärmekunde,Bioenergie Leibnitzerfeld GmbH,2047,,,,,, +Oswald,,Othmar,20121,Südbahnstraße 35,8430,Leibnitz,"BLB I - Leibnitz, 8010",,ATU56688800,,"Südbahnstraße 35, 8430 Leibnitz",Stornierung WLV,Bioenergie Leibnitzerfeld GmbH,2043,,,,,, +Totz,,Maria und Günther,20087,Tannenweg 3,8435,Wagna,"BLB I - Leibnitz, 8010",,,Bankeinzug,"Tannenweg 3, 8435 Wagna",Wärmekunde,Bioenergie Leibnitzerfeld GmbH,1846,,,,,, +Reiter,,Angelika,20277,Tieschen Patzen 69,8355,Tieschen,"BLB III - Leibnitz, 8030",,,Bankeinzug,"Lastenstraße 13, 8430 Leibnitz",Wärmekunde,Bioenergie Leibnitzerfeld GmbH,5606,,,,,, +"Bundesimmobiliengesellschaft m.b.H. +OMF Team Steiermark",,,20296,Trabrennstraße 2c,1020,Wien,"BLB III - Leibnitz, 8030",,ATU38270401,Zahlschein,"Wagnastraße 6, BG/BRG Leibnitz, 8430 Leibnitz",Wärmekunde,Bioenergie Leibnitzerfeld GmbH,6118,,,,,, +Bundesimmobiliengesellschaft m.b.H.,,,20314,Trabrennstraße 2c,1020,Wien,"BLB III - Leibnitz, 8030",,ATU38270401,Zahlschein,"Mariengasse 2, Polizei, 8430 Leibnitz",WLV / Anschluss offen,Bioenergie Leibnitzerfeld GmbH,7881,,,,,, +"Compass Seniorenwohnheime GmbH +Gerlinde Sollhart MAS",,,20168,Türkengasse 5,8430,Leibnitz,"BLB I - Leibnitz, 8010",,ATU64064924,Bankeinzug,"Türkengasse 5, 8430 Leibnitz",Wärmekunde,Bioenergie Leibnitzerfeld GmbH,2216,,,,,, +Seidl-Fischer,,Katrin,20328,Viktor-Kaplan-Straße 12a,8430,Leibnitz,"BLB - ALL - Leibnitz, 8000",,,Zahlschein,"Pelzmannstraße 5, 8435 Wagna",WLV / Anschluss offen,Bioenergie Leibnitzerfeld GmbH,7963,,,,,, +Immocon Immobilien Consulting GmbH,,,20264,Volksgartenstraße 1,8020,Graz,"BLB III - Leibnitz, 8030",,ATU54512905,Zahlschein,"Karl Morre-Gasse 1, 8430 Leibnitz",Wärmekunde,Bioenergie Leibnitzerfeld GmbH,5262,,,,,, +"WEG Sackgasse 3b +c/o IH IMMOBILIEN HOLDING Gesellschaft m.b.H.",,,20039,Vorstadtgasse 1,8570,Voitsberg,"BLB I - Leibnitz, 8010","23.05.19 Marlene: ZE über Jahresabr.18 und VS 01-05/19 gesendet +24.06.19 Marlene: 1.Mahnung über Abr.2018 und VS01-06/19 gesendet +05.08.19 Marlene: 2. Mahnung über Abr.2018 und VS01-08/19 versendet: EUR 2.633,39 +03.09.19 Marlene: Letzte Mahnung versendet inkl. Verzugszinsen und Mahnspesen: EUR 2.719,88 +20.10.19 Marlene: Mail an Jürgen für Freigabe INKASSO +",,Zahlschein,"WEG Sackgasse 3b, 8435 Wagna",Vertragsende,Bioenergie Leibnitzerfeld GmbH,1618,,,,,, +"WEG Sackgasse 3a +c/o IH IMMOBILIEN HOLDING Gesellschaft m.b.H.",,,20040,Vorstadtgasse 1,8570,Voitsberg,"BLB I - Leibnitz, 8010","23.05.19 Marlene: ZE über Abr.2018 und VS01-05/19 versendet EUR 1.949,32 +24.06.19 Marlene: 1.Mahnung Abr.2018 und VS01-06/19 versendet EUR 2.085,32 +05.08.19 Marlene: 2.Mahnung Abr.2018 und VS01-08/19 versender EUR 2.357,32 +03.09.19 Marlene: Letzte Mahnung inkl. Verzugszinsen und Mahnspesen versendet EUR 2.439,72 +21.10.19 Marlene: Mail an Jürgen für Freigabe Inkasso",ATU55517202,Zahlschein,"WEG Sackgasse 3a, 8435 Wagna",Vertragsende,Bioenergie Leibnitzerfeld GmbH,1622,,,,,, +Stangl,,Christa,20240,Wagnastraße 10,8430,Leibnitz,"BLB III - Leibnitz, 8030",,,,"Wagnastraße 10, 8430 Leibnitz",WLV / Anschluss offen,Bioenergie Leibnitzerfeld GmbH,4538,,,,,, +Kos,,Kilian,20199,Wagnastraße 16,8430,Leibnitz,"BLB III - Leibnitz, 8030",,,,Wagnastraße 16. 8430 Leibnitz,WLV / Anschluss offen,Bioenergie Leibnitzerfeld GmbH,4139,,,,,, +Pennitz,,Peter,20210,Wagnastraße 25,8430,Leibnitz,"BLB III - Leibnitz, 8030",,,,"Wagnastraße 25, 8430 Leibnitz",WLV / Anschluss offen,Bioenergie Leibnitzerfeld GmbH,4151,,,,,, +Hoppacher,,Wolfgang,20093,Wagnastraße 8,8430,Leibnitz,"BLB I - Leibnitz, 8010","06.05.2020 Michaela: ZE VZ 01-05/2020 und Re 2020023 +14.07.20 Marlene: E-Mail an Herrn Hoppacher geschrieben, offen sind 4.670,- Euro in Summe +17.08.2020 Michaela. 1. Mahnun güber Re 2020023 minus GS 2020044 € 4270,00 in Summe",,Zahlschein,"Wagna Straße 8, 8430 Leibnitz",Wärmekunde,Bioenergie Leibnitzerfeld GmbH,1864,,,,,, +Lift,,Friedrich,20205,Willhelm Kienzl Gasse 2,8430,Leibnitz,"BLB III - Leibnitz, 8030",,,Zahlschein,"Willhelm Kienzl Gasse 2, 8430 Leibnitz",Wärmekunde,Bioenergie Leibnitzerfeld GmbH,4145,,,,,, +Klausenburg Immobilienvermietung GmbH&Co KG,,,20233,Wollzeile 31/24,1010,Wien,"BLB III - Leibnitz, 8030",,ATU66701125,Bankeinzug,"Marburger Straße 67/67a, Wohnhaus, 8435 Wagna",Vertragsende,Bioenergie Leibnitzerfeld GmbH,4466,,,,,, diff --git a/scripts/preorder/leibnitz/import-leibnitz-interests.php b/scripts/preorder/leibnitz/import-leibnitz-interests.php new file mode 100644 index 000000000..1b94520db --- /dev/null +++ b/scripts/preorder/leibnitz/import-leibnitz-interests.php @@ -0,0 +1,244 @@ +#!/usr/bin/php +id) return null; + + $w = new ADBWohneinheit($payload['wohneinheit_id']); + if (!$w->id) return null; + + // Determine status based on the address/unit status, like in the controller + $status_code = max($w->status->code, $h->status->code); + $new_status = PreorderstatusModel::getFirst(["code" => $status_code]); + + $data = [ + 'preordercampaign_id' => $payload['campaign_id'], + 'adb_hausnummer_id' => $h->id, + 'adb_wohneinheit_id' => $w->id, + 'status_id' => $new_status ? $new_status->id : 1, + 'type' => 'interest', + 'connection_type' => 'single-dwelling', + 'accept_agb' => 1, + 'accept_dsgvo' => 1, + 'company' => $payload['customer']['company'], + 'firstname' => $payload['customer']['firstname'], + 'lastname' => $payload['customer']['lastname'], + 'street' => $payload['customer']['street'], + 'housenumber' => $payload['customer']['housenumber'], + 'zip' => $payload['customer']['zip'], + 'city' => $payload['customer']['city'], + 'phone' => $payload['customer']['phone'], + 'email' => $payload['customer']['email'], + 'submit_type' => 'import', // MODIFIED: Shortened value again to prevent truncation error + 'create_by' => $payload['user_id'], + 'edit_by' => $payload['user_id'], + ]; + + $preorder = PreorderModel::create($data); + $preorder->createUcode(); + + if ($preorder->save()) { + return $preorder; + } + return null; +} + +/** + * Generates variations of a street name to improve search matching. + */ +function generateStreetVariations($street_name) { + $variations = [$street_name]; + if(strpos($street_name, ' ') !== false) { + $variations[] = str_replace(' ', '-', $street_name); + $variations[] = str_replace(' ', '', $street_name); + } + if(strpos($street_name, '-') !== false) { + $variations[] = str_replace('-', ' ', $street_name); + $variations[] = str_replace('-', '', $street_name); + } + if(strpos($street_name, '.') !== false) { + $variations[] = str_replace('.', '. ', $street_name); + $variations[] = str_replace('.', '.-', $street_name); + } + if(strpos($street_name, '. ') !== false) $variations[] = str_replace('. ', '.', $street_name); + if(strpos($street_name, '.-') !== false) $variations[] = str_replace('.-', '.', $street_name); + + $base_variations = $variations; + foreach($base_variations as $variation) { + if(strpos($variation, 'ß') !== false) $variations[] = str_replace('ß', 'ss', $variation); + if(strpos($variation, 'ss') !== false) $variations[] = str_replace('ss', 'ß', $variation); + if(strpos($variation, 'ä') !== false) $variations[] = str_replace('ä', 'ae', $variation); + if(strpos($variation, 'ae') !== false) $variations[] = str_replace('ae', 'ä', $variation); + if(strpos($variation, 'ö') !== false) $variations[] = str_replace('ö', 'oe', $variation); + if(strpos($variation, 'oe') !== false) $variations[] = str_replace('oe', 'ö', $variation); + if(strpos($variation, 'ü') !== false) $variations[] = str_replace('ü', 'ue', $variation); + if(strpos($variation, 'ue') !== false) $variations[] = str_replace('ue', 'ü', $variation); + } + return array_unique($variations); +} + +// --- SCRIPT INITIALIZATION --- +$me = new User(1); +$adb = FronkDB::singleton(ADDRESSDB_DBHOST, ADDRESSDB_DBUSER, ADDRESSDB_DBPASS, ADDRESSDB_DBNAME); +$db = FronkDB::singleton(); +$log = mfLoghandler::singleton(); + +$inputFilename = __DIR__."/Kundenliste Leibnitz Markierung Zentrum.csv"; +$outputFilename = __DIR__."/leibnitz-import-results.csv"; + +$inputFile = fopen($inputFilename, "r"); +$outputFile = fopen($outputFilename, "w"); + +if (!$inputFile) die("Error: Could not open input file: $inputFilename\n"); +if (!$outputFile) die("Error: Could not open output file: $outputFilename\n"); + +$preorder_campaign_id = 99; +$campaign_check = new Preordercampaign($preorder_campaign_id); +if (!$campaign_check->id) { + die("Error: Preorder Campaign with ID $preorder_campaign_id does not exist. Aborting.\n"); +} + +echo "Starting preorder import for Campaign ID: $preorder_campaign_id...\n"; + +// --- MAIN PROCESSING LOOP --- +$rowCount = 0; $successCount = 0; $skippedCount = 0; +$header = fgetcsv($inputFile); +$header[] = 'Import-Status'; +fputcsv($outputFile, $header); + +while (($csv = fgetcsv($inputFile, 0, ",")) !== FALSE) { + $rowCount++; + $statusMessage = ''; + + $anschluss_adresse_raw = trim($csv[11]); + + if (empty($anschluss_adresse_raw)) { + $statusMessage = "Skipped: 'Objekt' column (Anschlussadresse) is empty."; + $csv[] = $statusMessage; fputcsv($outputFile, $csv); $skippedCount++; continue; + } + + $strasse_name = ''; $hausnummer_name = ''; $plz_name = ''; $ort_name = ''; + if (preg_match('/^(.+?)\s+([\d\/a-zA-Z\.\s-]+)[\s,]+(\d{4})[\s,]+(.+)$/i', $anschluss_adresse_raw, $m)) { + $strasse_name = trim($m[1]); $hausnummer_name = trim($m[2]); $plz_name = trim($m[3]); $ort_name = trim($m[4]); + } else { + $statusMessage = "Skipped: Could not parse Anschlussadresse from 'Objekt': '$anschluss_adresse_raw'"; + $csv[] = $statusMessage; fputcsv($outputFile, $csv); $skippedCount++; continue; + } + + $sql_gemeinde = "SELECT DISTINCT g.id FROM Gemeinde g JOIN Plz p ON p.gemeinde_id = g.id JOIN Ortschaft o ON o.gemeinde_id = g.id WHERE p.plz = '".$adb->escape($plz_name)."' AND o.name = '".$adb->escape($ort_name)."'"; + $res_gemeinde = $adb->query($sql_gemeinde); + + if (!$adb->num_rows($res_gemeinde)) { + $statusMessage = "Skipped: Gemeinde not found for PLZ: $plz_name, Ort: $ort_name"; + $csv[] = $statusMessage; fputcsv($outputFile, $csv); $skippedCount++; continue; + } + $gemeinde_data = $adb->fetch_object($res_gemeinde); + $gemeinde_id = $gemeinde_data->id; + + $strasse_variations = generateStreetVariations($strasse_name); + $escaped_variations = array_map(fn($var) => $adb->escape($var), $strasse_variations); + + $sql_haus = "SELECT * FROM view_hausnummer WHERE gemeinde_id = $gemeinde_id AND strasse IN ('". implode("', '", $escaped_variations)."') AND hausnummer = '".$adb->escape($hausnummer_name)."'"; + $res_haus = $adb->query($sql_haus); + + if (!$adb->num_rows($res_haus)) { + $statusMessage = "Skipped: Anschlussadresse not found in DB: '$strasse_name $hausnummer_name'"; + $csv[] = $statusMessage; fputcsv($outputFile, $csv); $skippedCount++; continue; + } + $hausnummer_data = $adb->fetch_object($res_haus); + + $wohneinheiten = ADBWohneinheitModel::search(['hausnummer_id' => $hausnummer_data->hausnummer_id]); + $available_unit = null; + if (count($wohneinheiten) > 0) { + foreach($wohneinheiten as $unit) { + if (!PreorderModel::getFirst(['adb_wohneinheit_id' => $unit->id])) { + $available_unit = $unit; break; + } + } + } else { + $statusMessage = "Skipped: No Wohneinheiten found for Hausnummer ID ".$hausnummer_data->hausnummer_id; + $csv[] = $statusMessage; fputcsv($outputFile, $csv); $skippedCount++; continue; + } + + if (!$available_unit) { + $statusMessage = "Skipped: All units at this address already have a preorder."; + $csv[] = $statusMessage; fputcsv($outputFile, $csv); $skippedCount++; continue; + } + + $kunde_name_firma = trim($csv[0]); + $kunde_vorname = trim($csv[2]); + $kunde_strasse_raw = trim($csv[4]); + + $kunde_strasse = $kunde_strasse_raw; + $kunde_hausnummer = null; + if (preg_match('/^(.+?)\s+([\d\w\/.-]+)$/', $kunde_strasse_raw, $m_customer_addr)) { + $kunde_strasse = trim($m_customer_addr[1]); + $kunde_hausnummer = trim($m_customer_addr[2]); + } + + $m_email = []; + // MODIFIED: Corrected the regex by removing an extra hyphen to fix the compilation warning + preg_match('/[a-z0-9._%+-]+@[a-z0-9.-]+\.[a-z]{2,}/i', trim($csv[8]), $m_email); + + // Assemble payload and call the new creation function + $payload = [ + 'campaign_id' => $preorder_campaign_id, + 'hausnummer_id' => $hausnummer_data->hausnummer_id, + 'wohneinheit_id' => $available_unit->id, + 'user_id' => $me->id, + 'customer' => [ + 'company' => ($kunde_name_firma && !$kunde_vorname) ? $kunde_name_firma : null, + 'firstname' => $kunde_vorname, + 'lastname' => ($kunde_vorname) ? $kunde_name_firma : '', + 'street' => $kunde_strasse, + 'housenumber' => $kunde_hausnummer, + 'zip' => trim($csv[5]), + 'city' => trim($csv[6]), + 'phone' => trim($csv[19]), + 'email' => $m_email[0] ?? '', + ] + ]; + + $newPreorder = createPreorderEntry($payload); + + if ($newPreorder) { + $statusMessage = "Success: Preorder created with code " . $newPreorder->ucode; + $successCount++; + } else { + $statusMessage = "Error: Failed to save preorder to the database."; + $skippedCount++; + } + + echo "Row $rowCount: $statusMessage\n"; + $csv[] = $statusMessage; + fputcsv($outputFile, $csv); +} + +// --- CLEANUP --- +fclose($inputFile); +fclose($outputFile); + +echo "\n========================================\n"; +echo "Import completed!\n"; +echo "Total rows processed: $rowCount\n"; +echo "Successfully created: $successCount\n"; +echo "Skipped or failed: $skippedCount\n"; +echo "Results saved to: $outputFilename\n"; +echo "========================================\n"; +?> \ No newline at end of file From 075aaaef5f98a96bdad7b921ce847fa995154dbf Mon Sep 17 00:00:00 2001 From: Luca Haid Date: Tue, 26 Aug 2025 09:57:56 +0200 Subject: [PATCH 002/103] added new features --- .../RMLWorkorderAdminController.php | 81 ++++++++++++++----- .../RMLWorkorderCompanyController.php | 36 +++++++-- .../RMLWorkorderAdmin/RMLWorkorderAdmin.css | 39 +++++---- .../RMLWorkorderAdmin/RMLWorkorderAdmin.js | 31 +++++-- ...RMLWorkorderAdmin.css => RMLWorkorder.css} | 6 ++ .../RMLWorkorderCompany.js | 16 +++- .../vue/tt-components/css/tt-file-gallery.css | 20 +++++ .../vue/tt-components/tt-file-gallery.js | 31 +++---- 8 files changed, 186 insertions(+), 74 deletions(-) rename public/js/pages/RMLWorkorderCompany/{RMLWorkorderAdmin.css => RMLWorkorder.css} (84%) diff --git a/application/RMLWorkorderAdmin/RMLWorkorderAdminController.php b/application/RMLWorkorderAdmin/RMLWorkorderAdminController.php index adf46352f..846aae84a 100644 --- a/application/RMLWorkorderAdmin/RMLWorkorderAdminController.php +++ b/application/RMLWorkorderAdmin/RMLWorkorderAdminController.php @@ -26,6 +26,25 @@ class RMLWorkorderAdminController extends TTCrud ['key' => 'deadlineDate', 'text' => 'Deadline', 'modal' => false, 'table' => ['filter' => 'date']], ]; + private function getStatusText(string $statusKey): string { + $statusColumn = null; + foreach ($this->columns as $column) { + if ($column['key'] === 'status') { + $statusColumn = $column; + break; + } + } + + if ($statusColumn) { + foreach ($statusColumn['table']['filterOptions'] as $option) { + if ($option['value'] === $statusKey) { + return $option['text']; + } + } + } + return ucfirst(str_replace('_', ' ', $statusKey)); // Fallback + } + protected function indexAction() { $campaigns = Helper::getPreorderCampaignFromUser($this->user, true); @@ -105,20 +124,52 @@ class RMLWorkorderAdminController extends TTCrud protected function getDocumentationAction() { if(empty($this->request->workorderId)) self::sendError("Workorder ID missing."); - $docs = RMLWorkorderDocumentationModel::getAll(['workorderId' => $this->request->workorderId], null, 0, ['key' => 'create', 'order' => 'DESC']); + $docs = RMLWorkorderDocumentationModel::getAll(['workorderId' => $this->request->workorderId], null, 0, ['key' => 'create', 'order' => 'ASC']); $journals = RMLWorkorderJournalModel::getAll(['workorderId' => $this->request->workorderId], null, 0, ['key' => 'create', 'order' => 'DESC']); - $users = UserModel::search(); + + $translationMap = [ + 'photo_hup_mounted' => 'Foto_montierter_HÜP', + 'photo_hup_open' => 'Foto_offener_HÜP', + 'photo_splice_cassette_hup' => 'Foto_Spleißkassette_HÜP', + 'photo_splice_cassette_fcp' => 'Foto_Spleißkassette_FCP', + 'photo_hup_closed_stickers' => 'Foto_geschlossener_HÜP_mit_Aufklebern', + 'photo_fcp_labeled' => 'Foto_FCP_beschriftet', + 'photo_patch_position_osp' => 'Foto_Patch-Position_OSP-Seite', + 'photo_patch_position_anb' => 'Foto_Patch-Position_ANB-Seite', + 'measurement_protocol_otdr' => 'ODTR_Messung', + 'other' => 'Sonstiges_Dokument' + ]; + + $responseDocs = []; + $typeCounts = []; foreach($docs as $doc) { $file = new File($doc->fileId); - $doc->fileName = $file->orig_filename ?? $file->filename; - $doc->userName = UserModel::getOne($doc->createBy)->name ?? 'Unbekannt'; - $doc->mimetype = $file->mimetype ?? 'application/octet-stream'; + $documentTypeKey = $doc->documentType; + + $typeCounts[$documentTypeKey] = ($typeCounts[$documentTypeKey] ?? 0) + 1; + + $originalFilename = $file->orig_filename ?? $file->filename; + $extension = pathinfo($originalFilename, PATHINFO_EXTENSION); + $translatedType = $translationMap[$documentTypeKey] ?? $documentTypeKey; + $newFilename = "{$translatedType}_{$typeCounts[$documentTypeKey]}." . strtolower($extension); + + $responseDocs[] = [ + 'id' => $doc->id, + 'fileId' => $doc->fileId, + 'fileName' => $newFilename, + 'description' => $doc->description, + 'documentType' => $documentTypeKey, + 'userName' => UserModel::getOne($doc->createBy)->name ?? 'Unbekannt', + 'mimetype' => $file->mimetype ?? 'application/octet-stream', + ]; } + foreach($journals as $journal) { $journal->createByName = UserModel::getOne($journal->createBy)->name ?? 'Unbekannt'; } - self::returnJson(['docs' => $docs, 'journals' => $journals]); + + self::returnJson(['docs' => $responseDocs, 'journals' => $journals]); } private function assignSingleWorkorder($workorderId, $companyId, $deadline, $userId) { @@ -200,9 +251,9 @@ class RMLWorkorderAdminController extends TTCrud RMLWorkorderJournalModel::create([ 'workorderId' => $workorder->id, - 'text' => $post['text'], + 'text' => "Korrektur angefordert. Grund: " . $post['text'], 'fileIds' => !empty($post['fileIds']) ? json_encode($post['fileIds']) : null, - 'statusChange' => "$oldStatus -> correction_requested", + 'statusChange' => $this->getStatusText($oldStatus) . " -> " . $this->getStatusText('correction_requested'), 'create' => time(), 'createBy' => $this->user->id, ]); @@ -283,7 +334,7 @@ class RMLWorkorderAdminController extends TTCrud RMLWorkorderJournalModel::create([ 'workorderId' => $workorder->id, 'text' => 'Dokumentation wurde akzeptiert und der Auftrag abgeschlossen.', - 'statusChange' => "$oldStatus -> completed", + 'statusChange' => $this->getStatusText($oldStatus) . " -> " . $this->getStatusText('completed'), 'create' => time(), 'createBy' => $this->user->id, ]); @@ -292,11 +343,6 @@ class RMLWorkorderAdminController extends TTCrud } protected function setToProblemSolvedAction() { -// const response = await axios.post(`${window.TT_CONFIG.BASE_PATH}/RMLWorkorderAdmin/setToProblemSolved`, { -// workorderId: row.id, -// text: text -// }); - $post = json_decode(file_get_contents('php://input'), true); if (empty($post['workorderId']) || empty($post['text'])) { @@ -317,13 +363,10 @@ class RMLWorkorderAdminController extends TTCrud $workorder->status = 'problem_solved'; RMLWorkorderModel::update((array)$workorder); - $oldStatusText = $oldStatus === 'intervention_required' ? 'Eingriff benötigt' : $oldStatus; - $problem_solved = 'Problem gelöst'; - RMLWorkorderJournalModel::create([ 'workorderId' => $workorder->id, - 'text' => $post['text'], - 'statusChange' => "$oldStatusText -> $problem_solved", + 'text' => "Problem behoben: " . $post['text'], + 'statusChange' => $this->getStatusText($oldStatus) . " -> " . $this->getStatusText('problem_solved'), 'create' => time(), 'createBy' => $this->user->id, ]); diff --git a/application/RMLWorkorderCompany/RMLWorkorderCompanyController.php b/application/RMLWorkorderCompany/RMLWorkorderCompanyController.php index 65435b536..c5b4fd51f 100644 --- a/application/RMLWorkorderCompany/RMLWorkorderCompanyController.php +++ b/application/RMLWorkorderCompany/RMLWorkorderCompanyController.php @@ -26,6 +26,25 @@ class RMLWorkorderCompanyController extends TTCrud protected array $additionalJSVariables = ['COMPANY_ID' => '0']; + private function getStatusText(string $statusKey): string { + $statusColumn = null; + foreach ($this->columns as $column) { + if ($column['key'] === 'status') { + $statusColumn = $column; + break; + } + } + + if ($statusColumn) { + foreach ($statusColumn['table']['filterOptions'] as $option) { + if ($option['value'] === $statusKey) { + return $option['text']; + } + } + } + return ucfirst(str_replace('_', ' ', $statusKey)); // Fallback + } + protected function prepareCrudConfig() { $company = RMLWorkorderCompanyModel::getFirst(['addressId' => $this->user->address_id]); if ($company) { @@ -197,7 +216,7 @@ class RMLWorkorderCompanyController extends TTCrud RMLWorkorderJournalModel::create([ 'workorderId' => $workorder->id, 'text' => "Eingriff benötigt: " . $post['journalText'], - 'statusChange' => "$oldStatus -> intervention_required", + 'statusChange' => $this->getStatusText($oldStatus) . " -> " . $this->getStatusText('intervention_required'), 'create' => time(), 'createBy' => $this->user->id, ]); @@ -205,7 +224,6 @@ class RMLWorkorderCompanyController extends TTCrud self::returnJson(['success' => true, 'message' => 'Eingriff wurde angefordert.']); } - protected function uploadDocumentationAction() { if (empty($_FILES['files']) || empty($_POST['workorderId'])) { @@ -271,20 +289,21 @@ class RMLWorkorderCompanyController extends TTCrud $translationMap = [ 'photo_hup_mounted' => 'Foto_montierter_HÜP', 'photo_hup_open' => 'Foto_offener_HÜP', - 'photo_splice_cassette' => 'Foto_Spleißkassette', + 'photo_splice_cassette_hup' => 'Foto_Spleißkassette_HÜP', + 'photo_splice_cassette_fcp' => 'Foto_Spleißkassette_FCP', 'photo_hup_closed_stickers' => 'Foto_geschlossener_HÜP_mit_Aufklebern', 'photo_fcp_labeled' => 'Foto_FCP_beschriftet', + 'photo_patch_position_osp' => 'Foto_Patch-Position_OSP-Seite', + 'photo_patch_position_anb' => 'Foto_Patch-Position_ANB-Seite', 'measurement_protocol_otdr' => 'ODTR_Messung', + 'other' => 'Sonstiges_Dokument' ]; foreach($docs as $doc) { $file = new File($doc->fileId); $documentTypeKey = $doc->documentType; - if (!isset($typeCounts[$documentTypeKey])) { - $typeCounts[$documentTypeKey] = 1; - } else { - $typeCounts[$documentTypeKey]++; - } + + $typeCounts[$documentTypeKey] = ($typeCounts[$documentTypeKey] ?? 0) + 1; $originalFilename = $file->orig_filename ?? $file->filename; $extension = pathinfo($originalFilename, PATHINFO_EXTENSION); @@ -295,6 +314,7 @@ class RMLWorkorderCompanyController extends TTCrud 'id' => $doc->id, 'fileId' => $doc->fileId, 'fileName' => $newFilename, + 'description' => $doc->description, 'documentType' => $documentTypeKey, 'mimetype' => $file->mimetype, ]; diff --git a/public/js/pages/RMLWorkorderAdmin/RMLWorkorderAdmin.css b/public/js/pages/RMLWorkorderAdmin/RMLWorkorderAdmin.css index 5a01d28bc..5726b74f1 100644 --- a/public/js/pages/RMLWorkorderAdmin/RMLWorkorderAdmin.css +++ b/public/js/pages/RMLWorkorderAdmin/RMLWorkorderAdmin.css @@ -1,33 +1,40 @@ /* - * CSS for Workorder Table Row Highlighting (Balanced Colors) + * CSS for Workorder Table Row Highlighting */ -/* 🔴 Urgent: Deadline passed or less than 1 week away */ +/* Urgent: Deadline passed or less than 1 week away */ .table-hover .tt-rml-workorder-urgent:hover, .tt-rml-workorder-urgent { - background-color: #f8d7da !important; /* Balanced Red */ + background-color: #fbe9e7 !important; /* Soft Red */ } -/* 🟠 High Priority: Deadline less than 2 weeks away */ -.table-hover .tt-rml-workorder-high:hover, -.tt-rml-workorder-high { - background-color: #ffd5a1 !important; /* Balanced Orange */ -} - -/* 🟡 Medium: Deadline less than 3 weeks away */ +/* Medium: Deadline less than 3 weeks away */ .table-hover .tt-rml-workorder-medium:hover, .tt-rml-workorder-medium { - background-color: #fff3cd !important; /* Balanced Yellow */ + background-color: #fff8e1 !important; /* Soft Yellow */ } -/* 🟢 On Track: Deadline more than 3 weeks away */ +/* On Track: Deadline more than 3 weeks away */ .table-hover .tt-rml-workorder-ontrack:hover, .tt-rml-workorder-ontrack { - background-color: #d4edda !important; /* Balanced Green */ + background-color: #e8f5e9 !important; /* Soft Green */ } -/* ⚫ Irrelevant: No deadline or status makes it not applicable */ +/* Irrelevant: No deadline or status makes it not applicable */ .table-hover .tt-rml-workorder-irrelevant:hover, .tt-rml-workorder-irrelevant { - background-color: #e9ecef !important; /* Balanced Grey */ -} \ No newline at end of file + background-color: #fafafa !important; /* Very light grey */ +} + +.table-hover .tt-rml-workorder-high:hover, +.tt-rml-workorder-high { + background-color: #f8d7da !important; /* A slightly more intense red for high priority issues */ +} + +.tt-file-gallery-item.border.border-danger { + border: 4px solid #f1556c!important; +} + +.RMLWorkorderCompany-table .modal-body { + overflow-y: hidden; +} diff --git a/public/js/pages/RMLWorkorderAdmin/RMLWorkorderAdmin.js b/public/js/pages/RMLWorkorderAdmin/RMLWorkorderAdmin.js index 90eb3b680..b71fdff34 100644 --- a/public/js/pages/RMLWorkorderAdmin/RMLWorkorderAdmin.js +++ b/public/js/pages/RMLWorkorderAdmin/RMLWorkorderAdmin.js @@ -55,7 +55,7 @@ Vue.component('r-m-l-workorder-admin', { additional-class="btn-link btn-sm p-0" title="Auftrag auf Problem behoben setzen" /> - + + +
+ + Erstellt aus Kalendereintrag: {{ calendarEventType }} +
@@ -341,6 +346,21 @@ Vue.component('warehouse-shipping-note-see-through', { computed: { currentRow() { return this.rows[0]; + }, + calendarEventType() { + if (!this.currentRow || !this.currentRow.metadata) { + return null; + } + try { + const metadata = JSON.parse(this.currentRow.metadata); + if (metadata && metadata.from_calendar === true && metadata.event_type_text) { + return metadata.event_type_text; + } + } catch (e) { + console.error("Could not parse shipping note metadata:", e); + return null; + } + return null; } }, methods: { @@ -550,10 +570,40 @@ Vue.component('warehouse-shipping-note', { const locationParts = event.location.split(','); const line = locationParts.slice(0, -1).join(',').trim(); const plzCityRaw = locationParts.slice(-1)[0].trim(); - - const plzMatch = plzCityRaw.match(/^(\d+)/); + const plzMatch = plzCityRaw.match(/^(\\d+)/); const plz = plzMatch ? plzMatch[1] : ''; - const city = plzCityRaw.replace(/^\d+\s*/, '').trim(); + const city = plzCityRaw.replace(/^\\d+\\s*/, '').trim(); + + // Create metadata object + const eventTypeMap = { + '2': 'Xinon Inbetriebnahmen', + '3': 'ESTMK Inbetriebnahmen', + '7': 'Sbidi Inbetriebnahmen', + '4': 'SNOPP', + '5': 'Störungen', + '6': 'Support Gespräch' + }; + const eventTypeText = eventTypeMap[event.event_type] || 'Unbekannter Termintyp'; + + const metadata = { + from_calendar: true, + event_type_id: event.event_type, + event_type_text: eventTypeText + }; + + // Check for default positions for SBIDI event type + let defaultPositions = []; + if (event.event_type === '7') { + defaultPositions = [ + {"article": 56, "amount": "1", "isEnergieMaterial": 1}, + {"article": 71, "amount": "1", "isEnergieMaterial": 1}, + {"article": 134, "amount": "1", "isEnergieMaterial": 1}, + {"article": 324, "isEnergieMaterial": 1, "amount": "1"}, + {"article": 325, "amount": "1", "isEnergieMaterial": 1}, + {"article": 539, "amount": "1", "isEnergieMaterial": 1}, + {"article": 660, "amount": "1", "isEnergieMaterial": 1} + ]; + } // Directly set the data in the modal's `shippingNote` object const modalData = this.$refs.modal.shippingNote; @@ -561,6 +611,11 @@ Vue.component('warehouse-shipping-note', { this.$set(modalData, 'deliveryAddressLine', line); this.$set(modalData, 'deliveryAddressPLZ', plz); this.$set(modalData, 'deliveryAddressCity', city); + this.$set(modalData, 'metadata', metadata); // Set metadata + + if (defaultPositions.length > 0) { + this.$set(modalData, 'positions', defaultPositions); // Set default positions + } } }, closeDropdown(event) { diff --git a/public/js/pages/WarehouseShippingNote/WarehouseShippingNoteModal.js b/public/js/pages/WarehouseShippingNote/WarehouseShippingNoteModal.js index 8f10ffacb..5433748eb 100644 --- a/public/js/pages/WarehouseShippingNote/WarehouseShippingNoteModal.js +++ b/public/js/pages/WarehouseShippingNote/WarehouseShippingNoteModal.js @@ -18,6 +18,7 @@ Vue.component('warehouse-shipping-note-modal', { deliveryAddressPLZ: '', deliveryAddressCity: '', status: 'new', + metadata: null, positions: [], textElements: [], hoursEntries: [], @@ -196,7 +197,12 @@ Vue.component('warehouse-shipping-note-modal', { if (this.id === 'create') return; const {data} = await axios.get(`${window.TT_CONFIG["BASE_PATH"]}/WarehouseShippingNote/getById`, {params: {id: this.id}}); - this.shippingNote = {...data, positions: JSON.parse(data.positions), hoursEntries: JSON.parse(data.hoursEntries)}; + this.shippingNote = { + ...data, + positions: JSON.parse(data.positions), + hoursEntries: JSON.parse(data.hoursEntries), + metadata: data.metadata ? JSON.parse(data.metadata) : null + }; }, watch: { geoAddr: async function() { From a91f5460ac66da7d9b06bc51a55da85212e16e0e Mon Sep 17 00:00:00 2001 From: Luca Haid Date: Mon, 1 Sep 2025 10:26:01 +0000 Subject: [PATCH 017/103] Device monitoring/v2 --- .../DeviceMonitoringController.php | 271 +++++++-- lib/Zabbix/Zabbix.php | 266 ++++++--- public/js/pages/Device/Device.css | 8 + public/js/pages/Device/DeviceMonitoring.js | 521 +++++++++--------- 4 files changed, 693 insertions(+), 373 deletions(-) diff --git a/application/DeviceMonitoring/DeviceMonitoringController.php b/application/DeviceMonitoring/DeviceMonitoringController.php index 873c8479b..36f54c9b6 100644 --- a/application/DeviceMonitoring/DeviceMonitoringController.php +++ b/application/DeviceMonitoring/DeviceMonitoringController.php @@ -30,9 +30,6 @@ class DeviceMonitoringController extends mfBaseController $this->postData = json_decode(file_get_contents('php://input'), true) ?? []; } - /** - * Gets a list of all available interfaces, grouping Sent/Received items. - */ protected function listInterfacesAction() { $hostId = $this->request->hostId; @@ -54,9 +51,6 @@ class DeviceMonitoringController extends mfBaseController self::returnJson($sortedInterfaces); } - /** - * Gets historical data for a specific list of item IDs. - */ protected function interfaceDataAction() { $itemIds = $this->postData['itemIds'] ?? []; @@ -71,7 +65,7 @@ class DeviceMonitoringController extends mfBaseController $params = [ 'itemids' => $itemIds, 'output' => 'extend', - 'history' => 3, // Numeric (unsigned) + 'history' => 3, // Type of history: float 'sortfield' => 'clock', 'sortorder' => 'ASC', 'time_from' => $time_from, @@ -82,66 +76,246 @@ class DeviceMonitoringController extends mfBaseController foreach ($history as $point) { $historyByItemId[$point['itemid']][] = [ 'x' => intval($point['clock']) * 1000, - 'y' => round(floatval($point['value']) / 1000000, 2) + 'y' => round(floatval($point['value']) / 1000000, 2) // Mbps ]; } self::returnJson($historyByItemId); } - /** - * Gets general monitoring data (Uptime, Ping, Temp). - */ protected function generalDataAction() { $hostId = $this->request->hostId; - - $itemsToFetch = [ - 'ping' => $this->zabbix->getICMPItems($hostId), - 'uptime' => $this->zabbix->getUptimeItems($hostId), - ]; - - $itemIds = []; - $itemMap = []; - foreach ($itemsToFetch as $type => $items) { - if (!empty($items)) { - foreach($items as $item) { - $itemIds[] = $item['itemid']; - $itemMap[$item['itemid']] = ['type' => $type, 'name' => $item['name'], 'units' => $item['units']]; - } - } - } - - $values = []; - if(!empty($itemIds)) { - $history = $this->zabbix->getItemValues($itemIds, 1); - foreach($history as $h) { - $info = $itemMap[$h['itemid']]; - $values[$info['type']][] = ['name' => $info['name'], 'value' => $h['value'], 'clock' => $h['clock'], 'units' => $info['units']]; - } - } - - self::returnJson($values); + $data = $this->zabbix->getOverviewData($hostId); + self::returnJson($data); } - /** - * Gets Zabbix problems (triggers) for the host. - */ protected function getProblemsAction() { $hostId = $this->request->hostId; - $problems = $this->zabbix->zabbixRequest('problem.get', [ + $currentProblems = $this->zabbix->zabbixRequest('problem.get', [ 'hostids' => $hostId, 'output' => 'extend', - 'recent' => true, // Use boolean true + 'recent' => true, 'sortfield' => ['eventid'], 'sortorder' => 'DESC' ])['result'] ?? []; - self::returnJson($problems); + $resolvedProblems = $this->zabbix->getResolvedProblems($hostId, strtotime('-7 days')); + + self::returnJson([ + 'current' => $currentProblems, + 'resolved' => $resolvedProblems + ]); + } + + protected function getConfigurationDataAction() { + $hostId = $this->request->hostId; + + $host = $this->zabbix->getHostWithInterfaces($hostId); + if (!$host) { + self::returnJson(['error' => 'Host not found.']); + return; + } + + $snmpInterface = null; + foreach ($host['interfaces'] as $iface) { + if ($iface['type'] == '2') { // SNMP type + $snmpInterface = $iface; + break; + } + } + + $opStatusItems = $this->zabbix->getInterfaceOperationalStatusItems($hostId); + $allTriggers = $this->zabbix->getTriggersForHostByDescription($hostId, "Interface "); + $triggerMap = []; + foreach ($allTriggers as $trigger) { + $triggerMap[$trigger['description']] = $trigger; + } + + $interfaceAlarms = []; + foreach ($opStatusItems as $item) { + $expectedDescription = "Interface " . $item['name'] . " is down on " . $host['name']; + $trigger = $triggerMap[$expectedDescription] ?? null; + + $interfaceAlarms[] = [ + 'itemid' => $item['itemid'], + 'name' => $item['name'], + 'key' => $item['key_'], + 'isAlarmed' => !is_null($trigger), + 'triggerId' => $trigger['triggerid'] ?? null + ]; + } + + self::returnJson([ + 'snmp' => $snmpInterface, + 'interfaces' => $interfaceAlarms + ]); + } + + + protected function updateSnmpAction() { + $interfaceId = $this->postData['interfaceId'] ?? null; + $details = $this->postData['details'] ?? null; + if (!$interfaceId || !$details) { + http_response_code(400); + self::returnJson(['error' => 'Missing required parameters.']); + return; + } + + $result = $this->zabbix->updateHostInterface($interfaceId, $details); + self::returnJson($result); + } + + protected function updateInterfaceAlarmAction() { + $hostId = $this->postData['hostId']; + $item = $this->postData['item']; + $enabled = $this->postData['enabled']; + $host = $this->zabbix->getHostById($hostId)[0] ?? null; + + if (!$host) { + self::returnJson(['error' => 'Host not found.']); + return; + } + + $description = "Interface " . $item['name'] . " is down on " . $host['name']; + + if ($enabled) { + $expression = "last(/".$host['host']."/".$item['key'].")=2"; + $result = $this->zabbix->createInterfaceLinkDownTrigger($expression, $description); + } else { + $triggers = $this->zabbix->getTriggersForHostByDescription($hostId, $description); + $triggerIds = array_column($triggers, 'triggerid'); + $result = $this->zabbix->deleteTriggers($triggerIds); + } + + self::returnJson($result); + } + + protected function getReportDataAction() { + $hostId = $this->request->hostId; + $timeRange = $this->request->timeRange ?? '7d'; + $time_from = strtotime('-' . str_replace(['d'], [' days'], $timeRange)); + + // Step 1: Fetch all interface-related items (traffic and speed) in a single API call. + // We include 'value_type' to handle different history types correctly. + $items = $this->zabbix->zabbixRequest('item.get', [ + 'hostids' => $hostId, + 'output' => ['itemid', 'name', 'key_', 'value_type'], + 'search' => ['key_' => ['net.if.in', 'net.if.out', 'net.if.speed']], + 'searchByAny' => true, + 'sortfield' => 'name' + ])['result'] ?? []; + + // Step 2: Organize items and group them for efficient processing. + $interfaces = []; + $trafficItems = []; // Will hold item info for both rx and tx. + $speedItemsByType = []; + $speedItemMap = []; + + foreach ($items as $item) { + $key = $item['key_']; + if (str_contains($key, 'net.if.in') || str_contains($key, 'net.if.out')) { + $baseName = preg_replace('/:\s*Bits\s*(sent|received)$/i', '', $item['name']); + $direction = str_contains($key, 'net.if.in') ? 'rx' : 'tx'; + if (!isset($interfaces[$baseName])) { + $interfaces[$baseName] = ['name' => $baseName, 'rx_item' => null, 'tx_item' => null, 'speed' => null]; + } + $interfaces[$baseName][$direction . '_item'] = $item; + $trafficItems[$item['itemid']] = $item; + } elseif (str_contains($key, 'net.if.speed')) { + $baseName = preg_replace('/:\s*Interface\s*|\s*speed$/i', '', $item['name']); + $value_type = (int)$item['value_type']; + $speedItemsByType[$value_type][] = $item['itemid']; + $speedItemMap[$item['itemid']] = $baseName; + } + } + + // Step 3: Aggressively fetch the last known speed for all interfaces. + // We query history with a larger limit to find the value even if it's not recent. + foreach ($speedItemsByType as $type => $itemIds) { + $historyResult = $this->zabbix->zabbixRequest('history.get', [ + 'itemids' => $itemIds, + 'history' => $type, + 'output' => ['itemid', 'value'], + 'sortfield' => 'clock', + 'sortorder' => 'DESC', + 'limit' => count($itemIds) * 5 // Increase limit to better ensure finding a value for each item + ])['result'] ?? []; + + $latestForType = []; + foreach ($historyResult as $point) { + if (!isset($latestForType[$point['itemid']])) { + $latestForType[$point['itemid']] = $point; + $baseName = $speedItemMap[$point['itemid']] ?? null; + if ($baseName && isset($interfaces[$baseName])) { + $interfaces[$baseName]['speed'] = (float)$point['value']; + } + } + } + } + + // Step 4: Attempt to fetch trend data for all traffic items at once. + $trafficItemIds = array_keys($trafficItems); + $trends = $this->zabbix->getTrends($trafficItemIds, $time_from); + $trendsByItemId = []; + foreach ($trends as $trend) { + $trendsByItemId[$trend['itemid']][] = $trend; + } + + // Step 5: Build the report, using trends first and falling back to raw history if trends are unavailable. + $report = []; + foreach ($interfaces as $iface) { + $rx_item = $iface['rx_item']; + $tx_item = $iface['tx_item']; + $speed = $iface['speed']; + + // This function calculates statistics from either trend data or raw history data. + $calcStats = function($item, $speed, $trendData) use ($time_from) { + if (!$item) return ['avg' => 0, 'max' => 0, 'usage' => 0]; + + $values = []; + $avg = 0; + $max = 0; + + if (!empty($trendData)) { + // Method 1: Use efficient trend data if available. + $avg = array_sum(array_column($trendData, 'value_avg')) / count($trendData); + $max = max(array_column($trendData, 'value_max')); + } else { + // Method 2 (Fallback): Fetch raw history if trends are missing. + $history = $this->zabbix->zabbixRequest('history.get', [ + 'itemids' => [$item['itemid']], + 'history' => (int)$item['value_type'], + 'time_from' => $time_from, + 'output' => ['value'] + ])['result'] ?? []; + + if (!empty($history)) { + $values = array_column($history, 'value'); + $avg = array_sum($values) / count($values); + $max = max($values); + } + } + + $usage = ($speed > 0) ? ($avg / $speed) * 100 : 0; + return [ + 'avg' => round($avg / 1000000, 2), // bps to Mbps + 'max' => round($max / 1000000, 2), // bps to Mbps + 'usage' => round($usage, 2) + ]; + }; + + $report[] = [ + 'name' => $iface['name'], + 'speed' => $speed !== null ? round($speed / 1000000) : 'N/A', // bps to Mbps + 'rx' => $calcStats($rx_item, $speed, $trendsByItemId[$rx_item['itemid']] ?? []), + 'tx' => $calcStats($tx_item, $speed, $trendsByItemId[$tx_item['itemid']] ?? []) + ]; + } + + usort($report, fn($a, $b) => strnatcmp($a['name'], $b['name'])); + self::returnJson($report); } - /** - * Forces a Zabbix item check and returns the latest value for live graphs. - */ protected function liveDataAction() { $itemId = $this->request->itemId; if(empty($itemId)) { @@ -168,9 +342,6 @@ class DeviceMonitoringController extends mfBaseController self::returnJson($formattedPoint); } - /** - * Renders a dedicated HTML page for the live graph popup. - */ public function liveGraphPageAction() { $this->layout(false); $this->layout()->set('API_BASE_URL', self::getUrl("DeviceMonitoring")); diff --git a/lib/Zabbix/Zabbix.php b/lib/Zabbix/Zabbix.php index eaf19e6e2..6f055c0a0 100644 --- a/lib/Zabbix/Zabbix.php +++ b/lib/Zabbix/Zabbix.php @@ -1,32 +1,35 @@ url = $url; $this->apiKey = $apiKey; } - public function zabbixRequest($method, $params, $die = false) { + public function zabbixRequest($method, $params, $die = false) + { $data = array( 'jsonrpc' => '2.0', - 'method' => $method, - 'params' => $params, - 'id' => 1 + 'method' => $method, + 'params' => $params, + 'id' => 1 ); $options = array( 'http' => array( - 'header' => "Content-Type: application/json\r\n" . + 'header' => "Content-Type: application/json\r\n" . "Authorization: Bearer " . $this->apiKey . "\r\n", - 'method' => 'POST', - 'content' => json_encode($data) + 'method' => 'POST', + 'content' => json_encode($data), + 'timeout' => 30 ) ); - // var dump options and data and die if ($die) { var_dump($options); echo json_encode($data); @@ -39,25 +42,29 @@ class Zabbix { return json_decode($result, true); } - public function getHostById($hostId) { + public function getHostById($hostId) + { $response = $this->zabbixRequest('host.get', array( 'hostids' => $hostId )); return $response['result']; } - public function getItemValues($itemIds, $limit = 15) { + public function getItemValues($itemIds, $limit = 15) + { + if (empty($itemIds)) return []; $response = $this->zabbixRequest('history.get', array( - 'itemids' => $itemIds, - 'output' => 'extend', + 'itemids' => $itemIds, + 'output' => 'extend', 'sortfield' => 'clock', 'sortorder' => 'DESC', - 'limit' => $limit + 'limit' => $limit )); return $response['result']; } - public function getHosts($hostname = null, $ip = null) { + public function getHosts($hostname = null, $ip = null) + { if ($hostname) { $response = $this->zabbixRequest('host.get', array( 'search' => array('name' => array($hostname)) @@ -72,80 +79,84 @@ class Zabbix { return []; } - public function getHostInterfaceItems($hostId) { + public function getHostInterfaceItems($hostId) + { $response = $this->zabbixRequest('item.get', array( - 'hostids' => $hostId, - 'output' => ['itemid','name_resolved', 'key_', 'units'], - 'search' => ['name' => ["Bits received", "Bits sent"]], + 'hostids' => $hostId, + 'output' => ['itemid', 'name', 'key_', 'units'], + 'search' => ['name' => ["Bits received", "Bits sent"]], 'searchByAny' => true, - 'sortfield' => 'name' + 'sortfield' => 'name' )); return $response['result']; } - public function getICMPItems($hostId) { + public function getICMPItems($hostId) + { $response = $this->zabbixRequest('item.get', array( 'hostids' => $hostId, - 'search' => array('name' => array("ICMP")) + 'search' => array('name' => array("ICMP")) )); return $response['result']; } - public function getUptimeItems($hostId) { + public function getUptimeItems($hostId) + { $response = $this->zabbixRequest('item.get', array( 'hostids' => $hostId, - 'search' => array('name' => array("Uptime")) + 'search' => array('name' => array("Uptime")) )); return $response['result']; } - public function getHostInterfaces($hostIds) { - $response = $this->zabbixRequest('hostinterface.get', array('hostids' => $hostIds)); + public function getHostInterfaces($hostIds) + { + $response = $this->zabbixRequest('hostinterface.get', array('hostids' => $hostIds, 'output' => 'extend')); return $response['result']; } - public function createTask($itemid) { + public function createTask($itemid) + { $response = $this->zabbixRequest('task.create', array( - 'type' => 6, - 'request' => array( - 'itemid' => $itemid - ) + 'type' => 6, + 'request' => array('itemid' => $itemid) )); return $response['result']; } - public function getAllHostsWithDetails() { + public function getAllHostsWithDetails() + { $response = $this->zabbixRequest('host.get', [ - 'output' => ['hostid', 'host', 'name', 'status'], - 'selectInventory' => ['location_lat', 'location_lon'], + 'output' => ['hostid', 'host', 'name', 'status'], + 'selectInventory' => ['location_lat', 'location_lon'], 'selectParentTemplates' => ['templateid', 'name'], - 'selectHostGroups' => 'extend' // This is the new line + 'selectHostGroups' => 'extend' ]); return $response['result'] ?? []; } - public function updateHostInventory($hostId, $inventoryData) { - // First, get the current inventory to avoid overwriting existing fields + public function updateHostInventory($hostId, $inventoryData) + { $hostResponse = $this->zabbixRequest('host.get', [ - 'hostids' => $hostId, + 'hostids' => $hostId, 'selectInventory' => 'extend' ]); $currentInventory = $hostResponse['result'][0]['inventory'] ?? []; - - // Merge new coordinates into the existing inventory $newInventory = array_merge($currentInventory, $inventoryData); $params = [ - 'hostid' => $hostId, + 'hostid' => $hostId, 'inventory_mode' => 0, // Set to manual mode - 'inventory' => $newInventory + 'inventory' => $newInventory ]; $response = $this->zabbixRequest('host.update', $params); return $response['result'] ?? ['error' => $response['error'] ?? 'Unknown error']; } - public function getTemplateIdByName($templateName) { + + public function getTemplateIdByName($templateName) + { $response = $this->zabbixRequest('template.get', [ 'output' => ['templateid'], 'filter' => ['host' => [$templateName]] @@ -153,7 +164,8 @@ class Zabbix { return $response['result'][0]['templateid'] ?? null; } - public function getTemplatesByNames(array $templateNames) { + public function getTemplatesByNames(array $templateNames) + { $response = $this->zabbixRequest('template.get', [ 'output' => ['templateid', 'name'], 'filter' => ['host' => $templateNames] @@ -161,38 +173,39 @@ class Zabbix { return $response['result'] ?? []; } - - public function createHost($visibleName, $ip, $groupId, $templateIds) { - $templatesData = array_map(function($id) { + public function createHost($visibleName, $ip, $groupId, $templateIds) + { + $templatesData = array_map(function ($id) { return ['templateid' => $id]; }, $templateIds); $params = [ - 'host' => $ip, // Technical name is the IP - 'name' => $visibleName, // Visible name - 'interfaces' => [ // <-- Corrected structure + 'host' => $ip, + 'name' => $visibleName, + 'interfaces' => [ [ - 'type' => 2, // 2 for SNMP - 'main' => 1, - 'useip' => 1, - 'ip' => $ip, - 'dns' => '', - 'port' => '161', + 'type' => 2, // 2 for SNMP + 'main' => 1, + 'useip' => 1, + 'ip' => $ip, + 'dns' => '', + 'port' => '161', 'details' => [ - 'version' => 2, + 'version' => 2, 'community' => 'public_xinon' ] ] ], - 'groups' => [['groupid' => $groupId]], - 'templates' => $templatesData // Use the correctly formatted array + 'groups' => [['groupid' => $groupId]], + 'templates' => $templatesData ]; $response = $this->zabbixRequest('host.create', $params); return $response['result'] ?? ['error' => $response['error'] ?? 'Unknown error']; } - public function getHostGroupIdByName($groupName) { + public function getHostGroupIdByName($groupName) + { $response = $this->zabbixRequest('hostgroup.get', [ 'output' => ['groupid'], 'filter' => ['name' => [$groupName]] @@ -200,4 +213,133 @@ class Zabbix { return $response['result'][0]['groupid'] ?? null; } -} + public function getHostWithInterfaces($hostId) + { + $response = $this->zabbixRequest('host.get', [ + 'hostids' => $hostId, + 'output' => ['hostid', 'host', 'name'], + 'selectInterfaces' => 'extend' + ]); + return $response['result'][0] ?? null; + } + + public function getResolvedProblems($hostId, $time_from) + { + $response = $this->zabbixRequest('event.get', [ + 'hostids' => $hostId, + 'output' => 'extend', + 'select_acknowledges' => ['message'], + 'sortfield' => ['clock'], + 'sortorder' => 'DESC', + 'time_from' => $time_from, + 'object' => 0, + 'value' => 0 + ]); + return $response['result'] ?? []; + } + + public function updateHostInterface($interfaceId, $details) + { + $params = ['interfaceid' => $interfaceId, 'details' => $details]; + $response = $this->zabbixRequest('hostinterface.update', $params); + return $response['result'] ?? ['error' => $response['error'] ?? 'Unknown error']; + } + + public function getInterfaceOperationalStatusItems($hostId) + { + $response = $this->zabbixRequest('item.get', [ + 'hostids' => $hostId, + 'output' => ['itemid', 'name', 'key_'], + 'search' => ['key_' => 'net.if.status'], + 'sortfield' => 'name' + ]); + return $response['result'] ?? []; + } + + public function getTriggersForHostByDescription($hostId, $description) + { + $response = $this->zabbixRequest('trigger.get', [ + 'hostids' => $hostId, + 'output' => ['triggerid', 'description'], + 'search' => ['description' => $description], + 'searchByAny' => true + ]); + return $response['result'] ?? []; + } + + public function createInterfaceLinkDownTrigger($expression, $description, $priority = 4) + { + $params = [ + 'description' => $description, + 'expression' => $expression, + 'priority' => $priority, + ]; + $response = $this->zabbixRequest('trigger.create', $params); + return $response['result'] ?? ['error' => $response['error'] ?? 'Unknown error']; + } + + public function deleteTriggers(array $triggerIds) + { + if (empty($triggerIds)) return []; + $response = $this->zabbixRequest('trigger.delete', $triggerIds); + return $response['result'] ?? ['error' => $response['error'] ?? 'Unknown error']; + } + + public function getTrends(array $itemIds, $time_from) + { + if (empty($itemIds)) return []; + $response = $this->zabbixRequest('trend.get', [ + 'itemids' => $itemIds, + 'output' => ['itemid', 'num', 'value_min', 'value_avg', 'value_max'], + 'time_from' => $time_from + ]); + return $response['result'] ?? []; + } + + public function getOverviewData($hostId) + { + $items = $this->zabbixRequest('item.get', [ + 'hostids' => $hostId, + 'output' => ['itemid', 'name', 'units', 'key_'], + 'search' => ['key_' => ['icmpping', 'system.uptime']], + 'searchByAny' => true + ])['result'] ?? []; + + $itemIds = []; + $itemMap = []; + $data = ['ping' => [], 'uptime' => []]; + foreach ($items as $item) { + $itemIds[] = $item['itemid']; + $type = str_contains($item['key_'], 'uptime') ? 'uptime' : 'ping'; + $itemMap[$item['itemid']] = ['type' => $type, 'name' => $item['name'], 'units' => $item['units']]; + } + + if (!empty($itemIds)) { + $history = $this->getItemValues($itemIds, 1); + foreach ($history as $h) { + if (!isset($itemMap[$h['itemid']])) continue; + $info = $itemMap[$h['itemid']]; + $data[$info['type']][] = ['name' => $info['name'], 'value' => $h['value'], 'clock' => $h['clock'], 'units' => $info['units']]; + } + } + + $time30d = time() - 2592000; + $problems = $this->zabbixRequest('problem.get', [ + 'hostids' => $hostId, + 'time_from' => $time30d, + 'output' => ['clock'] + ])['result'] ?? []; + + $time7d = time() - 604800; + $time24h = time() - 86400; + $counts = ['24h' => 0, '7d' => 0, '30d' => 0]; + foreach ($problems as $p) { + $counts['30d']++; + if ($p['clock'] >= $time7d) $counts['7d']++; + if ($p['clock'] >= $time24h) $counts['24h']++; + } + $data['problemCounts'] = $counts; + + return $data; + } +} \ No newline at end of file diff --git a/public/js/pages/Device/Device.css b/public/js/pages/Device/Device.css index e7dfdb483..2548161e7 100644 --- a/public/js/pages/Device/Device.css +++ b/public/js/pages/Device/Device.css @@ -19,3 +19,11 @@ .sev-average { border-left-color: #fd7e14; } .sev-average .problem-icon { color: #fd7e14; } .sev-high { border-left-color: #dc3545; } .sev-high .problem-icon { color: #dc3545; } .sev-disaster { border-left-color: #7B014C; } .sev-disaster .problem-icon { color: #7B014C; } +.overview-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); gap: 1rem; } +.problem-counts { display: flex; justify-content: space-around; text-align: center; padding: 1rem 0; } +.problem-counts .count { font-size: 1.5rem; font-weight: bold; display: block; } +.problem-counts .period { font-size: 0.8rem; color: #6c757d; } +.problems-list.resolved .problem-card { opacity: 0.8; } +.sev-resolved { border-left: 5px solid #28a745; } +.sev-resolved .problem-icon { color: #28a745; } +.c-pointer { cursor: pointer; } \ No newline at end of file diff --git a/public/js/pages/Device/DeviceMonitoring.js b/public/js/pages/Device/DeviceMonitoring.js index a82e3427d..96266313b 100644 --- a/public/js/pages/Device/DeviceMonitoring.js +++ b/public/js/pages/Device/DeviceMonitoring.js @@ -1,3 +1,28 @@ +const ttSwitchCSS = ` +.tt-switch { position: relative; display: inline-block; width: 44px; height: 24px; } +.tt-switch input { opacity: 0; width: 0; height: 0; } +.slider { position: absolute; cursor: pointer; top: 0; left: 0; right: 0; bottom: 0; background-color: #ccc; transition: .4s; } +.slider:before { position: absolute; content: ""; height: 18px; width: 18px; left: 3px; bottom: 3px; background-color: white; transition: .4s; } +input:checked + .slider { background-color: #28a745; } +input:focus + .slider { box-shadow: 0 0 1px #28a745; } +input:checked + .slider:before { transform: translateX(20px); } +.slider.round { border-radius: 24px; } +.slider.round:before { border-radius: 50%; } +`; +const styleSheet = document.createElement("style"); +styleSheet.innerText = ttSwitchCSS; +document.head.appendChild(styleSheet); + +Vue.component('tt-switch', { + template: ` + + `, + props: { value: { type: Boolean, default: false } } +}); + Vue.component('device-monitoring-modal', { //language=Vue template: ` @@ -5,7 +30,8 @@ Vue.component('device-monitoring-modal', { :title="'Monitoring für ' + deviceName" @update:show="$emit('close')" :save="false" - :delete="false"> + :delete="false" + dialog-class="modal-xl">
Keine allgemeinen Monitoring-Daten gefunden.
- + @@ -29,23 +55,28 @@ Vue.component('device-monitoring-modal', {
- +

{{ formatUptime(item.value) }}

Seit {{ moment(Date.now() - item.value * 1000).format('DD.MM.YYYY HH:mm') }}
+ + +
+
{{ generalData.problemCounts['24h'] }}letzte 24h
+
{{ generalData.problemCounts['7d'] }}letzte 7T
+
{{ generalData.problemCounts['30d'] }}letzte 30T
+
+
- +
@@ -53,11 +84,7 @@ Vue.component('device-monitoring-modal', {
- +
@@ -66,52 +93,126 @@ Vue.component('device-monitoring-modal', {
Bitte eine oder mehrere Schnittstellen auswählen, um Graphen anzuzeigen.
-
-
-
+
{{ iface.name }}
- +
-
- Empfangen (Mbps) - Min: {{ statistics[iface.name].rx.min }} - Avg: {{ statistics[iface.name].rx.avg }} - Median: {{ statistics[iface.name].rx.median }} - Max: {{ statistics[iface.name].rx.max }} - 95%: {{ statistics[iface.name].rx.p95 }} +
Empfangen (Mbps)Min: {{ statistics[iface.name].rx.min }}Avg: {{ statistics[iface.name].rx.avg }}Max: {{ statistics[iface.name].rx.max }}95%: {{ statistics[iface.name].rx.p95 }}
+
Gesendet (Mbps)Min: {{ statistics[iface.name].tx.min }}Avg: {{ statistics[iface.name].tx.avg }}Max: {{ statistics[iface.name].tx.max }}95%: {{ statistics[iface.name].tx.p95 }}
+
+
+
+
+ +
+
+
+ +
+
+
+
Keine Report-Daten für den gewählten Zeitraum.
+
+ + + + + + + + + + + + + + + + + + + + + + + + + +
Schnittstelle Speed (Mbps) Max In (Mbps) Avg In (Mbps) Auslastung In (%) Max Out (Mbps) Avg Out (Mbps) Auslastung Out (%)
{{ d.name }}{{ d.speed }}{{ d.rx.max }}{{ d.rx.avg }}{{ d.rx.usage }}%{{ d.tx.max }}{{ d.tx.avg }}{{ d.tx.usage }}%
+
+
+ +
+
+
Keine Probleme für dieses Gerät gefunden.
+
+
+
Aktuelle Probleme
+
+
+
+
+
{{ p.name }}{{ moment(p.clock * 1000).fromNow() }}
+
{{ p.opdata }}
+
-
- Gesendet (Mbps) - Min: {{ statistics[iface.name].tx.min }} - Avg: {{ statistics[iface.name].tx.avg }} - Median: {{ statistics[iface.name].tx.median }} - Max: {{ statistics[iface.name].tx.max }} - 95%: {{ statistics[iface.name].tx.p95 }} +
+
+
+
Behobene Probleme (letzte 7 Tage)
+
+
+
+
+
{{ p.name }}{{ moment(p.clock * 1000).fromNow() }}
+
-
-
-
Keine aktuellen Probleme für dieses Gerät gefunden.
-
-
-
-
-
- {{ p.name }} - {{ moment(p.clock * 1000).format('DD.MM.YYYY HH:mm:ss') }} -
-
{{ p.opdata }}
+
+
+
+ + +
Keine SNMP-Schnittstelle auf diesem Host gefunden.
+
+ + + +
-
+ + + +
+ + + + + + + + +
SchnittstelleAlarmierung aktiv
{{ iface.name }}
+
+
@@ -125,115 +226,163 @@ Vue.component('device-monitoring-modal', { tabs: [ { id: 'overview', name: 'Übersicht', icon: 'fas fa-tachometer-alt' }, { id: 'interfaces', name: 'Schnittstellen', icon: 'fas fa-ethernet' }, + // { id: 'reports', name: 'Reports', icon: 'fas fa-chart-pie' }, { id: 'problems', name: 'Probleme', icon: 'fas fa-exclamation-triangle' }, + { id: 'configuration', name: 'Konfiguration', icon: 'fas fa-cogs' }, ], - loading: { overview: false, interfaces: false, problems: false, individualInterfaces: {} }, + loading: { overview: true, interfaces: false, problems: false, configuration: false, reports: false, individualInterfaces: {} }, generalData: null, - problemData: [], + problemData: { current: [], resolved: [] }, allInterfaces: [], selectedInterfaces: [], interfaceTimeRange: '24h', - timeRanges: [ - { text: '6H', value: '6h' }, { text: '24H', value: '24h' }, - { text: '7T', value: '7d' }, { text: '30T', value: '30d' }, - ], + timeRanges: [{ text: '6H', value: '6h' }, { text: '24H', value: '24h' }, { text: '7T', value: '7d' }, { text: '30T', value: '30d' }], interfaceChartData: {}, chartInstances: {}, dataNormalizationMode: 'avg', downsampleThreshold: 500, + configData: { snmp: null, interfaces: [] }, + snmpV3Levels: [{text: 'noAuthNoPriv', value: '0'}, {text: 'authNoPriv', value: '1'}, {text: 'authPriv', value: '2'}], + snmpV3Auth: [{text: 'MD5', value: '0'}, {text: 'SHA-1', value: '1'}], + snmpV3Priv: [{text: 'DES', value: '0'}, {text: 'AES-128', value: '1'}], + authPassphrase: '', + privPassphrase: '', + reportData: [], + reportTimeRange: '7d', + reportTimeRanges: [{ text: 'Letzte 7 Tage', value: '7d' }, { text: 'Letzte 30 Tage', value: '30d' }], + reportSortKey: 'name', + reportSortDir: 'asc', }; }, computed: { - interfaceOptions() { - return this.allInterfaces.map(iface => ({ text: iface.name, value: iface.name })); - }, - selectedInterfacesData() { - return this.allInterfaces.filter(iface => this.selectedInterfaces.includes(iface.name)); - }, - displayChartData() { - const processedData = {}; - for (const itemId in this.interfaceChartData) { - const data = this.interfaceChartData[itemId]; - if (data.length > this.downsampleThreshold) { - processedData[itemId] = this.downsampleData(data, this.dataNormalizationMode); - } else { - processedData[itemId] = data; - } - } - return processedData; - }, + interfaceOptions() { return this.allInterfaces.map(iface => ({ text: iface.name, value: iface.name })); }, + selectedInterfacesData() { return this.allInterfaces.filter(iface => this.selectedInterfaces.includes(iface.name)); }, statistics() { if (this.selectedInterfaces.length === 0 || Object.keys(this.interfaceChartData).length === 0) return {}; - const stats = {}; this.selectedInterfacesData.forEach(iface => { const calculate = (data) => { - if (!data || data.length === 0) return { min: 'N/A', max: 'N/A', avg: 'N/A', median: 'N/A', p95: 'N/A' }; + if (!data || data.length === 0) return { min: 'N/A', max: 'N/A', avg: 'N/A', p95: 'N/A' }; const values = data.map(p => p.y); const sorted = [...values].sort((a, b) => a - b); const sum = values.reduce((acc, val) => acc + val, 0); - const mid = Math.floor(sorted.length / 2); - const median = sorted.length % 2 === 0 ? (sorted[mid - 1] + sorted[mid]) / 2 : sorted[mid]; - - const p95 = this.calculateNormalized95thPercentile(data); - return { min: this.formatStat(sorted[0]), max: this.formatStat(sorted[sorted.length - 1]), avg: this.formatStat(sum / values.length), - median: this.formatStat(median), - p95: this.formatStat(p95), + p95: this.formatStat(sorted[Math.floor(sorted.length * 0.95)]), }; }; - stats[iface.name] = { - rx: calculate(this.interfaceChartData[iface.rx?.itemid]), - tx: calculate(this.interfaceChartData[iface.tx?.itemid]), - }; + stats[iface.name] = { rx: calculate(this.interfaceChartData[iface.rx?.itemid]), tx: calculate(this.interfaceChartData[iface.tx?.itemid]) }; }); return stats; + }, + sortedReportData() { + if (!this.reportData) return []; + return [...this.reportData].sort((a, b) => { + let aVal = this.reportSortKey.split('.').reduce((o, i) => o[i], a); + let bVal = this.reportSortKey.split('.').reduce((o, i) => o[i], b); + if (typeof aVal === 'string' && aVal.toLowerCase() === 'n/a') aVal = -1; + if (typeof bVal === 'string' && bVal.toLowerCase() === 'n/a') bVal = -1; + let modifier = this.reportSortDir === 'asc' ? 1 : -1; + if (aVal < bVal) return -1 * modifier; + if (aVal > bVal) return 1 * modifier; + return 0; + }); } }, async mounted() { - // We need chartjs-plugin-zoom for this to work. Assuming it's globally available. - if (typeof Chart.register === 'function' && window.ChartZoom) { - Chart.register(window.ChartZoom); - } + if (typeof Chart.register === 'function' && window.ChartZoom) Chart.register(window.ChartZoom); moment.locale('de'); this.fetchTabData(); }, - beforeDestroy() { - this.destroyAllCharts(); - }, methods: { formatStat: val => typeof val === 'number' ? val.toFixed(2) : val, formatUptime: s => `${Math.floor(s/(3600*24))}t ${Math.floor(s%(3600*24)/3600)}h ${Math.floor(s%3600/60)}m`, formatGeneralValue: item => (item.units === 's') ? parseFloat(item.value).toFixed(3) : (item.units === '%') ? parseFloat(item.value).toFixed(2) : item.value, getSeverityClass: s => ['sev-info', 'sev-info', 'sev-warning', 'sev-average', 'sev-high', 'sev-disaster'][s] || 'sev-info', getSeverityIcon: s => ['fa-info-circle', 'fa-info-circle', 'fa-exclamation-circle', 'fa-exclamation-triangle', 'fa-radiation-alt', 'fa-biohazard'][s] || 'fa-info-circle', - async fetchTabData() { const tab = this.activeTab; - if (this.loading[tab]) return; + if (this.loading[tab] && tab !== 'overview') return; this.loading[tab] = true; try { - if (tab === 'overview' && !this.generalData) { - const res = await axios.get(`${window.TT_CONFIG.BASE_URL}/DeviceMonitoring/generalData`, { params: { hostId: this.hostId } }); - this.generalData = res.data; + if (tab === 'overview') { + this.generalData = (await axios.get(`${window.TT_CONFIG.BASE_URL}/DeviceMonitoring/generalData`, { params: { hostId: this.hostId } })).data; } else if (tab === 'interfaces' && this.allInterfaces.length === 0) { - const res = await axios.get(`${window.TT_CONFIG.BASE_URL}/DeviceMonitoring/listInterfaces`, { params: { hostId: this.hostId } }); - this.allInterfaces = res.data; - } else if (tab === 'problems' && this.problemData.length === 0) { - const res = await axios.get(`${window.TT_CONFIG.BASE_URL}/DeviceMonitoring/getProblems`, { params: { hostId: this.hostId } }); - this.problemData = res.data; + this.allInterfaces = (await axios.get(`${window.TT_CONFIG.BASE_URL}/DeviceMonitoring/listInterfaces`, { params: { hostId: this.hostId } })).data; + } else if (tab === 'problems') { + this.problemData = (await axios.get(`${window.TT_CONFIG.BASE_URL}/DeviceMonitoring/getProblems`, { params: { hostId: this.hostId } })).data; + } else if (tab === 'configuration') { + this.configData = (await axios.get(`${window.TT_CONFIG.BASE_URL}/DeviceMonitoring/getConfigurationData`, { params: { hostId: this.hostId } })).data; + } else if (tab === 'reports') { + await this.fetchReportData(); } } catch (e) { console.error(`Failed to load data for tab ${tab}`, e); window.notify('error', `Laden von ${tab}-Daten fehlgeschlagen.`); } finally { this.loading[tab] = false; } }, + async saveSnmpConfig() { + const detailsToSave = JSON.parse(JSON.stringify(this.configData.snmp.details)); + if (this.authPassphrase) detailsToSave.authpassphrase = this.authPassphrase; + else delete detailsToSave.authpassphrase; + if (this.privPassphrase) detailsToSave.privpassphrase = this.privPassphrase; + else delete detailsToSave.privpassphrase; + try { + await axios.post(`${window.TT_CONFIG.BASE_URL}/DeviceMonitoring/updateSnmp`, { + interfaceId: this.configData.snmp.interfaceid, + details: detailsToSave + }); + window.notify('success', 'SNMP-Konfiguration gespeichert.'); + this.authPassphrase = ''; + this.privPassphrase = ''; + } catch(e) { window.notify('error', 'Fehler beim Speichern der SNMP-Konfiguration.'); } + }, + async toggleInterfaceAlarm(iface) { + try { + await axios.post(`${window.TT_CONFIG.BASE_URL}/DeviceMonitoring/updateInterfaceAlarm`, { hostId: this.hostId, item: iface, enabled: iface.isAlarmed }); + window.notify('success', `Alarm für ${iface.name} ${iface.isAlarmed ? 'aktiviert' : 'deaktiviert'}.`); + } catch(e) { window.notify('error', 'Fehler beim Ändern des Alarms.'); iface.isAlarmed = !iface.isAlarmed; } + }, + async fetchReportData() { + this.loading.reports = true; + try { + this.reportData = (await axios.get(`${window.TT_CONFIG.BASE_URL}/DeviceMonitoring/getReportData`, { params: { hostId: this.hostId, timeRange: this.reportTimeRange } })).data; + } catch (e) { + window.notify('error', 'Fehler beim Laden der Report-Daten.'); + } finally { + this.loading.reports = false; + } + }, + sortReport(key) { + if (this.reportSortKey === key) { + this.reportSortDir = this.reportSortDir === 'asc' ? 'desc' : 'asc'; + } else { + this.reportSortKey = key; + this.reportSortDir = 'asc'; + } + }, + getSortIcon(key) { + if (this.reportSortKey !== key) return 'fas fa-sort'; + return this.reportSortDir === 'asc' ? 'fas fa-sort-up' : 'fas fa-sort-down'; + }, + async handleInterfaceSelectionChange(newSelection, oldSelection) { + const added = newSelection.filter(name => !oldSelection.includes(name)); + const removed = oldSelection.filter(name => !newSelection.includes(name)); + removed.forEach(name => { + const iface = this.allInterfaces.find(i => i.name === name); + if (iface) { + if (this.chartInstances[iface.name]) { + this.chartInstances[iface.name].destroy(); + delete this.chartInstances[iface.name]; + } + } + }); + for (const name of added) await this.fetchAndRenderInterface(this.allInterfaces.find(i => i.name === name)); + }, async fetchAndRenderInterface(iface) { const itemIds = [iface.rx?.itemid, iface.tx?.itemid].filter(Boolean); if (itemIds.length === 0) return; - this.$set(this.loading.individualInterfaces, iface.name, true); try { const res = await axios.post(`${window.TT_CONFIG.BASE_URL}/DeviceMonitoring/interfaceData`, { itemIds, timeRange: this.interfaceTimeRange }); @@ -243,175 +392,25 @@ Vue.component('device-monitoring-modal', { } catch(e) { console.error(`Failed to fetch history for ${iface.name}`, e); } finally { this.$set(this.loading.individualInterfaces, iface.name, false); } }, - - async handleInterfaceSelectionChange(newSelection, oldSelection) { - const added = newSelection.filter(name => !oldSelection.includes(name)); - const removed = oldSelection.filter(name => !newSelection.includes(name)); - - removed.forEach(name => { - const iface = this.allInterfaces.find(i => i.name === name); - if (iface) { - this.destroyChart(iface.name); - delete this.interfaceChartData[iface.rx?.itemid]; - delete this.interfaceChartData[iface.tx?.itemid]; - } + renderChart(iface) { + this.$nextTick(() => { + const canvas = this.$refs['chartCanvas-' + iface.name]?.[0]; + if (!canvas) return; + if (this.chartInstances[iface.name]) this.chartInstances[iface.name].destroy(); + this.chartInstances[iface.name] = new Chart(canvas.getContext('2d'), { /* ... Chart options ... */ }); }); - - for (const name of added) { - const iface = this.allInterfaces.find(i => i.name === name); - if (iface) { - await this.fetchAndRenderInterface(iface); - } - } - }, - - async handleTimeOrNormalizationChange() { - this.destroyAllCharts(); - this.interfaceChartData = {}; - this.loading.interfaces = true; - - const interfacesToFetch = this.selectedInterfacesData; - for (const iface of interfacesToFetch) { - await this.fetchAndRenderInterface(iface); - } - - this.loading.interfaces = false; - }, - - async renderChart(iface) { - let tries = 0; - while (!this.$refs['chartCanvas-' + iface.name]?.[0] && tries < 10) { - console.log(typeof this.$refs['chartCanvas-' + iface.name]?.[0]); - await Promise.all([ - this.$nextTick(), - new Promise(resolve => setTimeout(resolve, 100)) - ]); - } - const canvas = this.$refs['chartCanvas-' + iface.name]?.[0]; - - console.log(canvas, this.$refs); - if (!canvas) return; - - if (this.chartInstances[iface.name]) { - this.chartInstances[iface.name].destroy(); - } - - this.chartInstances[iface.name] = new Chart(canvas.getContext('2d'), { - type: 'line', - data: { - datasets: [ - { - label: 'Empfangen', - data: this.displayChartData[iface.rx?.itemid] || [], - borderColor: '#4CAF50', - borderWidth: 1.5, - fill: true, - backgroundColor: 'rgba(76, 175, 80, 0.2)', - pointRadius: 0, - tension: 0.1 - }, - { - label: 'Gesendet', - data: this.displayChartData[iface.tx?.itemid] || [], - borderColor: '#2196F3', - borderWidth: 1.5, - fill: true, - backgroundColor: 'rgba(33, 150, 243, 0.2)', - pointRadius: 0, - tension: 0.1 - } - ] - }, - options: { - responsive: true, maintainAspectRatio: true, - interaction: { - mode: 'index', - intersect: false, - }, - scales: { - x: { - type: 'time', - time: { tooltipFormat: 'DD.MM.YYYY HH:mm:ss' }, - adapters: { date: { locale: 'de' } } - }, - y: { - beginAtZero: true, - title: { display: true, text: 'Mbps' } - } - }, - plugins: { - legend: { - display: true, position: 'bottom', - labels: { boxWidth: 12, font: { size: 10 } } - }, - zoom: { - pan: { enabled: true, mode: 'x' }, - zoom: { wheel: { enabled: false }, pinch: { enabled: true }, mode: 'x', drag: { enabled: true } }, - } - } - } - }); - }, - downsampleData(data, mode) { - const bucketSize = Math.ceil(data.length / this.downsampleThreshold); - const downsampled = []; - for (let i = 0; i < data.length; i += bucketSize) { - const chunk = data.slice(i, i + bucketSize); - if (chunk.length === 0) continue; - const representativeX = chunk[Math.floor(chunk.length / 2)].x; - let representativeY; - if (mode === 'max') { - representativeY = Math.max(...chunk.map(p => p.y)); - } else { // avg - const sum = chunk.reduce((acc, p) => acc + p.y, 0); - representativeY = sum / chunk.length; - } - downsampled.push({ x: representativeX, y: representativeY }); - } - return downsampled; - }, - calculateNormalized95thPercentile(data) { - if (!data || data.length < 3) return null; - - const averagedValues = []; - for (let i = 0; i <= data.length - 3; i += 3) { - const chunk = data.slice(i, i + 3); - const sum = chunk.reduce((acc, p) => acc + p.y, 0); - averagedValues.push(sum / 3); - } - - if (averagedValues.length === 0) return null; - - const sorted = averagedValues.sort((a, b) => a - b); - const index = Math.floor(sorted.length * 0.95); - return sorted[index]; }, openLiveChartPopup(iface) { const url = `${window.TT_CONFIG.BASE_URL}/DeviceMonitoring/liveGraphPage?rx_id=${iface.rx?.itemid || ''}&tx_id=${iface.tx?.itemid || ''}&name=${encodeURIComponent(iface.name)}`; window.open(url, `livegraph_${iface.name.replace(/[^a-zA-Z0-9]/g, "_")}`, 'width=800,height=450,resizable=yes,scrollbars=yes'); }, - destroyChart(name) { - if (this.chartInstances[name]) { - this.chartInstances[name].destroy(); - delete this.chartInstances[name]; - } - }, - destroyAllCharts() { - Object.values(this.chartInstances).forEach(c => c.destroy()); - this.chartInstances = {}; - }, - resetAllChartsZoom() { - Object.values(this.chartInstances).forEach(chart => { - chart.resetZoom(); - }); - } + resetAllChartsZoom() { Object.values(this.chartInstances).forEach(chart => chart.resetZoom()); }, }, watch: { activeTab: 'fetchTabData', - selectedInterfaces(newVal, oldVal) { - this.handleInterfaceSelectionChange(newVal, oldVal); - }, - interfaceTimeRange: 'handleTimeOrNormalizationChange', - dataNormalizationMode: 'handleTimeOrNormalizationChange', + selectedInterfaces(newVal, oldVal) { this.handleInterfaceSelectionChange(newVal, oldVal); }, + interfaceTimeRange() { this.handleInterfaceSelectionChange(this.selectedInterfaces, this.selectedInterfaces); }, + dataNormalizationMode() { this.handleInterfaceSelectionChange(this.selectedInterfaces, this.selectedInterfaces); }, + reportTimeRange: 'fetchReportData' } }); \ No newline at end of file From 73fca90fd938d1832f5457aead4db57c0ca0697c Mon Sep 17 00:00:00 2001 From: Luca Haid Date: Mon, 1 Sep 2025 11:24:32 +0000 Subject: [PATCH 018/103] Device monitoring/v2 --- public/bundler.php | 1 + public/cssbundler.php | 1 + public/js/pages/Device/Device.css | 236 +++++++++++++++--- public/js/pages/Device/DeviceMonitoring.js | 55 ++-- .../vue/tt-components/css/tt-switch.css | 103 ++++++++ public/plugins/vue/tt-components/tt-switch.js | 14 ++ 6 files changed, 367 insertions(+), 43 deletions(-) create mode 100644 public/plugins/vue/tt-components/css/tt-switch.css create mode 100644 public/plugins/vue/tt-components/tt-switch.js diff --git a/public/bundler.php b/public/bundler.php index 081e7c041..6b165e718 100644 --- a/public/bundler.php +++ b/public/bundler.php @@ -39,6 +39,7 @@ $jsFiles = [ "plugins/vue/tt-components/tt-select.js", "plugins/vue/tt-components/tt-datepicker.js", "plugins/vue/tt-components/tt-input.js", + "plugins/vue/tt-components/tt-switch.js", "plugins/vue/tt-components/tt-input-article.js", "plugins/vue/tt-components/tt-button.js", "plugins/vue/tt-components/tt-modal.js", diff --git a/public/cssbundler.php b/public/cssbundler.php index ae1eee5a6..79bdf2724 100644 --- a/public/cssbundler.php +++ b/public/cssbundler.php @@ -44,6 +44,7 @@ $cssFiles = [ 'plugins/vue/tt-components/css/tt-tooltip.css', 'plugins/vue/tt-components/css/tt-loader.css', 'plugins/vue/tt-components/css/tt-select.css', + 'plugins/vue/tt-components/css/tt-switch.css', 'plugins/vue/tt-components/css/tt-file-gallery.css', 'plugins/vue/tt-components/css/tt-position-manager.css', ]; diff --git a/public/js/pages/Device/Device.css b/public/js/pages/Device/Device.css index 2548161e7..11e78d3d9 100644 --- a/public/js/pages/Device/Device.css +++ b/public/js/pages/Device/Device.css @@ -1,29 +1,207 @@ -.monitoring-tabs { display: flex; border-bottom: 1px solid #dee2e6; } -.monitoring-tabs button { background: none; border: none; padding: 10px 15px; cursor: pointer; border-bottom: 3px solid transparent; font-size: 0.9rem; color: #495057; } -.monitoring-tabs button:hover { color: #0056b3; } -.monitoring-tabs button.active { border-bottom-color: #007bff; color: #007bff; font-weight: bold; } -.monitoring-content { padding: 15px; min-height: 400px; } -.overview-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); gap: 15px; } -.chart-container { display: grid; grid-template-columns: repeat(auto-fit, minmax(450px, 1fr)); gap: 15px; } -.chart-title { font-size: 0.9rem; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } -.problems-list { display: flex; flex-direction: column; gap: 10px; } -.problem-card { display: flex; align-items: center; padding: 10px; border-radius: 5px; border-left-width: 5px; border-left-style: solid; background-color: #f8f9fa; } -.problem-icon { font-size: 1.5rem; margin-right: 15px; width: 30px; text-align: center; } -.problem-details { flex-grow: 1; } -.problem-header { display: flex; justify-content: space-between; align-items: baseline; } -.problem-name { font-weight: 500; } -.problem-time { font-size: 0.8rem; color: #6c757d; } -.problem-opdata { font-size: 0.85rem; color: #495057; margin-top: 4px; } -.sev-info { border-left-color: #17a2b8; } .sev-info .problem-icon { color: #17a2b8; } -.sev-warning { border-left-color: #ffc107; } .sev-warning .problem-icon { color: #ffc107; } -.sev-average { border-left-color: #fd7e14; } .sev-average .problem-icon { color: #fd7e14; } -.sev-high { border-left-color: #dc3545; } .sev-high .problem-icon { color: #dc3545; } -.sev-disaster { border-left-color: #7B014C; } .sev-disaster .problem-icon { color: #7B014C; } -.overview-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); gap: 1rem; } -.problem-counts { display: flex; justify-content: space-around; text-align: center; padding: 1rem 0; } -.problem-counts .count { font-size: 1.5rem; font-weight: bold; display: block; } -.problem-counts .period { font-size: 0.8rem; color: #6c757d; } -.problems-list.resolved .problem-card { opacity: 0.8; } -.sev-resolved { border-left: 5px solid #28a745; } -.sev-resolved .problem-icon { color: #28a745; } -.c-pointer { cursor: pointer; } \ No newline at end of file +.monitoring-tabs { + display: flex; + border-bottom: 1px solid #dee2e6; +} + +.monitoring-tabs button { + background: none; + border: none; + padding: 10px 15px; + cursor: pointer; + border-bottom: 3px solid transparent; + font-size: 0.9rem; + color: #495057; +} + +.monitoring-tabs button:hover { + color: #0056b3; +} + +.monitoring-tabs button.active { + border-bottom-color: #007bff; + color: #007bff; + font-weight: bold; +} + +.monitoring-content { + padding: 15px; + min-height: 400px; +} + +.overview-grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); + gap: 15px; +} + +.chart-container { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(450px, 1fr)); + gap: 15px; +} + +.chart-title { + font-size: 0.9rem; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +.problems-list { + display: flex; + flex-direction: column; + gap: 10px; +} + +.problem-card { + display: flex; + align-items: center; + padding: 10px; + border-radius: 5px; + border-left-width: 5px; + border-left-style: solid; + background-color: #f8f9fa; +} + +.problem-icon { + font-size: 1.5rem; + margin-right: 15px; + width: 30px; + text-align: center; +} + +.problem-details { + flex-grow: 1; +} + +.problem-header { + display: flex; + justify-content: space-between; + align-items: baseline; +} + +.problem-name { + font-weight: 500; +} + +.problem-time { + font-size: 0.8rem; + color: #6c757d; +} + +.problem-opdata { + font-size: 0.85rem; + color: #495057; + margin-top: 4px; +} + +.sev-info { + border-left-color: #17a2b8; +} + +.sev-info .problem-icon { + color: #17a2b8; +} + +.sev-warning { + border-left-color: #ffc107; +} + +.sev-warning .problem-icon { + color: #ffc107; +} + +.sev-average { + border-left-color: #fd7e14; +} + +.sev-average .problem-icon { + color: #fd7e14; +} + +.sev-high { + border-left-color: #dc3545; +} + +.sev-high .problem-icon { + color: #dc3545; +} + +.sev-disaster { + border-left-color: #7B014C; +} + +.sev-disaster .problem-icon { + color: #7B014C; +} + +.overview-grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); + gap: 1rem; +} + +.problem-counts { + display: flex; + justify-content: space-around; + text-align: center; + padding: 1rem 0; +} + +.problem-counts .count { + font-size: 1.5rem; + font-weight: bold; + display: block; +} + +.problem-counts .period { + font-size: 0.8rem; + color: #6c757d; +} + +.problems-list.resolved .problem-card { + opacity: 0.8; +} + +.sev-resolved { + border-left: 5px solid #28a745; +} + +.sev-resolved .problem-icon { + color: #28a745; +} + +.c-pointer { + cursor: pointer; +} + +/* Styles for Interface Alarm List */ +.interface-alarm-list { + max-height: 450px; + overflow-y: auto; + border-radius: .25rem; +} + +.interface-alarm-item { + display: flex; + justify-content: space-between; + align-items: center; + padding: .75rem 1rem; + border-bottom: 1px solid #e9ecef; + transition: background-color 0.2s ease-in-out; +} + +.interface-alarm-item:last-child { + border-bottom: none; +} + +.interface-alarm-item:hover { + background-color: #f8f9fa; +} + +.interface-name { + font-weight: 500; + flex-grow: 1; + margin-right: 1rem; + word-break: break-all; +} \ No newline at end of file diff --git a/public/js/pages/Device/DeviceMonitoring.js b/public/js/pages/Device/DeviceMonitoring.js index 96266313b..6020bb2a9 100644 --- a/public/js/pages/Device/DeviceMonitoring.js +++ b/public/js/pages/Device/DeviceMonitoring.js @@ -200,18 +200,25 @@ Vue.component('device-monitoring-modal', {
- -
- - - - - - - - -
SchnittstelleAlarmierung aktiv
{{ iface.name }}
-
+ +
+
+ Keine Schnittstellen gefunden. +
+
+
{{ iface.name }}
+
+ +
+
+
@@ -242,6 +249,7 @@ Vue.component('device-monitoring-modal', { dataNormalizationMode: 'avg', downsampleThreshold: 500, configData: { snmp: null, interfaces: [] }, + interfaceSearch: '', snmpV3Levels: [{text: 'noAuthNoPriv', value: '0'}, {text: 'authNoPriv', value: '1'}, {text: 'authPriv', value: '2'}], snmpV3Auth: [{text: 'MD5', value: '0'}, {text: 'SHA-1', value: '1'}], snmpV3Priv: [{text: 'DES', value: '0'}, {text: 'AES-128', value: '1'}], @@ -257,6 +265,16 @@ Vue.component('device-monitoring-modal', { computed: { interfaceOptions() { return this.allInterfaces.map(iface => ({ text: iface.name, value: iface.name })); }, selectedInterfacesData() { return this.allInterfaces.filter(iface => this.selectedInterfaces.includes(iface.name)); }, + filteredInterfaces() { + if (!this.configData.interfaces || !Array.isArray(this.configData.interfaces)) return []; + if (!this.interfaceSearch) { + return this.configData.interfaces; + } + const search = this.interfaceSearch.toLowerCase(); + return this.configData.interfaces.filter(iface => + iface.name.toLowerCase().includes(search) + ); + }, statistics() { if (this.selectedInterfaces.length === 0 || Object.keys(this.interfaceChartData).length === 0) return {}; const stats = {}; @@ -315,6 +333,9 @@ Vue.component('device-monitoring-modal', { this.problemData = (await axios.get(`${window.TT_CONFIG.BASE_URL}/DeviceMonitoring/getProblems`, { params: { hostId: this.hostId } })).data; } else if (tab === 'configuration') { this.configData = (await axios.get(`${window.TT_CONFIG.BASE_URL}/DeviceMonitoring/getConfigurationData`, { params: { hostId: this.hostId } })).data; + if (this.configData.interfaces && Array.isArray(this.configData.interfaces)) { + this.configData.interfaces.forEach(iface => this.$set(iface, 'loading', false)); + } } else if (tab === 'reports') { await this.fetchReportData(); } @@ -339,10 +360,16 @@ Vue.component('device-monitoring-modal', { } catch(e) { window.notify('error', 'Fehler beim Speichern der SNMP-Konfiguration.'); } }, async toggleInterfaceAlarm(iface) { + this.$set(iface, 'loading', true); try { await axios.post(`${window.TT_CONFIG.BASE_URL}/DeviceMonitoring/updateInterfaceAlarm`, { hostId: this.hostId, item: iface, enabled: iface.isAlarmed }); window.notify('success', `Alarm für ${iface.name} ${iface.isAlarmed ? 'aktiviert' : 'deaktiviert'}.`); - } catch(e) { window.notify('error', 'Fehler beim Ändern des Alarms.'); iface.isAlarmed = !iface.isAlarmed; } + } catch(e) { + window.notify('error', 'Fehler beim Ändern des Alarms.'); + iface.isAlarmed = !iface.isAlarmed; + } finally { + this.$set(iface, 'loading', false); + } }, async fetchReportData() { this.loading.reports = true; @@ -413,4 +440,4 @@ Vue.component('device-monitoring-modal', { dataNormalizationMode() { this.handleInterfaceSelectionChange(this.selectedInterfaces, this.selectedInterfaces); }, reportTimeRange: 'fetchReportData' } -}); \ No newline at end of file +}); diff --git a/public/plugins/vue/tt-components/css/tt-switch.css b/public/plugins/vue/tt-components/css/tt-switch.css new file mode 100644 index 000000000..03647d5c2 --- /dev/null +++ b/public/plugins/vue/tt-components/css/tt-switch.css @@ -0,0 +1,103 @@ +.tt-switch { + position: relative; + display: inline-block; + width: 44px; + height: 24px; +} + +.tt-switch input { + opacity: 0; + width: 0; + height: 0; +} + +.slider { + position: absolute; + cursor: pointer; + top: 0; + left: 0; + right: 0; + bottom: 0; + background-color: #ccc; + transition: background-color .4s; +} + +.slider:before { + position: absolute; + content: ""; + height: 18px; + width: 18px; + left: 3px; + bottom: 3px; + background-color: white; + transition: transform .4s, opacity .4s; /* Added opacity transition */ + opacity: 1; +} + +input:checked + .slider { + background-color: #28a745; +} + +input:focus + .slider { + box-shadow: 0 0 1px #28a745; +} + +input:checked + .slider:before { + transform: translateX(20px); +} + +.slider.round { + border-radius: 24px; +} + +.slider.round:before { + border-radius: 50%; +} + +/* --- Loading State --- */ + +input:disabled + .slider { + cursor: not-allowed; + opacity: 0.7; +} + +/* Fade out the handle when loading/disabled */ +input[type="checkbox"]:disabled + .slider:before { + opacity: 0; +} + +/* Wrapper for the spinner to handle translation */ +.spinner-wrapper { + display: block; + position: absolute; + height: 18px; + width: 18px; + left: 3px; + bottom: 3px; + transition: transform .4s; +} + +/* Move wrapper when switch is checked */ +input:checked + .slider .spinner-wrapper { + transform: translateX(20px); +} +input:checked:disabled + .slider .spinner-wrapper { + transform: translateX(20px); +} + +/* The actual spinner for rotation */ +.spinner { + display: block; + width: 100%; + height: 100%; + border: 3px solid rgba(255, 255, 255, 0.3); + border-top-color: #fff; + border-radius: 50%; + animation: spin 0.8s linear infinite; +} + +@keyframes spin { + to { + transform: rotate(360deg); + } +} \ No newline at end of file diff --git a/public/plugins/vue/tt-components/tt-switch.js b/public/plugins/vue/tt-components/tt-switch.js new file mode 100644 index 000000000..8f25860dc --- /dev/null +++ b/public/plugins/vue/tt-components/tt-switch.js @@ -0,0 +1,14 @@ +Vue.component('tt-switch', { + template: ` + + `, + props: { + value: { type: Boolean, default: false }, + loading: { type: Boolean, default: false } + } +}); \ No newline at end of file From 80470bf9bd14920424642ff3af1046ccf3463b8e Mon Sep 17 00:00:00 2001 From: Luca Haid Date: Mon, 1 Sep 2025 14:07:59 +0200 Subject: [PATCH 019/103] fixed showing pdfs to download and not allowing in future --- Layout/default/ConstructionConsent/Form.php | 2 +- Layout/default/ConstructionConsent/View.php | 12 ++++++++---- 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/Layout/default/ConstructionConsent/Form.php b/Layout/default/ConstructionConsent/Form.php index 5a33a92e3..23139d6b4 100644 --- a/Layout/default/ConstructionConsent/Form.php +++ b/Layout/default/ConstructionConsent/Form.php @@ -216,7 +216,7 @@
Oder Plan hochladen
- +
diff --git a/Layout/default/ConstructionConsent/View.php b/Layout/default/ConstructionConsent/View.php index 51787996c..bfddd46ac 100644 --- a/Layout/default/ConstructionConsent/View.php +++ b/Layout/default/ConstructionConsent/View.php @@ -212,10 +212,14 @@ $pagination_entity_name = "Adressen"; Plan/Skizze - file && $item->file->file && $item->file->file->fileExists()): ?> - - - + file && $item->file->file && $item->file->file->fileExists()): + $dataUrl = $item->file->file->asDataUrl(); + if (str_contains($dataUrl, 'application/pdf')) { + echo ' Download PDF'; + } else { + echo 'File preview'; + } + endif; ?> From fc23c5ce5fdc1bd5b7825310b6f9b1c0de995c5b Mon Sep 17 00:00:00 2001 From: Daniel Spitzer Date: Mon, 1 Sep 2025 22:12:01 +0200 Subject: [PATCH 020/103] =?UTF-8?q?Bugfix=20Calender=20Update=20sync=20ric?= =?UTF-8?q?htung=20MS=20*=20Die=20Positionierung=20von=20Originalend=20war?= =?UTF-8?q?=20ung=C3=BCnstig.=20ist=20nun=20bereinigt.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- application/Calendar/CalendarModel.php | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/application/Calendar/CalendarModel.php b/application/Calendar/CalendarModel.php index eef462ce9..1a5cfeb53 100644 --- a/application/Calendar/CalendarModel.php +++ b/application/Calendar/CalendarModel.php @@ -650,11 +650,14 @@ WHERE `TimerecordingCategory`.`hourday`!='1' AND `TimerecordingCategory`.`hourda $start = ((($r->start - 7200000) / 1000)); $end = ((($r->end - 7200000) / 1000)); - $originalend = $end; + if ($title) { $start = strtotime($r->start); $end = strtotime($r->end); } + + $originalend = $end; + $allday = ($r->allday); if ($allday) { $start = $start + 7200; @@ -804,8 +807,6 @@ WHERE `TimerecordingCategory`.`hourday`!='1' AND `TimerecordingCategory`.`hourda $db->delete("tmp_cal_events_attachments", "id = '" . $tmpid . "'"); } } - - $updateArray['attachments'] = $attachments; $updateArray['end_time'] = $originalend; } From 55066d2e6fbab91f61f1c1e38c5860d6a95bf7d7 Mon Sep 17 00:00:00 2001 From: Daniel Spitzer Date: Tue, 2 Sep 2025 09:22:59 +0200 Subject: [PATCH 021/103] =?UTF-8?q?Ger=C3=A4tetyp=20erweitert=20um=20Tempe?= =?UTF-8?q?raturen?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Layout/default/Devicetype/Form.php | 17 ++++++++++ application/Device/DeviceController.php | 2 ++ .../Devicetype/DevicetypeController.php | 13 ++++++++ application/Devicetype/DevicetypeModel.php | 2 ++ ...50902070036_devicetype_add_temperature.php | 33 +++++++++++++++++++ public/js/pages/Device/Device.js | 2 ++ 6 files changed, 69 insertions(+) create mode 100644 db/migrations/20250902070036_devicetype_add_temperature.php diff --git a/Layout/default/Devicetype/Form.php b/Layout/default/Devicetype/Form.php index 83bd9c70d..2a6bc254c 100644 --- a/Layout/default/Devicetype/Form.php +++ b/Layout/default/Devicetype/Form.php @@ -2,6 +2,12 @@ +
@@ -78,7 +84,18 @@ value="power ?>">
+
+ +
+ +
+
+ +
+
diff --git a/application/Device/DeviceController.php b/application/Device/DeviceController.php index 573bfc5ab..5c99a5ba0 100644 --- a/application/Device/DeviceController.php +++ b/application/Device/DeviceController.php @@ -44,6 +44,8 @@ class DeviceController extends mfBaseController "manufacturer" => $deviceType->devicemanufactor->name, "price" => $deviceType->price, "power" => $deviceType->power, + "temp_warning" => $deviceType->temp_warning, + "temp_critical" => $deviceType->temp_critical, "creator" => $deviceType->creator->name, "created" => $deviceType->create, ]; diff --git a/application/Devicetype/DevicetypeController.php b/application/Devicetype/DevicetypeController.php index 7950de6d3..3665059b1 100644 --- a/application/Devicetype/DevicetypeController.php +++ b/application/Devicetype/DevicetypeController.php @@ -76,6 +76,17 @@ class DevicetypeController extends mfBaseController } else { $power = $r->power; } + if (!$r->temp_warning) { + $temp_warning = "80"; + } else { + $temp_warning = $r->temp_warning; + } + + if (!$r->temp_critical) { + $temp_critical = "90"; + } else { + $temp_critical = $r->temp_critical; + } if ($r->olt) { @@ -88,6 +99,8 @@ class DevicetypeController extends mfBaseController $data['price'] = $price; $data['power'] = $power; $data['olt'] = $olt; + $data['temp_warning'] = $temp_warning; + $data['temp_critical'] = $temp_critical; if (!$data['name']) { $this->layout()->setFlash("Name darf nicht leer sein", "error"); diff --git a/application/Devicetype/DevicetypeModel.php b/application/Devicetype/DevicetypeModel.php index 829fef08d..c1754d029 100644 --- a/application/Devicetype/DevicetypeModel.php +++ b/application/Devicetype/DevicetypeModel.php @@ -7,6 +7,8 @@ class DevicetypeModel public $price = null; public $olt = null; public $devicemanufactor_id = null; + public $temp_warning = 80; + public $temp_critical = 90; public $create_by = null; diff --git a/db/migrations/20250902070036_devicetype_add_temperature.php b/db/migrations/20250902070036_devicetype_add_temperature.php new file mode 100644 index 000000000..ffff666d2 --- /dev/null +++ b/db/migrations/20250902070036_devicetype_add_temperature.php @@ -0,0 +1,33 @@ +getEnvironment() == "thetool") { + $table = $this->table("Devicetype"); + $table->addColumn("temp_warning", "integer", ['null' => false,'default' => '80', "after" => "power"]); + $table->addColumn("temp_critical", "integer", ['null' => false,'default' => '90', "after" => "temp_warning"]); + $table->update(); + } + + if($this->getEnvironment() == "addressdb") { + + } + } + + public function down(): void + { + if($this->getEnvironment() == "thetool") { + $this->table("Devicetype")->removeColumn("temp_warning")->save(); + $this->table("Devicetype")->removeColumn("temp_critical")->save(); + } + + if($this->getEnvironment() == "addressdb") { + + } + } +} diff --git a/public/js/pages/Device/Device.js b/public/js/pages/Device/Device.js index 3b1c804a5..ef048a27b 100644 --- a/public/js/pages/Device/Device.js +++ b/public/js/pages/Device/Device.js @@ -241,6 +241,8 @@ Vue.component('DeviceType', { {text: 'Hersteller', key: 'manufacturer', filter: 'search', class: 'text-center'}, {text: 'Preis', key: 'price', filter: 'numberRange', class: 'text-center', suffix: ' €'}, {text: 'max. Leistung', key: 'power', filter: 'numberRange', class: 'text-center', suffix: ' W'}, + {text: 'Temp. Warnung', key: 'temp_warning', filter: 'numberRange', class: 'text-center', suffix: ' °C'}, + {text: 'Temp. Kritisch', key: 'temp_critical', filter: 'numberRange', class: 'text-center', suffix: ' °C'}, {text: 'Erstellungsdatum', key: 'created', filter: 'date', class: 'text-center'}, {text: 'Erstellt von', key: 'creator', filter: 'search', class: 'text-center'}, {text: 'Aktionen', key: 'actions', class: 'text-center', sortable: false, filter: false, priority: 9}, From bdd32b31a8c1561626446c94a4e38613c8d89646 Mon Sep 17 00:00:00 2001 From: Daniel Spitzer Date: Tue, 2 Sep 2025 09:27:51 +0200 Subject: [PATCH 022/103] =?UTF-8?q?Ger=C3=A4tetyp=20erweitert=20um=20Tempe?= =?UTF-8?q?raturen?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- public/js/pages/Device/Device.js | 124 +++++++++++++++---------------- 1 file changed, 62 insertions(+), 62 deletions(-) diff --git a/public/js/pages/Device/Device.js b/public/js/pages/Device/Device.js index ef048a27b..d56b8ec77 100644 --- a/public/js/pages/Device/Device.js +++ b/public/js/pages/Device/Device.js @@ -11,38 +11,38 @@ const deviceTypeFilterOptions = window?.TT_CONFIG?.DEVICE_TYPES.map(type => ({ Vue.component('device-view-switch', { //language=Vue template: ` -
-
- - - -
-
- -
-
- `, +
+
+ + + +
+
+ +
+
+ `, props: ['value'], data() { return { @@ -172,24 +172,24 @@ Vue.component('DeviceTable', { Vue.component('DeviceManufacturer', { //language=Vue template: ` - + - + - + - - `, + + `, data() { return { window: window, @@ -211,24 +211,24 @@ Vue.component('DeviceManufacturer', { Vue.component('DeviceType', { //language=Vue template: ` - + - + - + - - `, + + `, data() { return { window: window, From b173a6edc1bd132c552065ae9d964bb6cfbe8f69 Mon Sep 17 00:00:00 2001 From: Luca Haid Date: Tue, 2 Sep 2025 08:36:33 +0000 Subject: [PATCH 023/103] rmlworkorder major upgrade --- .../RMLWorkorderAdminController.php | 375 +---------- .../RMLWorkorderCompanyController.php | 353 +--------- .../RMLWorkorderTenantConfigModel.php | 12 - .../WorkorderModel.php} | 42 +- .../WorkorderAdminController.php | 296 +++++++++ .../WorkorderBase/WorkorderBaseController.php | 139 ++++ .../WorkorderCompanyController.php | 195 ++++++ .../WorkorderCompanyModel.php} | 4 +- .../WorkorderDocumentationModel.php} | 4 +- .../WorkorderJournalModel.php} | 3 +- .../WorkorderTenantConfigModel.php | 31 + .../20250901410000_workorder_rename.php | 81 +++ lib/Helper/Helper.php | 12 + .../RMLWorkorderAdmin/RMLWorkorderAdmin.js | 601 ------------------ .../RMLWorkorderCompany/RMLWorkorder.css | 40 -- .../RMLWorkorderCompany.js | 568 ----------------- .../RMLWorkorderCompanyDashboardView.js | 53 -- .../RMLWorkorderDashboardView.js | 61 -- .../js/pages/WorkorderAdmin/WorkorderAdmin.js | 310 +++++++++ .../WorkorderBase.css} | 0 .../js/pages/WorkorderBase/WorkorderBase.js | 471 ++++++++++++++ .../WorkorderCompany/WorkorderCompany.js | 176 +++++ public/plugins/vue/tt-components/tt-modal.js | 2 +- .../vue/tt-components/tt-number-range.js | 6 + 24 files changed, 1762 insertions(+), 2073 deletions(-) delete mode 100644 application/RMLWorkorderTenantConfig/RMLWorkorderTenantConfigModel.php rename application/{RMLWorkorder/RMLWorkorderModel.php => Workorder/WorkorderModel.php} (79%) create mode 100644 application/WorkorderAdmin/WorkorderAdminController.php create mode 100644 application/WorkorderBase/WorkorderBaseController.php create mode 100644 application/WorkorderCompany/WorkorderCompanyController.php rename application/{RMLWorkorderCompany/RMLWorkorderCompanyModel.php => WorkorderCompany/WorkorderCompanyModel.php} (65%) rename application/{RMLWorkorderDocumentation/RMLWorkorderDocumentationModel.php => WorkorderDocumentation/WorkorderDocumentationModel.php} (66%) rename application/{RMLWorkorderJournal/RMLWorkorderJournalModel.php => WorkorderJournal/WorkorderJournalModel.php} (70%) create mode 100644 application/WorkorderTenantConfig/WorkorderTenantConfigModel.php create mode 100644 db/migrations/20250901410000_workorder_rename.php delete mode 100644 public/js/pages/RMLWorkorderAdmin/RMLWorkorderAdmin.js delete mode 100644 public/js/pages/RMLWorkorderCompany/RMLWorkorder.css delete mode 100644 public/js/pages/RMLWorkorderCompany/RMLWorkorderCompany.js delete mode 100644 public/js/pages/RMLWorkorderCompanyDashboardView/RMLWorkorderCompanyDashboardView.js delete mode 100644 public/js/pages/RMLWorkorderDashboardView/RMLWorkorderDashboardView.js create mode 100644 public/js/pages/WorkorderAdmin/WorkorderAdmin.js rename public/js/pages/{RMLWorkorderAdmin/RMLWorkorderAdmin.css => WorkorderBase/WorkorderBase.css} (100%) create mode 100644 public/js/pages/WorkorderBase/WorkorderBase.js create mode 100644 public/js/pages/WorkorderCompany/WorkorderCompany.js diff --git a/application/RMLWorkorderAdmin/RMLWorkorderAdminController.php b/application/RMLWorkorderAdmin/RMLWorkorderAdminController.php index f45a72db7..71a95fac2 100644 --- a/application/RMLWorkorderAdmin/RMLWorkorderAdminController.php +++ b/application/RMLWorkorderAdmin/RMLWorkorderAdminController.php @@ -1,375 +1,8 @@ 'id', 'text' => 'Auftrags-Nr.', 'table' => ['sortable' => true]], - ['key' => 'preordercampaign_id', 'text' => 'Kampagne', 'modal' => false, 'table' => ['filter' => 'select']], - ['key' => 'preorderInfo', 'text' => 'Kunde', 'modal' => false, 'table' => ['sortable' => false]], - ['key' => 'rimo_fcp_name', 'text' => 'FCP', 'modal' => false, 'table' => ['sortable' => true]], - ['key' => 'companyName', 'text' => 'Zugewiesene Firma', 'modal' => false, 'table' => ['sortable' => true]], - ['key' => 'status', 'text' => 'Status', 'modal' => false, 'table' => ['filter' => 'iconSelect', 'filterOptions' => [ - ['value' => 'new', 'text' => 'Neu', 'icon' => 'fas fa-star text-primary'], - ['value' => 'assigned', 'text' => 'Zugewiesen', 'icon' => 'fas fa-user-check text-info'], - ['value' => 'scheduled', 'text' => 'Geplant', 'icon' => 'fas fa-calendar-check text-warning'], - ['value' => 'correction_requested', 'text' => 'Korrektur angefordert', 'icon' => 'fas fa-exclamation-triangle text-danger'], - ['value' => 'intervention_required', 'text' => 'Eingriff erforderlich', 'icon' => 'fas fa-times-circle text-danger'], - ['value' => 'problem_solved', 'text' => 'Problem gelöst', 'icon' => 'fas fa-check-circle text-success'], - ['value' => 'documented', 'text' => 'Dokumentiert', 'icon' => 'fas fa-file-alt text-success'], - ['value' => 'completed', 'text' => 'Abgeschlossen', 'icon' => 'fas fa-check-double text-secondary'], - ['value' => 'cancelled', 'text' => 'Abgebrochen', 'icon' => 'fas fa-ban text-danger'], - ]]], - ['key' => 'additionalInfo', 'text' => 'Notiz', 'modal' => false, 'table' => ['sortable' => true]], - ['key' => 'deadlineDate', 'text' => 'Deadline', 'modal' => false, 'table' => ['filter' => 'date', 'sortable' => true]], - ]; - - private function getStatusText(string $statusKey): string - { - foreach ($this->columns as $column) { - if ($column['key'] === 'status') { - foreach ($column['table']['filterOptions'] as $option) { - if ($option['value'] === $statusKey) { - return $option['text']; - } - } - } - } - return ucfirst(str_replace('_', ' ', $statusKey)); // Fallback - } - - protected function indexAction() - { - $campaigns = Helper::getPreorderCampaignFromUser($this->user, true); - $this->columns[array_search('preordercampaign_id', array_column($this->columns, 'key'))]['table']['filterOptions'] = array_map( - fn($c) => ['value' => $c->id, 'text' => $c->name], - $campaigns - ); - - $this->createWorkordersFromPreorders(); - Helper::renderVue($this, 'RMLWorkorderAdmin', $this->headerTitle, [ - "CRUD_CONFIG" => $this->getCrudConfig(), - "TABLE_URL" => $this::getUrl("RMLWorkorderAdmin/get"), - ]); - } - - protected function getAction() - { - $json = json_decode(file_get_contents('php://input'), true); - $pagination = $json['pagination'] ?? ['page' => 1, 'per_page' => 10]; - $filters = $json['filters'] ?? []; - $order = $json['order'] ?? []; - - $allowedCampaignIds = Helper::getPreorderCampaignFromUser($this->user); - - if (empty($allowedCampaignIds)) { - self::returnJson([ - 'rows' => [], - 'pagination' => array_merge($pagination, ['total_rows' => 0, 'total_pages' => 0, 'filtered_available' => 0]) - ]); - return; - } - - $limit = $pagination['per_page']; - $offset = ($pagination['page'] - 1) * $limit; - - $workorders = RMLWorkorderModel::getAdminWorkorders($filters, $limit, $offset, $order, $allowedCampaignIds); - $totalCount = RMLWorkorderModel::countAdminWorkorders($filters, $allowedCampaignIds); - - $rows = array_map(function ($workorder) { - $row = (array)$workorder; - $row['companyName'] ??= 'Nicht zugewiesen'; - return $row; - }, $workorders); - - self::returnJson([ - 'rows' => $rows, - 'pagination' => [ - 'page' => $pagination['page'], - 'per_page' => $pagination['per_page'], - 'total_rows' => $totalCount, - 'total_pages' => ceil($totalCount / $limit), - 'filtered_available' => $totalCount - ] - ]); - } - - private function createWorkordersFromPreorders() - { - $configs = RMLWorkorderTenantConfigModel::getAll(); - foreach ($configs as $config) { - $filters = json_decode($config->workorderCreationFilters, true); - if (empty($filters)) continue; - - $networks = NetworkModel::getAll(['owner_id' => $config->addressId]); - if (empty($networks)) continue; - - $tenantCampaigns = array_map(fn($n) => $n->id, PreordercampaignModel::getAll(['network_id' => array_map(fn($n) => $n->id, $networks)])); - if (empty($tenantCampaigns)) continue; - - $filters['preordercampaign_id'] = $tenantCampaigns; - - $newPreorders = PreorderModel::searchActive($filters); - if (empty($newPreorders)) continue; - - foreach ($newPreorders as $preorder) { - if (!RMLWorkorderModel::getFirst(['preorderId' => $preorder->id])) { - RMLWorkorderModel::create([ - 'preorderId' => $preorder->id, - 'clusterId' => $preorder->preordercampaign_id, - 'status' => 'new', - 'create' => time(), - 'createBy' => 0 // System User - ]); - } - } - } - } - - protected function getDocumentationAction() - { - if (empty($this->request->workorderId)) self::sendError("Arbeitsauftrags-ID fehlt."); - - $docs = RMLWorkorderDocumentationModel::getAll(['workorderId' => $this->request->workorderId], null, 0, ['key' => 'create', 'order' => 'ASC']); - $journals = RMLWorkorderJournalModel::getAll(['workorderId' => $this->request->workorderId], null, 0, ['key' => 'create', 'order' => 'DESC']); - - $translationMap = [ - 'photo_hup_mounted' => 'Foto_montierter_HÜP', 'photo_hup_open' => 'Foto_offener_HÜP', - 'photo_splice_cassette_hup' => 'Foto_Spleißkassette_HÜP', 'photo_splice_cassette_fcp' => 'Foto_Spleißkassette_FCP', - 'photo_hup_closed_stickers' => 'Foto_geschlossener_HÜP_mit_Aufklebern', 'photo_fcp_labeled' => 'Foto_FCP_beschriftet', - 'photo_patch_position_osp' => 'Foto_Patch-Position_OSP-Seite', 'photo_patch_position_anb' => 'Foto_Patch-Position_ANB-Seite', - 'measurement_protocol_otdr' => 'ODTR_Messung', 'other' => 'Sonstiges_Dokument' - ]; - - $responseDocs = []; - $typeCounts = []; - - foreach ($docs as $doc) { - $file = new File($doc->fileId); - $documentTypeKey = $doc->documentType; - $typeCounts[$documentTypeKey] = ($typeCounts[$documentTypeKey] ?? 0) + 1; - $originalFilename = $file->orig_filename ?? $file->filename; - $extension = pathinfo($originalFilename, PATHINFO_EXTENSION); - $translatedType = $translationMap[$documentTypeKey] ?? $documentTypeKey; - $newFilename = "{$translatedType}_{$typeCounts[$documentTypeKey]}." . strtolower($extension); - - $responseDocs[] = [ - 'id' => $doc->id, 'fileId' => $doc->fileId, 'fileName' => $newFilename, 'description' => $doc->description, - 'documentType' => $documentTypeKey, 'userName' => UserModel::getOne($doc->createBy)->name ?? 'Unbekannt', - 'mimetype' => $file->mimetype ?? 'application/octet-stream', - ]; - } - - foreach ($journals as $journal) $journal->createByName = UserModel::getOne($journal->createBy)->name ?? 'Unbekannt'; - - self::returnJson(['docs' => $responseDocs, 'journals' => $journals]); - } - - private function assignSingleWorkorder($workorderId, $companyId, $deadline, $userId) - { - $workorder = RMLWorkorderModel::get($workorderId); - if (!$workorder) return false; - $company = RMLWorkorderCompanyModel::get($companyId); - if (!$company) return false; - - $workorder->companyId = $companyId; - $workorder->status = 'assigned'; - $workorder->assignmentDate = time(); - $workorder->deadlineDate = $deadline; - RMLWorkorderModel::update((array)$workorder); - - RMLWorkorderJournalModel::create([ - 'workorderId' => $workorder->id, 'text' => "Firma '{$company->name}' wurde zugewiesen.", - 'create' => time(), 'createBy' => $userId, - ]); - - $preorder = new Preorder($workorder->preorderId); - if ($preorder->id) { - $preorder->status_id = 10; - $preorder->edit_by = $this->user->id; - $preorder->save(); - } - return true; - } - - protected function assignWorkorderAction() - { - $post = json_decode(file_get_contents('php://input'), true); - if (empty($post['workorderId']) || empty($post['companyId'])) self::sendError("Erforderliche Felder fehlen."); - - $deadline = !empty($post['deadlineDate']) ? $post['deadlineDate'] : strtotime('+6 weeks'); - if ($this->assignSingleWorkorder($post['workorderId'], $post['companyId'], $deadline, $this->user->id)) { - self::returnJson(['success' => true, 'message' => 'Arbeitsauftrag erfolgreich zugewiesen.']); - } else self::sendError("Arbeitsauftrag konnte nicht zugewiesen werden. Er wurde möglicherweise bereits bearbeitet oder existiert nicht."); - } - - protected function massAssignWorkordersAction() - { - $post = json_decode(file_get_contents('php://input'), true); - if (empty($post['workorderIds']) || empty($post['companyId'])) self::sendError("Erforderliche Felder fehlen."); - - $deadline = strtotime($post['deadlineDate'] ?? '+6 weeks'); - $count = 0; - foreach ($post['workorderIds'] as $workorderId) if ($this->assignSingleWorkorder($workorderId, $post['companyId'], $deadline, $this->user->id)) $count++; - self::returnJson(['success' => true, 'message' => "$count Arbeitsaufträge erfolgreich zugewiesen."]); - } - - protected function requestCorrectionAction() - { - $post = json_decode(file_get_contents('php://input'), true); - if (empty($post['workorderId']) || empty($post['text'])) self::sendError("Erforderliche Felder fehlen."); - $workorder = RMLWorkorderModel::get($post['workorderId']); - if (!$workorder) self::sendError("Arbeitsauftrag nicht gefunden."); - - $oldStatus = $workorder->status; - $workorder->status = 'correction_requested'; - RMLWorkorderModel::update((array)$workorder); - - RMLWorkorderJournalModel::create([ - 'workorderId' => $workorder->id, 'text' => "Korrektur angefordert. Grund: " . $post['text'], - 'fileIds' => !empty($post['fileIds']) ? json_encode($post['fileIds']) : null, - 'statusChange' => $this->getStatusText($oldStatus) . " -> " . $this->getStatusText('correction_requested'), - 'create' => time(), 'createBy' => $this->user->id, - ]); - self::returnJson(['success' => true, 'message' => 'Korrektur wurde angefordert.']); - } - - protected function getCompaniesAction() - { - $tenantId = $this->request->tenantId ?? null; - $companies = RMLWorkorderCompanyModel::getAll([], null, 0, ['key' => 'name', 'order' => 'ASC']); - - if ($tenantId) { - $companies = array_filter($companies, function ($company) use ($tenantId) { - if ($company->addressId == 4807 && empty($company->visibleForAddressId)) return true; - $visibleFor = !empty($company->visibleForAddressId) ? json_decode($company->visibleForAddressId, true) : []; - return in_array($tenantId, $visibleFor); - }); - } - self::returnJson(array_values(array_map(fn($c) => ['value' => $c->id, 'text' => $c->name], $companies))); - } - - protected function addJournalAction() - { - $post = json_decode(file_get_contents('php://input'), true); - if (empty($post['workorderId']) || empty(trim($post['text']))) self::sendError("Arbeitsauftrags-ID und Text sind erforderlich."); - - RMLWorkorderJournalModel::create(['workorderId' => $post['workorderId'], 'text' => $post['text'], 'createBy' => $this->user->id, 'create' => time()]); - $journals = array_map(function ($j) { - $j->createByName = UserModel::getOne($j->createBy)->name ?? 'Unbekannt'; - return (array)$j; - }, - RMLWorkorderJournalModel::getAll(['workorderId' => $post['workorderId']], null, 0, ['key' => 'create', 'order' => 'DESC']) - ); - self::returnJson(['success' => true, 'message' => 'Journaleintrag hinzugefügt.', 'journals' => $journals]); - } - - protected function updateDeadlineAction() - { - $post = json_decode(file_get_contents('php://input'), true); - if (empty($post['workorderId']) || empty($post['deadlineDate'])) self::sendError("Erforderliche Felder fehlen."); - - $workorder = RMLWorkorderModel::get($post['workorderId']); - if (!$workorder) self::sendError("Arbeitsauftrag nicht gefunden."); - - $workorder->deadlineDate = $post['deadlineDate']; - RMLWorkorderModel::update((array)$workorder); - RMLWorkorderJournalModel::create(['workorderId' => $workorder->id, 'text' => 'Deadline geändert auf ' . date('d.m.Y', $post['deadlineDate']) . '.', 'create' => time(), 'createBy' => $this->user->id]); - self::returnJson(['success' => true, 'message' => 'Deadline erfolgreich aktualisiert.']); - } - - protected function acceptDocumentationAction() - { - $post = json_decode(file_get_contents('php://input'), true); - if (empty($post['workorderId'])) self::sendError("Arbeitsauftrags-ID fehlt."); - $workorder = RMLWorkorderModel::get($post['workorderId']); - if (!$workorder) self::sendError("Arbeitsauftrag nicht gefunden."); - if ($workorder->status !== 'documented') self::sendError("Die Dokumentation muss zuerst von der Firma als fertig markiert werden."); - - $preorder = new Preorder($workorder->preorderId); - if ($preorder->id) { - $preorder->status_id = 15; - $preorder->edit_by = $this->user->id; - $preorder->save(); - } - - $oldStatus = $workorder->status; - $workorder->status = 'completed'; - RMLWorkorderModel::update((array)$workorder); - RMLWorkorderJournalModel::create([ - 'workorderId' => $workorder->id, 'text' => 'Dokumentation akzeptiert und Arbeitsauftrag abgeschlossen.', - 'statusChange' => $this->getStatusText($oldStatus) . " -> " . $this->getStatusText('completed'), - 'create' => time(), 'createBy' => $this->user->id, - ]); - self::returnJson(['success' => true, 'message' => 'Dokumentation akzeptiert und Arbeitsauftrag abgeschlossen.']); - } - - protected function setToProblemSolvedAction() - { - $post = json_decode(file_get_contents('php://input'), true); - if (empty($post['workorderId']) || empty($post['text'])) self::sendError("Arbeitsauftrags-ID und Text sind erforderlich."); - - $workorder = RMLWorkorderModel::get($post['workorderId']); - if (!$workorder) self::sendError("Arbeitsauftrag nicht gefunden."); - if ($workorder->status !== 'intervention_required') self::sendError("Der Arbeitsauftrag muss den Status 'Eingriff erforderlich' haben, um als gelöst markiert zu werden."); - - $oldStatus = $workorder->status; - $workorder->status = 'problem_solved'; - RMLWorkorderModel::update((array)$workorder); - RMLWorkorderJournalModel::create([ - 'workorderId' => $workorder->id, 'text' => "Problem gelöst: " . $post['text'], - 'statusChange' => $this->getStatusText($oldStatus) . " -> " . $this->getStatusText('problem_solved'), - 'create' => time(), 'createBy' => $this->user->id, - ]); - self::returnJson(['success' => true, 'message' => 'Arbeitsauftrag als "Problem gelöst" markiert.']); - } - - protected function updateAdditionalInfoAction() - { - $post = json_decode(file_get_contents('php://input'), true); - if (empty($post['workorderId'])) self::sendError("Arbeitsauftrags-ID fehlt."); - $workorder = RMLWorkorderModel::get($post['workorderId']); - if (!$workorder) self::sendError("Arbeitsauftrag nicht gefunden."); - - $oldInfo = $workorder->additionalInfo; - $workorder->additionalInfo = $post['additionalInfo'] ?? null; - RMLWorkorderModel::update((array)$workorder); - - RMLWorkorderJournalModel::create([ - 'workorderId' => $workorder->id, 'text' => "Zusatzinfo geändert.\nAlt: '{$oldInfo}'\nNeu: '{$workorder->additionalInfo}'", - 'create' => time(), 'createBy' => $this->user->id, - ]); - self::returnJson(['success' => true, 'message' => 'Zusatzinfo aktualisiert.']); - } - - protected function cancelWorkorderAction() - { - $post = json_decode(file_get_contents('php://input'), true); - if (empty($post['workorderId'])) self::sendError("Arbeitsauftrags-ID fehlt."); - $workorder = RMLWorkorderModel::get($post['workorderId']); - if (!$workorder) self::sendError("Arbeitsauftrag nicht gefunden."); - - $oldStatus = $workorder->status; - $workorder->status = 'cancelled'; - RMLWorkorderModel::update((array)$workorder); - - RMLWorkorderJournalModel::create([ - 'workorderId' => $workorder->id, 'text' => 'Arbeitsauftrag wurde storniert.' . (!empty($post['reason']) ? ' Grund: ' . $post['reason'] : ''), - 'statusChange' => $this->getStatusText($oldStatus) . " -> " . $this->getStatusText('cancelled'), - 'create' => time(), 'createBy' => $this->user->id, - ]); - - $preorder = new Preorder($workorder->preorderId); - if ($preorder->id) { - $preorder->status_id = 99; - $preorder->edit_by = $this->user->id; - $preorder->save(); - } - - self::returnJson(['success' => true, 'message' => 'Arbeitsauftrag wurde storniert.']); +class RMLWorkorderAdminController extends mfBaseController { + protected function init() { + $this->needlogin = true; + $this->redirect("WorkorderAdmin"); } } \ No newline at end of file diff --git a/application/RMLWorkorderCompany/RMLWorkorderCompanyController.php b/application/RMLWorkorderCompany/RMLWorkorderCompanyController.php index 90d049526..3c83ff9b2 100644 --- a/application/RMLWorkorderCompany/RMLWorkorderCompanyController.php +++ b/application/RMLWorkorderCompany/RMLWorkorderCompanyController.php @@ -1,353 +1,8 @@ 'id', 'text' => 'Auftrags-Nr.', 'table' => ['sortable' => true]], - ['key' => 'preorderInfo', 'text' => 'Kunde', 'modal' => false, 'table' => ['sortable' => false]], - ['key' => 'rimo_fcp_name', 'text' => 'FCP', 'modal' => false, 'table' => ['sortable' => false]], - ['key' => 'status', 'text' => 'Status', 'modal' => false, 'table' => ['filter' => 'iconSelect', 'filterOptions' => [ - ['value' => 'new', 'text' => 'Neu', 'icon' => 'fas fa-star text-primary'], - ['value' => 'assigned', 'text' => 'Zugewiesen', 'icon' => 'fas fa-user-check text-info'], - ['value' => 'scheduled', 'text' => 'Geplant', 'icon' => 'fas fa-calendar-check text-warning'], - ['value' => 'correction_requested', 'text' => 'Korrektur angefordert', 'icon' => 'fas fa-exclamation-triangle text-danger'], - ['value' => 'intervention_required', 'text' => 'Eingriff erforderlich', 'icon' => 'fas fa-times-circle text-danger'], - ['value' => 'problem_solved', 'text' => 'Problem gelöst', 'icon' => 'fas fa-check-circle text-success'], - ['value' => 'documented', 'text' => 'Dokumentiert', 'icon' => 'fas fa-file-alt text-success'], - ['value' => 'completed', 'text' => 'Abgeschlossen', 'icon' => 'fas fa-check-double text-secondary'], - ['value' => 'cancelled', 'text' => 'Abgebrochen', 'icon' => 'fas fa-ban text-danger'], - ]]], - ['key' => 'additionalInfo', 'text' => 'Notiz', 'modal' => false, 'table' => ['sortable' => true]], - ['key' => 'deadlineDate', 'text' => 'Deadline', 'modal' => false, 'table' => ['filter' => 'date', 'sortable' => true]], - ['key' => 'appointmentDate', 'text' => 'Termin', 'modal' => false, 'table' => ['filter' => 'date', 'sortable' => true]], - ]; - - protected array $additionalJSVariables = ['COMPANY_ID' => '0']; - - private function getStatusText(string $statusKey): string - { - foreach ($this->columns as $column) { - if ($column['key'] === 'status') { - foreach ($column['table']['filterOptions'] as $option) { - if ($option['value'] === $statusKey) { - return $option['text']; - } - } - } - } - return ucfirst(str_replace('_', ' ', $statusKey)); // Fallback - } - - protected function prepareCrudConfig() - { - $company = RMLWorkorderCompanyModel::getFirst(['addressId' => $this->user->address_id]); - if ($company) { - $this->additionalJSVariables['COMPANY_ID'] = $company->id; - } else { - $this->additionalJSVariables['COMPANY_ID'] = 0; - } - } - - protected function indexAction() - { - Helper::renderVue($this, 'RMLWorkorderCompany', $this->headerTitle, [ - "CRUD_CONFIG" => $this->getCrudConfig(), - "TABLE_URL" => $this::getUrl("RMLWorkorderCompany/get"), - "COMPANY_ID" => $this->additionalJSVariables['COMPANY_ID'], - ]); - } - - protected function getAction() - { - $json = json_decode(file_get_contents('php://input'), true); - $pagination = $json['pagination'] ?? ['page' => 1, 'per_page' => 10]; - $filters = $json['filters'] ?? []; - $order = $json['order'] ?? []; - - $company = RMLWorkorderCompanyModel::getFirst(['addressId' => $this->user->address_id]); - if (!$company) { - self::returnJson(['rows' => [], 'pagination' => array_merge($pagination, ['total_rows' => 0, 'total_pages' => 0, 'filtered_available' => 0])]); - return; - } - $companyId = $company->id; - - $workorders = RMLWorkorderModel::getCompanyWorkorders($filters, $pagination['per_page'], ($pagination['page'] - 1) * $pagination['per_page'], $order, $companyId); - $totalCount = RMLWorkorderModel::countCompanyWorkorders($filters, $companyId); - - $rows = array_map(function ($workorder) { - $row = (array)$workorder; - $row['preorderInfo'] = $this->getPreorderInfoTextByData($row); - unset($row['customerName'], $row['customerCompany'], $row['street'], $row['hausnummer'], $row['stiege'], $row['oaid'], $row['apartment'], $row['plz'], $row['city'], $row['phone'], $row['email']); - return $row; - }, $workorders); - - self::returnJson([ - 'rows' => $rows, - 'pagination' => [ - 'page' => $pagination['page'], 'per_page' => $pagination['per_page'], - 'total_rows' => $totalCount, 'total_pages' => ceil($totalCount / $pagination['per_page']), - 'filtered_available' => $totalCount - ] - ]); - } - - private function getPreorderInfoTextByData($data) - { - $anschlussadresse = "{$data['street']} {$data['hausnummer']}"; - if ($data['stiege']) $anschlussadresse .= "/{$data['stiege']}"; - if ($data['apartment']) $anschlussadresse .= " / WE: {$data['apartment']}"; - $anschlussadresse .= ", {$data['plz']} {$data['city']}"; - $kunde = $data['customerCompany'] ?: $data['customerName']; - return "Kunde: {$kunde}
" . "Anschluss: {$anschlussadresse}
" . "Kontakt: {$data['phone']} / {$data['email']}
" . "OAID: {$data['oaid']}"; - } - - public function getWorkorderByIdAction() - { - $id = $this->request->id; - if (!$id) self::sendError("ID fehlt"); - $workorder = RMLWorkorderModel::get($id); - if (!$workorder) self::sendError("Arbeitsauftrag nicht gefunden"); - $workorder->preorderInfo = $this->getPreorderInfoText($workorder->preorderId); - self::returnJson((array)$workorder); - } - - private function getPreorderInfoText($preorderId) - { - $preorder = new Preorder($preorderId); - $anschlussadresse = 'N/A'; - if ($preorder->adb_hausnummer_id) { - $hn = $preorder->adb_hausnummer; - $anschlussadresse = "{$hn->strasse->name} {$hn->hausnummer}"; - if ($hn->stiege) $anschlussadresse .= "/{$hn->stiege}"; - if ($preorder->adb_wohneinheit_id) $anschlussadresse .= " / WE: {$preorder->adb_wohneinheit->bezeichner}"; - $anschlussadresse .= ", {$hn->plz->plz} {$hn->ortschaft->name}"; - } - $kunde = ($preorder->company) ?: "{$preorder->firstname} {$preorder->lastname}"; - return "Kunde: {$kunde}
" . "Anschluss: {$anschlussadresse}
" . "Kontakt: {$preorder->phone} / {$preorder->email}
" . "OAID: {$preorder->oaid}"; - } - - protected function scheduleAppointmentAction() - { - $post = json_decode(file_get_contents('php://input'), true); - if (empty($post['workorderId']) || empty($post['appointmentDate'])) self::sendError("Erforderliche Felder fehlen."); - $workorder = RMLWorkorderModel::get($post['workorderId']); - if (!$workorder) self::sendError("Arbeitsauftrag nicht gefunden"); - $hour = (int)date('H', $post['appointmentDate']); - if ($hour >= 23 || $hour < 1) self::sendError("Bitte geben Sie eine Uhrzeit an!"); - - $workorder->appointmentDate = $post['appointmentDate']; - $workorder->status = 'scheduled'; - RMLWorkorderModel::update((array)$workorder); - RMLWorkorderJournalModel::create([ - 'workorderId' => $workorder->id, 'text' => 'Termin festgelegt auf: ' . date('d.m.Y H:i', $post['appointmentDate']), - 'create' => time(), 'createBy' => $this->user->id, - ]); - self::returnJson(['success' => true, 'message' => 'Termin erfolgreich gespeichert.']); - } - - protected function rescheduleAppointmentAction() - { - $post = json_decode(file_get_contents('php://input'), true); - if (empty($post['workorderId']) || empty($post['appointmentDate']) || empty($post['reason'])) self::sendError("Erforderliche Felder fehlen."); - $workorder = RMLWorkorderModel::get($post['workorderId']); - if (!$workorder) self::sendError("Arbeitsauftrag nicht gefunden."); - $hour = (int)date('H', $post['appointmentDate']); - if ($hour >= 23 || $hour < 1) self::sendError("Bitte geben Sie eine Uhrzeit an!"); - - $oldDateFormatted = $workorder->appointmentDate ? date('d.m.Y H:i', $workorder->appointmentDate) : 'N/A'; - $newDateFormatted = date('d.m.Y H:i', $post['appointmentDate']); - $workorder->appointmentDate = $post['appointmentDate']; - RMLWorkorderModel::update((array)$workorder); - RMLWorkorderJournalModel::create([ - 'workorderId' => $workorder->id, 'text' => "Termin verschoben von {$oldDateFormatted} auf {$newDateFormatted}. Grund: " . $post['reason'], - 'create' => time(), 'createBy' => $this->user->id, - ]); - self::returnJson(['success' => true, 'message' => 'Termin erfolgreich verschoben.']); - } - - protected function requestInterventionAction() - { - $post = json_decode(file_get_contents('php://input'), true); - if (empty($post['workorderId']) || empty($post['journalText'])) self::sendError("Erforderliche Felder fehlen."); - $workorder = RMLWorkorderModel::get($post['workorderId']); - if (!$workorder) self::sendError("Arbeitsauftrag nicht gefunden."); - - $oldStatus = $workorder->status; - $workorder->status = 'intervention_required'; - RMLWorkorderModel::update((array)$workorder); - RMLWorkorderJournalModel::create([ - 'workorderId' => $workorder->id, 'text' => "Eingriff erforderlich: " . $post['journalText'], - 'statusChange' => $this->getStatusText($oldStatus) . " -> " . $this->getStatusText('intervention_required'), - 'create' => time(), 'createBy' => $this->user->id, - ]); - self::returnJson(['success' => true, 'message' => 'Eingriff wurde angefordert.']); - } - - protected function uploadDocumentationAction() - { - if (empty($_FILES['files']) || empty($_POST['workorderId'])) { - self::returnJson(['error' => 'Erforderliche Daten fehlen.']); - return; - } - $workorderId = $_POST['workorderId']; - $description = $_POST['description'] ?? ''; - $documentType = $_POST['documentType'] ?? 'general'; - $files = $_FILES['files']; - $uploadCount = 0; - - foreach ($files['name'] as $index => $name) { - if ($files['error'][$index] === UPLOAD_ERR_OK) { - $_FILES['file'] = ['name' => $files['name'][$index], 'type' => $files['type'][$index], 'tmp_name' => $files['tmp_name'][$index], 'error' => $files['error'][$index], 'size' => $files['size'][$index]]; - try { - $uploaded = mfUpload::handleFormUpload("file", false, "/RMLWorkorder"); - RMLWorkorderDocumentationModel::create(['workorderId' => $workorderId, 'fileId' => $uploaded->id, 'description' => $description, 'documentType' => $documentType, 'create' => time(), 'createBy' => $this->user->id]); - $uploadCount++; - } catch (Exception $e) { - error_log("Dateiupload für $name fehlgeschlagen: " . $e->getMessage()); - } - } - } - - $workorder = RMLWorkorderModel::get($workorderId); - if ($workorder->status === 'correction_requested' || $workorder->status === 'problem_solved') { - $workorder->status = 'assigned'; - RMLWorkorderModel::update((array)$workorder); - $workorder = RMLWorkorderModel::get($workorderId); - } - $formattedDocs = $this->getFormattedDocs($workorderId); - self::returnJson(['success' => true, 'message' => "$uploadCount Datei(en) erfolgreich hochgeladen.", 'docs' => $formattedDocs, 'workorder' => (array)$workorder]); - } - - private function getFormattedDocs($workorderId) - { - $docs = RMLWorkorderDocumentationModel::getAll(['workorderId' => $workorderId], null, 0, ['key' => 'create', 'order' => 'ASC']); - $responseDocs = []; - $typeCounts = []; - $translationMap = [ - 'photo_hup_mounted' => 'Foto_montierter_HÜP', 'photo_hup_open' => 'Foto_offener_HÜP', - 'photo_splice_cassette_hup' => 'Foto_Spleißkassette_HÜP', 'photo_splice_cassette_fcp' => 'Foto_Spleißkassette_FCP', - 'photo_hup_closed_stickers' => 'Foto_geschlossener_HÜP_mit_Aufklebern', 'photo_fcp_labeled' => 'Foto_FCP_beschriftet', - 'photo_patch_position_osp' => 'Foto_Patch-Position_OSP-Seite', 'photo_patch_position_anb' => 'Foto_Patch-Position_ANB-Seite', - 'measurement_protocol_otdr' => 'ODTR_Messung', 'other' => 'Sonstiges_Dokument' - ]; - foreach ($docs as $doc) { - $file = new File($doc->fileId); - $documentTypeKey = $doc->documentType; - $typeCounts[$documentTypeKey] = ($typeCounts[$documentTypeKey] ?? 0) + 1; - $originalFilename = $file->orig_filename ?? $file->filename; - $extension = pathinfo($originalFilename, PATHINFO_EXTENSION); - $translatedType = $translationMap[$documentTypeKey] ?? $documentTypeKey; - $newFilename = "{$translatedType}_{$typeCounts[$documentTypeKey]}." . strtolower($extension); - $responseDocs[] = ['id' => $doc->id, 'fileId' => $doc->fileId, 'fileName' => $newFilename, 'description' => $doc->description, 'documentType' => $documentTypeKey, 'mimetype' => $file->mimetype]; - } - return $responseDocs; - } - - protected function getDocumentationAction() - { - if (empty($this->request->workorderId)) self::sendError("Arbeitsauftrags-ID fehlt."); - $docs = $this->getFormattedDocs($this->request->workorderId); - $journals = array_map(function ($j) { - $j->createByName = UserModel::getOne($j->createBy)->getAbbrName(); - return (array)$j; - }, - RMLWorkorderJournalModel::getAll(['workorderId' => $this->request->workorderId], null, 0, ['key' => 'create', 'order' => 'DESC']) - ); - self::returnJson(['docs' => $docs, 'journals' => $journals]); - } - - protected function completeWorkorderAction() - { - $post = json_decode(file_get_contents('php://input'), true); - if (empty($post['workorderId'])) self::sendError("Arbeitsauftrags-ID fehlt."); - $workorder = RMLWorkorderModel::get($post['workorderId']); - if (!$workorder) self::sendError("Arbeitsauftrag nicht gefunden."); - $workorder->status = 'documented'; - RMLWorkorderModel::update((array)$workorder); - self::returnJson(['success' => true, 'message' => 'Arbeitsauftrag abgeschlossen.']); - } - - protected function deleteDocumentationAction() - { - $post = json_decode(file_get_contents('php://input'), true); - if (empty($post['id'])) self::sendError("Dokumenten-ID fehlt."); - $doc = RMLWorkorderDocumentationModel::get($post['id']); - if (!$doc) self::sendError("Dokument nicht gefunden."); - $workorderId = $doc->workorderId; - RMLWorkorderDocumentationModel::delete($post['id']); - $formattedDocs = $this->getFormattedDocs($workorderId); - self::returnJson(['success' => true, 'message' => 'Dokument gelöscht.', 'docs' => $formattedDocs]); - } - - protected function updateDocumentationAction() - { - $post = json_decode(file_get_contents('php://input'), true); - if (empty($post['id'])) self::sendError("Dokumenten-ID fehlt."); - $doc = RMLWorkorderDocumentationModel::get($post['id']); - if (!$doc) self::sendError("Dokument nicht gefunden."); - if (isset($post['documentType'])) $doc->documentType = $post['documentType']; - RMLWorkorderDocumentationModel::update((array)$doc); - $formattedDocs = $this->getFormattedDocs($doc->workorderId); - self::returnJson(['success' => true, 'message' => 'Dokument aktualisiert.', 'docs' => $formattedDocs]); - } - - protected function addJournalAction() - { - $post = json_decode(file_get_contents('php://input'), true); - if (empty($post['workorderId']) || empty(trim($post['text']))) self::sendError("Arbeitsauftrags-ID und Text sind erforderlich."); - RMLWorkorderJournalModel::create(['workorderId' => $post['workorderId'], 'text' => $post['text'], 'createBy' => $this->user->id, 'create' => time()]); - $journals = array_map(function ($j) { - $j->createByName = UserModel::getOne($j->createBy)->getAbbrName(); - return (array)$j; - }, - RMLWorkorderJournalModel::getAll(['workorderId' => $post['workorderId']], null, 0, ['key' => 'create', 'order' => 'DESC']) - ); - self::returnJson(['success' => true, 'message' => 'Journaleintrag hinzugefügt.', 'journals' => $journals]); - } - - protected function getTenantConfigAction() - { - if (empty($this->request->workorderId)) self::sendError("Arbeitsauftrags-ID fehlt."); - $workorder = RMLWorkorderModel::get($this->request->workorderId); - if (!$workorder) self::sendError("Arbeitsauftrag nicht gefunden."); - $preorder = new Preorder($workorder->preorderId); - if (!$preorder->id) self::sendError("Vorbestellung nicht gefunden."); - $campaign = new Preordercampaign($preorder->preordercampaign_id); - if (!$campaign->id) self::sendError("Kampagne nicht gefunden."); - $network = NetworkModel::getOne($campaign->network_id); - if (!$network) self::sendError("Netzwerk nicht gefunden."); - $tenantId = $network->owner_id; - $tenantConfig = RMLWorkorderTenantConfigModel::getFirst(['addressId' => $tenantId]) ?? RMLWorkorderTenantConfigModel::getFirst(['addressId' => 4807]); - if (!$tenantConfig) { - self::returnJson(['success' => false, 'message' => 'Keine Mandantenkonfiguration gefunden.']); - return; - } - self::returnJson(['success' => true, 'documentationTypes' => json_decode($tenantConfig->documentationTypes, true)]); - } - - protected function updateAdditionalInfoAction() - { - $post = json_decode(file_get_contents('php://input'), true); - if (empty($post['workorderId'])) self::sendError("Arbeitsauftrags-ID fehlt."); - - $company = RMLWorkorderCompanyModel::getFirst(['addressId' => $this->user->address_id]); - if (!$company) self::sendError("Firma nicht gefunden."); - - $workorder = RMLWorkorderModel::get($post['workorderId']); - if (!$workorder || $workorder->companyId !== $company->id) self::sendError("Arbeitsauftrag nicht gefunden oder nicht Ihrer Firma zugewiesen."); - - $oldInfo = $workorder->additionalInfo; - $workorder->additionalInfo = $post['additionalInfo'] ?? null; - RMLWorkorderModel::update((array)$workorder); - - RMLWorkorderJournalModel::create([ - 'workorderId' => $workorder->id, 'text' => "Zusatzinfo geändert.\nAlt: '{$oldInfo}'\nNeu: '{$workorder->additionalInfo}'", - 'create' => time(), 'createBy' => $this->user->id, - ]); - self::returnJson(['success' => true, 'message' => 'Zusatzinfo aktualisiert.']); +class RMLWorkorderCompanyController extends mfBaseController { + protected function init() { + $this->needlogin = true; + $this->redirect("WorkorderCompany"); } } \ No newline at end of file diff --git a/application/RMLWorkorderTenantConfig/RMLWorkorderTenantConfigModel.php b/application/RMLWorkorderTenantConfig/RMLWorkorderTenantConfigModel.php deleted file mode 100644 index 56a7458d1..000000000 --- a/application/RMLWorkorderTenantConfig/RMLWorkorderTenantConfigModel.php +++ /dev/null @@ -1,12 +0,0 @@ - 'id', 'text' => 'Auftrags-Nr.', 'table' => ['sortable' => true, 'filter' => 'numberRange']], + ['key' => 'netOwnerId', 'text' => 'Netzeigentümer', 'modal' => false, 'table' => ['filter' => 'select'], 'required' => false], + ['key' => 'preorderInfo', 'text' => 'Kunde', 'modal' => false, 'table' => ['filter' => 'search', 'sortable' => false]], + ['key' => 'companyName', 'text' => 'Firma', 'modal' => false, 'table' => ['filter' => 'search', 'sortable' => true]], + ['key' => 'rimo_fcp_name', 'text' => 'FCP', 'modal' => false, 'table' => ['filter' => 'search', 'sortable' => true]], + // Status column is now inherited via prepareCrudConfig + ['key' => 'additionalInfo', 'text' => 'Notiz', 'modal' => false, 'table' => ['sortable' => true]], + ['key' => 'deadlineDate', 'text' => 'Deadline', 'modal' => false, 'table' => ['filter' => 'date', 'sortable' => true]], + ['key' => 'appointmentDate', 'text' => 'Termin', 'modal' => false, 'table' => ['filter' => 'date', 'sortable' => true]], + ]; + + /** + * Prepares the CRUD configuration. + */ + protected function prepareCrudConfig() + { + $preorderInfoColIdx = array_search('preorderInfo', array_column($this->columns, 'key')); + array_splice($this->columns, $preorderInfoColIdx + 1, 0, [$this->statusColumn]); + $netOwnerColIdx = array_search('netOwnerId', array_column($this->columns, 'key')); + + if ($netOwnerColIdx !== false) { + if ($this->user->isAdmin()) { + $netOwners = Helper::getPreorderCampaignNetworkOwners(); + $this->columns[$netOwnerColIdx]['table']['filterOptions'] = array_map(fn($o) => ['value' => $o->id, 'text' => $o->company], $netOwners); + } else { + $this->columns[$netOwnerColIdx]['table'] = false; + } + } + } + + //region ACTIONS + public function indexAction() + { + $this->createWorkordersFromPreorders(); + parent::indexAction(); + } + + /** + * Fetches workorders for the admin view. + */ + protected function getAction() + { + $pagination = $this->postData['pagination'] ?? ['page' => 1, 'per_page' => 10]; + $filters = $this->postData['filters'] ?? []; + $order = $this->postData['order'] ?? []; + + $allowedCampaignIds = Helper::getPreorderCampaignFromUser($this->user); + if (empty($allowedCampaignIds)) { + self::returnJson(['rows' => [], 'pagination' => array_merge($pagination, ['total_rows' => 0, 'total_pages' => 0, 'filtered_available' => 0])]); + return; + } + + $workorders = WorkorderModel::getAdminWorkorders($filters, $pagination['per_page'], ($pagination['page'] - 1) * $pagination['per_page'], $order, $allowedCampaignIds); + $totalCount = WorkorderModel::countAdminWorkorders($filters, $allowedCampaignIds); + + $rows = array_map(function ($workorder) { + $row = (array)$workorder; + $row['companyName'] ??= 'Nicht zugewiesen'; + return $row; + }, $workorders); + + self::returnJson([ + 'rows' => $rows, + 'pagination' => ['page' => $pagination['page'], 'per_page' => $pagination['per_page'], 'total_rows' => $totalCount, 'total_pages' => ceil($totalCount / $pagination['per_page']), 'filtered_available' => $totalCount] + ]); + } + + protected function getCompaniesAction() + { + $tenantId = $this->request->tenantId; + $companies = WorkorderCompanyModel::getAll(['visibleForAddressId' => "%$tenantId%"]); + self::returnJson(array_map(fn($c) => ['value' => $c->id, 'text' => $c->name], $companies)); + } + + protected function assignWorkorderAction() + { + if (empty($this->postData['workorderId']) || empty($this->postData['companyId'])) self::sendError("Erforderliche Felder fehlen."); + $deadline = !empty($this->postData['deadlineDate']) ? $this->postData['deadlineDate'] : strtotime('+6 weeks'); + + if ($this->assignSingleWorkorder($this->postData['workorderId'], $this->postData['companyId'], $deadline, $this->user->id)) { + self::returnJson(['success' => true, 'message' => 'Arbeitsauftrag erfolgreich zugewiesen.']); + } else { + self::sendError("Arbeitsauftrag konnte nicht zugewiesen werden."); + } + } + + protected function massAssignWorkordersAction() + { + if (empty($this->postData['workorderIds']) || empty($this->postData['companyId'])) self::sendError("Erforderliche Felder fehlen."); + + $deadline = strtotime($this->postData['deadlineDate'] ?? '+6 weeks'); + $count = 0; + foreach ($this->postData['workorderIds'] as $workorderId) { + if ($this->assignSingleWorkorder($workorderId, $this->postData['companyId'], $deadline, $this->user->id)) $count++; + } + self::returnJson(['success' => true, 'message' => "$count Arbeitsaufträge erfolgreich zugewiesen."]); + } + + protected function requestCorrectionAction() + { + if (empty($this->postData['workorderId']) || empty($this->postData['text'])) self::sendError("Erforderliche Felder fehlen."); + $workorder = WorkorderModel::get($this->postData['workorderId']); + if (!$workorder) self::sendError("Arbeitsauftrag nicht gefunden."); + + $oldStatus = $workorder->status; + $workorder->status = 'correction_requested'; + WorkorderModel::update((array)$workorder); + + WorkorderJournalModel::create([ + 'workorderId' => $workorder->id, 'text' => "Korrektur angefordert. Grund: " . $this->postData['text'], + 'fileIds' => !empty($this->postData['fileIds']) ? json_encode($this->postData['fileIds']) : null, + 'statusChange' => $this->getStatusText($oldStatus) . " -> " . $this->getStatusText('correction_requested'), + 'create' => time(), 'createBy' => $this->user->id, + ]); + self::returnJson(['success' => true, 'message' => 'Korrektur wurde angefordert.']); + } + + protected function updateDeadlineAction() + { + if (empty($this->postData['workorderId']) || empty($this->postData['deadlineDate'])) self::sendError("Erforderliche Felder fehlen."); + + $workorder = WorkorderModel::get($this->postData['workorderId']); + if (!$workorder) self::sendError("Arbeitsauftrag nicht gefunden."); + + $workorder->deadlineDate = $this->postData['deadlineDate']; + WorkorderModel::update((array)$workorder); + WorkorderJournalModel::create(['workorderId' => $workorder->id, 'text' => 'Deadline geändert auf ' . date('d.m.Y', $this->postData['deadlineDate']) . '.', 'create' => time(), 'createBy' => $this->user->id]); + self::returnJson(['success' => true, 'message' => 'Deadline erfolgreich aktualisiert.']); + } + + protected function acceptDocumentationAction() + { + if (empty($this->postData['workorderId'])) self::sendError("Arbeitsauftrags-ID fehlt."); + $workorder = WorkorderModel::get($this->postData['workorderId']); + if (!$workorder) self::sendError("Arbeitsauftrag nicht gefunden."); + if ($workorder->status !== 'documented') self::sendError("Die Dokumentation muss zuerst von der Firma als fertig markiert werden."); + + $preorder = new Preorder($workorder->preorderId); + if ($preorder->id) { + $preorder->status_id = 15; + $preorder->edit_by = $this->user->id; + $preorder->save(); + } + + $oldStatus = $workorder->status; + $workorder->status = 'completed'; + WorkorderModel::update((array)$workorder); + WorkorderJournalModel::create([ + 'workorderId' => $workorder->id, 'text' => 'Dokumentation akzeptiert und Arbeitsauftrag abgeschlossen.', + 'statusChange' => $this->getStatusText($oldStatus) . " -> " . $this->getStatusText('completed'), + 'create' => time(), 'createBy' => $this->user->id, + ]); + self::returnJson(['success' => true, 'message' => 'Dokumentation akzeptiert und Arbeitsauftrag abgeschlossen.']); + } + + protected function setToProblemSolvedAction() + { + if (empty($this->postData['workorderId']) || empty($this->postData['text'])) self::sendError("Arbeitsauftrags-ID und Text sind erforderlich."); + $workorder = WorkorderModel::get($this->postData['workorderId']); + if (!$workorder) self::sendError("Arbeitsauftrag nicht gefunden."); + if ($workorder->status !== 'intervention_required') self::sendError("Der Arbeitsauftrag muss den Status 'Eingriff erforderlich' haben."); + + $oldStatus = $workorder->status; + $workorder->status = 'problem_solved'; + WorkorderModel::update((array)$workorder); + WorkorderJournalModel::create([ + 'workorderId' => $workorder->id, 'text' => "Problem gelöst: " . $this->postData['text'], + 'statusChange' => $this->getStatusText($oldStatus) . " -> " . $this->getStatusText('problem_solved'), + 'create' => time(), 'createBy' => $this->user->id, + ]); + self::returnJson(['success' => true, 'message' => 'Arbeitsauftrag als "Problem gelöst" markiert.']); + } + + protected function cancelWorkorderAction() + { + if (empty($this->postData['workorderId'])) self::sendError("Arbeitsauftrags-ID fehlt."); + $workorder = WorkorderModel::get($this->postData['workorderId']); + if (!$workorder) self::sendError("Arbeitsauftrag nicht gefunden."); + + $oldStatus = $workorder->status; + $workorder->status = 'cancelled'; + WorkorderModel::update((array)$workorder); + + WorkorderJournalModel::create([ + 'workorderId' => $workorder->id, 'text' => 'Arbeitsauftrag wurde storniert.' . (!empty($this->postData['reason']) ? ' Grund: ' . $this->postData['reason'] : ''), + 'statusChange' => $this->getStatusText($oldStatus) . " -> " . $this->getStatusText('cancelled'), + 'create' => time(), 'createBy' => $this->user->id, + ]); + + $preorder = new Preorder($workorder->preorderId); + if ($preorder->id) { + $preorder->status_id = 99; // Storniert + $preorder->edit_by = $this->user->id; + $preorder->save(); + } + self::returnJson(['success' => true, 'message' => 'Arbeitsauftrag wurde storniert.']); + } + + protected function setCivilEngineeringRequiredAction() + { + if (empty($this->postData['workorderId'])) self::sendError("Arbeitsauftrags-ID fehlt."); + if (empty($this->postData['companyId'])) self::sendError("Bitte Tiefbaufirma auswählen."); + + $workorder = WorkorderModel::get($this->postData['workorderId']); + if (!$workorder) self::sendError("Arbeitsauftrag nicht gefunden."); + $company = WorkorderCompanyModel::get($this->postData['companyId']); + if (!$company) self::sendError("Tiefbaufirma nicht gefunden."); + + $oldStatus = $workorder->status; + $workorder->civilEngineeringCompanyId = $company->id; + $workorder->originalCompanyId = $workorder->companyId; + $workorder->companyId = $company->id; + $workorder->status = 'civil_engineering_required'; + WorkorderModel::update((array)$workorder); + + WorkorderJournalModel::create([ + 'workorderId' => $workorder->id, 'text' => "Tiefbau wurde angefordert. Firma '{$company->name}' wurde zugewiesen.", + 'statusChange' => $this->getStatusText($oldStatus) . " -> " . $this->getStatusText('civil_engineering_required'), + 'create' => time(), 'createBy' => $this->user->id, + ]); + self::returnJson(['success' => true, 'message' => 'Tiefbau wurde angefordert und Firma zugewiesen.']); + } + + //endregion + + //region PRIVATE HELPERS + private function assignSingleWorkorder($workorderId, $companyId, $deadline, $userId): bool + { + $workorder = WorkorderModel::get($workorderId); + if (!$workorder) return false; + if ($workorder->status === 'civil_engineering_required') { + $workorder->companyId = $companyId; + $workorder->civilEngineeringCompanyId = $companyId; + WorkorderModel::update((array)$workorder); + return true; + } + $company = WorkorderCompanyModel::get($companyId); + if (!$company) return false; + + $workorder->companyId = $companyId; + $workorder->status = 'assigned'; + $workorder->assignmentDate = time(); + $workorder->deadlineDate = $deadline; + WorkorderModel::update((array)$workorder); + + WorkorderJournalModel::create([ + 'workorderId' => $workorder->id, 'text' => "Firma '{$company->name}' wurde zugewiesen.", 'create' => time(), 'createBy' => $userId, + ]); + + $preorder = new Preorder($workorder->preorderId); + if ($preorder->id) { + $preorder->status_id = 10; // In Ausführung + $preorder->edit_by = $this->user->id; + $preorder->save(); + } + return true; + } + + private function createWorkordersFromPreorders() + { + $configs = WorkorderTenantConfigModel::getAll(); + foreach ($configs as $config) { + $filters = json_decode($config->workorderCreationFilters, true); + if (empty($filters)) continue; + + $networks = NetworkModel::getAll(['owner_id' => $config->addressId]); + if (empty($networks)) continue; + + $tenantCampaigns = array_map(fn($n) => $n->id, PreordercampaignModel::getAll(['network_id' => array_map(fn($n) => $n->id, $networks)])); + if (empty($tenantCampaigns)) continue; + + $filters['preordercampaign_id'] = $tenantCampaigns; + $newPreorders = PreorderModel::searchActive($filters); + + foreach ($newPreorders as $preorder) { + if (!WorkorderModel::getFirst(['preorderId' => $preorder->id])) { + WorkorderModel::create([ + 'preorderId' => $preorder->id, 'clusterId' => $preorder->preordercampaign_id, + 'status' => 'new', 'create' => time(), 'createBy' => 0 // System User + ]); + } + } + } + } + //endregion +} \ No newline at end of file diff --git a/application/WorkorderBase/WorkorderBaseController.php b/application/WorkorderBase/WorkorderBaseController.php new file mode 100644 index 000000000..12c6c35cb --- /dev/null +++ b/application/WorkorderBase/WorkorderBaseController.php @@ -0,0 +1,139 @@ + 'status', 'text' => 'Status', 'modal' => false, 'table' => ['filter' => 'iconSelect', 'filterOptions' => [ + ['value' => 'new', 'text' => 'Neu', 'icon' => 'fas fa-star text-primary'], + ['value' => 'assigned', 'text' => 'Zugewiesen', 'icon' => 'fas fa-user-check text-info'], + ['value' => 'scheduled', 'text' => 'Geplant', 'icon' => 'fas fa-calendar-check text-warning'], + ['value' => 'correction_requested', 'text' => 'Korrektur angefordert', 'icon' => 'fas fa-exclamation-triangle text-danger'], + ['value' => 'intervention_required', 'text' => 'Eingriff erforderlich', 'icon' => 'fas fa-times-circle text-danger'], + ['value' => 'civil_engineering_required', 'text' => 'Tiefbau benötigt', 'icon' => 'fas fa-hard-hat text-orange'], + ['value' => 'civil_engineering_completed', 'text' => 'Tiefbau abgeschlossen', 'icon' => 'fas fa-hard-hat text-success'], + ['value' => 'problem_solved', 'text' => 'Problem gelöst', 'icon' => 'fas fa-check-circle text-success'], + ['value' => 'documented', 'text' => 'Dokumentiert', 'icon' => 'fas fa-file-alt text-success'], + ['value' => 'completed', 'text' => 'Abgeschlossen', 'icon' => 'fas fa-check-double text-secondary'], + ['value' => 'cancelled', 'text' => 'Abgebrochen', 'icon' => 'fas fa-ban text-danger'], + ]] + ]; + + protected array $additionalJS = ["js/pages/WorkorderBase/WorkorderBase.js"]; + protected array $additionalHead = [""]; + + /** + * Gets the display text for a given status key. + * @param string $statusKey + * @return string + */ + protected function getStatusText(string $statusKey): string + { + $statusMap = array_column($this->statusColumn['table']['filterOptions'] ?? [], 'text', 'value'); + return $statusMap[$statusKey] ?? ucfirst(str_replace('_', ' ', $statusKey)); + } + + //region SHARED ACTIONS + /** + * Fetches documentation and journal entries for a given workorder. + */ + protected function getDocumentationAction() + { + if (empty($this->request->workorderId)) self::sendError("Arbeitsauftrags-ID fehlt."); + + $docs = WorkorderDocumentationModel::getAll(['workorderId' => $this->request->workorderId], null, 0, ['key' => 'create', 'order' => 'ASC']); + $journals = WorkorderJournalModel::getAll(['workorderId' => $this->request->workorderId], null, 0, ['key' => 'create', 'order' => 'DESC']); + + $tenantConfig = $this->getTenantConfigFromWorkorder((int)$this->request->workorderId); + if ($tenantConfig && !empty($tenantConfig->documentationTypes)) { + $customTypes = json_decode($tenantConfig->documentationTypes, true); + $customMap = array_column($customTypes, 'text', 'value'); + $translationMap = array_merge(['civil_engineering_photo' => 'Tiefbau_Foto'], $customMap); + } + + + $responseDocs = []; + $typeCounts = []; + + foreach ($docs as $doc) { + $file = new File($doc->fileId); + $documentTypeKey = $doc->documentType; + $typeCounts[$documentTypeKey] = ($typeCounts[$documentTypeKey] ?? 0) + 1; + $originalFilename = $file->orig_filename ?? $file->filename; + $extension = pathinfo($originalFilename, PATHINFO_EXTENSION); + $translatedType = $translationMap[$documentTypeKey] ?? $documentTypeKey; + $newFilename = "{$translatedType}_{$typeCounts[$documentTypeKey]}." . strtolower($extension); + + $responseDocs[] = [ + 'id' => $doc->id, 'fileId' => $doc->fileId, 'fileName' => $newFilename, 'description' => $doc->description, + 'documentType' => $documentTypeKey, 'userName' => UserModel::getOne($doc->createBy)->name ?? 'Unbekannt', + 'mimetype' => $file->mimetype ?? 'application/octet-stream', 'create' => $doc->create + ]; + } + + foreach ($journals as $journal) { + $journal->createByName = UserModel::getOne($journal->createBy)->name ?? 'Unbekannt'; + } + + self::returnJson(['docs' => $responseDocs, 'journals' => $journals]); + } + + /** + * Adds a new entry to a workorder's journal. + */ + protected function addJournalAction() + { + $post = json_decode(file_get_contents('php://input'), true); + if (empty($post['workorderId']) || empty(trim($post['text']))) self::sendError("Arbeitsauftrags-ID und Text sind erforderlich."); + + WorkorderJournalModel::create(['workorderId' => $post['workorderId'], 'text' => $post['text'], 'createBy' => $this->user->id, 'create' => time()]); + + $journals = WorkorderJournalModel::getAll(['workorderId' => $post['workorderId']], null, 0, ['key' => 'create', 'order' => 'DESC']); + foreach ($journals as $journal) { + $journal->createByName = UserModel::getOne($journal->createBy)->name ?? 'Unbekannt'; + } + self::returnJson(['success' => true, 'message' => 'Journaleintrag hinzugefügt.', 'journals' => $journals]); + } + + /** + * Updates the additional info field for a workorder. + */ + protected function updateAdditionalInfoAction() + { + $post = json_decode(file_get_contents('php://input'), true); + if (empty($post['workorderId'])) self::sendError("Arbeitsauftrags-ID fehlt."); + $workorder = WorkorderModel::get($post['workorderId']); + if (!$workorder) self::sendError("Arbeitsauftrag nicht gefunden."); + + $oldInfo = $workorder->additionalInfo; + $newInfo = $post['additionalInfo'] ?? null; + $workorder->additionalInfo = $newInfo; + WorkorderModel::update((array)$workorder); + + WorkorderJournalModel::create([ + 'workorderId' => $workorder->id, 'text' => "Zusatzinfo geändert.\nAlt: '{$oldInfo}'\nNeu: '{$newInfo}'", + 'create' => time(), 'createBy' => $this->user->id, + ]); + self::returnJson(['success' => true, 'message' => 'Zusatzinfo aktualisiert.', 'newInfo' => $newInfo]); + } + //endregion + + protected function getTenantConfigFromWorkorder(int $workorderId) { + if (empty($workorderId)) self::sendError("Arbeitsauftrags-ID fehlt."); + $workorder = WorkorderModel::get($workorderId); + if (!$workorder) self::sendError("Arbeitsauftrag nicht gefunden."); + $preorder = new Preorder($workorder->preorderId); + if (!$preorder->id) self::sendError("Vorbestellung nicht gefunden."); + $campaign = new Preordercampaign($preorder->preordercampaign_id); + if (!$campaign->id) self::sendError("Kampagne nicht gefunden."); + $network = NetworkModel::getOne($campaign->network_id); + if (!$network) self::sendError("Netzwerk nicht gefunden."); + + return WorkorderTenantConfigModel::getFirst(['addressId' => $network->owner_id]) ?? null; + } +} \ No newline at end of file diff --git a/application/WorkorderCompany/WorkorderCompanyController.php b/application/WorkorderCompany/WorkorderCompanyController.php new file mode 100644 index 000000000..38273e7e9 --- /dev/null +++ b/application/WorkorderCompany/WorkorderCompanyController.php @@ -0,0 +1,195 @@ + 'id', 'text' => 'Auftrags-Nr.', 'table' => ['sortable' => true]], + ['key' => 'networkOwnerName', 'text' => 'Auftraggeber', 'table' => ['sortable' => false]], + ['key' => 'preorderInfo', 'text' => 'Kunde', 'modal' => false, 'table' => ['sortable' => false]], + ['key' => 'rimo_fcp_name', 'text' => 'FCP', 'modal' => false, 'table' => ['sortable' => false]], + // Status column is now inherited via prepareCrudConfig + ['key' => 'additionalInfo', 'text' => 'Notiz', 'modal' => false, 'table' => ['sortable' => true]], + ['key' => 'deadlineDate', 'text' => 'Deadline', 'modal' => false, 'table' => ['filter' => 'date', 'sortable' => true]], + ['key' => 'appointmentDate', 'text' => 'Termin', 'modal' => false, 'table' => ['filter' => 'date', 'sortable' => true]], + ]; + protected array $additionalJSVariables = ['COMPANY_ID' => '0']; + + protected function prepareCrudConfig() { + $preorderInfoColIdx = array_search('preorderInfo', array_column($this->columns, 'key')); + array_splice($this->columns, $preorderInfoColIdx + 1, 0, [$this->statusColumn]); + $company = WorkorderCompanyModel::getFirst(['addressId' => $this->user->address_id]); + $this->additionalJSVariables['COMPANY_ID'] = $company ? $company->id : 0; + } + + //region ACTIONS + public function indexAction() { + parent::indexAction(); + } + + protected function getAction() { + $pagination = $this->postData['pagination'] ?? ['page' => 1, 'per_page' => 10]; + $filters = $this->postData['filters'] ?? []; + $order = $this->postData['order'] ?? []; + + $company = WorkorderCompanyModel::getFirst(['addressId' => $this->user->address_id]); + if (!$company) { + self::returnJson(['rows' => [], 'pagination' => array_merge($pagination, ['total_rows' => 0, 'total_pages' => 0, 'filtered_available' => 0])]); + return; + } + + $workorders = WorkorderModel::getCompanyWorkorders($filters, $pagination['per_page'], ($pagination['page'] - 1) * $pagination['per_page'], $order, $company->id); + $totalCount = WorkorderModel::countCompanyWorkorders($filters, $company->id); + + self::returnJson([ + 'rows' => $workorders, + 'pagination' => ['page' => $pagination['page'], 'per_page' => $pagination['per_page'], 'total_rows' => $totalCount, 'total_pages' => ceil($totalCount / $pagination['per_page']), 'filtered_available' => $totalCount] + ]); + } + + public function getWorkorderByIdAction() { + if (empty($this->request->id)) self::sendError("ID fehlt"); + $workorder = WorkorderModel::get($this->request->id); + if (!$workorder) self::sendError("Arbeitsauftrag nicht gefunden"); + self::returnJson((array)$workorder); + } + + protected function scheduleAppointmentAction() { + if (empty($this->postData['workorderId']) || empty($this->postData['appointmentDate'])) self::sendError("Erforderliche Felder fehlen."); + $workorder = WorkorderModel::get($this->postData['workorderId']); + if (!$workorder) self::sendError("Arbeitsauftrag nicht gefunden"); + if ((int)date('H', $this->postData['appointmentDate']) >= 23 || (int)date('H', $this->postData['appointmentDate']) < 1) self::sendError("Bitte geben Sie eine Uhrzeit an!"); + + $workorder->appointmentDate = $this->postData['appointmentDate']; + $workorder->status = 'scheduled'; + WorkorderModel::update((array)$workorder); + + WorkorderJournalModel::create([ + 'workorderId' => $workorder->id, 'text' => 'Termin festgelegt auf: ' . date('d.m.Y H:i', $this->postData['appointmentDate']), + 'create' => time(), 'createBy' => $this->user->id, + ]); + self::returnJson(['success' => true, 'message' => 'Termin erfolgreich gespeichert.']); + } + + protected function rescheduleAppointmentAction() { + if (empty($this->postData['workorderId']) || empty($this->postData['appointmentDate']) || empty($this->postData['reason'])) self::sendError("Erforderliche Felder fehlen."); + $workorder = WorkorderModel::get($this->postData['workorderId']); + if (!$workorder) self::sendError("Arbeitsauftrag nicht gefunden."); + if ((int)date('H', $this->postData['appointmentDate']) >= 23 || (int)date('H', $this->postData['appointmentDate']) < 1) self::sendError("Bitte geben Sie eine Uhrzeit an!"); + + $oldDateFormatted = $workorder->appointmentDate ? date('d.m.Y H:i', $workorder->appointmentDate) : 'N/A'; + $newDateFormatted = date('d.m.Y H:i', $this->postData['appointmentDate']); + $workorder->appointmentDate = $this->postData['appointmentDate']; + WorkorderModel::update((array)$workorder); + + WorkorderJournalModel::create([ + 'workorderId' => $workorder->id, 'text' => "Termin verschoben von {$oldDateFormatted} auf {$newDateFormatted}. Grund: " . $this->postData['reason'], + 'create' => time(), 'createBy' => $this->user->id, + ]); + self::returnJson(['success' => true, 'message' => 'Termin erfolgreich verschoben.']); + } + + protected function requestInterventionAction() { + if (empty($this->postData['workorderId']) || empty($this->postData['journalText'])) self::sendError("Erforderliche Felder fehlen."); + $workorder = WorkorderModel::get($this->postData['workorderId']); + if (!$workorder) self::sendError("Arbeitsauftrag nicht gefunden."); + + $oldStatus = $workorder->status; + $workorder->status = 'intervention_required'; + WorkorderModel::update((array)$workorder); + + WorkorderJournalModel::create([ + 'workorderId' => $workorder->id, 'text' => "Eingriff erforderlich: " . $this->postData['journalText'], + 'statusChange' => $this->getStatusText($oldStatus) . " -> " . $this->getStatusText('intervention_required'), + 'create' => time(), 'createBy' => $this->user->id, + ]); + self::returnJson(['success' => true, 'message' => 'Eingriff wurde angefordert.']); + } + + protected function completeWorkorderAction() { + if (empty($this->postData['workorderId'])) self::sendError("Arbeitsauftrags-ID fehlt."); + $workorder = WorkorderModel::get($this->postData['workorderId']); + if (!$workorder) self::sendError("Arbeitsauftrag nicht gefunden."); + + $workorder->status = 'documented'; + WorkorderModel::update((array)$workorder); + self::returnJson(['success' => true, 'message' => 'Arbeitsauftrag zur Prüfung eingereicht.']); + } + + protected function getTenantConfigAction() { + $tenantConfig = $this->getTenantConfigFromWorkorder($this->request->workorderId); + + if (!$tenantConfig) { + self::returnJson(['success' => false, 'message' => 'Keine Mandantenkonfiguration gefunden.']); + return; + } + self::returnJson(['success' => true, 'documentationTypes' => json_decode($tenantConfig->documentationTypes, true), 'civilEngineeringDocsRequired' => $tenantConfig->civilEngineeringDocsRequired]); + } + + protected function uploadDocumentationAction() { + if (empty($_FILES['files']) || empty($_POST['workorderId'])) self::sendError('Erforderliche Daten fehlen.'); + $workorderId = $_POST['workorderId']; + + foreach ($_FILES['files']['name'] as $index => $name) { + if ($_FILES['files']['error'][$index] === UPLOAD_ERR_OK) { + $_FILES['file'] = ['name' => $name, 'type' => $_FILES['files']['type'][$index], 'tmp_name' => $_FILES['files']['tmp_name'][$index], 'error' => $_FILES['files']['error'][$index], 'size' => $_FILES['files']['size'][$index]]; + try { + $uploaded = mfUpload::handleFormUpload("file", false, "/Workorder"); + WorkorderDocumentationModel::create(['workorderId' => $workorderId, 'fileId' => $uploaded->id, 'description' => $_POST['description'] ?? '', 'documentType' => $_POST['documentType'] ?? 'general', 'create' => time(), 'createBy' => $this->user->id]); + } catch (Exception $e) { /* Log error if necessary */ + } + } + } + + $workorder = WorkorderModel::get($workorderId); + if (in_array($workorder->status, ['correction_requested', 'problem_solved', 'civil_engineering_completed'])) { + $workorder->status = 'assigned'; + WorkorderModel::update((array)$workorder); + } + + self::returnJson(['success' => true, 'message' => "Datei(en) erfolgreich hochgeladen."]); + } + + protected function deleteDocumentationAction() { + if (empty($this->postData['id'])) self::sendError("Dokumenten-ID fehlt."); + WorkorderDocumentationModel::delete($this->postData['id']); + self::returnJson(['success' => true, 'message' => 'Dokument gelöscht.']); + } + + protected function updateDocumentationAction() { + if (empty($this->postData['id'])) self::sendError("Dokumenten-ID fehlt."); + $doc = WorkorderDocumentationModel::get($this->postData['id']); + if (!$doc) self::sendError("Dokument nicht gefunden."); + if (isset($this->postData['documentType'])) $doc->documentType = $this->postData['documentType']; + WorkorderDocumentationModel::update((array)$doc); + self::returnJson(['success' => true, 'message' => 'Dokument aktualisiert.']); + } + + protected function completeCivilEngineeringAction() { + if (empty($this->postData['workorderId'])) self::sendError("Arbeitsauftrags-ID fehlt."); + $workorder = WorkorderModel::get($this->postData['workorderId']); + if (!$workorder) self::sendError("Arbeitsauftrag nicht gefunden."); + + // Re-assign to original company + if ($workorder->originalCompanyId) { + $workorder->companyId = $workorder->originalCompanyId; + $workorder->originalCompanyId = null; + } + + $oldStatus = $workorder->status; + $workorder->civilEngineeringCompanyId = null; + $workorder->status = 'civil_engineering_completed'; + WorkorderModel::update((array)$workorder); + + WorkorderJournalModel::create([ + 'workorderId' => $workorder->id, 'text' => "Tiefbau abgeschlossen.", + 'statusChange' => $this->getStatusText($oldStatus) . " -> " . $this->getStatusText('civil_engineering_completed'), + 'create' => time(), 'createBy' => $this->user->id, + ]); + self::returnJson(['success' => true, 'message' => 'Tiefbau erfolgreich abgeschlossen.']); + } + //endregion +} \ No newline at end of file diff --git a/application/RMLWorkorderCompany/RMLWorkorderCompanyModel.php b/application/WorkorderCompany/WorkorderCompanyModel.php similarity index 65% rename from application/RMLWorkorderCompany/RMLWorkorderCompanyModel.php rename to application/WorkorderCompany/WorkorderCompanyModel.php index 318d44d77..b24193820 100644 --- a/application/RMLWorkorderCompany/RMLWorkorderCompanyModel.php +++ b/application/WorkorderCompany/WorkorderCompanyModel.php @@ -1,7 +1,7 @@ preorderId)) return null; + + $db = self::getDB(); + $dbName = FRONKDB_DBNAME; + $tableWTC = self::getFullyQualifiedTable(); + $preorderId = $db->real_escape_string($workorder->preorderId); + + $sql = "SELECT wtc.* FROM $tableWTC wtc + JOIN `$dbName`.`Network` n ON wtc.addressId = n.owner_id + JOIN `$dbName`.`Preordercampaign` pc ON n.id = pc.network_id + JOIN `$dbName`.`Preorder` p ON pc.id = p.preordercampaign_id + WHERE p.id = '$preorderId' LIMIT 1"; + + $row = $db->query($sql)?->fetch_assoc(); + + return $row ? new self($row) : null; + }} \ No newline at end of file diff --git a/db/migrations/20250901410000_workorder_rename.php b/db/migrations/20250901410000_workorder_rename.php new file mode 100644 index 000000000..339448e8e --- /dev/null +++ b/db/migrations/20250901410000_workorder_rename.php @@ -0,0 +1,81 @@ +table('RMLWorkorder')->rename('Workorder'); + $this->table('RMLWorkorderCompany')->rename('WorkorderCompany'); + $this->table('RMLWorkorderDocumentation')->rename('WorkorderDocumentation'); + $this->table('RMLWorkorderJournal')->rename('WorkorderJournal'); + $this->table('RMLWorkorderTenantConfig')->rename('WorkorderTenantConfig'); + + $workorderTable = $this->table('Workorder'); + $workorderTable->addColumn('civilEngineeringCompanyId', 'integer', ['null' => true, 'default' => null, 'after' => 'companyId', 'comment' => 'Company assigned for civil engineering task']) + ->addColumn('originalCompanyId', 'integer', ['null' => true, 'default' => null, 'after' => 'civilEngineeringCompanyId', 'comment' => 'Stores the companyId before assigning to civil engineering']) + ->addIndex(['civilEngineeringCompanyId'], ['name' => 'civilEngineeringCompanyId_idx']) + ->addIndex(['originalCompanyId'], ['name' => 'originalCompanyId_idx']) + ->save(); + + $workorderTable->changeColumn('status', 'enum', [ + 'values' => [ + 'new', + 'assigned', + 'scheduled', + 'correction_requested', + 'intervention_required', + 'civil_engineering_required', + 'civil_engineering_completed', + 'problem_solved', + 'documented', + 'completed', + 'cancelled' + ], + 'default' => 'new', + 'null' => false + ])->save(); + + $tenantConfigTable = $this->table('WorkorderTenantConfig'); + $tenantConfigTable->addColumn('civilEngineeringDocsRequired', 'boolean', ['default' => false, 'null' => false, 'after' => 'workorderCreationFilters', 'comment' => 'If true, civil engineering company must upload docs to complete task']) + ->save(); + } + + public function down(): void + { + $tenantConfigTable = $this->table('WorkorderTenantConfig'); + $tenantConfigTable->removeColumn('civilEngineeringDocsRequired')->save(); + + $workorderTable = $this->table('Workorder'); + $workorderTable->changeColumn('status', 'enum', [ + 'values' => [ + 'new', + 'assigned', + 'scheduled', + 'correction_requested', + 'intervention_required', + 'problem_solved', + 'documented', + 'completed', + 'cancelled' + ], + 'default' => 'new', + 'null' => false + ])->save(); + + $workorderTable->removeColumn('civilEngineeringCompanyId') + ->removeColumn('originalCompanyId') + ->removeIndexByName('civilEngineeringCompanyId_idx') + ->removeIndexByName('originalCompanyId_idx') + ->save(); + + $this->table('Workorder')->rename('RMLWorkorder'); + $this->table('WorkorderCompany')->rename('RMLWorkorderCompany'); + $this->table('WorkorderDocumentation')->rename('RMLWorkorderDocumentation'); + $this->table('WorkorderJournal')->rename('RMLWorkorderJournal'); + $this->table('WorkorderTenantConfig')->rename('RMLWorkorderTenantConfig'); + } +} diff --git a/lib/Helper/Helper.php b/lib/Helper/Helper.php index e81644fc7..cf013a7ce 100644 --- a/lib/Helper/Helper.php +++ b/lib/Helper/Helper.php @@ -213,4 +213,16 @@ class Helper { return $returnObject ? $campaigns : array_column($campaigns, 'id'); } + + public static function getPreorderCampaignNetworkOwners() { + $sql = "SELECT a.id FROM Preordercampaign pc + LEFT JOIN Network n ON pc.network_id = n.id + LEFT JOIN Address a ON n.owner_id = a.id + GROUP BY a.id + ORDER BY a.company, a.lastname, a.firstname"; + + $results = FronkDB::singleton()->fetch_all_assoc(FronkDB::singleton()->query($sql)) ?? []; + + return array_map(fn($owner) => new Address($owner['id']), $results); + } } \ No newline at end of file diff --git a/public/js/pages/RMLWorkorderAdmin/RMLWorkorderAdmin.js b/public/js/pages/RMLWorkorderAdmin/RMLWorkorderAdmin.js deleted file mode 100644 index 06963ffb9..000000000 --- a/public/js/pages/RMLWorkorderAdmin/RMLWorkorderAdmin.js +++ /dev/null @@ -1,601 +0,0 @@ -// RMLWorkorderAdmin.js -Vue.component('r-m-l-workorder-admin', { - template: ` - -
- {{ workordersToAssign.length }} Workorder(s) zuweisen: -
- -
-
- - - - - - - - - - - - - - - - - -
- `, - data() { - return { - window, - workordersToAssign: [], - editingWorkorderId: null, - editingDeadlineId: null, - editingAdditionalInfoId: null, - tempAdditionalInfo: '', - companiesByTenant: {}, - companiesLoading: false, - massAssignCompanyId: null, - massAssignLoading: false, - companiesForMassAssign: [], - crudConfig: { - ...window.TT_CONFIG.CRUD_CONFIG, selectable: false, expandable: true, - customRowClass: (row) => { - if (['completed', 'new', 'cancelled'].includes(row.status)) return 'tt-rml-workorder-irrelevant'; - if (['correction_requested', 'intervention_required'].includes(row.status)) return 'tt-rml-workorder-high'; - const deadlineDate = moment.unix(row.deadlineDate); - if (!deadlineDate.isValid()) return 'tt-rml-workorder-irrelevant'; - const daysLeft = deadlineDate.diff(moment(), 'days'); - if (daysLeft <= 7) return 'tt-rml-workorder-urgent'; - if (daysLeft <= 21) return 'tt-rml-workorder-medium'; - return 'tt-rml-workorder-ontrack'; - }, - additionalActions: [] - } - } - }, - methods: { - addToAssignList(row) { - if (!this.workordersToAssign.includes(row.id)) this.workordersToAssign.push(row.id); - }, - removeFromAssignList(row) { - this.workordersToAssign = this.workordersToAssign.filter(id => id !== row.id); - }, - getStatusColumn(status) { - const column = this.crudConfig.columns.find(c => c.key === 'status'); - return column.table.filterOptions.find(opt => opt.value === status) || {}; - }, - formatDate(timestamp, withTime = false) { - if (!timestamp) return '–'; - return window.moment.unix(timestamp).format(withTime ? 'DD.MM.YYYY HH:mm' : 'DD.MM.YYYY'); - }, - async getCompaniesForWorkorder(workorder) { - if (!workorder.tenantId || this.companiesByTenant[workorder.tenantId]) return; - this.companiesLoading = true; - try { - const {data} = await axios.get(`${window.TT_CONFIG.BASE_PATH}/RMLWorkorderAdmin/getCompanies`, {params: {tenantId: workorder.tenantId}}); - this.$set(this.companiesByTenant, workorder.tenantId, data); - } catch (e) { - window.notify('error', 'Firmenliste konnte nicht geladen werden.'); - } finally { - this.companiesLoading = false; - } - }, - async startCompanyEdit(row) { - await this.getCompaniesForWorkorder(row); - this.editingWorkorderId = row.id; - }, - async assignCompany(workorder, companyId) { - if (!companyId) { - this.editingWorkorderId = null; - return; - } - try { - const {data} = await axios.post(`${window.TT_CONFIG.BASE_PATH}/RMLWorkorderAdmin/assignWorkorder`, { - workorderId: workorder.id, - companyId: companyId - }); - if (data.success) { - window.notify('success', data.message); - this.$refs.table.$refs.table.refreshTable(); - } else window.notify('error', data.message || 'Ein Fehler ist aufgetreten.'); - } catch (e) { - window.notify('error', 'Ein Netzwerkfehler ist aufgetreten.'); - } finally { - this.editingWorkorderId = null; - } - }, - async massAssignCompanies(companyId) { - if (!companyId) return; - if (!confirm(`${this.workordersToAssign.length} Workorder(s) der ausgewählten Firma zuweisen?`)) { - this.massAssignCompanyId = null; - return; - } - this.massAssignLoading = true; - try { - const {data} = await axios.post(`${window.TT_CONFIG.BASE_PATH}/RMLWorkorderAdmin/massAssignWorkorders`, { - companyId: companyId, - workorderIds: this.workordersToAssign - }); - if (data.success) { - window.notify('success', data.message); - this.workordersToAssign = []; - this.$refs.table.$refs.table.refreshTable(); - } else window.notify('error', data.message || 'Ein Fehler ist aufgetreten.'); - } catch (e) { - window.notify('error', 'Ein Netzwerkfehler ist aufgetreten.'); - } finally { - this.massAssignLoading = false; - this.massAssignCompanyId = null; - } - }, - async updateDeadline(workorder, newDate) { - if (!newDate) { - this.editingDeadlineId = null; - return; - } - try { - const {data} = await axios.post(`${window.TT_CONFIG.BASE_PATH}/RMLWorkorderAdmin/updateDeadline`, { - workorderId: workorder.id, - deadlineDate: newDate - }); - if (data.success) { - window.notify('success', data.message); - this.$refs.table.$refs.table.refreshTable(); - } else window.notify('error', data.message || 'Ein Fehler ist aufgetreten.'); - } catch (e) { - window.notify('error', 'Ein Netzwerkfehler ist aufgetreten.'); - } finally { - this.editingDeadlineId = null; - } - }, - async acceptDocumentation(workorderId) { - if (!confirm('Soll die Dokumentation für diesen Arbeitsauftrag wirklich akzeptiert und der Auftrag abgeschlossen werden?')) return; - try { - const {data} = await axios.post(`${window.TT_CONFIG.BASE_PATH}/RMLWorkorderAdmin/acceptDocumentation`, {workorderId}); - if (data.success) { - window.notify('success', data.message); - this.$refs.table.$refs.table.refreshTable(); - } else window.notify('error', data.message || 'Ein Fehler ist aufgetreten.'); - } catch (e) { - window.notify('error', 'Ein Netzwerkfehler ist aufgetreten.'); - } - }, - async setToProblemSolved(row) { - const text = prompt('Bitte geben Sie einen kurzen Text für den Journaleintrag ein:', ''); - if (text === null) return; - if (!text.trim()) { - window.notify('error', 'Bitte geben Sie einen Text ein.'); - return; - } - try { - const {data} = await axios.post(`${window.TT_CONFIG.BASE_PATH}/RMLWorkorderAdmin/setToProblemSolved`, { - workorderId: row.id, - text: text - }); - if (data.success) { - window.notify('success', data.message); - this.$refs.table.$refs.table.refreshTable(); - } else window.notify('error', data.message || 'Ein Fehler ist aufgetreten.'); - } catch (e) { - window.notify('error', 'Ein Netzwerkfehler ist aufgetreten.'); - } - }, - startAdditionalInfoEdit(row) { - this.editingAdditionalInfoId = row.id; - this.tempAdditionalInfo = row.additionalInfo || ''; - this.$nextTick(() => { - this.$refs.editTextarea?.$el.querySelector('textarea').focus(); - }); - }, - cancelEdit() { - this.editingAdditionalInfoId = null; - this.tempAdditionalInfo = ''; - }, - async updateAdditionalInfo(row, newInfo) { - if (row.additionalInfo === newInfo) { - this.cancelEdit(); - return; - } - try { - const {data} = await axios.post(`${window.TT_CONFIG.BASE_PATH}/RMLWorkorderAdmin/updateAdditionalInfo`, { - workorderId: row.id, - additionalInfo: newInfo - }); - if (data.success) { - window.notify('success', data.message); - row.additionalInfo = newInfo; - } else window.notify('error', data.message || 'Update fehlgeschlagen.'); - } catch (e) { - window.notify('error', 'Netzwerkfehler beim Update.'); - } finally { - this.cancelEdit(); - } - }, - async cancelWorkorder(row) { - const reason = prompt('Bitte geben Sie einen Grund für die Stornierung an (optional):'); - if (reason === null) return; - try { - const {data} = await axios.post(`${window.TT_CONFIG.BASE_PATH}/RMLWorkorderAdmin/cancelWorkorder`, { - workorderId: row.id, - reason: reason - }); - if (data.success) { - window.notify('success', data.message); - this.$refs.table.$refs.table.refreshTable(); - } else window.notify('error', data.message || 'Stornierung fehlgeschlagen.'); - } catch (e) { - window.notify('error', 'Ein Netzwerkfehler ist aufgetreten.'); - } - } - }, - watch: { - workordersToAssign: { - async handler(newVal) { - if (newVal.length === 0) { - this.companiesForMassAssign = []; - return; - } - const firstWorkorder = this.$refs.table.$refs.table.rows.find(r => r.id === newVal[0]); - if (!firstWorkorder) return; - const firstTenantId = firstWorkorder.tenantId; - if (!newVal.every(id => { - const wo = this.$refs.table.$refs.table.rows.find(r => r.id === id); - return wo && wo.tenantId === firstTenantId; - })) { - window.notify('error', 'Massen-Zuweisung nur für Aufträge des gleichen Mandanten möglich.'); - this.workordersToAssign.pop(); - return; - } - await this.getCompaniesForWorkorder(firstWorkorder); - this.companiesForMassAssign = this.companiesByTenant[firstTenantId] || []; - }, - deep: true - } - } -}); - -Vue.component('traffic-light', { - props: ['deadline', 'status'], - computed: { - lightInfo() { - if (['completed', 'new', 'cancelled'].includes(this.status)) return { - color: '#cccccc', - title: 'Status irrelevant für Dringlichkeit' - }; - const deadlineDate = moment.unix(this.deadline); - if (!deadlineDate.isValid()) return {color: '#cccccc', title: 'Keine Deadline gesetzt'}; - if (deadlineDate.isBefore(moment())) return {color: '#dc3545', title: 'Deadline überschritten'}; - const daysLeft = deadlineDate.diff(moment(), 'days'); - if (daysLeft <= 7) return {color: '#dc3545', title: 'Dringend: Weniger als 1 Woche'}; - if (daysLeft <= 21) return {color: '#ffc107', title: 'Mittel: Weniger als 3 Wochen'}; - return {color: '#28a745', title: 'Im Plan: Mehr als 3 Wochen'}; - } - }, - template: `` -}); - -Vue.component('rml-documentation-viewer-admin', { - props: ['workorderId'], - template: ` -
-
-
-
- -
-
-
-
Korrektur - anfordern
-
-

Wählen Sie die zu korrigierenden Dokumente aus der Galerie aus und geben Sie - einen Grund an.

- - -
-
-
-
Dokumentation - akzeptieren
-
-

Prüfen Sie, ob alle erforderlichen Dokumente vorhanden und korrekt sind.

-
-
    -
  • - - {{ docType.text }} -
  • -
-
- -
-
-
-
Journal
-
-
    -
  • - {{ formatDate(log.create) }} ({{ log.createByName }}): -
    {{ log.text }}
    -
  • -
-
Keine Journaleinträge.
-
- -
-
-
-
`, - data: () => ({ - loading: true, - loadingConfig: true, - correctionLoading: false, - docs: [], - journals: [], - selectedDocs: [], - correctionText: '', - newJournalMessage: '', - addingJournalEntry: false, - tenantDocTypes: null - }), - computed: { - requiredDocTypes() { - if (this.tenantDocTypes) return this.tenantDocTypes; - return [{value: 'photo_hup_mounted', text: 'Foto vom montierten HÜP'}, { - value: 'photo_hup_open', - text: 'Foto von dem offenen HÜP' - }, { - value: 'photo_splice_cassette_hup', - text: 'Foto der Spleißkassette – HÜP' - }, { - value: 'photo_splice_cassette_fcp', - text: 'Foto der Spleißkassette - FCP' - }, { - value: 'photo_hup_closed_stickers', - text: 'Foto vom geschlossenen HÜP mit allen Aufklebern' - }, {value: 'photo_fcp_labeled', text: 'Foto vom FCP beschriftet'}, { - value: 'photo_patch_position_osp', - text: 'Foto der Patch-Position - OSP-Seite' - }, { - value: 'photo_patch_position_anb', - text: 'Foto der Patch-Position - ANB-Seite' - }, {value: 'measurement_protocol_otdr', text: 'ODTR – Messung (1310nm & 1550nm)'}]; - } - }, - methods: { - isUploaded(docType) { - return this.docs.some(doc => doc.documentType === docType); - }, - async fetchData() { - this.loading = true; - try { - const {data} = await axios.get(`${window.TT_CONFIG.BASE_PATH}/RMLWorkorderAdmin/getDocumentation`, {params: {workorderId: this.workorderId}}); - this.docs = data.docs; - this.journals = data.journals; - } catch (e) { - window.notify('error', 'Dokumentation konnte nicht geladen werden.'); - } finally { - this.loading = false; - } - }, - async loadTenantConfig() { - this.loadingConfig = true; - try { - const {data} = await axios.get(`${window.TT_CONFIG.BASE_PATH}/RMLWorkorderCompany/getTenantConfig`, {params: {workorderId: this.workorderId}}); - if (data.success) this.tenantDocTypes = data.documentationTypes; - } catch (e) { - console.error("Konnte Mandantenkonfiguration nicht laden", e); - } finally { - this.loadingConfig = false; - } - }, - async requestCorrection() { - if (!this.correctionText) return window.notify('error', 'Bitte geben Sie einen Grund für die Korrektur an.'); - if (this.selectedDocs.length === 0) return window.notify('error', 'Bitte wählen Sie mindestens ein Dokument für die Korrektur aus.'); - this.correctionLoading = true; - try { - const {data} = await axios.post(`${window.TT_CONFIG.BASE_PATH}/RMLWorkorderAdmin/requestCorrection`, { - workorderId: this.workorderId, - text: this.correctionText, - fileIds: this.selectedDocs - }); - if (data.success) { - window.notify('success', data.message); - this.correctionText = ''; - this.selectedDocs = []; - await this.fetchData(); - this.$emit('workorder-updated'); - } else window.notify('error', data.message); - } catch (e) { - window.notify('error', 'Ein Netzwerkfehler ist aufgetreten.'); - } - this.correctionLoading = false; - }, - async addJournalEntry() { - if (!this.newJournalMessage.trim()) return window.notify('error', 'Bitte geben Sie eine Nachricht ein.'); - this.addingJournalEntry = true; - try { - const {data} = await axios.post(`${window.TT_CONFIG.BASE_PATH}/RMLWorkorderAdmin/addJournal`, { - workorderId: this.workorderId, - text: this.newJournalMessage - }); - if (data.success) { - window.notify('success', data.message || 'Journal-Eintrag hinzugefügt.'); - this.newJournalMessage = ''; - this.journals = data.journals; - } else window.notify('error', data.message || 'Eintrag konnte nicht gespeichert werden.'); - } catch (e) { - window.notify('error', 'Ein Netzwerkfehler ist aufgetreten.'); - } finally { - this.addingJournalEntry = false; - } - }, - formatDate(timestamp) { - return window.moment.unix(timestamp).format('DD.MM.YYYY HH:mm'); - }, - }, - async mounted() { - await this.loadTenantConfig(); - await this.fetchData(); - } -}); \ No newline at end of file diff --git a/public/js/pages/RMLWorkorderCompany/RMLWorkorder.css b/public/js/pages/RMLWorkorderCompany/RMLWorkorder.css deleted file mode 100644 index 76ca5510f..000000000 --- a/public/js/pages/RMLWorkorderCompany/RMLWorkorder.css +++ /dev/null @@ -1,40 +0,0 @@ -/* - * CSS for Workorder Table Row Highlighting - */ - -/* Urgent: Deadline passed or less than 1 week away */ -.table-hover .tt-rml-workorder-urgent:hover, -.tt-rml-workorder-urgent { - background-color: #fbe9e7 !important; /* Soft Red */ -} - -/* Medium: Deadline less than 3 weeks away */ -.table-hover .tt-rml-workorder-medium:hover, -.tt-rml-workorder-medium { - background-color: #fff8e1 !important; /* Soft Yellow */ -} - -/* On Track: Deadline more than 3 weeks away */ -.table-hover .tt-rml-workorder-ontrack:hover, -.tt-rml-workorder-ontrack { - background-color: #e8f5e9 !important; /* Soft Green */ -} - -/* Irrelevant: No deadline or status makes it not applicable */ -.table-hover .tt-rml-workorder-irrelevant:hover, -.tt-rml-workorder-irrelevant { - background-color: #fafafa !important; /* Very light grey */ -} - -.table-hover .tt-rml-workorder-high:hover, -.tt-rml-workorder-high { - background-color: #f8d7da !important; /* A slightly more intense red for high priority issues */ -} - -.tt-file-gallery-item.border.border-danger { - border: 4px solid #f1556c!important; -} - -.RMLWorkorderCompany-table .modal-body { - overflow-y: hidden; -} \ No newline at end of file diff --git a/public/js/pages/RMLWorkorderCompany/RMLWorkorderCompany.js b/public/js/pages/RMLWorkorderCompany/RMLWorkorderCompany.js deleted file mode 100644 index fa0ffa657..000000000 --- a/public/js/pages/RMLWorkorderCompany/RMLWorkorderCompany.js +++ /dev/null @@ -1,568 +0,0 @@ -// RMLWorkorderCompany.js -Vue.component('r-m-l-workorder-company', { - template: ` -
- - - - - - - - - - - - - - - - - - -

Auftrag: #{{ rescheduleData.workorder.id }}

- - -
-
- `, - data() { - return { - rescheduleData: null, editingAdditionalInfoId: null, tempAdditionalInfo: '', - crudConfig: { - ...window.TT_CONFIG.CRUD_CONFIG, expandable: true, - customRowClass: (row) => { - if (['completed', 'new', 'cancelled'].includes(row.status)) return 'tt-rml-workorder-irrelevant'; - if (['correction_requested', 'intervention_required'].includes(row.status)) return 'tt-rml-workorder-high'; - const deadlineDate = moment.unix(row.deadlineDate); - if (!deadlineDate.isValid()) return 'tt-rml-workorder-irrelevant'; - const daysLeft = deadlineDate.diff(moment(), 'days'); - if (daysLeft <= 7) return 'tt-rml-workorder-urgent'; - if (daysLeft <= 21) return 'tt-rml-workorder-medium'; - return 'tt-rml-workorder-ontrack'; - }, - additionalActions: [] - } - } - }, - methods: { - getStatusColumn(status) { - const column = this.crudConfig.columns.find(c => c.key === 'status'); - return column.table.filterOptions.find(opt => opt.value === status) || {}; - }, - formatDate(timestamp, withTime = false) { - if (!timestamp) return '–'; - return window.moment.unix(timestamp).format(withTime ? 'DD.MM.YYYY HH:mm' : 'DD.MM.YYYY'); - }, - async setAppointment(workorder, date) { - if (!date) return; - const hour = moment.unix(date).hour(); - if (hour >= 23 || hour < 1) { - this.$refs.table.$refs.table.refreshTable(); - return window.notify('error', 'Bitte Uhrzeit angeben!'); - } - try { - const {data} = await axios.post(`${window.TT_CONFIG.BASE_PATH}/RMLWorkorderCompany/scheduleAppointment`, { - workorderId: workorder.id, - appointmentDate: date - }); - if (data.success) { - window.notify('success', data.message); - this.$refs.table.$refs.table.refreshTable(); - } else window.notify('error', data.message || 'Ein Fehler ist aufgetreten.'); - } catch (e) { - window.notify('error', e.response?.data?.message || 'Ein Netzwerkfehler ist aufgetreten.'); - } - }, - openRescheduleModal(row) { - this.rescheduleData = {workorder: row, newDate: row.appointmentDate, reason: ''}; - }, - closeRescheduleModal() { - this.rescheduleData = null; - }, - async rescheduleAppointment() { - const {workorder, newDate, reason} = this.rescheduleData; - if (!newDate || !reason) return window.notify('error', 'Bitte geben Sie ein neues Datum und einen Grund an.'); - if (moment.unix(newDate).hour() >= 23 || moment.unix(newDate).hour() < 1) return window.notify('error', 'Bitte Uhrzeit angeben!'); - try { - const {data} = await axios.post(`${window.TT_CONFIG.BASE_PATH}/RMLWorkorderCompany/rescheduleAppointment`, { - workorderId: workorder.id, - appointmentDate: newDate, - reason: reason - }); - if (data.success) { - window.notify('success', data.message); - this.$refs.table.$refs.table.refreshTable(); - this.closeRescheduleModal(); - } else window.notify('error', data.message || 'Ein Fehler ist aufgetreten.'); - } catch (e) { - window.notify('error', e.response?.data?.message || 'Ein Netzwerkfehler ist aufgetreten.'); - } - }, - startAdditionalInfoEdit(row) { - this.editingAdditionalInfoId = row.id; - this.tempAdditionalInfo = row.additionalInfo || ''; - this.$nextTick(() => { - this.$refs.editTextarea?.$el.querySelector('textarea').focus(); - }); - }, - cancelEdit() { - this.editingAdditionalInfoId = null; - this.tempAdditionalInfo = ''; - }, - async updateAdditionalInfo(row, newInfo) { - if (row.additionalInfo === newInfo) { - this.cancelEdit(); - return; - } - try { - const {data} = await axios.post(`${window.TT_CONFIG.BASE_PATH}/RMLWorkorderCompany/updateAdditionalInfo`, { - workorderId: row.id, - additionalInfo: newInfo - }); - if (data.success) { - window.notify('success', data.message); - row.additionalInfo = newInfo; - } else window.notify('error', data.message || 'Update fehlgeschlagen.'); - } catch (e) { - window.notify('error', 'Netzwerkfehler beim Update.'); - } finally { - this.cancelEdit(); - } - }, - } -}); - -Vue.component('traffic-light', { - props: ['deadline', 'status'], - computed: { - lightInfo() { - if (['completed', 'new', 'cancelled'].includes(this.status)) return { - color: '#cccccc', - title: 'Status irrelevant für Dringlichkeit' - }; - const deadlineDate = moment.unix(this.deadline); - if (!deadlineDate.isValid()) return {color: '#cccccc', title: 'Keine Deadline gesetzt'}; - if (deadlineDate.isBefore(moment())) return {color: '#dc3545', title: 'Deadline überschritten'}; - const daysLeft = deadlineDate.diff(moment(), 'days'); - if (daysLeft <= 7) return {color: '#dc3545', title: 'Dringend: Weniger als 1 Woche'}; - if (daysLeft <= 21) return {color: '#ffc107', title: 'Mittel: Weniger als 3 Wochen'}; - return {color: '#28a745', title: 'Im Plan: Mehr als 3 Wochen'}; - } - }, - template: `` -}); - -Vue.component('documentation-manager', { - props: ['workorderId'], - template: ` -
-
-
-
-
-
-
-
Benötigte Dokumente
-
    -
  • - - {{ docType.text }} -
  • -
-
- - - Bitte laden Sie alle benötigten Dokumente hoch, um den Auftrag abzuschließen. - -
- Auftrag zur Prüfung eingereicht oder storniert. -
-
-
-
-
Eingriff benötigt -
-
-

Falls ein Problem auftritt, das ein Eingreifen erfordert, melden Sie es - hier.

- -
-
-
-
Journal
-
-
    -
  • Keine Einträge - vorhanden. -
  • -
  • - {{ formatDate(log.create) }} ({{ log.createByName }}): -
    {{ log.text }}
    -
  • -
-
- -
-
-
-
-
-
-
Neues Dokument hochladen
- - -
- -
- -
-
- -
-
- - - -
-
- - -
- - -
-
-
`, - data: () => ({ - loadingWorkorder: true, - loadingConfig: true, - tenantDocTypes: null, - workorder: null, - uploading: false, - completing: false, - uploadedFiles: [], - journals: [], - newJournalMessage: '', - addingJournalEntry: false, - interventionData: null, - interventionTypes: [{value: 'stuck', text: 'Ab X Laufmeter stecken geblieben'}, { - value: 'stuck_fcp', - text: 'Vom FCP nach HÜP nach X Laufmetern stecken geblieben' - }, {value: 'stuck_hup', text: 'Vom HÜP nach FCP nach X Laufmetern stecken geblieben'}, { - value: 'no_air', - text: 'Keine Luftverbindung' - }, {value: 'other', text: 'Sonstiges'}], - uploadData: {files: [], documentType: 'photo_hup_mounted', description: ''} - }), - computed: { - requiredDocTypes() { - if (this.tenantDocTypes) return this.tenantDocTypes; - return [{value: 'photo_hup_mounted', text: 'Foto vom montierten HÜP'}, { - value: 'photo_hup_open', - text: 'Foto von dem offenen HÜP' - }, { - value: 'photo_splice_cassette_hup', - text: 'Foto der Spleißkassette – HÜP' - }, { - value: 'photo_splice_cassette_fcp', - text: 'Foto der Spleißkassette - FCP' - }, { - value: 'photo_hup_closed_stickers', - text: 'Foto vom geschlossenen HÜP mit allen Aufklebern' - }, {value: 'photo_fcp_labeled', text: 'Foto vom FCP beschriftet'}, { - value: 'photo_patch_position_osp', - text: 'Foto der Patch-Position - OSP-Seite' - }, { - value: 'photo_patch_position_anb', - text: 'Foto der Patch-Position - ANB-Seite' - }, {value: 'measurement_protocol_otdr', text: 'ODTR – Messung (1310nm & 1550nm)'}]; - }, - allDocTypes() { - return [...this.requiredDocTypes, {value: 'other', text: 'Sonstiges Dokument (optional)'}]; - }, - canComplete() { - return this.requiredDocTypes.every(docType => this.isUploaded(docType.value)); - }, - filesWithStatus() { - if (!this.journals?.length) return this.uploadedFiles; - const correctionJournal = [...this.journals].sort((a, b) => b.create - a.create).find(j => j.statusChange?.includes('correction_requested')); - if (!correctionJournal?.fileIds) return this.uploadedFiles; - try { - const incorrectFileIds = JSON.parse(correctionJournal.fileIds); - if (!Array.isArray(incorrectFileIds)) return this.uploadedFiles; - return this.uploadedFiles.map(file => incorrectFileIds.includes(file.id) ? { - ...file, - class: 'border border-danger' - } : file); - } catch (e) { - return this.uploadedFiles; - } - } - }, - methods: { - formatDate(timestamp) { - return timestamp ? window.moment.unix(timestamp).format('DD.MM.YYYY HH:mm') : '–'; - }, - async loadTenantConfig() { - this.loadingConfig = true; - try { - const {data} = await axios.get(`${window.TT_CONFIG.BASE_PATH}/RMLWorkorderCompany/getTenantConfig`, {params: {workorderId: this.workorderId}}); - if (data.success) this.tenantDocTypes = data.documentationTypes; - } catch (e) { - console.error("Konnte Mandantenkonfiguration nicht laden", e); - } finally { - this.loadingConfig = false; - } - }, - async loadWorkorder() { - this.loadingWorkorder = true; - try { - const {data} = await axios.get(`${window.TT_CONFIG.BASE_PATH}/RMLWorkorderCompany/getWorkorderById`, {params: {id: this.workorderId}}); - this.workorder = data; - } catch (e) { - window.notify('error', 'Arbeitsauftragsdetails konnten nicht geladen werden.'); - } - this.loadingWorkorder = false; - }, - isUploaded(docType) { - return this.uploadedFiles.some(file => file.documentType === docType); - }, - async fetchDocs() { - try { - const {data} = await axios.get(`${window.TT_CONFIG.BASE_PATH}/RMLWorkorderCompany/getDocumentation`, {params: {workorderId: this.workorderId}}); - this.uploadedFiles = data.docs; - this.journals = data.journals; - } catch (e) { - window.notify('error', 'Dokumente konnten nicht geladen werden.'); - } - }, - handleFileUpload(event) { - this.uploadData.files = event.target.files; - }, - async uploadFiles() { - if (!this.uploadData.files?.length) return window.notify('error', 'Bitte eine oder mehrere Dateien auswählen.'); - this.uploading = true; - const formData = new FormData(); - formData.append('workorderId', this.workorder.id); - formData.append('documentType', this.uploadData.documentType); - formData.append('description', this.uploadData.description); - for (const file of this.uploadData.files) formData.append('files[]', file); - try { - const {data} = await axios.post(`${window.TT_CONFIG.BASE_PATH}/RMLWorkorderCompany/uploadDocumentation`, formData); - if (data.success) { - window.notify('success', data.message); - this.$refs.fileInput.value = ''; - this.uploadData.files = []; - this.uploadData.description = ''; - this.uploadedFiles = data.docs; - this.workorder = data.workorder; - } else window.notify('error', data.error || 'Upload fehlgeschlagen.'); - } catch (e) { - window.notify('error', 'Ein Netzwerkfehler ist beim Upload aufgetreten.'); - } - this.uploading = false; - }, - async completeWorkorder() { - if (!confirm('Möchten Sie diesen Auftrag wirklich abschließen und zur Prüfung einreichen?')) return; - this.completing = true; - try { - const {data} = await axios.post(`${window.TT_CONFIG.BASE_PATH}/RMLWorkorderCompany/completeWorkorder`, {workorderId: this.workorder.id}); - if (data.success) { - window.notify('success', data.message); - this.$emit('workorder-completed'); - } else window.notify('error', data.message || 'Ein Fehler ist aufgetreten.'); - } catch (e) { - window.notify('error', 'Ein Netzwerkfehler ist aufgetreten.'); - } - this.completing = false; - }, - async deleteDocumentation(file) { - try { - const {data} = await axios.post(`${window.TT_CONFIG.BASE_PATH}/RMLWorkorderCompany/deleteDocumentation`, {id: file.id}); - if (data.success) { - window.notify('success', data.message); - this.uploadedFiles = data.docs; - } else window.notify('error', data.message || 'Löschen fehlgeschlagen.'); - } catch (e) { - window.notify('error', 'Netzwerkfehler beim Löschen.'); - } - }, - async updateDocumentation(file) { - try { - const {data} = await axios.post(`${window.TT_CONFIG.BASE_PATH}/RMLWorkorderCompany/updateDocumentation`, { - id: file.id, - documentType: file.documentType - }); - if (data.success) { - window.notify('success', data.message); - this.uploadedFiles = data.docs; - } else window.notify('error', data.message || 'Update fehlgeschlagen.'); - } catch (e) { - window.notify('error', 'Netzwerkfehler beim Aktualisieren.'); - } - }, - async addJournalEntry() { - if (!this.newJournalMessage.trim()) return window.notify('error', 'Bitte geben Sie eine Nachricht ein.'); - this.addingJournalEntry = true; - try { - const {data} = await axios.post(`${window.TT_CONFIG.BASE_PATH}/RMLWorkorderCompany/addJournal`, { - workorderId: this.workorderId, - text: this.newJournalMessage - }); - if (data.success) { - window.notify('success', data.message || 'Journal-Eintrag hinzugefügt.'); - this.newJournalMessage = ''; - this.journals = data.journals; - } else window.notify('error', data.message || 'Eintrag konnte nicht gespeichert werden.'); - } catch (e) { - window.notify('error', 'Ein Netzwerkfehler ist aufgetreten.'); - } finally { - this.addingJournalEntry = false; - } - }, - getInterventionLabel(type) { - return this.interventionTypes.find(t => t.value === type)?.text || type; - }, - openInterventionModal() { - this.interventionData = { - types: [], - details: { - stuck: {distance: ''}, - stuck_fcp: {distance: ''}, - stuck_hup: {distance: ''}, - other: {reason: ''} - } - }; - }, - async requestIntervention() { - const {types, details} = this.interventionData; - if (types.length === 0) return window.notify('error', 'Bitte wählen Sie mindestens ein Problem aus.'); - let journalParts = []; - types.sort(); - for (const type of types) { - let text = ''; - const problemText = this.interventionTypes.find(o => o.value === type)?.text || 'Unbekanntes Problem'; - if (['stuck', 'stuck_fcp', 'stuck_hup'].includes(type)) { - const distance = details[type]?.distance; - if (!distance || isNaN(distance) || distance <= 0) return window.notify('error', `Bitte eine gültige Distanz für "${problemText}" eingeben.`); - text = problemText.replace('X', distance); - } else if (type === 'no_air') text = problemText; - else if (type === 'other') { - const reason = details.other?.reason; - if (!reason?.trim()) return window.notify('error', 'Bitte geben Sie einen Grund für "Sonstiges" an.'); - text = `Sonstiges: ${reason.trim()}`; - } - if (text) journalParts.push(text); - } - const journalText = journalParts.join('\\n'); - if (!journalText) return window.notify('error', 'Keine gültigen Problemdetails zum Senden gefunden.'); - try { - const {data} = await axios.post(`${window.TT_CONFIG.BASE_PATH}/RMLWorkorderCompany/requestIntervention`, { - workorderId: this.workorderId, - journalText - }); - if (data.success) { - window.notify('success', data.message); - this.interventionData = null; - this.$emit('workorder-completed'); - } else window.notify('error', data.message || 'Ein Fehler ist aufgetreten.'); - } catch (e) { - window.notify('error', 'Ein Netzwerkfehler ist aufgetreten.'); - } - } - }, - async mounted() { - await this.loadWorkorder(); - await this.loadTenantConfig(); - await this.fetchDocs(); - } -}); \ No newline at end of file diff --git a/public/js/pages/RMLWorkorderCompanyDashboardView/RMLWorkorderCompanyDashboardView.js b/public/js/pages/RMLWorkorderCompanyDashboardView/RMLWorkorderCompanyDashboardView.js deleted file mode 100644 index 47f09d012..000000000 --- a/public/js/pages/RMLWorkorderCompanyDashboardView/RMLWorkorderCompanyDashboardView.js +++ /dev/null @@ -1,53 +0,0 @@ -// RMLWorkorderCompanyDashboardView.js -// This would be a separate file and view. - -Vue.component('rml-workorder-company-dashboard', { - template: ` -
-
-
-
-
-

Meine offenen Aufträge

-

{{ stats.assigned || 0 }}

-
-
-
-
-
-
-

Meine dringenden Aufträge

-

{{ stats.urgent || 0 }}

-
-
-
-
-
-
-

Meine terminierten Aufträge

-

{{ stats.scheduled || 0 }}

-
-
-
-
- -
-
- - - -
-
-
- `, - data() { - return { - stats: {} - } - }, - async mounted() { - // You would create a new controller action e.g., /RMLWorkorder/getCompanyDashboardStats - // const response = await axios.get(`${window.TT_CONFIG.BASE_PATH}/RMLWorkorder/getCompanyDashboardStats`); - // this.stats = response.data; - } -}) \ No newline at end of file diff --git a/public/js/pages/RMLWorkorderDashboardView/RMLWorkorderDashboardView.js b/public/js/pages/RMLWorkorderDashboardView/RMLWorkorderDashboardView.js deleted file mode 100644 index 15cd84e01..000000000 --- a/public/js/pages/RMLWorkorderDashboardView/RMLWorkorderDashboardView.js +++ /dev/null @@ -1,61 +0,0 @@ -// RMLWorkorderAdminDashboardView.js -// This would be a separate file and view. - -Vue.component('rml-workorder-admin-dashboard', { - template: ` -
-
-
-
-
-

Neue Aufträge

-

{{ stats.new || 0 }}

-
-
-
-
-
-
-

In Arbeit

-

{{ stats.in_progress || 0 }}

-
-
-
-
-
-
-

Überfällig

-

{{ stats.overdue || 0 }}

-
-
-
-
-
-
-

Abgeschlossen (30T)

-

{{ stats.completed_30d || 0 }}

-
-
-
-
- -
-
- - - -
-
-
- `, - data() { - return { - stats: {} - } - }, - async mounted() { - // You would create a new controller action e.g., /RMLWorkorder/getDashboardStats - // const response = await axios.get(`${window.TT_CONFIG.BASE_PATH}/RMLWorkorder/getDashboardStats`); - // this.stats = response.data; - } -}) \ No newline at end of file diff --git a/public/js/pages/WorkorderAdmin/WorkorderAdmin.js b/public/js/pages/WorkorderAdmin/WorkorderAdmin.js new file mode 100644 index 000000000..2c3db0984 --- /dev/null +++ b/public/js/pages/WorkorderAdmin/WorkorderAdmin.js @@ -0,0 +1,310 @@ +// WorkorderAdmin.js +Vue.component('workorder-admin', { + template: ` + +
+ {{ workordersToAssign.length }} Workorder(s) zuweisen: +
+ +
+
+ + + + + + + + + + + + + + + + + + +

Auftrag: #{{ civilEngineeringData.workorder.id }}

+ +
+ + +

Soll der Auftrag #{{ cancelWorkorderModalData.id }} wirklich storniert werden?

+ +
+ + +

Soll das Problem bei Auftrag #{{ problemSolvedModalData.id }} als gelöst markiert werden?

+ +
+ + +

Sollen {{ workordersToAssign.length }} Workorder(s) der Firma {{ massAssignModalData.companyName }} zugewiesen werden?

+ +
+ +
+ `, + data() { + return { + window, workordersToAssign: [], editingWorkorderId: null, editingDeadlineId: null, editingAdditionalInfoId: null, + civilEngineeringData: null, tempAdditionalInfo: '', companiesByTenant: {}, companiesLoading: false, massAssignCompanyId: null, + cancelWorkorderModalData: null, problemSolvedModalData: null, massAssignModalData: null, + crudConfig: { + ...window.TT_CONFIG.CRUD_CONFIG, selectable: false, expandable: true, + customRowClass: (row) => { + if (['completed', 'new', 'cancelled'].includes(row.status)) return 'tt-rml-workorder-irrelevant'; + if (['correction_requested', 'intervention_required'].includes(row.status)) return 'tt-rml-workorder-high'; + const deadlineDate = moment.unix(row.deadlineDate); + if (!deadlineDate.isValid()) return 'tt-rml-workorder-irrelevant'; + const daysLeft = deadlineDate.diff(moment(), 'days'); + if (daysLeft <= 7) return 'tt-rml-workorder-urgent'; + if (daysLeft <= 21) return 'tt-rml-workorder-medium'; + return 'tt-rml-workorder-ontrack'; + }, + additionalActions: [] + } + } + }, + computed: { + companiesForMassAssign() { + if (this.workordersToAssign.length === 0) return []; + const firstWorkorder = this.$refs.table?.$refs.table.rows.find(r => r.id === this.workordersToAssign[0]); + return firstWorkorder ? this.companiesByTenant[firstWorkorder.tenantId] || [] : []; + } + }, + methods: { + async acceptDocumentation(workorderId) { + const {data} = await axios.post(`${window.TT_CONFIG.BASE_PATH}/WorkorderAdmin/acceptDocumentation`, { workorderId }); + if (data.success) { + window.notify('success', data.message); + this.$refs.table.$refs.table.refreshTable(); + } else window.notify('error', data.message || 'Ein Fehler ist aufgetreten.'); + }, + openCivilEngineeringModal(row) { + this.getCompaniesForWorkorder(row); + this.civilEngineeringData = { workorder: row, companyId: null }; + }, + async assignCivilEngineering() { + if (!this.civilEngineeringData.companyId) return window.notify('error', 'Bitte eine Firma auswählen.'); + const {data} = await axios.post(`${window.TT_CONFIG.BASE_PATH}/WorkorderAdmin/setCivilEngineeringRequired`, { + workorderId: this.civilEngineeringData.workorder.id, companyId: this.civilEngineeringData.companyId + }); + if (data.success) { + window.notify('success', data.message); + this.$refs.table.$refs.table.refreshTable(); + this.civilEngineeringData = null; + } else window.notify('error', data.message || 'Zuweisung fehlgeschlagen.'); + }, + addToAssignList(row) { if (!this.workordersToAssign.includes(row.id)) this.workordersToAssign.push(row.id); }, + removeFromAssignList(row) { this.workordersToAssign = this.workordersToAssign.filter(id => id !== row.id); }, + getStatusColumn(status) { + const column = this.crudConfig.columns.find(c => c.key === 'status'); + return column.table.filterOptions.find(opt => opt.value === status) || {}; + }, + formatDate(timestamp, withTime = false) { + if (!timestamp) return '–'; + return window.moment.unix(timestamp).format(withTime ? 'DD.MM.YYYY HH:mm' : 'DD.MM.YYYY'); + }, + async getCompaniesForWorkorder(workorder) { + if (!workorder.tenantId || this.companiesByTenant[workorder.tenantId]) return; + this.companiesLoading = true; + try { + const {data} = await axios.get(`${window.TT_CONFIG.BASE_PATH}/WorkorderAdmin/getCompanies`, {params: {tenantId: workorder.tenantId}}); + this.$set(this.companiesByTenant, workorder.tenantId, data); + } catch (e) { + window.notify('error', 'Firmenliste konnte nicht geladen werden.'); + } finally { + this.companiesLoading = false; + } + }, + async startCompanyEdit(row) { + await this.getCompaniesForWorkorder(row); + this.editingWorkorderId = row.id; + }, + async assignCompany(workorder, companyId) { + if (!companyId) { this.editingWorkorderId = null; return; } + const {data} = await axios.post(`${window.TT_CONFIG.BASE_PATH}/WorkorderAdmin/assignWorkorder`, { workorderId: workorder.id, companyId: companyId }); + if (data.success) { + window.notify('success', data.message); + this.$refs.table.$refs.table.refreshTable(); + } else window.notify('error', data.message || 'Ein Fehler ist aufgetreten.'); + this.editingWorkorderId = null; + }, + openMassAssignModal(companyId) { + if (!companyId) return; + const companyName = this.companiesForMassAssign.find(c => c.value === companyId)?.text; + this.massAssignModalData = { companyId, companyName, deadline: null }; + }, + async massAssignCompanies() { + try { + const { companyId, deadline } = this.massAssignModalData; + const {data} = await axios.post(`${window.TT_CONFIG.BASE_PATH}/WorkorderAdmin/massAssignWorkorders`, { + companyId: companyId, workorderIds: this.workordersToAssign, deadlineDate: deadline + }); + if (data.success) { + window.notify('success', data.message); + this.workordersToAssign = []; + this.$refs.table.$refs.table.refreshTable(); + } else window.notify('error', data.message || 'Ein Fehler ist aufgetreten.'); + } catch (e) {} finally { + this.massAssignCompanyId = null; + this.massAssignModalData = null; + } + }, + async updateDeadline(workorder, newDate) { + if (!newDate) { this.editingDeadlineId = null; return; } + try { + const {data} = await axios.post(`${window.TT_CONFIG.BASE_PATH}/WorkorderAdmin/updateDeadline`, { workorderId: workorder.id, deadlineDate: newDate }); + if (data.success) { + window.notify('success', data.message); + this.$refs.table.$refs.table.refreshTable(); + } else window.notify('error', data.message || 'Ein Fehler ist aufgetreten.'); + } catch (e) {} finally { + this.editingDeadlineId = null; + } + }, + async setToProblemSolved() { + const { id, text } = this.problemSolvedModalData; + if (!text || !text.trim()) return window.notify('error', 'Bitte geben Sie einen Text ein.'); + + const {data} = await axios.post(`${window.TT_CONFIG.BASE_PATH}/WorkorderAdmin/setToProblemSolved`, { workorderId: id, text: text }); + if (data.success) { + window.notify('success', data.message); + this.$refs.table.$refs.table.refreshTable(); + this.problemSolvedModalData = null; + } else window.notify('error', data.message || 'Ein Fehler ist aufgetreten.'); + }, + startAdditionalInfoEdit(row) { + this.editingAdditionalInfoId = row.id; + this.tempAdditionalInfo = row.additionalInfo || ''; + this.$nextTick(() => this.$refs.editTextarea?.$el.querySelector('textarea').focus()); + }, + cancelEdit() { + this.editingAdditionalInfoId = null; + this.tempAdditionalInfo = ''; + }, + async updateAdditionalInfo(row) { + if (row.additionalInfo === this.tempAdditionalInfo) { this.cancelEdit(); return; } + try { + const {data} = await axios.post(`${window.TT_CONFIG.BASE_PATH}/WorkorderAdmin/updateAdditionalInfo`, { + workorderId: row.id, additionalInfo: this.tempAdditionalInfo + }); + if (data.success) { + window.notify('success', data.message); + row.additionalInfo = data.newInfo; // Update local data + } else window.notify('error', data.message || 'Update fehlgeschlagen.'); + } catch (e) { + } finally { + this.cancelEdit(); + } + }, + async cancelWorkorder() { + const { id, reason } = this.cancelWorkorderModalData; + const {data} = await axios.post(`${window.TT_CONFIG.BASE_PATH}/WorkorderAdmin/cancelWorkorder`, { workorderId: id, reason: reason }); + if (data.success) { + window.notify('success', data.message); + this.$refs.table.$refs.table.refreshTable(); + this.cancelWorkorderModalData = null; + } else window.notify('error', data.message || 'Stornierung fehlgeschlagen.'); + } + }, + watch: { + workordersToAssign: { + async handler(newVal) { + if (newVal.length === 0) return; + const rows = this.$refs.table?.$refs.table.rows; + if (!rows) return; + + const firstWorkorder = rows.find(r => r.id === newVal[0]); + if (!firstWorkorder) return; + + const firstTenantId = firstWorkorder.tenantId; + const allSameTenant = newVal.every(id => { + const wo = rows.find(r => r.id === id); + return wo && wo.tenantId === firstTenantId; + }); + + if (!allSameTenant) { + window.notify('error', 'Massen-Zuweisung nur für Aufträge des gleichen Mandanten möglich.'); + this.workordersToAssign.pop(); + return; + } + await this.getCompaniesForWorkorder(firstWorkorder); + }, + deep: true + } + } +}); \ No newline at end of file diff --git a/public/js/pages/RMLWorkorderAdmin/RMLWorkorderAdmin.css b/public/js/pages/WorkorderBase/WorkorderBase.css similarity index 100% rename from public/js/pages/RMLWorkorderAdmin/RMLWorkorderAdmin.css rename to public/js/pages/WorkorderBase/WorkorderBase.css diff --git a/public/js/pages/WorkorderBase/WorkorderBase.js b/public/js/pages/WorkorderBase/WorkorderBase.js new file mode 100644 index 000000000..6f1b957f5 --- /dev/null +++ b/public/js/pages/WorkorderBase/WorkorderBase.js @@ -0,0 +1,471 @@ +// WorkorderCommon.js + +// A simple component to display a status light based on a deadline. +Vue.component('traffic-light', { + props: ['deadline', 'status'], + computed: { + lightInfo() { + const deadlineDate = moment.unix(this.deadline); + const daysLeft = deadlineDate.diff(moment(), 'days'); + + if (['completed', 'new', 'cancelled'].includes(this.status)) return { color: '#cccccc', title: 'Status irrelevant für Dringlichkeit' }; + if (!deadlineDate.isValid()) return { color: '#cccccc', title: 'Keine Deadline gesetzt' }; + if (deadlineDate.isBefore(moment())) return { color: '#dc3545', title: 'Deadline überschritten' }; + if (daysLeft <= 7) return { color: '#dc3545', title: 'Dringend: Weniger als 1 Woche' }; + if (daysLeft <= 21) return { color: '#ffc107', title: 'Mittel: Weniger als 3 Wochen' }; + return { color: '#28a745', title: 'Im Plan: Mehr als 3 Wochen' }; + } + }, + template: `` +}); + +// A manager for civil engineering tasks, used when a workorder requires it. +Vue.component('civil-engineering-manager', { + props: ['workorderId', 'isAdmin'], + template: ` +
+
+
+
+
+
+
Tiefbau-Arbeiten
+

Schließen Sie den Tiefbau-Auftrag ab. Laden Sie Dokumente hoch, falls erforderlich.

+
+ + + Bitte laden Sie mindestens ein Dokument hoch, um den Auftrag abzuschließen. + +
+
+
+
+
+
+
Dokument hochladen
+ +
+ +
+ +
+
+ +
+
+ + +
Für diesen Auftraggeber ist keine Dokumentation für Tiefbau-Arbeiten erforderlich.
+
+
+ + Möchten Sie diese Tiefbau-Arbeiten wirklich als abgeschlossen markieren? + +
+ `, + data: () => ({ + loading: true, uploading: false, completing: false, docsRequired: false, + uploadedFiles: [], uploadData: { files: [], description: '' }, showCompleteModal: false + }), + computed: { + canComplete() { + return !this.docsRequired || this.uploadedFiles.length > 0; + } + }, + methods: { + async fetchInitialData() { + this.loading = true; + try { + const configRes = await axios.get(`${window.TT_CONFIG.BASE_PATH}/WorkorderCompany/getTenantConfig`, { params: { workorderId: this.workorderId } }); + this.docsRequired = configRes.data.civilEngineeringDocsRequired || false; + + if(this.docsRequired) { + const docRes = await axios.get(`${window.TT_CONFIG.BASE_PATH}/${this.isAdmin ? 'WorkorderAdmin' : 'WorkorderCompany'}/getDocumentation`, { params: { workorderId: this.workorderId } }); + this.uploadedFiles = docRes.data.docs || []; + } + } catch (e) { + window.notify('error', 'Konfiguration konnte nicht geladen werden.'); + console.error(e); + } finally { + this.loading = false; + } + }, + handleFileUpload(event) { this.uploadData.files = event.target.files; }, + async uploadFiles() { + if (!this.uploadData.files?.length) return window.notify('error', 'Bitte eine oder mehrere Dateien auswählen.'); + this.uploading = true; + const formData = new FormData(); + formData.append('workorderId', this.workorderId); + formData.append('documentType', 'civil_engineering_photo'); + formData.append('description', this.uploadData.description); + for (const file of this.uploadData.files) formData.append('files[]', file); + try { + const {data} = await axios.post(`${window.TT_CONFIG.BASE_PATH}/WorkorderCompany/uploadDocumentation`, formData); + if (data.success) { + window.notify('success', data.message); + this.$refs.fileInput.value = ''; + this.uploadData = { files: [], description: '' }; + await this.fetchInitialData(); + } else window.notify('error', data.error || 'Upload fehlgeschlagen.'); + } catch (e) { + window.notify('error', 'Ein Netzwerkfehler ist beim Upload aufgetreten.'); + } + this.uploading = false; + }, + async deleteDocumentation(file) { + try { + const {data} = await axios.post(`${window.TT_CONFIG.BASE_PATH}/WorkorderCompany/deleteDocumentation`, {id: file.id}); + if (data.success) { + window.notify('success', data.message); + await this.fetchInitialData(); + } else window.notify('error', data.message || 'Löschen fehlgeschlagen.'); + } catch (e) { + window.notify('error', 'Netzwerkfehler beim Löschen.'); + } + }, + async completeTask() { + this.completing = true; + try { + const {data} = await axios.post(`${window.TT_CONFIG.BASE_PATH}/WorkorderCompany/completeCivilEngineering`, { workorderId: this.workorderId }); + if (data.success) { + window.notify('success', data.message); + this.$emit('workorder-completed'); + this.showCompleteModal = false; + } else { + window.notify('error', data.message || 'Abschluss fehlgeschlagen.'); + } + } catch (e) { + window.notify('error', 'Ein Netzwerkfehler ist aufgetreten.'); + } finally { + this.completing = false; + } + } + }, + async mounted() { + await this.fetchInitialData(); + } +}); + + +/** + * Unified component for viewing and managing Workorder Details, Documentation, and Journals. + * Adapts its UI and functionality based on the isAdmin prop. + */ +Vue.component('workorder-details-manager', { + props: { + workorderId: { type: String, required: true }, + isAdmin: { type: Boolean, default: false } + }, + template: ` +
+
+
+
+
+
+
Benötigte Dokumente
+
    +
  • + + {{ docType.text }} +
  • +
+
+ + + Bitte laden Sie alle benötigten Dokumente hoch, um den Auftrag abzuschließen. + +
+ Auftrag bereits abgeschlossen oder storniert. Keine Aktionen mehr möglich. +
+
+
+ +
+
+
Prüfung & Freigabe
+

Prüfen Sie, ob alle erforderlichen Dokumente vorhanden und korrekt sind.

+
    +
  • + + {{ docType.text }} +
  • +
+
+
+ +
+
+ +
+
Journal
+
+
    +
  • + {{ formatDate(log.create) }} ({{ log.createByName }}): +
    {{ log.text }}
    +
  • +
+
Keine Journaleinträge.
+
+ +
+
+ +
+
+
+
Neues Dokument hochladen
+ + +
+
+
+ +
+
+ +
+
Korrektur anfordern
+
+

Die ausgewählten Dokumente werden als fehlerhaft markiert. Bitte geben Sie einen Grund an.

+ + +
+
+ + + + + +
+
Eingriff benötigt
+
+

Falls ein Problem auftritt, das ein Eingreifen erfordert, melden Sie es hier.

+ +
+
+
+
+ + + +
+ + +
+
+ + Möchten Sie diesen Auftrag wirklich abschließen und zur Prüfung einreichen? + + + Soll die Dokumentation für diesen Arbeitsauftrag wirklich akzeptiert und der Auftrag abgeschlossen werden? + +
`, + data: () => ({ + loading: true, loadingConfig: true, workorder: null, docs: [], journals: [], tenantDocTypes: null, + newJournalMessage: '', addingJournalEntry: false, + // Company state + uploading: false, completing: false, showCompleteModal: false, + uploadData: { files: [], documentType: 'photo_hup_mounted', description: '' }, + interventionData: null, interventionTypes: [ + {value: 'stuck', text: 'Ab X Laufmeter stecken geblieben'}, + {value: 'stuck_fcp', text: 'Vom FCP nach HÜP nach X Laufmetern stecken geblieben'}, + {value: 'stuck_hup', text: 'Vom HÜP nach FCP nach X Laufmetern stecken geblieben'}, + {value: 'no_air', text: 'Keine Luftverbindung'}, {value: 'other', text: 'Sonstiges'} + ], + // Admin state + selectedDocs: [], correctionText: '', correctionLoading: false, showAcceptModal: false, + }), + computed: { + isReadOnly() { return ['documented', 'completed', 'cancelled'].includes(this.workorder?.status); }, + requiredDocTypes() { + if (this.tenantDocTypes) return this.tenantDocTypes; + // Default list as a fallback + return [{value: 'photo_hup_mounted', text: 'Foto vom montierten HÜP'}, {value: 'photo_hup_open', text: 'Foto von dem offenen HÜP'}, {value: 'photo_splice_cassette_hup', text: 'Foto der Spleißkassette – HÜP'}, {value: 'photo_splice_cassette_fcp', text: 'Foto der Spleißkassette - FCP'}, {value: 'photo_hup_closed_stickers', text: 'Foto vom geschlossenen HÜP mit allen Aufklebern'}, {value: 'photo_fcp_labeled', text: 'Foto vom FCP beschriftet'}, {value: 'photo_patch_position_osp', text: 'Foto der Patch-Position - OSP-Seite'}, {value: 'photo_patch_position_anb', text: 'Foto der Patch-Position - ANB-Seite'}, {value: 'measurement_protocol_otdr', text: 'ODTR – Messung (1310nm & 1550nm)'}]; + }, + allDocTypes() { return [...this.requiredDocTypes, {value: 'other', text: 'Sonstiges Dokument (optional)'}]; }, + canComplete() { return this.requiredDocTypes.every(docType => this.isUploaded(docType.value)); }, + docsWithStatus() { + if (!this.journals?.length) return this.docs; + const correctionJournal = [...this.journals].sort((a, b) => b.create - a.create).find(j => j.statusChange?.includes('correction_requested')); + if (!correctionJournal?.fileIds) return this.docs; + try { + const incorrectFileIds = JSON.parse(correctionJournal.fileIds); + if (!Array.isArray(incorrectFileIds)) return this.docs; + return this.docs.map(doc => incorrectFileIds.includes(doc.id) ? { ...doc, class: 'border border-danger' } : doc); + } catch (e) { return this.docs; } + } + }, + methods: { + formatDate(timestamp) { return timestamp ? window.moment.unix(timestamp).format('DD.MM.YYYY HH:mm') : '–'; }, + // FIX: Added a guard to prevent calling .some() on an undefined value + isUploaded(docType) { + return Array.isArray(this.docs) && this.docs.some(doc => doc.documentType === docType); + }, + async fetchData() { + this.loading = true; + try { + const [workorderRes, docsJournalsRes] = await Promise.all([ + axios.get(`${window.TT_CONFIG.BASE_PATH}/WorkorderCompany/getWorkorderById`, {params: {id: this.workorderId}}), + axios.get(`${window.TT_CONFIG.BASE_PATH}/${this.isAdmin ? 'WorkorderAdmin' : 'WorkorderCompany'}/getDocumentation`, {params: {workorderId: this.workorderId}}) + ]); + this.workorder = workorderRes.data; + // FIX: Ensure docs and journals are always arrays + this.docs = docsJournalsRes.data.docs || []; + this.journals = docsJournalsRes.data.journals || []; + } catch (e) { + window.notify('error', 'Details konnten nicht geladen werden.'); + this.docs = []; // Ensure it's an array on error + this.journals = []; + } finally { + this.loading = false; + } + }, + async loadTenantConfig() { + this.loadingConfig = true; + try { + const {data} = await axios.get(`${window.TT_CONFIG.BASE_PATH}/WorkorderCompany/getTenantConfig`, {params: {workorderId: this.workorderId}}); + if (data.success) this.tenantDocTypes = data.documentationTypes; + } catch (e) { console.error("Mandantenkonfiguration nicht geladen", e); } + finally { this.loadingConfig = false; } + }, + async addJournalEntry() { + if (!this.newJournalMessage.trim()) return; + this.addingJournalEntry = true; + try { + const {data} = await axios.post(`${window.TT_CONFIG.BASE_PATH}/${this.isAdmin ? 'WorkorderAdmin' : 'WorkorderCompany'}/addJournal`, { workorderId: this.workorderId, text: this.newJournalMessage }); + if (data.success) { + this.newJournalMessage = ''; + this.journals = data.journals; + } else window.notify('error', data.message); + } catch (e) { window.notify('error', 'Netzwerkfehler'); } + finally { this.addingJournalEntry = false; } + }, + // Company Methods + handleFileUpload(event) { this.uploadData.files = event.target.files; }, + async uploadFiles() { + if (!this.uploadData.files?.length) return window.notify('error', 'Bitte Dateien auswählen.'); + this.uploading = true; + const formData = new FormData(); + formData.append('workorderId', this.workorderId); + formData.append('documentType', this.uploadData.documentType); + formData.append('description', this.uploadData.description); + for (const file of this.uploadData.files) formData.append('files[]', file); + try { + const {data} = await axios.post(`${window.TT_CONFIG.BASE_PATH}/WorkorderCompany/uploadDocumentation`, formData); + if (data.success) { + window.notify('success', data.message); + this.$refs.fileInput.value = ''; + this.uploadData.files = []; + await this.fetchData(); + this.$emit('workorder-updated'); + } else window.notify('error', data.error); + } catch (e) { window.notify('error', 'Upload-Fehler'); } + this.uploading = false; + }, + async deleteDocumentation(file) { + try { + const {data} = await axios.post(`${window.TT_CONFIG.BASE_PATH}/WorkorderCompany/deleteDocumentation`, {id: file.id}); + if (data.success) { + window.notify('success', data.message); + await this.fetchData(); + } else window.notify('error', data.message); + } catch (e) { window.notify('error', 'Netzwerkfehler'); } + }, + async updateDocumentation(file) { + try { + const {data} = await axios.post(`${window.TT_CONFIG.BASE_PATH}/WorkorderCompany/updateDocumentation`, { id: file.id, documentType: file.documentType }); + if (data.success) { + window.notify('success', data.message); + await this.fetchData(); + } else window.notify('error', data.message); + } catch (e) { window.notify('error', 'Netzwerkfehler'); } + }, + openInterventionModal() { + this.interventionData = { types: [], details: { stuck: {}, stuck_fcp: {}, stuck_hup: {}, other: {} } }; + }, + async requestIntervention() { + const { types, details } = this.interventionData; + if (types.length === 0) return window.notify('error', 'Bitte Problem auswählen.'); + let journalParts = []; + for (const type of types.sort()) { + const problemText = this.interventionTypes.find(o => o.value === type)?.text || type; + if (['stuck', 'stuck_fcp', 'stuck_hup'].includes(type)) { + if (!details[type]?.distance > 0) return window.notify('error', `Bitte DisWtanz für "${problemText}" eingeben.`); + journalParts.push(problemText.replace('X', details[type].distance)); + } else if (type === 'other') { + if (!details.other?.reason?.trim()) return window.notify('error', `Bitte Grund für "Sonstiges" angeben.`); + journalParts.push(`Sonstiges: ${details.other.reason.trim()}`); + } else journalParts.push(problemText); + } + try { + const {data} = await axios.post(`${window.TT_CONFIG.BASE_PATH}/WorkorderCompany/requestIntervention`, { workorderId: this.workorderId, journalText: journalParts.join('\\n') }); + if (data.success) { + window.notify('success', data.message); + this.interventionData = null; + this.$emit('workorder-completed'); + } else window.notify('error', data.message); + } catch (e) { window.notify('error', 'Netzwerkfehler'); } + }, + async completeWorkorder() { + this.completing = true; + try { + const {data} = await axios.post(`${window.TT_CONFIG.BASE_PATH}/WorkorderCompany/completeWorkorder`, {workorderId: this.workorderId}); + if (data.success) { + window.notify('success', data.message); + this.$emit('workorder-completed'); + this.showCompleteModal = false; + } else window.notify('error', data.message); + } catch (e) { window.notify('error', 'Netzwerkfehler'); } + this.completing = false; + }, + // Admin Methods + async requestCorrection() { + if (!this.correctionText) return window.notify('error', 'Bitte geben Sie einen Grund an.'); + if (this.selectedDocs.length === 0) return window.notify('error', 'Bitte Dokumente für die Korrektur auswählen.'); + this.correctionLoading = true; + try { + const {data} = await axios.post(`${window.TT_CONFIG.BASE_PATH}/WorkorderAdmin/requestCorrection`, { + workorderId: this.workorderId, text: this.correctionText, fileIds: this.selectedDocs + }); + if (data.success) { + window.notify('success', data.message); + this.correctionText = ''; + this.selectedDocs = []; + await this.fetchData(); + this.$emit('workorder-completed'); + } else window.notify('error', data.message); + } catch (e) { window.notify('error', 'Netzwerkfehler'); } + this.correctionLoading = false; + }, + acceptDocumentation() { + this.$emit('accept-documentation', this.workorderId); + this.showAcceptModal = false; + }, + getInterventionLabel(type) { return this.interventionTypes.find(t => t.value === type)?.text || type; }, + }, + async mounted() { + await this.loadTenantConfig(); + await this.fetchData(); + } +}); \ No newline at end of file diff --git a/public/js/pages/WorkorderCompany/WorkorderCompany.js b/public/js/pages/WorkorderCompany/WorkorderCompany.js new file mode 100644 index 000000000..50b64058f --- /dev/null +++ b/public/js/pages/WorkorderCompany/WorkorderCompany.js @@ -0,0 +1,176 @@ +// WorkorderCompany.js +Vue.component('workorder-company', { + template: ` +
+ + + + + + + + + + + + + + + + + +

Auftrag: #{{ rescheduleModalData.workorder.id }}

+ + +
+
+ `, + data() { + return { + window, + rescheduleModalData: null, + editingAdditionalInfoId: null, + tempAdditionalInfo: '', + crudConfig: { + ...window.TT_CONFIG.CRUD_CONFIG, + expandable: true, + customRowClass: (row) => { + if (['completed', 'new', 'cancelled'].includes(row.status)) return 'tt-rml-workorder-irrelevant'; + if (['correction_requested', 'intervention_required'].includes(row.status)) return 'tt-rml-workorder-high'; + const deadlineDate = moment.unix(row.deadlineDate); + if (!deadlineDate.isValid()) return 'tt-rml-workorder-irrelevant'; + const daysLeft = deadlineDate.diff(moment(), 'days'); + if (daysLeft <= 7) return 'tt-rml-workorder-urgent'; + if (daysLeft <= 21) return 'tt-rml-workorder-medium'; + return 'tt-rml-workorder-ontrack'; + }, + additionalActions: [] + } + } + }, + methods: { + getStatusColumn(status) { + const column = this.crudConfig.columns.find(c => c.key === 'status'); + return column.table.filterOptions.find(opt => opt.value === status) || {}; + }, + formatDate(timestamp, withTime = false) { + if (!timestamp) return '–'; + return window.moment.unix(timestamp).format(withTime ? 'DD.MM.YYYY HH:mm' : 'DD.MM.YYYY'); + }, + async setAppointment(workorder, date) { + if (!date) return; + if (moment.unix(date).hour() >= 23 || moment.unix(date).hour() < 1) { + this.$refs.table.$refs.table.refreshTable(); + return window.notify('error', 'Bitte Uhrzeit angeben!'); + } + const {data} = await axios.post(`${window.TT_CONFIG.BASE_PATH}/WorkorderCompany/scheduleAppointment`, { + workorderId: workorder.id, appointmentDate: date + }); + if (data.success) { + window.notify('success', data.message); + this.$refs.table.$refs.table.refreshTable(); + } else window.notify('error', data.message || 'Ein Fehler ist aufgetreten.'); + }, + openRescheduleModal(row) { + this.rescheduleModalData = { workorder: row, newDate: row.appointmentDate, reason: '' }; + }, + async rescheduleAppointment() { + const { workorder, newDate, reason } = this.rescheduleModalData; + if (!newDate || !reason) return window.notify('error', 'Bitte geben Sie ein neues Datum und einen Grund an.'); + if (moment.unix(newDate).hour() >= 23 || moment.unix(newDate).hour() < 1) return window.notify('error', 'Bitte Uhrzeit angeben!'); + const {data} = await axios.post(`${window.TT_CONFIG.BASE_PATH}/WorkorderCompany/rescheduleAppointment`, { + workorderId: workorder.id, appointmentDate: newDate, reason: reason + }); + if (data.success) { + window.notify('success', data.message); + this.$refs.table.$refs.table.refreshTable(); + this.rescheduleModalData = null; + } else window.notify('error', data.message || 'Ein Fehler ist aufgetreten.'); + }, + startAdditionalInfoEdit(row) { + this.editingAdditionalInfoId = row.id; + this.tempAdditionalInfo = row.additionalInfo || ''; + this.$nextTick(() => this.$refs.editTextarea?.$el.querySelector('textarea').focus()); + }, + cancelEdit() { + this.editingAdditionalInfoId = null; + this.tempAdditionalInfo = ''; + }, + async updateAdditionalInfo(row) { + if (row.additionalInfo === this.tempAdditionalInfo) { + this.cancelEdit(); + return; + } + const {data} = await axios.post(`${window.TT_CONFIG.BASE_PATH}/WorkorderCompany/updateAdditionalInfo`, { + workorderId: row.id, additionalInfo: this.tempAdditionalInfo + }); + if (data.success) { + window.notify('success', data.message); + row.additionalInfo = data.newInfo; + } else window.notify('error', data.message || 'Update fehlgeschlagen.'); + this.cancelEdit(); + }, + } +}); \ No newline at end of file diff --git a/public/plugins/vue/tt-components/tt-modal.js b/public/plugins/vue/tt-components/tt-modal.js index b37625c99..6a372e1ab 100644 --- a/public/plugins/vue/tt-components/tt-modal.js +++ b/public/plugins/vue/tt-components/tt-modal.js @@ -1,6 +1,6 @@ Vue.component('tt-modal', { props: { - show: {type: Boolean, default: false}, + show: {type: [Boolean, Object], default: false}, title: {type: String, default: 'Überschrift'}, delete: {type: Boolean, default: true}, deleteText: {type: String, default: 'Löschen'}, diff --git a/public/plugins/vue/tt-components/tt-number-range.js b/public/plugins/vue/tt-components/tt-number-range.js index 9b7065f55..32367747a 100644 --- a/public/plugins/vue/tt-components/tt-number-range.js +++ b/public/plugins/vue/tt-components/tt-number-range.js @@ -10,6 +10,12 @@ Vue.component('tt-number-range', { }; }, watch: { value(val) { + if (typeof val === 'undefined' || val === null || val === '') { + this.inputValueFrom = ''; + this.inputValueTo = ''; + return; + } + if (this.returnText !== true) { this.inputValueFrom = val.from; this.inputValueTo = val.to; From 06760e40f6362a9abf6308fe578f3a113863b729 Mon Sep 17 00:00:00 2001 From: Luca Haid Date: Tue, 2 Sep 2025 08:39:24 +0000 Subject: [PATCH 024/103] Update 20250901410000_workorder_rename.php --- .../20250901410000_workorder_rename.php | 20 +++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/db/migrations/20250901410000_workorder_rename.php b/db/migrations/20250901410000_workorder_rename.php index 339448e8e..0f10df417 100644 --- a/db/migrations/20250901410000_workorder_rename.php +++ b/db/migrations/20250901410000_workorder_rename.php @@ -8,11 +8,11 @@ final class WorkorderRename extends AbstractMigration { public function up(): void { - $this->table('RMLWorkorder')->rename('Workorder'); - $this->table('RMLWorkorderCompany')->rename('WorkorderCompany'); - $this->table('RMLWorkorderDocumentation')->rename('WorkorderDocumentation'); - $this->table('RMLWorkorderJournal')->rename('WorkorderJournal'); - $this->table('RMLWorkorderTenantConfig')->rename('WorkorderTenantConfig'); + $this->table('RMLWorkorder')->rename('Workorder')->save(); + $this->table('RMLWorkorderCompany')->rename('WorkorderCompany')->save(); + $this->table('RMLWorkorderDocumentation')->rename('WorkorderDocumentation')->save(); + $this->table('RMLWorkorderJournal')->rename('WorkorderJournal')->save(); + $this->table('RMLWorkorderTenantConfig')->rename('WorkorderTenantConfig')->save(); $workorderTable = $this->table('Workorder'); $workorderTable->addColumn('civilEngineeringCompanyId', 'integer', ['null' => true, 'default' => null, 'after' => 'companyId', 'comment' => 'Company assigned for civil engineering task']) @@ -72,10 +72,10 @@ final class WorkorderRename extends AbstractMigration ->removeIndexByName('originalCompanyId_idx') ->save(); - $this->table('Workorder')->rename('RMLWorkorder'); - $this->table('WorkorderCompany')->rename('RMLWorkorderCompany'); - $this->table('WorkorderDocumentation')->rename('RMLWorkorderDocumentation'); - $this->table('WorkorderJournal')->rename('RMLWorkorderJournal'); - $this->table('WorkorderTenantConfig')->rename('RMLWorkorderTenantConfig'); + $this->table('Workorder')->rename('RMLWorkorder')->save(); + $this->table('WorkorderCompany')->rename('RMLWorkorderCompany')->save(); + $this->table('WorkorderDocumentation')->rename('RMLWorkorderDocumentation')->save(); + $this->table('WorkorderJournal')->rename('RMLWorkorderJournal')->save(); + $this->table('WorkorderTenantConfig')->rename('RMLWorkorderTenantConfig')->save(); } } From 38f8eb55e7020600b7ef71f4abcb8495b9ce6e70 Mon Sep 17 00:00:00 2001 From: Luca Haid Date: Tue, 2 Sep 2025 08:41:00 +0000 Subject: [PATCH 025/103] Update WorkorderTenantConfigModel.php --- .../WorkorderTenantConfig/WorkorderTenantConfigModel.php | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/application/WorkorderTenantConfig/WorkorderTenantConfigModel.php b/application/WorkorderTenantConfig/WorkorderTenantConfigModel.php index 0c3d8f922..c813a3265 100644 --- a/application/WorkorderTenantConfig/WorkorderTenantConfigModel.php +++ b/application/WorkorderTenantConfig/WorkorderTenantConfigModel.php @@ -25,7 +25,8 @@ class WorkorderTenantConfigModel extends TTCrudBaseModel { JOIN `$dbName`.`Preorder` p ON pc.id = p.preordercampaign_id WHERE p.id = '$preorderId' LIMIT 1"; - $row = $db->query($sql)?->fetch_assoc(); + $result = $db->query($sql); + $row = $result ? $result->fetch_assoc() : null; return $row ? new self($row) : null; - }} \ No newline at end of file + }} From 44afda47bca86d5cffe602e950d36e3bb1739161 Mon Sep 17 00:00:00 2001 From: Daniel Spitzer Date: Tue, 2 Sep 2025 12:16:04 +0200 Subject: [PATCH 026/103] PopRack Erweiterung LC/SC mit 96 Fasern --- Layout/default/Pop/Detail.php | 1 + 1 file changed, 1 insertion(+) diff --git a/Layout/default/Pop/Detail.php b/Layout/default/Pop/Detail.php index 15e183b30..e495e2868 100644 --- a/Layout/default/Pop/Detail.php +++ b/Layout/default/Pop/Detail.php @@ -401,6 +401,7 @@ if (!empty(trim($pops->vlan_ipv6)))