From 74541d765dcf861cc91a3396a3c6fe8ab711c7de Mon Sep 17 00:00:00 2001 From: Dennis Eichhorn Date: Tue, 2 Jan 2024 23:34:17 +0000 Subject: [PATCH] update --- Admin/Install/Media.install.json | 30 + Admin/Install/Media.php | 43 + Admin/Install/Navigation.install.json | 30 + Admin/Install/assettype.json | 1610 +++++++++++++++++ Admin/Install/attributes.json | 178 ++ Admin/Install/db.json | 400 ++++ Admin/Installer.php | 299 +++ Admin/Routes/Web/Backend.php | 82 +- Controller/ApiAssetAttributeController.php | 524 ++++++ Controller/ApiAssetController.php | 634 +++++++ Controller/ApiAssetTypeController.php | 405 +++++ Controller/BackendController.php | 140 +- Docs/Dev/en/SUMMARY.md | 3 + Docs/Dev/en/structure.md | 5 + Docs/Dev/img/er.png | Bin 0 -> 53362 bytes Models/Asset.php | 79 + Models/AssetMapper.php | 116 ++ Models/AssetStatus.php | 38 + Models/AssetType.php | 32 + Models/AssetTypeL11nMapper.php | 69 + Models/AssetTypeMapper.php | 84 + Models/Attribute/AssetAttributeMapper.php | 86 + .../AssetAttributeTypeL11nMapper.php | 69 + Models/Attribute/AssetAttributeTypeMapper.php | 94 + .../AssetAttributeValueL11nMapper.php | 69 + .../Attribute/AssetAttributeValueMapper.php | 89 + Models/NullAsset.php | 47 + Models/NullAssetType.php | 47 + ...issionState.php => PermissionCategory.php} | 10 +- Theme/Backend/Lang/Navigation.de.lang.php | 1 + Theme/Backend/Lang/Navigation.en.lang.php | 1 + Theme/Backend/Lang/en.lang.php | 9 + Theme/Backend/asset-list.tpl.php | 87 +- Theme/Backend/asset-profile.tpl.php | 227 +++ Theme/Backend/attribute-type-list.tpl.php | 71 + info.json | 7 +- 36 files changed, 5706 insertions(+), 9 deletions(-) create mode 100644 Admin/Install/Media.install.json create mode 100644 Admin/Install/Media.php create mode 100644 Admin/Install/assettype.json create mode 100644 Admin/Install/attributes.json create mode 100644 Admin/Install/db.json create mode 100644 Controller/ApiAssetAttributeController.php create mode 100644 Controller/ApiAssetController.php create mode 100644 Controller/ApiAssetTypeController.php create mode 100644 Docs/Dev/en/SUMMARY.md create mode 100644 Docs/Dev/en/structure.md create mode 100644 Docs/Dev/img/er.png create mode 100644 Models/Asset.php create mode 100644 Models/AssetMapper.php create mode 100644 Models/AssetStatus.php create mode 100644 Models/AssetType.php create mode 100644 Models/AssetTypeL11nMapper.php create mode 100644 Models/AssetTypeMapper.php create mode 100644 Models/Attribute/AssetAttributeMapper.php create mode 100644 Models/Attribute/AssetAttributeTypeL11nMapper.php create mode 100644 Models/Attribute/AssetAttributeTypeMapper.php create mode 100644 Models/Attribute/AssetAttributeValueL11nMapper.php create mode 100644 Models/Attribute/AssetAttributeValueMapper.php create mode 100644 Models/NullAsset.php create mode 100644 Models/NullAssetType.php rename Models/{PermissionState.php => PermissionCategory.php} (71%) create mode 100644 Theme/Backend/asset-profile.tpl.php create mode 100644 Theme/Backend/attribute-type-list.tpl.php diff --git a/Admin/Install/Media.install.json b/Admin/Install/Media.install.json new file mode 100644 index 0000000..3b1bf61 --- /dev/null +++ b/Admin/Install/Media.install.json @@ -0,0 +1,30 @@ +[ + { + "type": "type", + "name": "equipment_profile_image", + "l11n": [ + { + "title": "Profile image", + "lang": "en" + }, + { + "title": "Profilbild", + "lang": "de" + } + ] + }, + { + "type": "collection", + "create_directory": true, + "name": "AssetManagement", + "virtualPath": "/Modules", + "user": 1 + }, + { + "type": "collection", + "create_directory": true, + "name": "Asset", + "virtualPath": "/Modules/AssetManagement", + "user": 1 + } +] \ No newline at end of file diff --git a/Admin/Install/Media.php b/Admin/Install/Media.php new file mode 100644 index 0000000..9839d76 --- /dev/null +++ b/Admin/Install/Media.php @@ -0,0 +1,43 @@ + __DIR__ . '/Media.install.json']); + } +} diff --git a/Admin/Install/Navigation.install.json b/Admin/Install/Navigation.install.json index 33b959a..2ebedd9 100644 --- a/Admin/Install/Navigation.install.json +++ b/Admin/Install/Navigation.install.json @@ -27,6 +27,36 @@ "permission": { "permission": 2, "type": null, "element": null }, "parent": 1006601001, "children": [] + }, + { + "id": 1006603001, + "pid": "/accounting/asset", + "type": 3, + "subtype": 1, + "name": "Table", + "uri": "{/base}/accounting/asset/table", + "target": "self", + "icon": null, + "order": 1, + "from": "AssetManagement", + "permission": { "permission": 2, "type": null, "element": null }, + "parent": 1006601001, + "children": [] + }, + { + "id": 1006604001, + "pid": "/accounting/asset", + "type": 3, + "subtype": 1, + "name": "Entries", + "uri": "{/base}/accounting/asset/entry/list", + "target": "self", + "icon": null, + "order": 15, + "from": "AssetManagement", + "permission": { "permission": 2, "type": null, "element": null }, + "parent": 1006601001, + "children": [] } ] } diff --git a/Admin/Install/assettype.json b/Admin/Install/assettype.json new file mode 100644 index 0000000..b7503f8 --- /dev/null +++ b/Admin/Install/assettype.json @@ -0,0 +1,1610 @@ +[ + { + "industry": 0, + "duration": 168, + "l11n": { + "en": "Halls in lightweight construction", + "de": "Hallen in Leichtbauweise" + } + }, + { + "industry": 0, + "duration": 240, + "l11n": { + "en": "Tennis halls, squash courts, etc.", + "de": "Tennishallen, Squashhallen u.ä." + } + }, + { + "industry": 0, + "duration": 120, + "l11n": { + "en": "Air domes", + "de": "Traglufthallen" + } + }, + { + "industry": 0, + "duration": 240, + "l11n": { + "en": "Cold storage halls", + "de": "Kühlhallen" + } + }, + { + "industry": 0, + "duration": 192, + "l11n": { + "en": "Barracks and sheds", + "de": "Baracken und Schuppen" + } + }, + { + "industry": 0, + "duration": 96, + "l11n": { + "en": "Construction huts", + "de": "Baubuden" + } + }, + { + "industry": 0, + "duration": 96, + "l11n": { + "en": "Beer tents", + "de": "Bierzelte" + } + }, + { + "industry": 0, + "duration": 240, + "l11n": { + "en": "Pump houses, transformer station houses and switch houses", + "de": "Pumpenhäuser, Trafostationshäuser und Schalthäuser" + } + }, + { + "industry": 0, + "duration": 396, + "l11n": { + "en": "Concrete silo buildings", + "de": "Silobauten aus Beton" + } + }, + { + "industry": 0, + "duration": 300, + "l11n": { + "en": "Steel silo buildings", + "de": "Silobauten aus Stahl" + } + }, + { + "industry": 0, + "duration": 204, + "l11n": { + "en": "Plastic silo buildings", + "de": "Silobauten aus Kunststoff" + } + }, + { + "industry": 0, + "duration": 396, + "l11n": { + "en": "Masonry or concrete chimneys", + "de": "Schornsteine aus Mauerwerk oder Beton" + } + }, + { + "industry": 0, + "duration": 120, + "l11n": { + "en": "Metal chimneys", + "de": "Schornsteine aus Metall" + } + }, + { + "industry": 0, + "duration": 300, + "l11n": { + "en": "Loading ramps", + "de": "Laderampen" + } + }, + { + "industry": 0, + "duration": 228, + "l11n": { + "en": "Roadways, parking lots and yard pavements with packing layer", + "de": "Fahrbahnen, Parkplätze und Hofbefestigungen mit Packlage" + } + }, + { + "industry": 0, + "duration": 108, + "l11n": { + "en": "Roadways, parking lots and yard pavements in gravel, crushed stone, slag", + "de": "Fahrbahnen, Parkplätze und Hofbefestigungen in Kies, Schotter, Schlacken" + } + }, + { + "industry": 0, + "duration": 396, + "l11n": { + "en": "Road and path bridges made of steel and concrete", + "de": "Strafen- und Wegebrücken aus Stahl und Beton" + } + }, + { + "industry": 0, + "duration": 180, + "l11n": { + "en": "Road and path bridges made of wood", + "de": "Strafen- und Wegebrücken aus Holz" + } + }, + { + "industry": 0, + "duration": 60, + "l11n": { + "en": "Wooden fencing", + "de": "Umzäunungen aus Holz" + } + }, + { + "industry": 0, + "duration": 204, + "l11n": { + "en": "Fencing Other", + "de": "Umzäunungen Sonstige" + } + }, + { + "industry": 0, + "duration": 228, + "l11n": { + "en": "Outdoor lighting, street lighting", + "de": "Aufenbeleuchtung, Strafenbeleuchtung" + } + }, + { + "industry": 0, + "duration": 120, + "l11n": { + "en": "Orientation systems, sign bridges", + "de": "Orientierungssysteme, Schilderbrücken" + } + }, + { + "industry": 0, + "duration": 240, + "l11n": { + "en": "Bank reinforcements", + "de": "Uferbefestigungen" + } + }, + { + "industry": 0, + "duration": 240, + "l11n": { + "en": "Fountains", + "de": "Brunnen" + } + }, + { + "industry": 0, + "duration": 396, + "l11n": { + "en": "Concrete or masonry drainage systems", + "de": "Drainagen aus Beton oder Mauerwerk" + } + }, + { + "industry": 0, + "duration": 156, + "l11n": { + "en": "Clay or plastic drainage systems", + "de": "Drainagen aus Ton oder Kunststoff" + } + }, + { + "industry": 0, + "duration": 240, + "l11n": { + "en": "Sewage treatment plants with inlet and outlet", + "de": "Kläranlagen m. Zu- und Ableitung" + } + }, + { + "industry": 0, + "duration": 240, + "l11n": { + "en": "Fire water ponds", + "de": "Löschwasserteiche" + } + }, + { + "industry": 0, + "duration": 240, + "l11n": { + "en": "Water reservoirs", + "de": "Wasserspeicher" + } + }, + { + "industry": 0, + "duration": 180, + "l11n": { + "en": "Green spaces", + "de": "Grünanlagen" + } + }, + { + "industry": 0, + "duration": 240, + "l11n": { + "en": "Golf courses", + "de": "Golfplätze" + } + }, + { + "industry": 0, + "duration": 180, + "l11n": { + "en": "Steam generation (steam boilers with accessories)", + "de": "Dampferzeugung (Dampfkessel mit Zubehör)" + } + }, + { + "industry": 0, + "duration": 228, + "l11n": { + "en": "Power generation (rectifiers, charging units, emergency power generators, power generators, power converters, etc.)", + "de": "Stromerzeugung (Gleichrichter, Ladeaggregate, Notstromaggregate, Stromgeneratoren, Stromumformer usw.)" + } + }, + { + "industry": 0, + "duration": 120, + "l11n": { + "en": "Accumulators", + "de": "Akkumulatoren" + } + }, + { + "industry": 0, + "duration": 120, + "l11n": { + "en": "Combined heat and power plants (cogeneration plants)", + "de": "Kraft-Wärmekopplungsanlagen (Blockheizkraftwerke)" + } + }, + { + "industry": 0, + "duration": 192, + "l11n": { + "en": "Wind power plants", + "de": "Windkraftanlagen" + } + }, + { + "industry": 0, + "duration": 240, + "l11n": { + "en": "Photovoltaic systems", + "de": "Photovoltaikanlagen" + } + }, + { + "industry": 0, + "duration": 120, + "l11n": { + "en": "solar systems", + "de": "Solaranlagen" + } + }, + { + "industry": 0, + "duration": 168, + "l11n": { + "en": "Hot air, refrigeration systems, compressors, fans, etc.", + "de": "Heifluft-, Kälteanlagen, Kompressoren, Ventilatoren usw." + } + }, + { + "industry": 0, + "duration": 180, + "l11n": { + "en": "Boilers incl. pressure boilers", + "de": "Kessel einschl. Druckkessel" + } + }, + { + "industry": 0, + "duration": 144, + "l11n": { + "en": "Water treatment plants", + "de": "Wasseraufbereitungsanlagen" + } + }, + { + "industry": 0, + "duration": 144, + "l11n": { + "en": "Water softening systems", + "de": "Wasserenthärtungsanlagen" + } + }, + { + "industry": 0, + "duration": 132, + "l11n": { + "en": "Water purification systems", + "de": "Wasserreinigungsanlagen" + } + }, + { + "industry": 0, + "duration": 144, + "l11n": { + "en": "Compressed air systems", + "de": "Druckluftanlagen" + } + }, + { + "industry": 0, + "duration": 180, + "l11n": { + "en": "Heat exchangers", + "de": "Wärmetauscher" + } + }, + { + "industry": 0, + "duration": 120, + "l11n": { + "en": "Recovery systems", + "de": "Rückgewinnungsanlagen" + } + }, + { + "industry": 0, + "duration": 216, + "l11n": { + "en": "Measuring and control equipment in general", + "de": "Mess- und Regeleinrichtungen allgemein" + } + }, + { + "industry": 0, + "duration": 96, + "l11n": { + "en": "Emission measuring devices", + "de": "Emissionsmessgeräte" + } + }, + { + "industry": 0, + "duration": 120, + "l11n": { + "en": "Material testing devices", + "de": "Materialprüfgeräte" + } + }, + { + "industry": 0, + "duration": 120, + "l11n": { + "en": "Ultrasonic devices (non-medical)", + "de": "Ultraschallgeräte (nicht medizinisch)" + } + }, + { + "industry": 0, + "duration": 96, + "l11n": { + "en": "Electronic surveying equipment", + "de": "Vermessungsgeräte elektronisch" + } + }, + { + "industry": 0, + "duration": 144, + "l11n": { + "en": "Mechanical measuring devices", + "de": "Vermessungsgeräte mechanisch" + } + }, + { + "industry": 0, + "duration": 168, + "l11n": { + "en": "Elevators, screw conveyors, roller conveyors, overhead conveyors, conveyor belts, conveyor belts and apron conveyors", + "de": "Elevatoren, Förderschnecken, Rollenbahnen, Hängebahnen, Transportbänder, Förderbänder und Plattenbänder" + } + }, + { + "industry": 0, + "duration": 396, + "l11n": { + "en": "Track systems with turntables, switches, signaling systems, etc. in accordance with legal regulations", + "de": "Gleisanlagen mit Drehscheiben, Weichen, Signalanlagen u.ä. nach gesetzlichen Vorschriften" + } + }, + { + "industry": 0, + "duration": 180, + "l11n": { + "en": "Track systems with turntables, switches, signaling systems, etc. other", + "de": "Gleisanlagen mit Drehscheiben, Weichen, Signalanlagen u.ä. sonstige" + } + }, + { + "industry": 0, + "duration": 252, + "l11n": { + "en": "Crane systems fixed or on rails", + "de": "Krananlagen ortsfest oder auf Schienen" + } + }, + { + "industry": 0, + "duration": 168, + "l11n": { + "en": "Other crane systems", + "de": "Krananlagen sonstige" + } + }, + { + "industry": 0, + "duration": 180, + "l11n": { + "en": "Elevators, winches, working platforms, lifting platforms, scaffolding, stationary lifts", + "de": "Aufzüge, Winden, Arbeitsbühnen, Hebebühnen, Gerüste, Hublifte stationär" + } + }, + { + "industry": 0, + "duration": 132, + "l11n": { + "en": "Elevators, winches, work platforms, lifting platforms, scaffolding, mobile lifts", + "de": "Aufzüge, Winden, Arbeitsbühnen, Hebebühnen, Gerüste, Hublifte mobil" + } + }, + { + "industry": 0, + "duration": 180, + "l11n": { + "en": "High-bay warehouse", + "de": "Hochregallager" + } + }, + { + "industry": 0, + "duration": 120, + "l11n": { + "en": "Transport containers, construction containers, office containers and residential containers", + "de": "Transportcontainer, Baucontainer, Bürocontainer und Wohncontainer" + } + }, + { + "industry": 0, + "duration": 96, + "l11n": { + "en": "Store fittings, restaurant fittings Shop window systems and fittings", + "de": "Ladeneinbauten, Gaststätteneinbauten Schaufensteranlagen u. -einbauten" + } + }, + { + "industry": 0, + "duration": 108, + "l11n": { + "en": "illuminated advertising", + "de": "Lichtreklame" + } + }, + { + "industry": 0, + "duration": 108, + "l11n": { + "en": "Showcases, display cases", + "de": "Schaukästen, Vitrinen" + } + }, + { + "industry": 0, + "duration": 240, + "l11n": { + "en": "weighbridges", + "de": "Brückenwaagen" + } + }, + { + "industry": 0, + "duration": 168, + "l11n": { + "en": "Tank and dispensing systems for fuels and lubricants", + "de": "Tank- und Zapfanlagen für Treib- und Schmierstoffe" + } + }, + { + "industry": 0, + "duration": 300, + "l11n": { + "en": "Fuel tanks", + "de": "Brennstofftanks" + } + }, + { + "industry": 0, + "duration": 120, + "l11n": { + "en": "Car washes", + "de": "Autowaschanlagen" + } + }, + { + "industry": 0, + "duration": 168, + "l11n": { + "en": "Fume extractors, dust extraction systems", + "de": "Abzugsvorrichtungen, Entstaubungsvorrichtungen" + } + }, + { + "industry": 0, + "duration": 132, + "l11n": { + "en": "Alarm and monitoring systems", + "de": "Alarmanlagen und cberwachungsanlagen" + } + }, + { + "industry": 0, + "duration": 240, + "l11n": { + "en": "Sprinkler systems", + "de": "Sprinkleranlagen" + } + }, + { + "industry": 0, + "duration": 300, + "l11n": { + "en": "Rail vehicles", + "de": "Schienenfahrzeuge" + } + }, + { + "industry": 0, + "duration": 72, + "l11n": { + "en": "Passenger cars and station wagons", + "de": "Personenkraftwagen und Kombiwagen" + } + }, + { + "industry": 0, + "duration": 84, + "l11n": { + "en": "Motorcycles, scooters, bicycles, etc.", + "de": "Motorräder, Motorroller, Fahrräder u.ä." + } + }, + { + "industry": 0, + "duration": 108, + "l11n": { + "en": "Trucks, articulated lorries, tippers", + "de": "Lastkraftwagen, Sattelschlepper, Kipper" + } + }, + { + "industry": 0, + "duration": 144, + "l11n": { + "en": "Tractors and tractors", + "de": "Traktoren und Schlepper" + } + }, + { + "industry": 0, + "duration": 96, + "l11n": { + "en": "Small tractors", + "de": "Kleintraktoren" + } + }, + { + "industry": 0, + "duration": 132, + "l11n": { + "en": "Trailers, semi-trailers, swap bodies", + "de": "Anhänger, Auflieger, Wechselaufbauten" + } + }, + { + "industry": 0, + "duration": 108, + "l11n": { + "en": "Buses and coaches", + "de": "Omnibusse" + } + }, + { + "industry": 0, + "duration": 120, + "l11n": { + "en": "Fire engines", + "de": "Feuerwehrfahrzeuge" + } + }, + { + "industry": 0, + "duration": 72, + "l11n": { + "en": "Rescue vehicles and ambulances", + "de": "Rettungsfahrzeuge und Krankentransportfahrzeuge" + } + }, + { + "industry": 0, + "duration": 96, + "l11n": { + "en": "Motorhomes, caravans", + "de": "Wohnmobile, Wohnwagen" + } + }, + { + "industry": 0, + "duration": 144, + "l11n": { + "en": "construction trailers", + "de": "Bauwagen" + } + }, + { + "industry": 0, + "duration": 252, + "l11n": { + "en": "Aircraft under 20 tons maximum permissible flight weight", + "de": "Flugzeuge unter 20 t höchstzulässigem Fluggewicht" + } + }, + { + "industry": 0, + "duration": 228, + "l11n": { + "en": "Rotorcraft (helicopters)", + "de": "Drehflügler (Hubschrauber)" + } + }, + { + "industry": 0, + "duration": 60, + "l11n": { + "en": "Hot air balloons", + "de": "Heifluftballone" + } + }, + { + "industry": 0, + "duration": 96, + "l11n": { + "en": "Airships", + "de": "Luftschiffe" + } + }, + { + "industry": 0, + "duration": 240, + "l11n": { + "en": "launches", + "de": "Barkassen" + } + }, + { + "industry": 0, + "duration": 360, + "l11n": { + "en": "pontoons", + "de": "Pontons" + } + }, + { + "industry": 0, + "duration": 240, + "l11n": { + "en": "sailing yachts", + "de": "Segelyachten" + } + }, + { + "industry": 0, + "duration": 96, + "l11n": { + "en": "Other means of transportation (electric carts, forklifts, pallet trucks, etc.)", + "de": "sonstige Beförderungsmittel (Elektrokarren, Stapler, Hubwagen usw.)" + } + }, + { + "industry": 0, + "duration": 156, + "l11n": { + "en": "dressing machines", + "de": "Abrichtmaschinen" + } + }, + { + "industry": 0, + "duration": 156, + "l11n": { + "en": "bending machines", + "de": "Biegemaschinen" + } + }, + { + "industry": 0, + "duration": 192, + "l11n": { + "en": "Stationary drilling machines", + "de": "Bohrmaschinen stationär" + } + }, + { + "industry": 0, + "duration": 96, + "l11n": { + "en": "Mobile drilling machines", + "de": "Bohrmaschinen mobil" + } + }, + { + "industry": 0, + "duration": 84, + "l11n": { + "en": "Drill hammers and pneumatic hammers", + "de": "Bohrhämmer und Presslufthämmer" + } + }, + { + "industry": 0, + "duration": 120, + "l11n": { + "en": "Brushing machines", + "de": "Bürstmaschinen" + } + }, + { + "industry": 0, + "duration": 192, + "l11n": { + "en": "Lathes", + "de": "Drehbänke" + } + }, + { + "industry": 0, + "duration": 180, + "l11n": { + "en": "Stationary milling machines", + "de": "Fräsmaschinen stationär" + } + }, + { + "industry": 0, + "duration": 96, + "l11n": { + "en": "Mobile milling machines", + "de": "Fräsmaschinen mobil" + } + }, + { + "industry": 0, + "duration": 84, + "l11n": { + "en": "Spark erosion machines", + "de": "Funkenerosionsmaschinen" + } + }, + { + "industry": 0, + "duration": 192, + "l11n": { + "en": "Planing machines stationary", + "de": "Hobelmaschinen stationär" + } + }, + { + "industry": 0, + "duration": 108, + "l11n": { + "en": "Mobile planing machines", + "de": "Hobelmaschinen mobil" + } + }, + { + "industry": 0, + "duration": 156, + "l11n": { + "en": "Polishing machines stationary", + "de": "Poliermaschinen stationär" + } + }, + { + "industry": 0, + "duration": 60, + "l11n": { + "en": "Mobile polishing machines", + "de": "Poliermaschinen mobil" + } + }, + { + "industry": 0, + "duration": 168, + "l11n": { + "en": "Presses and punching machines", + "de": "Pressen und Stanzen" + } + }, + { + "industry": 0, + "duration": 120, + "l11n": { + "en": "Upsetting machines", + "de": "Stauchmaschinen" + } + }, + { + "industry": 0, + "duration": 132, + "l11n": { + "en": "Tampers and vibrating plates", + "de": "Stampfer und Rüttelplatten" + } + }, + { + "industry": 0, + "duration": 168, + "l11n": { + "en": "All types of stationary saws", + "de": "Sägen aller Art stationär" + } + }, + { + "industry": 0, + "duration": 96, + "l11n": { + "en": "All types of mobile saws", + "de": "Sägen aller Art mobil" + } + }, + { + "industry": 0, + "duration": 120, + "l11n": { + "en": "Stationary cut-off machines", + "de": "Trennmaschinen stationär" + } + }, + { + "industry": 0, + "duration": 84, + "l11n": { + "en": "Mobile cutting machines", + "de": "Trennmaschinen mobil" + } + }, + { + "industry": 0, + "duration": 108, + "l11n": { + "en": "Sandblasting blowers", + "de": "Sandstrahlgebläse" + } + }, + { + "industry": 0, + "duration": 180, + "l11n": { + "en": "Stationary sanding machines", + "de": "Schleifmaschinen stationär" + } + }, + { + "industry": 0, + "duration": 96, + "l11n": { + "en": "Mobile grinding machines", + "de": "Schleifmaschinen mobil" + } + }, + { + "industry": 0, + "duration": 156, + "l11n": { + "en": "Cutting machines and shears stationary", + "de": "Schneidemaschinen und Scheren stationär" + } + }, + { + "industry": 0, + "duration": 96, + "l11n": { + "en": "Cutting machines and shears mobile", + "de": "Schneidemaschinen und Scheren mobil" + } + }, + { + "industry": 0, + "duration": 72, + "l11n": { + "en": "Shredders", + "de": "Shredder" + } + }, + { + "industry": 0, + "duration": 156, + "l11n": { + "en": "Welding and soldering equipment", + "de": "Schweifgeräte und Lötgeräte" + } + }, + { + "industry": 0, + "duration": 156, + "l11n": { + "en": "Injection molding machines", + "de": "Spritzgussmaschinen" + } + }, + { + "industry": 0, + "duration": 120, + "l11n": { + "en": "Filling machines", + "de": "Abfüllanlagen" + } + }, + { + "industry": 0, + "duration": 156, + "l11n": { + "en": "Packaging machines, film sealers", + "de": "Verpackungsmaschinen, Folienschweifgeräte" + } + }, + { + "industry": 0, + "duration": 144, + "l11n": { + "en": "Collating machines", + "de": "Zusammentragmaschinen" + } + }, + { + "industry": 0, + "duration": 96, + "l11n": { + "en": "Stamping machines", + "de": "Stempelmaschinen" + } + }, + { + "industry": 0, + "duration": 96, + "l11n": { + "en": "Banding machines", + "de": "Banderoliermaschinen" + } + }, + { + "industry": 0, + "duration": 156, + "l11n": { + "en": "Other processing and finishing machines (bending, gluing, pointing, etching, coating, printing, anodizing, degreasing, deburring, eroding, labelling, folding, dyeing, filing, casting, electroplating, engraving, hardening, stapling, painting, riveting)", + "de": "Sonstige Be- und Verarbeitungsmaschinen (Abkanten, Anleimen, Anspitzen, Ätzen, Beschichten, Drucken, Eloxieren, Entfetten, Entgraten, Erodieren, Etikettieren, Falzen, Färben, Feilen, Giefen, Galvanisieren, Gravieren, Härten, Heften, Lackieren, Nieten)" + } + }, + { + "industry": 0, + "duration": 168, + "l11n": { + "en": "Economic goods of workshop, laboratory and storage equipment", + "de": "Wirtschaftsgüter der Werkstätten-, Labor- und Lagereinrichtungen" + } + }, + { + "industry": 0, + "duration": 96, + "l11n": { + "en": "Store equipment assets", + "de": "Wirtschaftsgüter der Ladeneinrichtungen" + } + }, + { + "industry": 0, + "duration": 72, + "l11n": { + "en": "Exhibition stands", + "de": "Messestände" + } + }, + { + "industry": 0, + "duration": 96, + "l11n": { + "en": "Refrigeration equipment", + "de": "Kühleinrichtungen" + } + }, + { + "industry": 0, + "duration": 132, + "l11n": { + "en": "Air conditioners (mobile)", + "de": "Klimageräte (mobil)" + } + }, + { + "industry": 0, + "duration": 120, + "l11n": { + "en": "Ventilation units, air extraction units (mobile)", + "de": "Belüftungsgeräte, Entlüftungsgeräte (mobil)" + } + }, + { + "industry": 0, + "duration": 60, + "l11n": { + "en": "Grease separators", + "de": "Fettabscheider" + } + }, + { + "industry": 0, + "duration": 72, + "l11n": { + "en": "Magnetic separators", + "de": "Magnetabscheider" + } + }, + { + "industry": 0, + "duration": 60, + "l11n": { + "en": "Wet separators", + "de": "Nassabscheider" + } + }, + { + "industry": 0, + "duration": 132, + "l11n": { + "en": "Hot air blowers, cold air blowers (mobile)", + "de": "Heifluftgebläse, Kaltluftgebläse (mobil)" + } + }, + { + "industry": 0, + "duration": 108, + "l11n": { + "en": "Space heaters (mobile)", + "de": "Raumheizgeräte (mobil)" + } + }, + { + "industry": 0, + "duration": 72, + "l11n": { + "en": "Work tents", + "de": "Arbeitszelte" + } + }, + { + "industry": 0, + "duration": 120, + "l11n": { + "en": "Telephone exchange systems", + "de": "Fernsprechnebenstellenanlagen" + } + }, + { + "industry": 0, + "duration": 96, + "l11n": { + "en": "General communication terminals", + "de": "Kommunikationsendgeräte Allgemein" + } + }, + { + "industry": 0, + "duration": 60, + "l11n": { + "en": "Mobile radio terminals", + "de": "Mobilfunkendgeräte" + } + }, + { + "industry": 0, + "duration": 72, + "l11n": { + "en": "Text terminals (fax machines, etc.)", + "de": "Textendeinrichtungen (Faxgeräte u.ä.)" + } + }, + { + "industry": 0, + "duration": 132, + "l11n": { + "en": "Company radio systems", + "de": "Betriebsfunkanlagen" + } + }, + { + "industry": 0, + "duration": 120, + "l11n": { + "en": "Antenna masts", + "de": "Antennenmasten" + } + }, + { + "industry": 0, + "duration": 96, + "l11n": { + "en": "Addressing machines, enveloping machines, franking machines", + "de": "Adressiermaschinen, Kuvertiermaschinen, Frankiermaschinen" + } + }, + { + "industry": 0, + "duration": 96, + "l11n": { + "en": "Pagination machines", + "de": "Paginiermaschinen" + } + }, + { + "industry": 0, + "duration": 84, + "l11n": { + "en": "Mainframe computers", + "de": "Grofrechner" + } + }, + { + "industry": 0, + "duration": 36, + "l11n": { + "en": "Workstations, personal computers, notebooks and their peripherals (printers, scanners, monitors, etc.)", + "de": "Workstations, Personalcomputer, Notebooks und deren Peripheriegeräte (Drucker, Scanner, Bildschirme u.ä.)" + } + }, + { + "industry": 0, + "duration": 84, + "l11n": { + "en": "Photo, film, video and audio equipment (televisions, CD players, recorders, loudspeakers, radios, amplifiers, cameras, monitors, etc.)", + "de": "Foto-, Film-, Video- und Audiogeräte (Fernseher, CD-Player, Recorder, Lautsprecher, Verstärker, Kameras, Monitore u.ä.)" + } + }, + { + "industry": 0, + "duration": 108, + "l11n": { + "en": "Public address systems", + "de": "Beschallungsanlagen" + } + }, + { + "industry": 0, + "duration": 96, + "l11n": { + "en": "Presentation devices, data display devices", + "de": "Präsentationsgeräte, Datensichtgeräte" + } + }, + { + "industry": 0, + "duration": 72, + "l11n": { + "en": "cash registers", + "de": "Registrierkassen" + } + }, + { + "industry": 0, + "duration": 108, + "l11n": { + "en": "Typewriters", + "de": "Schreibmaschinen" + } + }, + { + "industry": 0, + "duration": 96, + "l11n": { + "en": "Electronic drawing equipment", + "de": "Zeichengeräte elektronisch" + } + }, + { + "industry": 0, + "duration": 168, + "l11n": { + "en": "Mechanical drawing machines", + "de": "Zeichengeräte mechanisch" + } + }, + { + "industry": 0, + "duration": 84, + "l11n": { + "en": "Duplicating machines", + "de": "Vervielfältigungsgeräte" + } + }, + { + "industry": 0, + "duration": 96, + "l11n": { + "en": "Time recording devices", + "de": "Zeiterfassungsgeräte" + } + }, + { + "industry": 0, + "duration": 84, + "l11n": { + "en": "Money validators, money sorters, money changers and money counters", + "de": "Geldprüfgeräte, Geldsortiergeräte, Geldwechselgeräte und Geldzählgeräte" + } + }, + { + "industry": 0, + "duration": 96, + "l11n": { + "en": "Shredders (document shredders)", + "de": "Reifwölfe (Aktenvernichter)" + } + }, + { + "industry": 0, + "duration": 96, + "l11n": { + "en": "Card readers (EC, credit)", + "de": "Kartenleser (EC-, Kredit-)" + } + }, + { + "industry": 0, + "duration": 156, + "l11n": { + "en": "Office furniture", + "de": "Büromöbel" + } + }, + { + "industry": 0, + "duration": 120, + "l11n": { + "en": "Sales counters", + "de": "Verkaufstheken" + } + }, + { + "industry": 0, + "duration": 96, + "l11n": { + "en": "Sales booths, sales stands", + "de": "Verkaufsbuden, Verkaufsstände" + } + }, + { + "industry": 0, + "duration": 120, + "l11n": { + "en": "Plantings in buildings", + "de": "Bepflanzungen in Gebäuden" + } + }, + { + "industry": 0, + "duration": 168, + "l11n": { + "en": "Steel cabinets", + "de": "Stahlschränke" + } + }, + { + "industry": 0, + "duration": 276, + "l11n": { + "en": "Armored cabinets, safes", + "de": "Panzerschränke, Tresore" + } + }, + { + "industry": 0, + "duration": 300, + "l11n": { + "en": "Safe systems", + "de": "Tresoranlagen" + } + }, + { + "industry": 0, + "duration": 96, + "l11n": { + "en": "Carpets normal", + "de": "Teppiche normale" + } + }, + { + "industry": 0, + "duration": 180, + "l11n": { + "en": "High-quality carpets (from DM 1,000\/m2)", + "de": "Teppiche hochwertige (ab 1.000 DM\/m2)" + } + }, + { + "industry": 0, + "duration": 180, + "l11n": { + "en": "Works of art (excluding works by recognized artists)", + "de": "Kunstwerke (ohne Werke anerkannter Künstler)" + } + }, + { + "industry": 0, + "duration": 132, + "l11n": { + "en": "Scales (fruit, vegetable, meat, etc.)", + "de": "Waagen (Obst-, Gemüse-, Fleisch- u.ä.)" + } + }, + { + "industry": 0, + "duration": 120, + "l11n": { + "en": "pneumatic tube systems", + "de": "Rohrpostanlagen" + } + }, + { + "industry": 0, + "duration": 72, + "l11n": { + "en": "Small concrete mixers", + "de": "Betonkleinmischer" + } + }, + { + "industry": 0, + "duration": 96, + "l11n": { + "en": "floor polishers", + "de": "Bohnermaschinen" + } + }, + { + "industry": 0, + "duration": 120, + "l11n": { + "en": "Disinfection devices", + "de": "Desinfektionsgeräte" + } + }, + { + "industry": 0, + "duration": 84, + "l11n": { + "en": "Dishwashers and glasswashers", + "de": "Geschirr- und Gläserspülmaschinen" + } + }, + { + "industry": 0, + "duration": 96, + "l11n": { + "en": "High-pressure cleaners (steam and water)", + "de": "Hochdruckreiniger (Dampf- und Wasser-)" + } + }, + { + "industry": 0, + "duration": 84, + "l11n": { + "en": "Industrial vacuum cleaners", + "de": "Industriestaubsauger" + } + }, + { + "industry": 0, + "duration": 108, + "l11n": { + "en": "Sweepers", + "de": "Kehrmaschinen" + } + }, + { + "industry": 0, + "duration": 108, + "l11n": { + "en": "Sweepers", + "de": "Räumgeräte" + } + }, + { + "industry": 0, + "duration": 120, + "l11n": { + "en": "Sterilizers", + "de": "Sterilisatoren" + } + }, + { + "industry": 0, + "duration": 84, + "l11n": { + "en": "Carpet cleaners (portable)", + "de": "Teppichreinigungsgeräte (transportabel)" + } + }, + { + "industry": 0, + "duration": 120, + "l11n": { + "en": "Washing machines", + "de": "Waschmaschinen" + } + }, + { + "industry": 0, + "duration": 60, + "l11n": { + "en": "Building drying and dehumidifying appliances", + "de": "Bautrocknungs- und Entfeuchtungsgeräte" + } + }, + { + "industry": 0, + "duration": 96, + "l11n": { + "en": "Tumble dryers", + "de": "Wäschetrockner" + } + }, + { + "industry": 0, + "duration": 84, + "l11n": { + "en": "Beverage vending machines, reverse vending machines", + "de": "Getränkeautomaten, Leergutautomaten" + } + }, + { + "industry": 0, + "duration": 60, + "l11n": { + "en": "Vending machines", + "de": "Warenautomaten" + } + }, + { + "industry": 0, + "duration": 96, + "l11n": { + "en": "Cigarette vending machines", + "de": "Zigarettenautomaten" + } + }, + { + "industry": 0, + "duration": 60, + "l11n": { + "en": "Passport photo machines", + "de": "Passbildautomaten" + } + }, + { + "industry": 0, + "duration": 60, + "l11n": { + "en": "Business card machines", + "de": "Visitenkartenautomaten" + } + }, + { + "industry": 0, + "duration": 48, + "l11n": { + "en": "Gambling machines (machines with the possibility of winning)", + "de": "Geldspielgeräte (Spielgeräte mit Gewinnmöglichkeit)" + } + }, + { + "industry": 0, + "duration": 96, + "l11n": { + "en": "Music machines", + "de": "Musikautomaten" + } + }, + { + "industry": 0, + "duration": 72, + "l11n": { + "en": "video machines", + "de": "Videoautomaten" + } + }, + { + "industry": 0, + "duration": 60, + "l11n": { + "en": "Other amusement machines (e.g. pinball machines)", + "de": "sonstige Unterhaltungsautomaten (z.B. Flipper)" + } + }, + { + "industry": 0, + "duration": 120, + "l11n": { + "en": "flagpoles", + "de": "Fahnenmasten" + } + }, + { + "industry": 0, + "duration": 120, + "l11n": { + "en": "refrigerators", + "de": "Kühlschränke" + } + }, + { + "industry": 0, + "duration": 156, + "l11n": { + "en": "Laboratory equipment (microscopes, precision balances, etc.)", + "de": "Laborgeräte (Mikroskope, Präzisionswaagen u.ä.)" + } + }, + { + "industry": 0, + "duration": 96, + "l11n": { + "en": "Microwave ovens", + "de": "Mikrowellengeräte" + } + }, + { + "industry": 0, + "duration": 108, + "l11n": { + "en": "Lawn mowers", + "de": "Rasenmäher" + } + }, + { + "industry": 0, + "duration": 108, + "l11n": { + "en": "Toilet cubicles and toilet trailers", + "de": "Toilettenkabinen und Toilettenwagen" + } + }, + { + "industry": 0, + "duration": 120, + "l11n": { + "en": "Centrifuges", + "de": "Zentrifugen" + } + } +] \ No newline at end of file diff --git a/Admin/Install/attributes.json b/Admin/Install/attributes.json new file mode 100644 index 0000000..4ddd4b7 --- /dev/null +++ b/Admin/Install/attributes.json @@ -0,0 +1,178 @@ +[ + { + "name": "is_gauge", + "l11n": { + "en": "Gauge", + "de": "Messgerät" + }, + "value_type": 1, + "is_custom_allowed": false, + "validation_pattern": "", + "is_required": false, + "default_value": "", + "values": [ + { + "value": 0 + }, + { + "value": 1 + } + ] + }, + { + "name": "insurance_provider", + "l11n": { + "en": "Insurance provider", + "de": "Versicherung" + }, + "value_type": 2, + "is_custom_allowed": true, + "validation_pattern": "", + "is_required": false, + "default_value": "", + "values": [] + }, + { + "name": "insurance_number", + "l11n": { + "en": "Insurance number", + "de": "Versicherungsnummer" + }, + "value_type": 2, + "is_custom_allowed": true, + "validation_pattern": "", + "is_required": false, + "default_value": "", + "values": [] + }, + { + "name": "leasing_provider", + "l11n": { + "en": "Leasing provider", + "de": "Leasingsnummer" + }, + "value_type": 2, + "is_custom_allowed": true, + "validation_pattern": "", + "is_required": false, + "default_value": "", + "values": [] + }, + { + "name": "leasing_number", + "l11n": { + "en": "Leasing number", + "de": "Leasingsnummer" + }, + "value_type": 2, + "is_custom_allowed": true, + "validation_pattern": "", + "is_required": false, + "default_value": "", + "values": [] + }, + { + "name": "leasing_end", + "l11n": { + "en": "Leasing end", + "de": "Leasingende" + }, + "value_type": 4, + "is_custom_allowed": true, + "validation_pattern": "", + "is_required": false, + "default_value": "", + "values": [] + }, + { + "name": "make", + "l11n": { + "en": "Make", + "de": "Marke" + }, + "value_type": 2, + "is_custom_allowed": true, + "validation_pattern": "", + "is_required": false, + "default_value": "", + "values": [] + }, + { + "name": "model", + "l11n": { + "en": "Model", + "de": "Modell" + }, + "value_type": 2, + "is_custom_allowed": true, + "validation_pattern": "", + "is_required": false, + "default_value": "", + "values": [] + }, + { + "name": "build", + "l11n": { + "en": "Build year", + "de": "Baujahr" + }, + "value_type": 4, + "is_custom_allowed": true, + "validation_pattern": "", + "is_required": false, + "default_value": "", + "values": [] + }, + { + "name": "ownership_start", + "l11n": { + "en": "Ownership start", + "de": "Besitzbeginn" + }, + "value_type": 4, + "is_custom_allowed": true, + "validation_pattern": "", + "is_required": false, + "default_value": "", + "values": [] + }, + { + "name": "ownership_end", + "l11n": { + "en": "Ownership end", + "de": "Besitzende" + }, + "value_type": 4, + "is_custom_allowed": true, + "validation_pattern": "", + "is_required": false, + "default_value": "", + "values": [] + }, + { + "name": "purchase_price", + "l11n": { + "en": "Purchase price", + "de": "Kaufpreis" + }, + "value_type": 1, + "is_custom_allowed": true, + "validation_pattern": "", + "is_required": false, + "default_value": "", + "values": [] + }, + { + "name": "leasing_residual_value", + "l11n": { + "en": "Leasing residual value", + "de": "Leasing Restwert" + }, + "value_type": 1, + "is_custom_allowed": true, + "validation_pattern": "", + "is_required": false, + "default_value": "", + "values": [] + } +] \ No newline at end of file diff --git a/Admin/Install/db.json b/Admin/Install/db.json new file mode 100644 index 0000000..1330d6c --- /dev/null +++ b/Admin/Install/db.json @@ -0,0 +1,400 @@ +{ + "assetmgmt_asset_type": { + "name": "assetmgmt_asset_type", + "fields": { + "assetmgmt_asset_type_id": { + "name": "assetmgmt_asset_type_id", + "type": "INT", + "null": false, + "primary": true, + "autoincrement": true + }, + "assetmgmt_asset_type_name": { + "name": "assetmgmt_asset_type_name", + "type": "VARCHAR(255)", + "null": false + }, + "assetmgmt_asset_type_depreciation_duration": { + "name": "assetmgmt_asset_type_depreciation_duration", + "type": "INT", + "null": false + }, + "assetmgmt_asset_type_industry": { + "name": "assetmgmt_asset_type_industry", + "type": "INT", + "null": false + } + } + }, + "assetmgmt_asset_type_l11n": { + "name": "assetmgmt_asset_type_l11n", + "fields": { + "assetmgmt_asset_type_l11n_id": { + "name": "assetmgmt_asset_type_l11n_id", + "type": "INT", + "null": false, + "primary": true, + "autoincrement": true + }, + "assetmgmt_asset_type_l11n_title": { + "name": "assetmgmt_asset_type_l11n_title", + "type": "VARCHAR(255)", + "null": false + }, + "assetmgmt_asset_type_l11n_type": { + "name": "assetmgmt_asset_type_l11n_type", + "type": "INT(11)", + "null": false, + "foreignTable": "assetmgmt_asset_type", + "foreignKey": "assetmgmt_asset_type_id" + }, + "assetmgmt_asset_type_l11n_lang": { + "name": "assetmgmt_asset_type_l11n_lang", + "type": "VARCHAR(2)", + "null": false, + "foreignTable": "language", + "foreignKey": "language_639_1" + } + } + }, + "assetmgmt_asset": { + "name": "assetmgmt_asset", + "fields": { + "assetmgmt_asset_id": { + "name": "assetmgmt_asset_id", + "type": "INT", + "null": false, + "primary": true, + "autoincrement": true + }, + "assetmgmt_asset_name": { + "name": "assetmgmt_asset_name", + "type": "VARCHAR(255)", + "null": false + }, + "assetmgmt_asset_number": { + "name": "assetmgmt_asset_number", + "type": "VARCHAR(255)", + "null": false + }, + "assetmgmt_asset_status": { + "name": "assetmgmt_asset_status", + "type": "TINYINT", + "null": false + }, + "assetmgmt_asset_type": { + "name": "assetmgmt_asset_type", + "type": "INT", + "foreignTable": "assetmgmt_asset_type", + "foreignKey": "assetmgmt_asset_type_id" + }, + "assetmgmt_asset_info": { + "name": "assetmgmt_asset_info", + "type": "TEXT", + "null": false + }, + "assetmgmt_asset_created_at": { + "name": "assetmgmt_asset_created_at", + "type": "DATETIME", + "null": false + }, + "assetmgmt_asset_responsible": { + "name": "assetmgmt_asset_responsible", + "type": "INT", + "null": true, + "default": true, + "foreignTable": "account", + "foreignKey": "account_id" + }, + "assetmgmt_asset_unit": { + "name": "assetmgmt_asset_unit", + "type": "INT", + "default": null, + "null": true, + "foreignTable": "unit", + "foreignKey": "unit_id" + } + } + }, + "assetmgmt_attr_type": { + "name": "assetmgmt_attr_type", + "fields": { + "assetmgmt_attr_type_id": { + "name": "assetmgmt_attr_type_id", + "type": "INT", + "null": false, + "primary": true, + "autoincrement": true + }, + "assetmgmt_attr_type_name": { + "name": "assetmgmt_attr_type_name", + "type": "VARCHAR(255)", + "null": false, + "unique": true + }, + "assetmgmt_attr_type_datatype": { + "name": "assetmgmt_attr_type_datatype", + "type": "INT(11)", + "null": false + }, + "assetmgmt_attr_type_fields": { + "name": "assetmgmt_attr_type_fields", + "type": "INT(11)", + "null": false + }, + "assetmgmt_attr_type_custom": { + "name": "assetmgmt_attr_type_custom", + "type": "TINYINT(1)", + "null": false + }, + "assetmgmt_attr_type_required": { + "description": "Every asset must have this attribute type if set to true.", + "name": "assetmgmt_attr_type_required", + "type": "TINYINT(1)", + "null": false + }, + "assetmgmt_attr_type_pattern": { + "description": "This is a regex validation pattern.", + "name": "assetmgmt_attr_type_pattern", + "type": "VARCHAR(255)", + "null": false + } + } + }, + "assetmgmt_attr_type_l11n": { + "name": "assetmgmt_attr_type_l11n", + "fields": { + "assetmgmt_attr_type_l11n_id": { + "name": "assetmgmt_attr_type_l11n_id", + "type": "INT", + "null": false, + "primary": true, + "autoincrement": true + }, + "assetmgmt_attr_type_l11n_title": { + "name": "assetmgmt_attr_type_l11n_title", + "type": "VARCHAR(255)", + "null": false + }, + "assetmgmt_attr_type_l11n_type": { + "name": "assetmgmt_attr_type_l11n_type", + "type": "INT(11)", + "null": false, + "foreignTable": "assetmgmt_attr_type", + "foreignKey": "assetmgmt_attr_type_id" + }, + "assetmgmt_attr_type_l11n_lang": { + "name": "assetmgmt_attr_type_l11n_lang", + "type": "VARCHAR(2)", + "null": false, + "foreignTable": "language", + "foreignKey": "language_639_1" + } + } + }, + "assetmgmt_attr_value": { + "name": "assetmgmt_attr_value", + "fields": { + "assetmgmt_attr_value_id": { + "name": "assetmgmt_attr_value_id", + "type": "INT", + "null": false, + "primary": true, + "autoincrement": true + }, + "assetmgmt_attr_value_default": { + "name": "assetmgmt_attr_value_default", + "type": "TINYINT(1)", + "null": false + }, + "assetmgmt_attr_value_valueStr": { + "name": "assetmgmt_attr_value_valueStr", + "type": "VARCHAR(255)", + "null": true, + "default": null + }, + "assetmgmt_attr_value_valueInt": { + "name": "assetmgmt_attr_value_valueInt", + "type": "INT(11)", + "null": true, + "default": null + }, + "assetmgmt_attr_value_valueDec": { + "name": "assetmgmt_attr_value_valueDec", + "type": "DECIMAL(19,5)", + "null": true, + "default": null + }, + "assetmgmt_attr_value_valueDat": { + "name": "assetmgmt_attr_value_valueDat", + "type": "DATETIME", + "null": true, + "default": null + }, + "assetmgmt_attr_value_unit": { + "name": "assetmgmt_attr_value_unit", + "type": "VARCHAR(255)", + "null": false + }, + "assetmgmt_attr_value_deptype": { + "name": "assetmgmt_attr_value_deptype", + "type": "INT(11)", + "null": true, + "default": null, + "foreignTable": "assetmgmt_attr_type", + "foreignKey": "assetmgmt_attr_type_id" + }, + "assetmgmt_attr_value_depvalue": { + "name": "assetmgmt_attr_value_depvalue", + "type": "INT(11)", + "null": true, + "default": null, + "foreignTable": "assetmgmt_attr_value", + "foreignKey": "assetmgmt_attr_value_id" + } + } + }, + "assetmgmt_attr_value_l11n": { + "name": "assetmgmt_attr_value_l11n", + "fields": { + "assetmgmt_attr_value_l11n_id": { + "name": "assetmgmt_attr_value_l11n_id", + "type": "INT", + "null": false, + "primary": true, + "autoincrement": true + }, + "assetmgmt_attr_value_l11n_title": { + "name": "assetmgmt_attr_value_l11n_title", + "type": "VARCHAR(255)", + "null": false + }, + "assetmgmt_attr_value_l11n_value": { + "name": "assetmgmt_attr_value_l11n_value", + "type": "INT(11)", + "null": false, + "foreignTable": "assetmgmt_attr_value", + "foreignKey": "assetmgmt_attr_value_id" + }, + "assetmgmt_attr_value_l11n_lang": { + "name": "assetmgmt_attr_value_l11n_lang", + "type": "VARCHAR(2)", + "null": false, + "foreignTable": "language", + "foreignKey": "language_639_1" + } + } + }, + "assetmgmt_asset_attr_default": { + "name": "assetmgmt_asset_attr_default", + "fields": { + "assetmgmt_asset_attr_default_id": { + "name": "assetmgmt_asset_attr_default_id", + "type": "INT", + "null": false, + "primary": true, + "autoincrement": true + }, + "assetmgmt_asset_attr_default_type": { + "name": "assetmgmt_asset_attr_default_type", + "type": "INT(11)", + "null": false, + "foreignTable": "assetmgmt_attr_type", + "foreignKey": "assetmgmt_attr_type_id" + }, + "assetmgmt_asset_attr_default_value": { + "name": "assetmgmt_asset_attr_default_value", + "type": "INT(11)", + "null": false, + "foreignTable": "assetmgmt_attr_value", + "foreignKey": "assetmgmt_attr_value_id" + } + } + }, + "assetmgmt_asset_attr": { + "name": "assetmgmt_asset_attr", + "fields": { + "assetmgmt_asset_attr_id": { + "name": "assetmgmt_asset_attr_id", + "type": "INT", + "null": false, + "primary": true, + "autoincrement": true + }, + "assetmgmt_asset_attr_asset": { + "name": "assetmgmt_asset_attr_asset", + "type": "INT(11)", + "null": false, + "foreignTable": "assetmgmt_asset", + "foreignKey": "assetmgmt_asset_id" + }, + "assetmgmt_asset_attr_type": { + "name": "assetmgmt_asset_attr_type", + "type": "INT(11)", + "null": false, + "foreignTable": "assetmgmt_attr_type", + "foreignKey": "assetmgmt_attr_type_id" + }, + "assetmgmt_asset_attr_value": { + "name": "assetmgmt_asset_attr_value", + "type": "INT(11)", + "null": true, + "default": null, + "foreignTable": "assetmgmt_attr_value", + "foreignKey": "assetmgmt_attr_value_id" + } + } + }, + "assetmgmt_asset_media": { + "name": "assetmgmt_asset_media", + "fields": { + "assetmgmt_asset_media_id": { + "name": "assetmgmt_asset_media_id", + "type": "INT", + "null": false, + "primary": true, + "autoincrement": true + }, + "assetmgmt_asset_media_asset": { + "name": "assetmgmt_asset_media_asset", + "type": "INT", + "null": false, + "foreignTable": "assetmgmt_asset", + "foreignKey": "assetmgmt_asset_id" + }, + "assetmgmt_asset_media_media": { + "name": "assetmgmt_asset_media_media", + "type": "INT", + "null": false, + "foreignTable": "media", + "foreignKey": "media_id" + } + } + }, + "assetmgmt_asset_note": { + "name": "assetmgmt_asset_note", + "fields": { + "assetmgmt_asset_note_id": { + "name": "assetmgmt_asset_note_id", + "type": "INT", + "null": false, + "primary": true, + "autoincrement": true + }, + "assetmgmt_asset_note_asset": { + "name": "assetmgmt_asset_note_asset", + "type": "INT", + "null": false, + "foreignTable": "assetmgmt_asset", + "foreignKey": "assetmgmt_asset_id" + }, + "assetmgmt_asset_note_doc": { + "name": "assetmgmt_asset_note_doc", + "type": "INT", + "null": false, + "foreignTable": "editor_doc", + "foreignKey": "editor_doc_id" + } + } + } +} \ No newline at end of file diff --git a/Admin/Installer.php b/Admin/Installer.php index 455f1d0..c03a9c1 100644 --- a/Admin/Installer.php +++ b/Admin/Installer.php @@ -14,7 +14,13 @@ declare(strict_types=1); namespace Modules\AssetManagement\Admin; +use phpOMS\Application\ApplicationAbstract; +use phpOMS\Config\SettingsInterface; +use phpOMS\Message\Http\HttpRequest; +use phpOMS\Message\Http\HttpResponse; use phpOMS\Module\InstallerAbstract; +use phpOMS\Module\ModuleInfo; +use phpOMS\Uri\HttpUri; /** * Installer class. @@ -33,4 +39,297 @@ final class Installer extends InstallerAbstract * @since 1.0.0 */ public const PATH = __DIR__; + + /** + * {@inheritdoc} + */ + public static function install(ApplicationAbstract $app, ModuleInfo $info, SettingsInterface $cfgHandler) : void + { + parent::install($app, $info, $cfgHandler); + + /* Attributes */ + $fileContent = \file_get_contents(__DIR__ . '/Install/attributes.json'); + if ($fileContent === false) { + return; + } + + /** @var array $attributes */ + $attributes = \json_decode($fileContent, true); + $attrTypes = self::createAttributeTypes($app, $attributes); + $attrValues = self::createAttributeValues($app, $attrTypes, $attributes); + + /* Asset types */ + $fileContent = \file_get_contents(__DIR__ . '/Install/assettype.json'); + if ($fileContent === false) { + return; + } + + /** @var array $types */ + $types = \json_decode($fileContent, true); + $assetTypes = self::createAssetTypes($app, $types); + } + + /** + * Install asset type + * + * @param ApplicationAbstract $app Application + * @param array $types Attribute definition + * + * @return array + * + * @since 1.0.0 + */ + private static function createAssetTypes(ApplicationAbstract $app, array $types) : array + { + /** @var array $assetTypes */ + $assetTypes = []; + + /** @var \Modules\AssetManagement\Controller\ApiAssetTypeController $module */ + $module = $app->moduleManager->getModuleInstance('AssetManagement', 'ApiAssetType'); + + /** @var array $type */ + foreach ($types as $type) { + $response = new HttpResponse(); + $request = new HttpRequest(new HttpUri('')); + + $request->header->account = 1; + $request->setData('duration', $type['duration']); + $request->setData('industry', $type['industry']); + $request->setData('title', \reset($type['l11n'])); + $request->setData('language', \array_keys($type['l11n'])[0] ?? 'en'); + + $module->apiAssetTypeCreate($request, $response); + + $responseData = $response->getData(''); + if (!\is_array($responseData)) { + continue; + } + + $assetType = \is_array($responseData['response']) + ? $responseData['response'] + : $responseData['response']->toArray(); + + $assetTypes[] = $assetType; + + $isFirst = true; + foreach ($type['l11n'] as $language => $l11n) { + if ($isFirst) { + $isFirst = false; + continue; + } + + $response = new HttpResponse(); + $request = new HttpRequest(new HttpUri('')); + + $request->header->account = 1; + $request->setData('title', $l11n); + $request->setData('language', $language); + $request->setData('type', $assetType['id']); + + $module->apiAssetTypeL11nCreate($request, $response); + } + } + + return $assetTypes; + } + + /** + * Install inspection type + * + * @param ApplicationAbstract $app Application + * @param array $types Attribute definition + * + * @return array + * + * @since 1.0.0 + */ + private static function createInspectionTypes(ApplicationAbstract $app, array $types) : array + { + /** @var array $inspectionTypes */ + $inspectionTypes = []; + + /** @var \Modules\AssetManagement\Controller\ApiInspectionTypeController $module */ + $module = $app->moduleManager->getModuleInstance('AssetManagement', 'ApiInspectionType'); + + /** @var array $type */ + foreach ($types as $type) { + $response = new HttpResponse(); + $request = new HttpRequest(new HttpUri('')); + + $request->header->account = 1; + $request->setData('name', $type['name'] ?? ''); + $request->setData('title', \reset($type['l11n'])); + $request->setData('language', \array_keys($type['l11n'])[0] ?? 'en'); + + $module->apiInspectionTypeCreate($request, $response); + + $responseData = $response->getData(''); + if (!\is_array($responseData)) { + continue; + } + + $inspectionTypes[$type['name']] = \is_array($responseData['response']) + ? $responseData['response'] + : $responseData['response']->toArray(); + + $isFirst = true; + foreach ($type['l11n'] as $language => $l11n) { + if ($isFirst) { + $isFirst = false; + continue; + } + + $response = new HttpResponse(); + $request = new HttpRequest(new HttpUri('')); + + $request->header->account = 1; + $request->setData('title', $l11n); + $request->setData('language', $language); + $request->setData('type', $inspectionTypes[$type['name']]['id']); + + $module->apiInspectionTypeL11nCreate($request, $response); + } + } + + return $inspectionTypes; + } + + /** + * Install default attribute types + * + * @param ApplicationAbstract $app Application + * @param array $attributes Attribute definition + * + * @return array + * + * @since 1.0.0 + */ + private static function createAttributeTypes(ApplicationAbstract $app, array $attributes) : array + { + /** @var array $itemAttrType */ + $itemAttrType = []; + + /** @var \Modules\AssetManagement\Controller\ApiAssetAttributeController $module */ + $module = $app->moduleManager->getModuleInstance('AssetManagement', 'ApiAssetAttribute'); + + /** @var array $attribute */ + foreach ($attributes as $attribute) { + $response = new HttpResponse(); + $request = new HttpRequest(new HttpUri('')); + + $request->header->account = 1; + $request->setData('name', $attribute['name'] ?? ''); + $request->setData('title', \reset($attribute['l11n'])); + $request->setData('language', \array_keys($attribute['l11n'])[0] ?? 'en'); + $request->setData('is_required', $attribute['is_required'] ?? false); + $request->setData('custom', $attribute['is_custom_allowed'] ?? false); + $request->setData('validation_pattern', $attribute['validation_pattern'] ?? ''); + $request->setData('datatype', (int) $attribute['value_type']); + + $module->apiAssetAttributeTypeCreate($request, $response); + + $responseData = $response->getData(''); + if (!\is_array($responseData)) { + continue; + } + + $itemAttrType[$attribute['name']] = \is_array($responseData['response']) + ? $responseData['response'] + : $responseData['response']->toArray(); + + $isFirst = true; + foreach ($attribute['l11n'] as $language => $l11n) { + if ($isFirst) { + $isFirst = false; + continue; + } + + $response = new HttpResponse(); + $request = new HttpRequest(new HttpUri('')); + + $request->header->account = 1; + $request->setData('title', $l11n); + $request->setData('language', $language); + $request->setData('type', $itemAttrType[$attribute['name']]['id']); + + $module->apiAssetAttributeTypeL11nCreate($request, $response); + } + } + + return $itemAttrType; + } + + /** + * Create default attribute values for types + * + * @param ApplicationAbstract $app Application + * @param array $itemAttrType Attribute types + * @param array, is_required?:bool, is_custom_allowed?:bool, validation_pattern?:string, value_type?:string, values?:array}> $attributes Attribute definition + * + * @return array + * + * @since 1.0.0 + */ + private static function createAttributeValues(ApplicationAbstract $app, array $itemAttrType, array $attributes) : array + { + /** @var array $itemAttrValue */ + $itemAttrValue = []; + + /** @var \Modules\AssetManagement\Controller\ApiAssetAttributeController $module */ + $module = $app->moduleManager->getModuleInstance('AssetManagement', 'ApiAssetAttribute'); + + foreach ($attributes as $attribute) { + $itemAttrValue[$attribute['name']] = []; + + /** @var array $value */ + foreach ($attribute['values'] as $value) { + $response = new HttpResponse(); + $request = new HttpRequest(new HttpUri('')); + + $request->header->account = 1; + $request->setData('value', $value['value'] ?? ''); + $request->setData('unit', $value['unit'] ?? ''); + $request->setData('default', true); // always true since all defined values are possible default values + $request->setData('type', $itemAttrType[$attribute['name']]['id']); + + if (isset($value['l11n']) && !empty($value['l11n'])) { + $request->setData('title', \reset($value['l11n'])); + $request->setData('language', \array_keys($value['l11n'])[0] ?? 'en'); + } + + $module->apiAssetAttributeValueCreate($request, $response); + + $responseData = $response->getData(''); + if (!\is_array($responseData)) { + continue; + } + + $attrValue = \is_array($responseData['response']) + ? $responseData['response'] + : $responseData['response']->toArray(); + + $itemAttrValue[$attribute['name']][] = $attrValue; + + $isFirst = true; + foreach (($value['l11n'] ?? []) as $language => $l11n) { + if ($isFirst) { + $isFirst = false; + continue; + } + + $response = new HttpResponse(); + $request = new HttpRequest(new HttpUri('')); + + $request->header->account = 1; + $request->setData('title', $l11n); + $request->setData('language', $language); + $request->setData('value', $attrValue['id']); + + $module->apiAssetAttributeValueL11nCreate($request, $response); + } + } + } + + return $itemAttrValue; + } } diff --git a/Admin/Routes/Web/Backend.php b/Admin/Routes/Web/Backend.php index 0394b78..66d4c6d 100644 --- a/Admin/Routes/Web/Backend.php +++ b/Admin/Routes/Web/Backend.php @@ -1,11 +1,34 @@ [ + [ + 'dest' => '\Modules\EquipmentManagement\Controller\BackendController:viewEquipmentManagementAttributeTypeList', + 'verb' => RouteVerb::GET, + 'permission' => [ + 'module' => BackendController::NAME, + 'type' => PermissionType::READ, + 'state' => PermissionCategory::ASSET, + ], + ], + ], + '^.*/accounting/attribute/type\?.*$' => [ + [ + 'dest' => '\Modules\EquipmentManagement\Controller\BackendController:viewEquipmentManagementAttributeType', + 'verb' => RouteVerb::GET, + 'permission' => [ + 'module' => BackendController::NAME, + 'type' => PermissionType::READ, + 'state' => PermissionCategory::ASSET, + ], + ], + ], + '^.*/accounting/asset/list.*$' => [ [ 'dest' => '\Modules\AssetManagement\Controller\BackendController:viewAssetManagementList', @@ -13,8 +36,63 @@ return [ 'permission' => [ 'module' => BackendController::MODULE_NAME, 'type' => PermissionType::READ, - 'state' => PermissionState::ASSET, + 'state' => PermissionCategory::ASSET, ], ], ], + '^.*/accounting/asset/profile.*$' => [ + [ + 'dest' => '\Modules\AssetManagement\Controller\BackendController:viewAssetManagementProfile', + 'verb' => RouteVerb::GET, + 'permission' => [ + 'module' => BackendController::MODULE_NAME, + 'type' => PermissionType::READ, + 'state' => PermissionCategory::ASSET, + ], + ], + ], + '^.*/accounting/asset/entry/list.*$' => [ + [ + 'dest' => '\Modules\AssetManagement\Controller\BackendController:viewAssetManagementEntryList', + 'verb' => RouteVerb::GET, + 'permission' => [ + 'module' => BackendController::MODULE_NAME, + 'type' => PermissionType::READ, + 'state' => PermissionCategory::ASSET, + ], + ], + ], + '^.*/accounting/asset/entry/view.*$' => [ + [ + 'dest' => '\Modules\AssetManagement\Controller\BackendController:viewAssetManagementEntryView', + 'verb' => RouteVerb::GET, + 'permission' => [ + 'module' => BackendController::MODULE_NAME, + 'type' => PermissionType::READ, + 'state' => PermissionCategory::ASSET, + ], + ], + ], + '^.*/accounting/asset/create.*$' => [ + [ + 'dest' => '\Modules\AssetManagement\Controller\BackendController:viewAssetManagementCreate', + 'verb' => RouteVerb::GET, + 'permission' => [ + 'module' => BackendController::MODULE_NAME, + 'type' => PermissionType::READ, + 'state' => PermissionCategory::ASSET, + ], + ], + ], + '^.*/accounting/asset/table.*$' => [ + [ + 'dest' => '\Modules\AssetManagement\Controller\BackendController:viewAssetManagementAssetTable', + 'verb' => RouteVerb::GET, + 'permission' => [ + 'module' => BackendController::MODULE_NAME, + 'type' => PermissionType::READ, + 'state' => PermissionCategory::ASSET, + ], + ], + ] ]; diff --git a/Controller/ApiAssetAttributeController.php b/Controller/ApiAssetAttributeController.php new file mode 100644 index 0000000..5dfdf71 --- /dev/null +++ b/Controller/ApiAssetAttributeController.php @@ -0,0 +1,524 @@ +validateAttributeCreate($request))) { + $response->header->status = RequestStatusCode::R_400; + $this->createInvalidCreateResponse($request, $response, $val); + + return; + } + + $type = AssetAttributeTypeMapper::get()->with('defaults')->where('id', (int) $request->getData('type'))->execute(); + $attribute = $this->createAttributeFromRequest($request, $type); + $this->createModel($request->header->account, $attribute, AssetAttributeMapper::class, 'attribute', $request->getOrigin()); + $this->createStandardCreateResponse($request, $response, $attribute); + } + + /** + * Api method to create asset attribute l11n + * + * @param RequestAbstract $request Request + * @param ResponseAbstract $response Response + * @param array $data Generic data + * + * @return void + * + * @api + * + * @since 1.0.0 + */ + public function apiAssetAttributeTypeL11nCreate(RequestAbstract $request, ResponseAbstract $response, array $data = []) : void + { + if (!empty($val = $this->validateAttributeTypeL11nCreate($request))) { + $response->header->status = RequestStatusCode::R_400; + $this->createInvalidCreateResponse($request, $response, $val); + + return; + } + + $attrL11n = $this->createAttributeTypeL11nFromRequest($request); + $this->createModel($request->header->account, $attrL11n, AssetAttributeTypeL11nMapper::class, 'attr_type_l11n', $request->getOrigin()); + $this->createStandardCreateResponse($request, $response, $attrL11n); + } + + /** + * Api method to create asset attribute type + * + * @param RequestAbstract $request Request + * @param ResponseAbstract $response Response + * @param array $data Generic data + * + * @return void + * + * @api + * + * @since 1.0.0 + */ + public function apiAssetAttributeTypeCreate(RequestAbstract $request, ResponseAbstract $response, array $data = []) : void + { + if (!empty($val = $this->validateAttributeTypeCreate($request))) { + $response->header->status = RequestStatusCode::R_400; + $this->createInvalidCreateResponse($request, $response, $val); + + return; + } + + $attrType = $this->createAttributeTypeFromRequest($request); + $this->createModel($request->header->account, $attrType, AssetAttributeTypeMapper::class, 'attr_type', $request->getOrigin()); + $this->createStandardCreateResponse($request, $response, $attrType); + } + + /** + * Api method to create asset attribute value + * + * @param RequestAbstract $request Request + * @param ResponseAbstract $response Response + * @param array $data Generic data + * + * @return void + * + * @api + * + * @since 1.0.0 + */ + public function apiAssetAttributeValueCreate(RequestAbstract $request, ResponseAbstract $response, array $data = []) : void + { + if (!empty($val = $this->validateAttributeValueCreate($request))) { + $response->header->status = RequestStatusCode::R_400; + $this->createInvalidCreateResponse($request, $response, $val); + + return; + } + + /** @var \Modules\Attribute\Models\AttributeType $type */ + $type = AssetAttributeTypeMapper::get() + ->where('id', $request->getDataInt('type') ?? 0) + ->execute(); + + $attrValue = $this->createAttributeValueFromRequest($request, $type); + $this->createModel($request->header->account, $attrValue, AssetAttributeValueMapper::class, 'attr_value', $request->getOrigin()); + + if ($attrValue->isDefault) { + $this->createModelRelation( + $request->header->account, + (int) $request->getData('type'), + $attrValue->id, + AssetAttributeTypeMapper::class, 'defaults', '', $request->getOrigin() + ); + } + + $this->createStandardCreateResponse($request, $response, $attrValue); + } + + /** + * Api method to create asset attribute l11n + * + * @param RequestAbstract $request Request + * @param ResponseAbstract $response Response + * @param array $data Generic data + * + * @return void + * + * @api + * + * @since 1.0.0 + */ + public function apiAssetAttributeValueL11nCreate(RequestAbstract $request, ResponseAbstract $response, array $data = []) : void + { + if (!empty($val = $this->validateAttributeValueL11nCreate($request))) { + $response->header->status = RequestStatusCode::R_400; + $this->createInvalidCreateResponse($request, $response, $val); + + return; + } + + $attrL11n = $this->createAttributeValueL11nFromRequest($request); + $this->createModel($request->header->account, $attrL11n, AssetAttributeValueL11nMapper::class, 'attr_value_l11n', $request->getOrigin()); + $this->createStandardCreateResponse($request, $response, $attrL11n); + } + + /** + * Api method to update AssetAttribute + * + * @param RequestAbstract $request Request + * @param ResponseAbstract $response Response + * @param array $data Generic data + * + * @return void + * + * @api + * + * @since 1.0.0 + */ + public function apiAssetAttributeUpdate(RequestAbstract $request, ResponseAbstract $response, array $data = []) : void + { + if (!empty($val = $this->validateAttributeUpdate($request))) { + $response->header->status = RequestStatusCode::R_400; + $this->createInvalidUpdateResponse($request, $response, $val); + + return; + } + + /** @var Attribute $old */ + $old = AssetAttributeMapper::get() + ->with('type') + ->with('type/defaults') + ->with('value') + ->where('id', (int) $request->getData('id')) + ->execute(); + + $new = $this->updateAttributeFromRequest($request, clone $old); + + if ($new->id === 0) { + // Set response header to invalid request because of invalid data + $response->header->status = RequestStatusCode::R_400; + $this->createInvalidUpdateResponse($request, $response, $new); + + return; + } + + $this->updateModel($request->header->account, $old, $new, AssetAttributeMapper::class, 'asset_attribute', $request->getOrigin()); + + if ($new->value->getValue() !== $old->value->getValue() + && $new->type->custom + ) { + $this->updateModel($request->header->account, $old->value, $new->value, AssetAttributeValueMapper::class, 'attribute_value', $request->getOrigin()); + } + + $this->createStandardUpdateResponse($request, $response, $new); + } + + /** + * Api method to delete AssetAttribute + * + * @param RequestAbstract $request Request + * @param ResponseAbstract $response Response + * @param array $data Generic data + * + * @return void + * + * @api + * + * @since 1.0.0 + */ + public function apiAssetAttributeDelete(RequestAbstract $request, ResponseAbstract $response, array $data = []) : void + { + if (!empty($val = $this->validateAttributeDelete($request))) { + $response->header->status = RequestStatusCode::R_400; + $this->createInvalidDeleteResponse($request, $response, $val); + + return; + } + + $assetAttribute = AssetAttributeMapper::get() + ->with('type') + ->where('id', (int) $request->getData('id')) + ->execute(); + + if ($assetAttribute->type->isRequired) { + $this->createInvalidDeleteResponse($request, $response, []); + + return; + } + + $this->deleteModel($request->header->account, $assetAttribute, AssetAttributeMapper::class, 'asset_attribute', $request->getOrigin()); + $this->createStandardDeleteResponse($request, $response, $assetAttribute); + } + + /** + * Api method to update AssetAttributeTypeL11n + * + * @param RequestAbstract $request Request + * @param ResponseAbstract $response Response + * @param array $data Generic data + * + * @return void + * + * @api + * + * @since 1.0.0 + */ + public function apiAssetAttributeTypeL11nUpdate(RequestAbstract $request, ResponseAbstract $response, array $data = []) : void + { + if (!empty($val = $this->validateAttributeTypeL11nUpdate($request))) { + $response->header->status = RequestStatusCode::R_400; + $this->createInvalidUpdateResponse($request, $response, $val); + + return; + } + + /** @var BaseStringL11n $old */ + $old = AssetAttributeTypeL11nMapper::get()->where('id', (int) $request->getData('id'))->execute(); + $new = $this->updateAttributeTypeL11nFromRequest($request, clone $old); + + $this->updateModel($request->header->account, $old, $new, AssetAttributeTypeL11nMapper::class, 'asset_attribute_type_l11n', $request->getOrigin()); + $this->createStandardUpdateResponse($request, $response, $new); + } + + /** + * Api method to delete AssetAttributeTypeL11n + * + * @param RequestAbstract $request Request + * @param ResponseAbstract $response Response + * @param array $data Generic data + * + * @return void + * + * @api + * + * @since 1.0.0 + */ + public function apiAssetAttributeTypeL11nDelete(RequestAbstract $request, ResponseAbstract $response, array $data = []) : void + { + if (!empty($val = $this->validateAttributeTypeL11nDelete($request))) { + $response->header->status = RequestStatusCode::R_400; + $this->createInvalidDeleteResponse($request, $response, $val); + + return; + } + + /** @var BaseStringL11n $assetAttributeTypeL11n */ + $assetAttributeTypeL11n = AssetAttributeTypeL11nMapper::get()->where('id', (int) $request->getData('id'))->execute(); + $this->deleteModel($request->header->account, $assetAttributeTypeL11n, AssetAttributeTypeL11nMapper::class, 'asset_attribute_type_l11n', $request->getOrigin()); + $this->createStandardDeleteResponse($request, $response, $assetAttributeTypeL11n); + } + + /** + * Api method to update AssetAttributeType + * + * @param RequestAbstract $request Request + * @param ResponseAbstract $response Response + * @param array $data Generic data + * + * @return void + * + * @api + * + * @since 1.0.0 + */ + public function apiAssetAttributeTypeUpdate(RequestAbstract $request, ResponseAbstract $response, array $data = []) : void + { + if (!empty($val = $this->validateAttributeTypeUpdate($request))) { + $response->header->status = RequestStatusCode::R_400; + $this->createInvalidUpdateResponse($request, $response, $val); + + return; + } + + /** @var AttributeType $old */ + $old = AssetAttributeTypeMapper::get()->with('defaults')->where('id', (int) $request->getData('id'))->execute(); + $new = $this->updateAttributeTypeFromRequest($request, clone $old); + + $this->updateModel($request->header->account, $old, $new, AssetAttributeTypeMapper::class, 'asset_attribute_type', $request->getOrigin()); + $this->createStandardUpdateResponse($request, $response, $new); + } + + /** + * Api method to delete AssetAttributeType + * + * @param RequestAbstract $request Request + * @param ResponseAbstract $response Response + * @param array $data Generic data + * + * @return void + * + * @api + * + * @todo Implement API function + * + * @since 1.0.0 + */ + public function apiAssetAttributeTypeDelete(RequestAbstract $request, ResponseAbstract $response, array $data = []) : void + { + if (!empty($val = $this->validateAttributeTypeDelete($request))) { + $response->header->status = RequestStatusCode::R_400; + $this->createInvalidDeleteResponse($request, $response, $val); + + return; + } + + /** @var AttributeType $assetAttributeType */ + $assetAttributeType = AssetAttributeTypeMapper::get()->with('defaults')->where('id', (int) $request->getData('id'))->execute(); + $this->deleteModel($request->header->account, $assetAttributeType, AssetAttributeTypeMapper::class, 'asset_attribute_type', $request->getOrigin()); + $this->createStandardDeleteResponse($request, $response, $assetAttributeType); + } + + /** + * Api method to update AssetAttributeValue + * + * @param RequestAbstract $request Request + * @param ResponseAbstract $response Response + * @param array $data Generic data + * + * @return void + * + * @api + * + * @since 1.0.0 + */ + public function apiAssetAttributeValueUpdate(RequestAbstract $request, ResponseAbstract $response, array $data = []) : void + { + if (!empty($val = $this->validateAttributeValueUpdate($request))) { + $response->header->status = RequestStatusCode::R_400; + $this->createInvalidUpdateResponse($request, $response, $val); + + return; + } + + /** @var AttributeValue $old */ + $old = AssetAttributeValueMapper::get()->where('id', (int) $request->getData('id'))->execute(); + + /** @var \Modules\Attribute\Models\Attribute $attr */ + $attr = AssetAttributeMapper::get() + ->with('type') + ->where('id', $request->getDataInt('attribute') ?? 0) + ->execute(); + + $new = $this->updateAttributeValueFromRequest($request, clone $old, $attr); + + $this->updateModel($request->header->account, $old, $new, AssetAttributeValueMapper::class, 'asset_attribute_value', $request->getOrigin()); + $this->createStandardUpdateResponse($request, $response, $new); + } + + /** + * Api method to delete AssetAttributeValue + * + * @param RequestAbstract $request Request + * @param ResponseAbstract $response Response + * @param array $data Generic data + * + * @return void + * + * @api + * + * @since 1.0.0 + */ + public function apiAssetAttributeValueDelete(RequestAbstract $request, ResponseAbstract $response, array $data = []) : void + { + // @todo I don't think values can be deleted? Only Attributes + // However, It should be possible to remove UNUSED default values + // either here or other function? + // if (!empty($val = $this->validateAttributeValueDelete($request))) { + // $response->header->status = RequestStatusCode::R_400; + // $this->createInvalidDeleteResponse($request, $response, $val); + + // return; + // } + + // /** @var \Modules\AssetManagement\Models\AssetAttributeValue $assetAttributeValue */ + // $assetAttributeValue = AssetAttributeValueMapper::get()->where('id', (int) $request->getData('id'))->execute(); + // $this->deleteModel($request->header->account, $assetAttributeValue, AssetAttributeValueMapper::class, 'asset_attribute_value', $request->getOrigin()); + // $this->createStandardDeleteResponse($request, $response, $assetAttributeValue); + } + + /** + * Api method to update AssetAttributeValueL11n + * + * @param RequestAbstract $request Request + * @param ResponseAbstract $response Response + * @param array $data Generic data + * + * @return void + * + * @api + * + * @since 1.0.0 + */ + public function apiAssetAttributeValueL11nUpdate(RequestAbstract $request, ResponseAbstract $response, array $data = []) : void + { + if (!empty($val = $this->validateAttributeValueL11nUpdate($request))) { + $response->header->status = RequestStatusCode::R_400; + $this->createInvalidUpdateResponse($request, $response, $val); + + return; + } + + /** @var BaseStringL11n $old */ + $old = AssetAttributeValueL11nMapper::get()->where('id', (int) $request->getData('id')); + $new = $this->updateAttributeValueL11nFromRequest($request, clone $old); + + $this->updateModel($request->header->account, $old, $new, AssetAttributeValueL11nMapper::class, 'asset_attribute_value_l11n', $request->getOrigin()); + $this->createStandardUpdateResponse($request, $response, $new); + } + + /** + * Api method to delete AssetAttributeValueL11n + * + * @param RequestAbstract $request Request + * @param ResponseAbstract $response Response + * @param array $data Generic data + * + * @return void + * + * @api + * + * @since 1.0.0 + */ + public function apiAssetAttributeValueL11nDelete(RequestAbstract $request, ResponseAbstract $response, array $data = []) : void + { + if (!empty($val = $this->validateAttributeValueL11nDelete($request))) { + $response->header->status = RequestStatusCode::R_400; + $this->createInvalidDeleteResponse($request, $response, $val); + + return; + } + + /** @var BaseStringL11n $assetAttributeValueL11n */ + $assetAttributeValueL11n = AssetAttributeValueL11nMapper::get()->where('id', (int) $request->getData('id'))->execute(); + $this->deleteModel($request->header->account, $assetAttributeValueL11n, AssetAttributeValueL11nMapper::class, 'asset_attribute_value_l11n', $request->getOrigin()); + $this->createStandardDeleteResponse($request, $response, $assetAttributeValueL11n); + } +} diff --git a/Controller/ApiAssetController.php b/Controller/ApiAssetController.php new file mode 100644 index 0000000..bcf8680 --- /dev/null +++ b/Controller/ApiAssetController.php @@ -0,0 +1,634 @@ +validateAssetCreate($request))) { + $response->header->status = RequestStatusCode::R_400; + $this->createInvalidCreateResponse($request, $response, $val); + + return; + } + + /** @var Asset $asset */ + $asset = $this->createAssetFromRequest($request); + $this->createModel($request->header->account, $asset, AssetMapper::class, 'asset', $request->getOrigin()); + + if (!empty($request->files) + || !empty($request->getDataJson('media')) + ) { + $this->createAssetMedia($asset, $request); + } + + $this->createStandardCreateResponse($request, $response, $asset); + } + + /** + * Method to create asset from request. + * + * @param RequestAbstract $request Request + * + * @return Asset Returns the created asset from the request + * + * @since 1.0.0 + */ + public function createAssetFromRequest(RequestAbstract $request) : Asset + { + $asset = new Asset(); + $asset->name = $request->getDataString('name') ?? ''; + $asset->info = $request->getDataString('info') ?? ''; + $asset->type = new NullBaseStringL11nType((int) ($request->getDataInt('type') ?? 0)); + $asset->status = $request->getDataInt('status') ?? AssetStatus::INACTIVE; + $asset->unit = $request->getDataInt('unit') ?? $this->app->unitId; + + return $asset; + } + + /** + * Create media files for asset + * + * @param Asset $asset Asset + * @param RequestAbstract $request Request incl. media do upload + * + * @return void + * + * @since 1.0.0 + */ + private function createAssetMedia(Asset $asset, RequestAbstract $request) : void + { + $path = $this->createAssetDir($asset); + + if (!empty($uploadedFiles = $request->files)) { + $uploaded = $this->app->moduleManager->get('Media')->uploadFiles( + names: [], + fileNames: [], + files: $uploadedFiles, + account: $request->header->account, + basePath: __DIR__ . '/../../../Modules/Media/Files' . $path, + virtualPath: $path, + pathSettings: PathSettings::FILE_PATH + ); + + $collection = null; + foreach ($uploaded as $media) { + $this->createModelRelation( + $request->header->account, + $asset->id, + $media->id, + AssetMapper::class, + 'files', + '', + $request->getOrigin() + ); + + if ($collection === null) { + /** @var \Modules\Media\Models\Collection $collection */ + $collection = MediaMapper::getParentCollection($path)->limit(1)->execute(); + + if ($collection->id === 0) { + $collection = $this->app->moduleManager->get('Media')->createRecursiveMediaCollection( + $path, + $request->header->account, + __DIR__ . '/../../../Modules/Media/Files' . $path + ); + } + } + + $this->createModelRelation( + $request->header->account, + $collection->id, + $media->id, + CollectionMapper::class, + 'sources', + '', + $request->getOrigin() + ); + } + } + + if (!empty($mediaFiles = $request->getDataJson('media'))) { + $collection = null; + + foreach ($mediaFiles as $file) { + /** @var \Modules\Media\Models\Media $media */ + $media = MediaMapper::get()->where('id', (int) $file)->limit(1)->execute(); + + $this->createModelRelation( + $request->header->account, + $asset->id, + $media->id, + AssetMapper::class, + 'files', + '', + $request->getOrigin() + ); + + $ref = new Reference(); + $ref->name = $media->name; + $ref->source = new NullMedia($media->id); + $ref->createdBy = new NullAccount($request->header->account); + $ref->setVirtualPath($path); + + $this->createModel($request->header->account, $ref, ReferenceMapper::class, 'media_reference', $request->getOrigin()); + + if ($collection === null) { + /** @var \Modules\Media\Models\Collection $collection */ + $collection = MediaMapper::getParentCollection($path)->limit(1)->execute(); + + if ($collection->id === 0) { + $collection = $this->app->moduleManager->get('Media')->createRecursiveMediaCollection( + $path, + $request->header->account, + __DIR__ . '/../../../Modules/Media/Files' . $path + ); + } + } + + $this->createModelRelation( + $request->header->account, + $collection->id, + $ref->id, + CollectionMapper::class, + 'sources', + '', + $request->getOrigin() + ); + } + } + } + + /** + * Validate asset create request + * + * @param RequestAbstract $request Request + * + * @return array Returns the validation array of the request + * + * @since 1.0.0 + */ + private function validateAssetCreate(RequestAbstract $request) : array + { + $val = []; + if (($val['name'] = !$request->hasData('name')) + || ($val['type'] = !$request->hasData('type')) + ) { + return $val; + } + + return []; + } + + /** + * Api method to create a bill + * + * @param RequestAbstract $request Request + * @param ResponseAbstract $response Response + * @param array $data Generic data + * + * @return void + * + * @api + * + * @since 1.0.0 + */ + public function apiMediaAddToAsset(RequestAbstract $request, ResponseAbstract $response, array $data = []) : void + { + if (!empty($val = $this->validateMediaAddToAsset($request))) { + $response->header->status = RequestStatusCode::R_400; + $this->createInvalidAddResponse($request, $response, $val); + + return; + } + + /** @var \Modules\AssetManagement\Models\Asset $asset */ + $asset = AssetMapper::get()->where('id', (int) $request->getData('asset'))->execute(); + $path = $this->createAssetDir($asset); + + $uploaded = []; + if (!empty($uploadedFiles = $request->files)) { + $uploaded = $this->app->moduleManager->get('Media')->uploadFiles( + names: [], + fileNames: [], + files: $uploadedFiles, + account: $request->header->account, + basePath: __DIR__ . '/../../../Modules/Media/Files' . $path, + virtualPath: $path, + pathSettings: PathSettings::FILE_PATH, + hasAccountRelation: false, + readContent: $request->getDataBool('parse_content') ?? false + ); + + $collection = null; + foreach ($uploaded as $media) { + $this->createModelRelation( + $request->header->account, + $asset->id, + $media->id, + AssetMapper::class, + 'files', + '', + $request->getOrigin() + ); + + if ($request->hasData('type')) { + $this->createModelRelation( + $request->header->account, + $media->id, + $request->getDataInt('type'), + MediaMapper::class, + 'types', + '', + $request->getOrigin() + ); + } + + if ($collection === null) { + /** @var \Modules\Media\Models\Collection $collection */ + $collection = MediaMapper::getParentCollection($path)->limit(1)->execute(); + + if ($collection->id === 0) { + $collection = $this->app->moduleManager->get('Media')->createRecursiveMediaCollection( + $path, + $request->header->account, + __DIR__ . '/../../../Modules/Media/Files' . $path, + ); + } + } + + $this->createModelRelation( + $request->header->account, + $collection->id, + $media->id, + CollectionMapper::class, + 'sources', + '', + $request->getOrigin() + ); + } + } + + if (!empty($mediaFiles = $request->getDataJson('media'))) { + foreach ($mediaFiles as $media) { + $this->createModelRelation( + $request->header->account, + $asset->id, + (int) $media, + AssetMapper::class, + 'files', + '', + $request->getOrigin() + ); + } + } + + $this->fillJsonResponse($request, $response, NotificationLevel::OK, 'Media', 'Media added to asset.', [ + 'upload' => $uploaded, + 'media' => $mediaFiles, + ]); + } + + /** + * Create media directory path + * + * @param Asset $asset Asset + * + * @return string + * + * @since 1.0.0 + */ + private function createAssetDir(Asset $asset) : string + { + return '/Modules/AssetManagement/Asset/' + . $this->app->unitId . '/' + . $asset->id; + } + + /** + * Method to validate bill creation from request + * + * @param RequestAbstract $request Request + * + * @return array + * + * @since 1.0.0 + */ + private function validateMediaAddToAsset(RequestAbstract $request) : array + { + $val = []; + if (($val['media'] = (!$request->hasData('media') && empty($request->files))) + || ($val['asset'] = !$request->hasData('asset')) + ) { + return $val; + } + + return []; + } + + /** + * Api method to create notes + * + * @param RequestAbstract $request Request + * @param ResponseAbstract $response Response + * @param array $data Generic data + * + * @return void + * + * @api + * + * @since 1.0.0 + */ + public function apiNoteCreate(RequestAbstract $request, ResponseAbstract $response, array $data = []) : void + { + if (!empty($val = $this->validateNoteCreate($request))) { + $response->header->status = RequestStatusCode::R_400; + $this->createInvalidCreateResponse($request, $response, $val); + + return; + } + + $request->setData('virtualpath', '/Modules/AssetManagement/Asset/' . $request->getData('id'), true); + $this->app->moduleManager->get('Editor', 'Api')->apiEditorCreate($request, $response, $data); + + if ($response->header->status !== RequestStatusCode::R_200) { + return; + } + + $responseData = $response->getDataArray($request->uri->__toString()); + if (!\is_array($responseData)) { + return; + } + + $model = $responseData['response']; + $this->createModelRelation($request->header->account, (int) $request->getData('id'), $model->id, AssetMapper::class, 'notes', '', $request->getOrigin()); + } + + /** + * Validate item note create request + * + * @param RequestAbstract $request Request + * + * @return array + * + * @since 1.0.0 + */ + private function validateNoteCreate(RequestAbstract $request) : array + { + $val = []; + if (($val['id'] = !$request->hasData('id')) + ) { + return $val; + } + + return []; + } + + /** + * Api method to update Asset + * + * @param RequestAbstract $request Request + * @param ResponseAbstract $response Response + * @param array $data Generic data + * + * @return void + * + * @api + * + * @since 1.0.0 + */ + public function apiAssetFind(RequestAbstract $request, ResponseAbstract $response, array $data = []) : void + { + } + + /** + * Api method to update Asset + * + * @param RequestAbstract $request Request + * @param ResponseAbstract $response Response + * @param array $data Generic data + * + * @return void + * + * @api + * + * @since 1.0.0 + */ + public function apiAssetUpdate(RequestAbstract $request, ResponseAbstract $response, array $data = []) : void + { + if (!empty($val = $this->validateAssetUpdate($request))) { + $response->header->status = RequestStatusCode::R_400; + $this->createInvalidUpdateResponse($request, $response, $val); + + return; + } + + /** @var \Modules\AssetManagement\Models\Asset $old */ + $old = AssetMapper::get()->where('id', (int) $request->getData('id'))->execute(); + $new = $this->updateAssetFromRequest($request, clone $old); + + $this->updateModel($request->header->account, $old, $new, AssetMapper::class, 'asset', $request->getOrigin()); + $this->createStandardUpdateResponse($request, $response, $new); + } + + /** + * Method to update Asset from request. + * + * @param RequestAbstract $request Request + * @param Asset $new Model to modify + * + * @return Asset + * + * @todo Implement API update function + * + * @since 1.0.0 + */ + public function updateAssetFromRequest(RequestAbstract $request, Asset $new) : Asset + { + $new->name = $request->getDataString('name') ?? $new->name; + $new->info = $request->getDataString('info') ?? $new->info; + $new->type = $request->hasData('type') ? new NullBaseStringL11nType((int) ($request->getDataInt('type') ?? 0)) : $new->type; + $new->status = $request->getDataInt('status') ?? $new->status; + $new->unit = $request->getDataInt('unit') ?? $this->app->unitId; + + return $new; + } + + /** + * Validate Asset update request + * + * @param RequestAbstract $request Request + * + * @return array + * + * @todo Implement API validation function + * + * @since 1.0.0 + */ + private function validateAssetUpdate(RequestAbstract $request) : array + { + $val = []; + if (($val['id'] = !$request->hasData('id'))) { + return $val; + } + + return []; + } + + /** + * Api method to delete Asset + * + * @param RequestAbstract $request Request + * @param ResponseAbstract $response Response + * @param array $data Generic data + * + * @return void + * + * @api + * + * @since 1.0.0 + */ + public function apiAssetDelete(RequestAbstract $request, ResponseAbstract $response, array $data = []) : void + { + if (!empty($val = $this->validateAssetDelete($request))) { + $response->header->status = RequestStatusCode::R_400; + $this->createInvalidDeleteResponse($request, $response, $val); + + return; + } + + /** @var \Modules\AssetManagement\Models\Asset $asset */ + $asset = AssetMapper::get()->where('id', (int) $request->getData('id'))->execute(); + $this->deleteModel($request->header->account, $asset, AssetMapper::class, 'asset', $request->getOrigin()); + $this->createStandardDeleteResponse($request, $response, $asset); + } + + /** + * Validate Asset delete request + * + * @param RequestAbstract $request Request + * + * @return array + * + * @todo Implement API validation function + * + * @since 1.0.0 + */ + private function validateAssetDelete(RequestAbstract $request) : array + { + $val = []; + if (($val['id'] = !$request->hasData('id'))) { + return $val; + } + + return []; + } + + /** + * Api method to update Note + * + * @param RequestAbstract $request Request + * @param ResponseAbstract $response Response + * @param array $data Generic data + * + * @return void + * + * @api + * + * @since 1.0.0 + */ + public function apiNoteUpdate(RequestAbstract $request, ResponseAbstract $response, array $data = []) : void + { + $accountId = $request->header->account; + if (!$this->app->accountManager->get($accountId)->hasPermission( + PermissionType::MODIFY, $this->app->unitId, $this->app->appId, self::NAME, PermissionCategory::ASSET_NOTE, $request->getDataInt('id')) + ) { + $this->fillJsonResponse($request, $response, NotificationLevel::HIDDEN, '', '', []); + $response->header->status = RequestStatusCode::R_403; + + return; + } + + $this->app->moduleManager->get('Editor', 'Api')->apiEditorUpdate($request, $response, $data); + } + + /** + * Api method to delete Note + * + * @param RequestAbstract $request Request + * @param ResponseAbstract $response Response + * @param array $data Generic data + * + * @return void + * + * @api + * + * @since 1.0.0 + */ + public function apiNoteDelete(RequestAbstract $request, ResponseAbstract $response, array $data = []) : void + { + $accountId = $request->header->account; + if (!$this->app->accountManager->get($accountId)->hasPermission( + PermissionType::DELETE, $this->app->unitId, $this->app->appId, self::NAME, PermissionCategory::ASSET_NOTE, $request->getDataInt('id')) + ) { + $this->fillJsonResponse($request, $response, NotificationLevel::HIDDEN, '', '', []); + $response->header->status = RequestStatusCode::R_403; + + return; + } + + $this->app->moduleManager->get('Editor', 'Api')->apiEditorDelete($request, $response, $data); + } +} diff --git a/Controller/ApiAssetTypeController.php b/Controller/ApiAssetTypeController.php new file mode 100644 index 0000000..ddf2229 --- /dev/null +++ b/Controller/ApiAssetTypeController.php @@ -0,0 +1,405 @@ +validateAssetTypeCreate($request))) { + $response->header->status = RequestStatusCode::R_400; + $this->createInvalidCreateResponse($request, $response, $val); + + return; + } + + $assetType = $this->createAssetTypeFromRequest($request); + $this->createModel($request->header->account, $assetType, AssetTypeMapper::class, 'asset_type', $request->getOrigin()); + $this->createStandardCreateResponse($request, $response, $assetType); + } + + /** + * Method to create item attribute from request. + * + * @param RequestAbstract $request Request + * + * @return AssetType + * + * @since 1.0.0 + */ + private function createAssetTypeFromRequest(RequestAbstract $request) : AssetType + { + $assetType = new AssetType(); + $assetType->setL11n($request->getDataString('title') ?? '', $request->getDataString('language') ?? ISO639x1Enum::_EN); + $assetType->depreciationDuration = $request->getDataInt('duration') ?? 0; + $assetType->industry = $request->getDataInt('industry') ?? 0; + + return $assetType; + } + + /** + * Validate item attribute create request + * + * @param RequestAbstract $request Request + * + * @return array + * + * @since 1.0.0 + */ + private function validateAssetTypeCreate(RequestAbstract $request) : array + { + $val = []; + if (($val['title'] = !$request->hasData('title')) + ) { + return $val; + } + + return []; + } + + /** + * Api method to create item attribute l11n + * + * @param RequestAbstract $request Request + * @param ResponseAbstract $response Response + * @param array $data Generic data + * + * @return void + * + * @api + * + * @since 1.0.0 + */ + public function apiAssetTypeL11nCreate(RequestAbstract $request, ResponseAbstract $response, array $data = []) : void + { + if (!empty($val = $this->validateAssetTypeL11nCreate($request))) { + $response->header->status = RequestStatusCode::R_400; + $this->createInvalidCreateResponse($request, $response, $val); + + return; + } + + $assetTypeL11n = $this->createAssetTypeL11nFromRequest($request); + $this->createModel($request->header->account, $assetTypeL11n, AssetTypeL11nMapper::class, 'asset_type_l11n', $request->getOrigin()); + $this->createStandardCreateResponse($request, $response, $assetTypeL11n); + } + + /** + * Method to create item attribute l11n from request. + * + * @param RequestAbstract $request Request + * + * @return BaseStringL11n + * + * @since 1.0.0 + */ + private function createAssetTypeL11nFromRequest(RequestAbstract $request) : BaseStringL11n + { + $assetTypeL11n = new BaseStringL11n(); + $assetTypeL11n->ref = $request->getDataInt('type') ?? 0; + $assetTypeL11n->setLanguage( + $request->getDataString('language') ?? $request->header->l11n->language + ); + $assetTypeL11n->content = $request->getDataString('title') ?? ''; + + return $assetTypeL11n; + } + + /** + * Validate item attribute l11n create request + * + * @param RequestAbstract $request Request + * + * @return array + * + * @since 1.0.0 + */ + private function validateAssetTypeL11nCreate(RequestAbstract $request) : array + { + $val = []; + if (($val['title'] = !$request->hasData('title')) + || ($val['type'] = !$request->hasData('type')) + ) { + return $val; + } + + return []; + } + + /** + * Api method to update AssetType + * + * @param RequestAbstract $request Request + * @param ResponseAbstract $response Response + * @param array $data Generic data + * + * @return void + * + * @api + * + * @since 1.0.0 + */ + public function apiAssetTypeUpdate(RequestAbstract $request, ResponseAbstract $response, array $data = []) : void + { + if (!empty($val = $this->validateAssetTypeUpdate($request))) { + $response->header->status = RequestStatusCode::R_400; + $this->createInvalidUpdateResponse($request, $response, $val); + + return; + } + + /** @var BaseStringL11nType $old */ + $old = AssetTypeMapper::get()->where('id', (int) $request->getData('id')); + $new = $this->updateAssetTypeFromRequest($request, clone $old); + + $this->updateModel($request->header->account, $old, $new, AssetTypeMapper::class, 'asset_type', $request->getOrigin()); + $this->createStandardUpdateResponse($request, $response, $new); + } + + /** + * Method to update AssetType from request. + * + * @param RequestAbstract $request Request + * @param BaseStringL11nType $new Model to modify + * + * @return BaseStringL11nType + * + * @todo Implement API update function + * + * @since 1.0.0 + */ + public function updateAssetTypeFromRequest(RequestAbstract $request, BaseStringL11nType $new) : BaseStringL11nType + { + $new->title = $request->getDataString('name') ?? $new->title; + + return $new; + } + + /** + * Validate AssetType update request + * + * @param RequestAbstract $request Request + * + * @return array + * + * @todo Implement API validation function + * + * @since 1.0.0 + */ + private function validateAssetTypeUpdate(RequestAbstract $request) : array + { + $val = []; + if (($val['id'] = !$request->hasData('id'))) { + return $val; + } + + return []; + } + + /** + * Api method to delete AssetType + * + * @param RequestAbstract $request Request + * @param ResponseAbstract $response Response + * @param array $data Generic data + * + * @return void + * + * @api + * + * @since 1.0.0 + */ + public function apiAssetTypeDelete(RequestAbstract $request, ResponseAbstract $response, array $data = []) : void + { + if (!empty($val = $this->validateAssetTypeDelete($request))) { + $response->header->status = RequestStatusCode::R_400; + $this->createInvalidDeleteResponse($request, $response, $val); + + return; + } + + /** @var BaseStringL11nType $assetType */ + $assetType = AssetTypeMapper::get()->where('id', (int) $request->getData('id'))->execute(); + $this->deleteModel($request->header->account, $assetType, AssetTypeMapper::class, 'asset_type', $request->getOrigin()); + $this->createStandardDeleteResponse($request, $response, $assetType); + } + + /** + * Validate AssetType delete request + * + * @param RequestAbstract $request Request + * + * @return array + * + * @since 1.0.0 + */ + private function validateAssetTypeDelete(RequestAbstract $request) : array + { + $val = []; + if (($val['id'] = !$request->hasData('id'))) { + return $val; + } + + return []; + } + + /** + * Api method to update AssetTypeL11n + * + * @param RequestAbstract $request Request + * @param ResponseAbstract $response Response + * @param array $data Generic data + * + * @return void + * + * @api + * + * @since 1.0.0 + */ + public function apiAssetTypeL11nUpdate(RequestAbstract $request, ResponseAbstract $response, array $data = []) : void + { + if (!empty($val = $this->validateAssetTypeL11nUpdate($request))) { + $response->header->status = RequestStatusCode::R_400; + $this->createInvalidUpdateResponse($request, $response, $val); + + return; + } + + /** @var BaseStringL11n $old */ + $old = AssetTypeL11nMapper::get()->where('id', (int) $request->getData('id')); + $new = $this->updateAssetTypeL11nFromRequest($request, clone $old); + + $this->updateModel($request->header->account, $old, $new, AssetTypeL11nMapper::class, 'asset_type_l11n', $request->getOrigin()); + $this->createStandardUpdateResponse($request, $response, $new); + } + + /** + * Method to update AssetTypeL11n from request. + * + * @param RequestAbstract $request Request + * @param BaseStringL11n $new Model to modify + * + * @return BaseStringL11n + * + * @since 1.0.0 + */ + public function updateAssetTypeL11nFromRequest(RequestAbstract $request, BaseStringL11n $new) : BaseStringL11n + { + $new->setLanguage( + $request->getDataString('language') ?? $new->language + ); + $new->content = $request->getDataString('title') ?? $new->content; + + return $new; + } + + /** + * Validate AssetTypeL11n update request + * + * @param RequestAbstract $request Request + * + * @return array + * + * @since 1.0.0 + */ + private function validateAssetTypeL11nUpdate(RequestAbstract $request) : array + { + $val = []; + if (($val['id'] = !$request->hasData('id'))) { + return $val; + } + + return []; + } + + /** + * Api method to delete AssetTypeL11n + * + * @param RequestAbstract $request Request + * @param ResponseAbstract $response Response + * @param array $data Generic data + * + * @return void + * + * @api + * + * @since 1.0.0 + */ + public function apiAssetTypeL11nDelete(RequestAbstract $request, ResponseAbstract $response, array $data = []) : void + { + if (!empty($val = $this->validateAssetTypeL11nDelete($request))) { + $response->header->status = RequestStatusCode::R_400; + $this->createInvalidDeleteResponse($request, $response, $val); + + return; + } + + /** @var BaseStringL11n $assetTypeL11n */ + $assetTypeL11n = AssetTypeL11nMapper::get()->where('id', (int) $request->getData('id'))->execute(); + $this->deleteModel($request->header->account, $assetTypeL11n, AssetTypeL11nMapper::class, 'asset_type_l11n', $request->getOrigin()); + $this->createStandardDeleteResponse($request, $response, $assetTypeL11n); + } + + /** + * Validate AssetTypeL11n delete request + * + * @param RequestAbstract $request Request + * + * @return array + * + * @since 1.0.0 + */ + private function validateAssetTypeL11nDelete(RequestAbstract $request) : array + { + $val = []; + if (($val['id'] = !$request->hasData('id'))) { + return $val; + } + + return []; + } +} diff --git a/Controller/BackendController.php b/Controller/BackendController.php index a8a0aa7..02d525f 100644 --- a/Controller/BackendController.php +++ b/Controller/BackendController.php @@ -14,7 +14,17 @@ declare(strict_types=1); namespace Modules\AssetManagement\Controller; +use Modules\Admin\Models\LocalizationMapper; +use Modules\Admin\Models\SettingsEnum; +use Modules\AssetManagement\Models\AssetMapper; +use Modules\AssetManagement\Models\AssetTypeMapper; +use Modules\AssetManagement\Models\Attribute\AssetAttributeTypeL11nMapper; +use Modules\AssetManagement\Models\Attribute\AssetAttributeTypeMapper; +use Modules\Media\Models\MediaMapper; +use Modules\Media\Models\MediaTypeMapper; +use Modules\Organization\Models\UnitMapper; use phpOMS\Contract\RenderableInterface; +use phpOMS\DataStorage\Database\Query\Builder; use phpOMS\Message\RequestAbstract; use phpOMS\Message\ResponseAbstract; use phpOMS\Views\View; @@ -30,7 +40,7 @@ use phpOMS\Views\View; final class BackendController extends Controller { /** - * Routing end-point for application behaviour. + * Routing end-point for application behavior. * * @param RequestAbstract $request Request * @param ResponseAbstract $response Response @@ -47,6 +57,134 @@ final class BackendController extends Controller $view->setTemplate('/Modules/AssetManagement/Theme/Backend/asset-list'); $view->data['nav'] = $this->app->moduleManager->get('Navigation')->createNavigationMid(1006601001, $request, $response); + $list = AssetMapper::getAll() + ->with('type') + ->with('type/l11n') + ->where('type/l11n/language', $response->header->l11n->language) + ->sort('id', 'DESC') + ->execute(); + + $view->data['assets'] = $list; + + return $view; + } + + /** + * Routing end-point for application behavior. + * + * @param RequestAbstract $request Request + * @param ResponseAbstract $response Response + * @param array $data Generic data + * + * @return RenderableInterface + * + * @since 1.0.0 + * @codeCoverageIgnore + */ + public function viewAssetManagementAttributeType(RequestAbstract $request, ResponseAbstract $response, $data = null) : RenderableInterface + { + $view = new View($this->app->l11nManager, $request, $response); + $view->setTemplate('/Modules/AssetManagement/Theme/Backend/asset-profile'); + $view->data['nav'] = $this->app->moduleManager->get('Navigation')->createNavigationMid(1006601001, $request, $response); + + /** @var \Modules\Attribute\Models\AttributeType $attribute */ + $attribute = AssetAttributeTypeMapper::get() + ->with('l11n') + ->where('id', (int) $request->getData('id')) + ->where('l11n/language', $response->header->l11n->language) + ->execute(); + + $l11ns = AssetAttributeTypeL11nMapper::getAll() + ->where('ref', $attribute->id) + ->execute(); + + $view->data['attribute'] = $attribute; + $view->data['l11ns'] = $l11ns; + + return $view; + } + + /** + * Routing end-point for application behavior. + * + * @param RequestAbstract $request Request + * @param ResponseAbstract $response Response + * @param array $data Generic data + * + * @return RenderableInterface Returns a renderable object + * + * @since 1.0.0 + * @codeCoverageIgnore + */ + public function viewAssetManagementAssetProfile(RequestAbstract $request, ResponseAbstract $response, array $data = []) : RenderableInterface + { + $view = new View($this->app->l11nManager, $request, $response); + + $view->setTemplate('/Modules/AssetManagement/Theme/Backend/asset-profile'); + $view->data['nav'] = $this->app->moduleManager->get('Navigation')->createNavigationMid(1008402001, $request, $response); + + // @todo This langauge filtering doesn't work. But it was working with the old mappers. Maybe there is a bug in the where() definition. Need to inspect the actual query. + $asset = AssetMapper::get() + ->with('attributes') + ->with('attributes/type') + ->with('attributes/value') + ->with('attributes/type/l11n') + ->with('files') + ->with('files/types') + ->with('type') + ->with('type/l11n') + ->where('id', (int) $request->getData('id')) + ->where('type/l11n/language', $response->header->l11n->language) + ->where('attributes/type/l11n/language', $response->header->l11n->language) + ->execute(); + + $view->data['asset'] = $asset; + + $query = new Builder($this->app->dbPool->get()); + $results = $query->selectAs(AssetMapper::HAS_MANY['files']['external'], 'file') + ->from(AssetMapper::TABLE) + ->leftJoin(AssetMapper::HAS_MANY['files']['table']) + ->on(AssetMapper::HAS_MANY['files']['table'] . '.' . AssetMapper::HAS_MANY['files']['self'], '=', AssetMapper::TABLE . '.' . AssetMapper::PRIMARYFIELD) + ->leftJoin(MediaMapper::TABLE) + ->on(AssetMapper::HAS_MANY['files']['table'] . '.' . AssetMapper::HAS_MANY['files']['external'], '=', MediaMapper::TABLE . '.' . MediaMapper::PRIMARYFIELD) + ->leftJoin(MediaMapper::HAS_MANY['types']['table']) + ->on(MediaMapper::TABLE . '.' . MediaMapper::PRIMARYFIELD, '=', MediaMapper::HAS_MANY['types']['table'] . '.' . MediaMapper::HAS_MANY['types']['self']) + ->leftJoin(MediaTypeMapper::TABLE) + ->on(MediaMapper::HAS_MANY['types']['table'] . '.' . MediaMapper::HAS_MANY['types']['external'], '=', MediaTypeMapper::TABLE . '.' . MediaTypeMapper::PRIMARYFIELD) + ->where(AssetMapper::HAS_MANY['files']['self'], '=', $asset->id) + ->where(MediaTypeMapper::TABLE . '.' . MediaTypeMapper::getColumnByMember('name'), '=', 'asset_profile_image'); + + $assetImage = MediaMapper::get() + ->with('types') + ->where('id', $results) + ->limit(1) + ->execute(); + + $view->data['assetImage'] = $assetImage; + + $assetTypes = AssetTypeMapper::getAll() + ->with('l11n') + ->where('l11n/language', $response->header->l11n->language) + ->execute(); + + $view->data['types'] = $assetTypes; + + $units = UnitMapper::getAll() + ->execute(); + + $view->data['units'] = $units; + + /** @var \Model\Setting $settings */ + $settings = $this->app->appSettings->get(null, [ + SettingsEnum::DEFAULT_LOCALIZATION, + ]); + + $view->data['attributeView'] = new \Modules\Attribute\Theme\Backend\Components\AttributeView($this->app->l11nManager, $request, $response); + $view->data['attributeView']->data['defaultlocalization'] = LocalizationMapper::get()->where('id', (int) $settings->id)->execute(); + + $view->data['media-upload'] = new \Modules\Media\Theme\Backend\Components\Upload\BaseView($this->app->l11nManager, $request, $response); + $view->data['asset-notes'] = new \Modules\Editor\Theme\Backend\Components\Compound\BaseView($this->app->l11nManager, $request, $response); + return $view; } } diff --git a/Docs/Dev/en/SUMMARY.md b/Docs/Dev/en/SUMMARY.md new file mode 100644 index 0000000..8a97952 --- /dev/null +++ b/Docs/Dev/en/SUMMARY.md @@ -0,0 +1,3 @@ +# Developer Content + +* [Structure]({%}&page=Dev/structure) diff --git a/Docs/Dev/en/structure.md b/Docs/Dev/en/structure.md new file mode 100644 index 0000000..8d70862 --- /dev/null +++ b/Docs/Dev/en/structure.md @@ -0,0 +1,5 @@ +# Structure + +## ER + +![ER](Modules/AssetManagement/Docs/Dev/img/er.png) \ No newline at end of file diff --git a/Docs/Dev/img/er.png b/Docs/Dev/img/er.png new file mode 100644 index 0000000000000000000000000000000000000000..95dff175c1a28ddb08bc8da8230a647efc9dcf02 GIT binary patch literal 53362 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 = new \DateTimeImmutable('now'); + $this->type = new BaseStringL11nType(); + } + + /** + * {@inheritdoc} + */ + public function toArray() : array + { + return [ + 'id' => $this->id, + ]; + } + + /** + * {@inheritdoc} + */ + public function jsonSerialize() : mixed + { + return $this->toArray(); + } + + use \Modules\Media\Models\MediaListTrait; + use \Modules\Editor\Models\EditorDocListTrait; + use \Modules\Attribute\Models\AttributeHolderTrait; +} diff --git a/Models/AssetMapper.php b/Models/AssetMapper.php new file mode 100644 index 0000000..bb3f8cf --- /dev/null +++ b/Models/AssetMapper.php @@ -0,0 +1,116 @@ + + */ +final class AssetMapper extends DataMapperFactory +{ + /** + * Columns. + * + * @var array + * @since 1.0.0 + */ + public const COLUMNS = [ + 'assetmgmt_asset_id' => ['name' => 'assetmgmt_asset_id', 'type' => 'int', 'internal' => 'id'], + 'assetmgmt_asset_name' => ['name' => 'assetmgmt_asset_name', 'type' => 'string', 'internal' => 'name'], + 'assetmgmt_asset_number' => ['name' => 'assetmgmt_asset_number', 'type' => 'string', 'internal' => 'number'], + 'assetmgmt_asset_status' => ['name' => 'assetmgmt_asset_status', 'type' => 'int', 'internal' => 'status'], + 'assetmgmt_asset_info' => ['name' => 'assetmgmt_asset_info', 'type' => 'string', 'internal' => 'info'], + 'assetmgmt_asset_unit' => ['name' => 'assetmgmt_asset_unit', 'type' => 'int', 'internal' => 'unit'], + 'assetmgmt_asset_type' => ['name' => 'assetmgmt_asset_type', 'type' => 'int', 'internal' => 'type'], + 'assetmgmt_asset_responsible' => ['name' => 'assetmgmt_asset_responsible', 'type' => 'int', 'internal' => 'responsible'], + 'assetmgmt_asset_created_at' => ['name' => 'assetmgmt_asset_created_at', 'type' => 'DateTimeImmutable', 'internal' => 'createdAt', 'readonly' => true], + ]; + + /** + * Has many relation. + * + * @var array + * @since 1.0.0 + */ + public const HAS_MANY = [ + 'files' => [ + 'mapper' => MediaMapper::class, + 'table' => 'assetmgmt_asset_media', + 'external' => 'assetmgmt_asset_media_media', + 'self' => 'assetmgmt_asset_media_asset', + ], + 'attributes' => [ + 'mapper' => AssetAttributeMapper::class, + 'table' => 'assetmgmt_asset_attr', + 'self' => 'assetmgmt_asset_attr_asset', + 'external' => null, + ], + 'notes' => [ + 'mapper' => EditorDocMapper::class, /* mapper of the related object */ + 'table' => 'assetmgmt_asset_note', /* table of the related object, null if no relation table is used (many->1) */ + 'external' => 'assetmgmt_asset_note_doc', + 'self' => 'assetmgmt_asset_note_asset', + ], + ]; + + /** + * Has one relation. + * + * @var array + * @since 1.0.0 + */ + public const OWNS_ONE = [ + 'type' => [ + 'mapper' => AssetTypeMapper::class, + 'external' => 'assetmgmt_asset_type', + ], + ]; + + /** + * Primary table. + * + * @var string + * @since 1.0.0 + */ + public const TABLE = 'assetmgmt_asset'; + + /** + * Created at. + * + * @var string + * @since 1.0.0 + */ + public const CREATED_AT = 'assetmgmt_asset_created_at'; + + /** + * Primary field name. + * + * @var string + * @since 1.0.0 + */ + public const PRIMARYFIELD = 'assetmgmt_asset_id'; +} diff --git a/Models/AssetStatus.php b/Models/AssetStatus.php new file mode 100644 index 0000000..dc208b8 --- /dev/null +++ b/Models/AssetStatus.php @@ -0,0 +1,38 @@ + + */ +final class AssetTypeL11nMapper extends DataMapperFactory +{ + /** + * Columns. + * + * @var array + * @since 1.0.0 + */ + public const COLUMNS = [ + 'assetmgmt_asset_type_l11n_id' => ['name' => 'assetmgmt_asset_type_l11n_id', 'type' => 'int', 'internal' => 'id'], + 'assetmgmt_asset_type_l11n_title' => ['name' => 'assetmgmt_asset_type_l11n_title', 'type' => 'string', 'internal' => 'content', 'autocomplete' => true], + 'assetmgmt_asset_type_l11n_type' => ['name' => 'assetmgmt_asset_type_l11n_type', 'type' => 'int', 'internal' => 'ref'], + 'assetmgmt_asset_type_l11n_lang' => ['name' => 'assetmgmt_asset_type_l11n_lang', 'type' => 'string', 'internal' => 'language'], + ]; + + /** + * Primary table. + * + * @var string + * @since 1.0.0 + */ + public const TABLE = 'assetmgmt_asset_type_l11n'; + + /** + * Primary field name. + * + * @var string + * @since 1.0.0 + */ + public const PRIMARYFIELD = 'assetmgmt_asset_type_l11n_id'; + + /** + * Model to use by the mapper. + * + * @var class-string + * @since 1.0.0 + */ + public const MODEL = BaseStringL11n::class; +} diff --git a/Models/AssetTypeMapper.php b/Models/AssetTypeMapper.php new file mode 100644 index 0000000..9c6eb17 --- /dev/null +++ b/Models/AssetTypeMapper.php @@ -0,0 +1,84 @@ + + */ +final class AssetTypeMapper extends DataMapperFactory +{ + /** + * Columns. + * + * @var array + * @since 1.0.0 + */ + public const COLUMNS = [ + 'assetmgmt_asset_type_id' => ['name' => 'assetmgmt_asset_type_id', 'type' => 'int', 'internal' => 'id'], + 'assetmgmt_asset_type_name' => ['name' => 'assetmgmt_asset_type_name', 'type' => 'string', 'internal' => 'title', 'autocomplete' => true], + 'assetmgmt_asset_type_depreciation_duration' => ['name' => 'assetmgmt_asset_type_depreciation_duration', 'type' => 'int', 'internal' => 'depreciationDuration'], + 'assetmgmt_asset_type_industry' => ['name' => 'assetmgmt_asset_type_industry', 'type' => 'int', 'internal' => 'industry'], + + ]; + + /** + * Has many relation. + * + * @var array + * @since 1.0.0 + */ + public const HAS_MANY = [ + 'l11n' => [ + 'mapper' => AssetTypeL11nMapper::class, + 'table' => 'assetmgmt_asset_type_l11n', + 'self' => 'assetmgmt_asset_type_l11n_type', + 'column' => 'content', + 'external' => null, + ], + ]; + + /** + * Model to use by the mapper. + * + * @var class-string + * @since 1.0.0 + */ + public const MODEL = AssetType::class; + + /** + * Primary table. + * + * @var string + * @since 1.0.0 + */ + public const TABLE = 'assetmgmt_asset_type'; + + /** + * Primary field name. + * + * @var string + * @since 1.0.0 + */ + public const PRIMARYFIELD = 'assetmgmt_asset_type_id'; +} diff --git a/Models/Attribute/AssetAttributeMapper.php b/Models/Attribute/AssetAttributeMapper.php new file mode 100644 index 0000000..59ff448 --- /dev/null +++ b/Models/Attribute/AssetAttributeMapper.php @@ -0,0 +1,86 @@ + + */ +final class AssetAttributeMapper extends DataMapperFactory +{ + /** + * Columns. + * + * @var array + * @since 1.0.0 + */ + public const COLUMNS = [ + 'assetmgmt_asset_attr_id' => ['name' => 'assetmgmt_asset_attr_id', 'type' => 'int', 'internal' => 'id'], + 'assetmgmt_asset_attr_asset' => ['name' => 'assetmgmt_asset_attr_asset', 'type' => 'int', 'internal' => 'ref'], + 'assetmgmt_asset_attr_type' => ['name' => 'assetmgmt_asset_attr_type', 'type' => 'int', 'internal' => 'type'], + 'assetmgmt_asset_attr_value' => ['name' => 'assetmgmt_asset_attr_value', 'type' => 'int', 'internal' => 'value'], + ]; + + /** + * Has one relation. + * + * @var array + * @since 1.0.0 + */ + public const OWNS_ONE = [ + 'type' => [ + 'mapper' => AssetAttributeTypeMapper::class, + 'external' => 'assetmgmt_asset_attr_type', + ], + 'value' => [ + 'mapper' => AssetAttributeValueMapper::class, + 'external' => 'assetmgmt_asset_attr_value', + ], + ]; + + /** + * Model to use by the mapper. + * + * @var class-string + * @since 1.0.0 + */ + public const MODEL = Attribute::class; + + /** + * Primary table. + * + * @var string + * @since 1.0.0 + */ + public const TABLE = 'assetmgmt_asset_attr'; + + /** + * Primary field name. + * + * @var string + * @since 1.0.0 + */ + public const PRIMARYFIELD = 'assetmgmt_asset_attr_id'; +} diff --git a/Models/Attribute/AssetAttributeTypeL11nMapper.php b/Models/Attribute/AssetAttributeTypeL11nMapper.php new file mode 100644 index 0000000..4909e4e --- /dev/null +++ b/Models/Attribute/AssetAttributeTypeL11nMapper.php @@ -0,0 +1,69 @@ + + */ +final class AssetAttributeTypeL11nMapper extends DataMapperFactory +{ + /** + * Columns. + * + * @var array + * @since 1.0.0 + */ + public const COLUMNS = [ + 'assetmgmt_attr_type_l11n_id' => ['name' => 'assetmgmt_attr_type_l11n_id', 'type' => 'int', 'internal' => 'id'], + 'assetmgmt_attr_type_l11n_title' => ['name' => 'assetmgmt_attr_type_l11n_title', 'type' => 'string', 'internal' => 'content', 'autocomplete' => true], + 'assetmgmt_attr_type_l11n_type' => ['name' => 'assetmgmt_attr_type_l11n_type', 'type' => 'int', 'internal' => 'ref'], + 'assetmgmt_attr_type_l11n_lang' => ['name' => 'assetmgmt_attr_type_l11n_lang', 'type' => 'string', 'internal' => 'language'], + ]; + + /** + * Primary table. + * + * @var string + * @since 1.0.0 + */ + public const TABLE = 'assetmgmt_attr_type_l11n'; + + /** + * Primary field name. + * + * @var string + * @since 1.0.0 + */ + public const PRIMARYFIELD = 'assetmgmt_attr_type_l11n_id'; + + /** + * Model to use by the mapper. + * + * @var class-string + * @since 1.0.0 + */ + public const MODEL = BaseStringL11n::class; +} diff --git a/Models/Attribute/AssetAttributeTypeMapper.php b/Models/Attribute/AssetAttributeTypeMapper.php new file mode 100644 index 0000000..6ec6937 --- /dev/null +++ b/Models/Attribute/AssetAttributeTypeMapper.php @@ -0,0 +1,94 @@ + + */ +final class AssetAttributeTypeMapper extends DataMapperFactory +{ + /** + * Columns. + * + * @var array + * @since 1.0.0 + */ + public const COLUMNS = [ + 'assetmgmt_attr_type_id' => ['name' => 'assetmgmt_attr_type_id', 'type' => 'int', 'internal' => 'id'], + 'assetmgmt_attr_type_name' => ['name' => 'assetmgmt_attr_type_name', 'type' => 'string', 'internal' => 'name', 'autocomplete' => true], + 'assetmgmt_attr_type_datatype' => ['name' => 'assetmgmt_attr_type_datatype', 'type' => 'int', 'internal' => 'datatype'], + 'assetmgmt_attr_type_fields' => ['name' => 'assetmgmt_attr_type_fields', 'type' => 'int', 'internal' => 'fields'], + 'assetmgmt_attr_type_custom' => ['name' => 'assetmgmt_attr_type_custom', 'type' => 'bool', 'internal' => 'custom'], + 'assetmgmt_attr_type_pattern' => ['name' => 'assetmgmt_attr_type_pattern', 'type' => 'string', 'internal' => 'validationPattern'], + 'assetmgmt_attr_type_required' => ['name' => 'assetmgmt_attr_type_required', 'type' => 'bool', 'internal' => 'isRequired'], + ]; + + /** + * Has many relation. + * + * @var array + * @since 1.0.0 + */ + public const HAS_MANY = [ + 'l11n' => [ + 'mapper' => AssetAttributeTypeL11nMapper::class, + 'table' => 'assetmgmt_attr_type_l11n', + 'self' => 'assetmgmt_attr_type_l11n_type', + 'column' => 'content', + 'external' => null, + ], + 'defaults' => [ + 'mapper' => AssetAttributeValueMapper::class, + 'table' => 'assetmgmt_asset_attr_default', + 'self' => 'assetmgmt_asset_attr_default_type', + 'external' => 'assetmgmt_asset_attr_default_value', + ], + ]; + + /** + * Model to use by the mapper. + * + * @var class-string + * @since 1.0.0 + */ + public const MODEL = AttributeType::class; + + /** + * Primary table. + * + * @var string + * @since 1.0.0 + */ + public const TABLE = 'assetmgmt_attr_type'; + + /** + * Primary field name. + * + * @var string + * @since 1.0.0 + */ + public const PRIMARYFIELD = 'assetmgmt_attr_type_id'; +} diff --git a/Models/Attribute/AssetAttributeValueL11nMapper.php b/Models/Attribute/AssetAttributeValueL11nMapper.php new file mode 100644 index 0000000..716ce41 --- /dev/null +++ b/Models/Attribute/AssetAttributeValueL11nMapper.php @@ -0,0 +1,69 @@ + + */ +final class AssetAttributeValueL11nMapper extends DataMapperFactory +{ + /** + * Columns. + * + * @var array + * @since 1.0.0 + */ + public const COLUMNS = [ + 'assetmgmt_attr_value_l11n_id' => ['name' => 'assetmgmt_attr_value_l11n_id', 'type' => 'int', 'internal' => 'id'], + 'assetmgmt_attr_value_l11n_title' => ['name' => 'assetmgmt_attr_value_l11n_title', 'type' => 'string', 'internal' => 'content', 'autocomplete' => true], + 'assetmgmt_attr_value_l11n_value' => ['name' => 'assetmgmt_attr_value_l11n_value', 'type' => 'int', 'internal' => 'ref'], + 'assetmgmt_attr_value_l11n_lang' => ['name' => 'assetmgmt_attr_value_l11n_lang', 'type' => 'string', 'internal' => 'language'], + ]; + + /** + * Primary table. + * + * @var string + * @since 1.0.0 + */ + public const TABLE = 'assetmgmt_attr_value_l11n'; + + /** + * Primary field name. + * + * @var string + * @since 1.0.0 + */ + public const PRIMARYFIELD = 'assetmgmt_attr_value_l11n_id'; + + /** + * Model to use by the mapper. + * + * @var class-string + * @since 1.0.0 + */ + public const MODEL = BaseStringL11n::class; +} diff --git a/Models/Attribute/AssetAttributeValueMapper.php b/Models/Attribute/AssetAttributeValueMapper.php new file mode 100644 index 0000000..cbc16b4 --- /dev/null +++ b/Models/Attribute/AssetAttributeValueMapper.php @@ -0,0 +1,89 @@ + + */ +final class AssetAttributeValueMapper extends DataMapperFactory +{ + /** + * Columns. + * + * @var array + * @since 1.0.0 + */ + public const COLUMNS = [ + 'assetmgmt_attr_value_id' => ['name' => 'assetmgmt_attr_value_id', 'type' => 'int', 'internal' => 'id'], + 'assetmgmt_attr_value_default' => ['name' => 'assetmgmt_attr_value_default', 'type' => 'bool', 'internal' => 'isDefault'], + 'assetmgmt_attr_value_valueStr' => ['name' => 'assetmgmt_attr_value_valueStr', 'type' => 'string', 'internal' => 'valueStr'], + 'assetmgmt_attr_value_valueInt' => ['name' => 'assetmgmt_attr_value_valueInt', 'type' => 'int', 'internal' => 'valueInt'], + 'assetmgmt_attr_value_valueDec' => ['name' => 'assetmgmt_attr_value_valueDec', 'type' => 'float', 'internal' => 'valueDec'], + 'assetmgmt_attr_value_valueDat' => ['name' => 'assetmgmt_attr_value_valueDat', 'type' => 'DateTime', 'internal' => 'valueDat'], + 'assetmgmt_attr_value_unit' => ['name' => 'assetmgmt_attr_value_unit', 'type' => 'string', 'internal' => 'unit'], + 'assetmgmt_attr_value_deptype' => ['name' => 'assetmgmt_attr_value_deptype', 'type' => 'int', 'internal' => 'dependingAttributeType'], + 'assetmgmt_attr_value_depvalue' => ['name' => 'assetmgmt_attr_value_depvalue', 'type' => 'int', 'internal' => 'dependingAttributeValue'], + ]; + + /** + * Has many relation. + * + * @var array + * @since 1.0.0 + */ + public const HAS_MANY = [ + 'l11n' => [ + 'mapper' => AssetAttributeValueL11nMapper::class, + 'table' => 'assetmgmt_attr_value_l11n', + 'self' => 'assetmgmt_attr_value_l11n_value', + 'external' => null, + ], + ]; + + /** + * Model to use by the mapper. + * + * @var class-string + * @since 1.0.0 + */ + public const MODEL = AttributeValue::class; + + /** + * Primary table. + * + * @var string + * @since 1.0.0 + */ + public const TABLE = 'assetmgmt_attr_value'; + + /** + * Primary field name. + * + * @var string + * @since 1.0.0 + */ + public const PRIMARYFIELD = 'assetmgmt_attr_value_id'; +} diff --git a/Models/NullAsset.php b/Models/NullAsset.php new file mode 100644 index 0000000..53585d2 --- /dev/null +++ b/Models/NullAsset.php @@ -0,0 +1,47 @@ +id = $id; + parent::__construct(); + } + + /** + * {@inheritdoc} + */ + public function jsonSerialize() : mixed + { + return ['id' => $this->id]; + } +} diff --git a/Models/NullAssetType.php b/Models/NullAssetType.php new file mode 100644 index 0000000..d327bbe --- /dev/null +++ b/Models/NullAssetType.php @@ -0,0 +1,47 @@ +id = $id; + parent::__construct(); + } + + /** + * {@inheritdoc} + */ + public function jsonSerialize() : mixed + { + return ['id' => $this->id]; + } +} diff --git a/Models/PermissionState.php b/Models/PermissionCategory.php similarity index 71% rename from Models/PermissionState.php rename to Models/PermissionCategory.php index c8398f4..1bf6460 100644 --- a/Models/PermissionState.php +++ b/Models/PermissionCategory.php @@ -17,14 +17,20 @@ namespace Modules\AssetManagement\Models; use phpOMS\Stdlib\Base\Enum; /** - * Permision state enum. + * Permission category enum. * * @package Modules\AssetManagement\Models * @license OMS License 2.0 * @link https://jingga.app * @since 1.0.0 */ -abstract class PermissionState extends Enum +abstract class PermissionCategory extends Enum { public const ASSET = 1; + + public const ASSET_TYPE = 3; + + public const ASSET_ATTRIBUTE_TYPE = 6; + + public const ASSET_NOTE = 7; } diff --git a/Theme/Backend/Lang/Navigation.de.lang.php b/Theme/Backend/Lang/Navigation.de.lang.php index 2342b6a..7a75e4e 100644 --- a/Theme/Backend/Lang/Navigation.de.lang.php +++ b/Theme/Backend/Lang/Navigation.de.lang.php @@ -15,4 +15,5 @@ declare(strict_types=1); return ['Navigation' => [ 'Assets' => 'Anlagegüter', 'Dashboard' => 'Dashboard', + 'Table' => 'Tabelle', ]]; diff --git a/Theme/Backend/Lang/Navigation.en.lang.php b/Theme/Backend/Lang/Navigation.en.lang.php index 9d7d007..f9421b9 100644 --- a/Theme/Backend/Lang/Navigation.en.lang.php +++ b/Theme/Backend/Lang/Navigation.en.lang.php @@ -15,4 +15,5 @@ declare(strict_types=1); return ['Navigation' => [ 'Assets' => 'Assets', 'Dashboard' => 'Dashboard', + 'Table' => 'Table', ]]; diff --git a/Theme/Backend/Lang/en.lang.php b/Theme/Backend/Lang/en.lang.php index 80931e5..f80f250 100644 --- a/Theme/Backend/Lang/en.lang.php +++ b/Theme/Backend/Lang/en.lang.php @@ -13,4 +13,13 @@ declare(strict_types=1); return ['AssetManagement' => [ + 'Assets' => 'Assets', + 'Status' => 'Status', + 'Name' => 'Name', + 'Number' => 'Number', + 'Type' => 'Type', + ':status1' => 'Active', + ':status2' => 'Inactive', + ':status3' => 'Damaged', + ':status4' => 'Out of order', ]]; diff --git a/Theme/Backend/asset-list.tpl.php b/Theme/Backend/asset-list.tpl.php index 8896e4f..7b4b40f 100644 --- a/Theme/Backend/asset-list.tpl.php +++ b/Theme/Backend/asset-list.tpl.php @@ -1,4 +1,5 @@ data['nav']->render(); +use phpOMS\Uri\UriFactory; + +/** @var \phpOMS\Views\View $this */ +$assets = $this->data['assets'] ?? []; + +echo $this->data['nav']->render(); ?> +
+
+
+
getHtml('Assets'); ?>download
+
+ + + + + $value) : + ++$count; + $url = UriFactory::build('{/base}/accounting/asset/profile?{?}&id=' . $value->id); + ?> + +
+ getHtml('ID', '0', '0'); ?> + + + + getHtml('Status'); ?> + + + + getHtml('Name'); ?> + + + + getHtml('Type'); ?> + + + +
+ printHtml((string) $value->id); ?> + getHtml(':status' . $value->status); ?> + printHtml($value->name); ?> + printHtml($value->type->getL11n()); ?> + + +
getHtml('Empty', '0', '0'); ?> + +
+
+
+
+
diff --git a/Theme/Backend/asset-profile.tpl.php b/Theme/Backend/asset-profile.tpl.php new file mode 100644 index 0000000..8b6e34c --- /dev/null +++ b/Theme/Backend/asset-profile.tpl.php @@ -0,0 +1,227 @@ +data['asset'] ?? new NullAsset(); +$files = $asset->files; +$assetImage = $this->data['assetImage'] ?? new NullMedia(); +$assetTypes = $this->data['types'] ?? []; +$attributeView = $this->data['attributeView']; + +/** + * @var \phpOMS\Views\View $this + */ +echo $this->data['nav']->render(); +?> +
\ No newline at end of file diff --git a/Theme/Backend/attribute-type-list.tpl.php b/Theme/Backend/attribute-type-list.tpl.php new file mode 100644 index 0000000..51a227b --- /dev/null +++ b/Theme/Backend/attribute-type-list.tpl.php @@ -0,0 +1,71 @@ +data['attributes']; + +echo $this->data['nav']->render(); ?> + +
+
+
+
getHtml('AttributeTypes', 'Attribute', 'Backend'); ?>download
+
+ + + + + $value) : ++$count; + $url = UriFactory::build('{/base}/accounting/asset/attribute/type?{?}&id=' . $value->id); + ?> + +
getHtml('ID', '0', '0'); ?> + + + + getHtml('Name'); ?> + + + +
id; ?> + printHtml($value->getL11n()); ?> + + +
getHtml('Empty', '0', '0'); ?> + +
+
+
+
+
diff --git a/info.json b/info.json index a446007..2a8802e 100644 --- a/info.json +++ b/info.json @@ -14,17 +14,18 @@ "name": "Jingga", "website": "jingga.app" }, - "description": "Budget Management module.", "directory": "AssetManagement", "dependencies": { "Admin": "*", "Media": "*", "Finance": "*", "Controlling": "*", - "EquipmentManagement": "*" + "EquipmentManagement": "*", + "Editor": "1.0.0" }, "providing": { - "Navigation": "*" + "Navigation": "*", + "Media": "*" }, "load": [ {
+
+ +
+
+ request->uri->fragment === 'c-tab-1' ? ' checked' : ''; ?>> + + + request->uri->fragment === 'c-tab-2' ? ' checked' : ''; ?>> +
+
+ render( + $asset->attributes, + $this->data['attributeTypes'] ?? [], + $this->data['units'] ?? [], + '{/api}fleet/asset/attribute', + $asset->id + ); + ?> +
+
+ + request->uri->fragment === 'c-tab-3' ? ' checked' : ''; ?>> +
+ data['media-upload']->render('asset-file', 'files', '', $asset->files); ?> +
+ + request->uri->fragment === 'c-tab-4' ? ' checked' : ''; ?>> +
+ data['asset-notes']->render('asset-notes', '', $asset->notes); ?> +
+ + request->uri->fragment === 'c-tab-5' ? ' checked' : ''; ?>> +
+ +
+
+
+
getHtml('Upcoming'); ?>
+ + + + + data['inspections'] as $inspection) : + // @todo handle old inspections in the past? maybe use a status?! + if ($inspection->next === null) { + continue; + } + ?> + +
getHtml('Date'); ?> + getHtml('Type'); ?> + getHtml('Responsible'); ?> +
next->format('Y-m-d H:i'); ?> + printHtml($inspection->type->getL11n()); ?> + + +
+
+
+ +
+
+
getHtml('History'); ?>
+ + + + + data['inspections'] as $inspection) : ?> + +
getHtml('Date'); ?> + getHtml('Type'); ?> + getHtml('Responsible'); ?> +
date->format('Y-m-d H:i'); ?> + printHtml($inspection->type->getL11n()); ?> + + +
+
+
+
+
+ + request->uri->fragment === 'c-tab-8' ? ' checked' : ''; ?>> +
+
+
+
+
+
+
+