diff --git a/application/AssetManagement/AssetManagementController.php b/application/AssetManagement/AssetManagementController.php
new file mode 100644
index 000000000..1b861c6c9
--- /dev/null
+++ b/application/AssetManagement/AssetManagementController.php
@@ -0,0 +1,149 @@
+ 'id', 'text' => 'ID', 'modal' => false, 'table' => false],
+ ['key' => 'name', 'text' => 'Gerät', 'required' => true, 'modal' => ['type' => 'text']],
+ ['key' => 'assetNumber', 'text' => 'Kennzeichen / Nr.', 'required' => true, 'modal' => ['type' => 'text']],
+ ['key' => 'currentUser', 'text' => 'Akt. Mitarbeiter', 'modal' => false, 'table' => ['sortable' => false]],
+ ['key' => 'currentSite', 'text' => 'Akt. Baustelle', 'modal' => false, 'table' => ['sortable' => false]],
+ ['key' => 'borrowDate', 'text' => 'Ausgeliehen seit', 'modal' => false, 'table' => ['sortable' => false]],
+ ['key' => 'location', 'text' => 'Lagerort', 'required' => true, 'modal' => ['type' => 'text']],
+ ['key' => 'serviceDueDate', 'text' => 'Service fällig', 'required' => false, 'modal' => ['type' => 'datepicker']],
+ ['key' => 'journal', 'text' => 'Historie', 'modal' => false, 'table' => ['sortable' => false, 'filter' => false]],
+ ['key' => 'actions', 'text' => 'Aktionen', 'modal' => false, 'table' => ['filter' => false, 'sortable' => false]],
+ ];
+
+ protected array $permissionCheck = ['WarehouseAdmin']; // Or a new permission
+
+ protected function getAction()
+ {
+ $json = json_decode(file_get_contents('php://input'), true);
+ $assets = AssetManagementModel::getAll([], $this->request->order);
+ $assetIds = array_map(fn($asset) => $asset->id, $assets);
+
+ if (empty($assetIds)) {
+ self::returnJson(['rows' => [], 'pagination' => ['total_rows' => 0, 'total_pages' => 1, 'page' => 1, 'per_page' => 10, 'filtered_available' => 0]]);
+ return;
+ }
+
+ // Get the latest open journal entry for each asset
+ $journalEntries = AssetManagementJournalModel::getLatestOpenEntries($assetIds);
+ $journalMap = [];
+ foreach ($journalEntries as $entry) {
+ // Only map it if it's not returned
+ if ($entry->returnDate === null) {
+ $journalMap[$entry->assetId] = $entry;
+ }
+ }
+
+ $users = UserModel::search(['employee' => true]);
+ $userMap = array_reduce($users, function ($carry, $user) {
+ $carry[$user->id] = $user->name;
+ return $carry;
+ }, []);
+
+ $rows = [];
+ foreach ($assets as $asset) {
+ $row = (array)$asset;
+ $latestJournal = $journalMap[$asset->id] ?? null;
+
+ $row['journalId'] = $latestJournal->id ?? null;
+ $row['currentUser'] = $latestJournal ? ($userMap[$latestJournal->userId] ?? 'Unbekannt') : null;
+ $row['currentUserId'] = $latestJournal->userId ?? null;
+ $row['currentSite'] = $latestJournal->site ?? null;
+ $row['borrowDate'] = $latestJournal->borrowDate ?? null;
+ $rows[] = $row;
+ }
+
+ // Simple pagination/filtering after getting all data
+ // For larger datasets, this should be done in the SQL query
+ $totalRows = count($rows);
+ $pagination = $json['pagination'] ?? ['page' => 1, 'per_page' => 10];
+ $paginatedRows = array_slice($rows, ($pagination['page'] - 1) * $pagination['per_page'], $pagination['per_page']);
+
+ self::returnJson([
+ 'rows' => $paginatedRows,
+ 'pagination' => [
+ 'page' => $pagination['page'],
+ 'per_page' => $pagination['per_page'],
+ 'total_rows' => $totalRows,
+ 'total_pages' => ceil($totalRows / $pagination['per_page'])
+ ]
+ ]);
+ }
+
+ protected function suggestAssetNumberAction()
+ {
+ $lastAsset = AssetManagementModel::getOne([], ['order' => 'DESC', 'key' => 'id']);
+ if (!$lastAsset || !preg_match('/XI(\d+)/', $lastAsset->assetNumber, $matches)) {
+ $nextNumber = 1;
+ } else {
+ $nextNumber = intval($matches[1]) + 1;
+ }
+ $newAssetNumber = 'XI' . str_pad($nextNumber, 3, '0', STR_PAD_LEFT);
+ self::returnJson(['success' => true, 'assetNumber' => $newAssetNumber]);
+ }
+
+ protected function borrowAction()
+ {
+ $post = json_decode(file_get_contents('php://input'), true);
+ if (empty($post['assetId']) || empty($post['userId']) || empty($post['site']) || empty($post['reason'])) {
+ self::sendError("Alle Felder sind erforderlich.");
+ }
+
+ AssetManagementJournalModel::create([
+ 'assetId' => $post['assetId'],
+ 'userId' => $post['userId'],
+ 'site' => $post['site'],
+ 'borrowReason' => $post['reason'],
+ 'borrowDate' => time(),
+ 'createBy' => $this->user->id,
+ 'create' => time(),
+ ]);
+
+ self::returnJson(['success' => true, 'message' => 'Gerät erfolgreich ausgeliehen.']);
+ }
+
+ protected function returnAction()
+ {
+ $post = json_decode(file_get_contents('php://input'), true);
+ if (empty($post['journalId'])) {
+ self::sendError("Journal-Eintrag nicht gefunden.");
+ }
+
+ $journalEntry = AssetManagementJournalModel::get($post['journalId']);
+ if (!$journalEntry) {
+ self::sendError("Journal-Eintrag nicht gefunden.");
+ }
+
+ $journalEntry->returnDate = time();
+ $journalEntry->returnReason = $post['reason'] ?? 'Zurückgegeben';
+ AssetManagementJournalModel::update((array)$journalEntry);
+
+ self::returnJson(['success' => true, 'message' => 'Gerät erfolgreich zurückgegeben.']);
+ }
+
+ protected function getJournalAction()
+ {
+ if (empty($this->request->assetId)) self::sendError("Asset ID fehlt.");
+ $entries = AssetManagementJournalModel::getAll(['assetId' => $this->request->assetId], null, 0, ['key' => 'borrowDate', 'order' => 'DESC']);
+
+ // Enhance with user names
+ $users = UserModel::search(['employee' => true]);
+ $userMap = array_reduce($users, function ($carry, $user) {
+ $carry[$user->id] = $user->name;
+ return $carry;
+ }, []);
+
+ foreach ($entries as $entry) {
+ $entry->userName = $userMap[$entry->userId] ?? 'Unbekannt';
+ }
+
+ self::returnJson($entries);
+ }
+}
\ No newline at end of file
diff --git a/application/AssetManagement/AssetManagementModel.php b/application/AssetManagement/AssetManagementModel.php
new file mode 100644
index 000000000..93349738f
--- /dev/null
+++ b/application/AssetManagement/AssetManagementModel.php
@@ -0,0 +1,11 @@
+query($sql);
+
+ $entries = [];
+ while ($row = $result->fetch_assoc()) {
+ $entries[] = new self($row);
+ }
+ return $entries;
+ }
+
+}
\ No newline at end of file
diff --git a/db/migrations/20250626134500_create_asset_management_tables.php b/db/migrations/20250626134500_create_asset_management_tables.php
new file mode 100644
index 000000000..e23de4e57
--- /dev/null
+++ b/db/migrations/20250626134500_create_asset_management_tables.php
@@ -0,0 +1,59 @@
+getEnvironment() == "thetool") {
+
+ // Create the main asset table
+ $assetManagement = $this->table('AssetManagement', ['id' => false, 'primary_key' => ['id']]);
+ $assetManagement->addColumn('id', 'integer', ['identity' => true, 'signed' => false])
+ ->addColumn('name', 'string', ['limit' => 255, 'null' => false, 'comment' => 'Name of the asset or device'])
+ ->addColumn('assetNumber', 'string', ['limit' => 255, 'null' => false, 'comment' => 'Unique identifier for the asset (e.g., XI001)'])
+ ->addColumn('location', 'string', ['limit' => 255, 'null' => false, 'comment' => 'The primary storage location of the asset'])
+ ->addColumn('serviceDueDate', 'integer', ['null' => true, 'comment' => 'Date when the next service is due'])
+ ->addColumn('create', 'integer', ['null' => false, 'comment' => 'Unix timestamp of creation'])
+ ->addColumn('createBy', 'integer', ['null' => false, 'comment' => 'User ID of the creator'])
+ ->addIndex(['assetNumber'], ['unique' => true, 'name' => 'assetNumber'])
+ ->create();
+
+ // Create the journal table for asset borrowing and returning
+ $assetManagementJournal = $this->table('AssetManagementJournal', ['id' => false, 'primary_key' => ['id']]);
+ $assetManagementJournal->addColumn('id', 'integer', ['identity' => true, 'signed' => false])
+ ->addColumn('assetId', 'integer', ['null' => false, 'comment' => 'Foreign key to the AssetManagement table'])
+ ->addColumn('userId', 'integer', ['null' => false, 'comment' => 'User ID of the employee who borrowed the asset'])
+ ->addColumn('site', 'string', ['limit' => 255, 'null' => false, 'comment' => 'The construction site or project where the asset is being used'])
+ ->addColumn('borrowDate', 'integer', ['null' => false, 'comment' => 'Unix timestamp when the asset was borrowed'])
+ ->addColumn('returnDate', 'integer', ['null' => true, 'comment' => 'Unix timestamp when the asset was returned'])
+ ->addColumn('borrowReason', 'text', ['null' => false, 'comment' => 'Reason for borrowing the asset'])
+ ->addColumn('returnReason', 'text', ['null' => true, 'comment' => 'Notes or reason for returning the asset'])
+ ->addColumn('createBy', 'integer', ['null' => false, 'comment' => 'User ID of the person who created this journal entry'])
+ ->addColumn('create', 'integer', ['null' => false, 'comment' => 'Unix timestamp of journal entry creation'])
+ ->addIndex(['assetId'], ['name' => 'assetId'])
+ ->addIndex(['userId'], ['name' => 'userId'])
+ ->create();
+ }
+ }
+
+ /**
+ * Rollback the migration by dropping the tables.
+ */
+ public function down(): void
+ {
+ // Environment check to ensure this migration only runs on the specified environment.
+ if ($this->getEnvironment() == "thetool") {
+ // Drop tables in reverse order of creation to avoid foreign key issues.
+ $this->table('AssetManagementJournal')->drop()->save();
+ $this->table('AssetManagement')->drop()->save();
+ }
+ }
+}
diff --git a/lib/TTCrudBaseModel/TTCrudBaseModel.php b/lib/TTCrudBaseModel/TTCrudBaseModel.php
index eed546255..d02243fb3 100644
--- a/lib/TTCrudBaseModel/TTCrudBaseModel.php
+++ b/lib/TTCrudBaseModel/TTCrudBaseModel.php
@@ -10,14 +10,14 @@ class TTCrudBaseModel {
}
}
- private static function getFullyQualifiedTable(): string {
+ protected static function getFullyQualifiedTable(): string {
$table = str_replace('Model', '', get_called_class());
$tableIncludesADBSuffix = strpos($table, 'ADB') !== false;
$table = str_replace('ADB', '', $table);
return "`" . ($tableIncludesADBSuffix ? ADDRESSDB_DBNAME : FRONKDB_DBNAME) . "`.`" . $table . "`";
}
- private static function getDB() {
+ protected static function getDB() {
if (strpos(self::getFullyQualifiedTable(), ADDRESSDB_DBNAME) !== false)
$FronkDB = FronkDB::singleton(ADDRESSDB_DBHOST, ADDRESSDB_DBUSER, ADDRESSDB_DBPASS, ADDRESSDB_DBNAME);
else $FronkDB = FronkDB::singleton();
diff --git a/public/js/pages/AssetManagement/AssetManagement.js b/public/js/pages/AssetManagement/AssetManagement.js
new file mode 100644
index 000000000..125c95636
--- /dev/null
+++ b/public/js/pages/AssetManagement/AssetManagement.js
@@ -0,0 +1,318 @@
+// =================================================================================
+// Main Asset Management Component
+// =================================================================================
+Vue.component('asset-management', {
+ template: `
+
Gerät: {{ rowData.name }}
+Mitarbeiter: {{ selectedUserName }}
+Gerät: {{ rowData.name }}
+Soll dieses Gerät wirklich als zurückgegeben markiert werden?
+Grund: {{ entry.borrowReason }}
+