generateCode(); $data['qr_token'] = $data['qr_token'] ?? Str::uuid()->toString(); $this->maybeAutoTag($data); // Hitung warranty_end jika ada warranty dan purchase_date if (!empty($data['purchase_date']) && empty($data['warranty_end'])) { $days = (int) $this->settings->get('asset.warranty_reminder_days', 0); if ($days > 0) { $data['warranty_end'] = now()->parse($data['purchase_date'])->addDays($days); } } $asset = Asset::create($data); if ($this->settings->get('asset.qr_enabled', true)) { $this->generateQr($asset); } $this->logHistory($asset, 'created', 'Aset dibuat', $data); $this->logChangelog($asset, 'created', $data); $this->dispatchWebhook('asset.created', $asset, $data); return $asset; }); } /** * Perbarui aset dan catat riwayat. */ public function update(Asset $asset, array $data): Asset { return DB::transaction(function () use ($asset, $data) { $this->maybeAutoTag($data, $asset); $asset->update($data); if ($this->settings->get('asset.qr_enabled', true)) { $this->generateQr($asset); } $this->logHistory($asset, 'updated', 'Aset diperbarui', $data); $this->logChangelog($asset, 'updated', $data); $this->dispatchWebhook('asset.updated', $asset, $data); return $asset; }); } private function generateCode(): string { $prefix = $this->settings->get('asset.code_prefix', 'AST'); $last = Asset::where('code', 'like', $prefix.'-%') ->orderByDesc('code') ->value('code'); $nextNumber = 1; if ($last && preg_match('/\d+$/', $last, $m)) { $nextNumber = ((int) $m[0]) + 1; } return sprintf('%s-%05d', $prefix, $nextNumber); } public function logHistory(Asset $asset, string $action, string $description, array $payload = []): void { AssetHistory::create([ 'asset_id' => $asset->id, 'action' => $action, 'description' => $description, 'changed_by' => auth()->id(), 'payload' => $payload, ]); } private function generateQr(Asset $asset): void { $format = $this->settings->get('asset.qr_format', 'svg'); $size = (int) $this->settings->get('asset.qr_size', 300); $path = "qr/{$asset->code}.{$format}"; $url = route('assets.public.show', $asset); $image = QrCode::format($format) ->size(max(100, $size)) ->margin(1) ->generate($url); Storage::disk('public')->put($path, $image); $asset->updateQuietly(['qr_path' => $path]); } private function logChangelog(Asset $asset, string $action, array $payload = []): void { AssetChangelog::create([ 'asset_id' => $asset->id, 'changed_by' => auth()->id(), 'changed_at' => now(), 'changes' => ['action' => $action, 'data' => $payload], ]); } /** * Catat movement aset lalu update lokasi/departemen. */ public function move(Asset $asset, array $data): AssetMovement { $requireApproval = config('system.workflow.require_approval.movement', false); return DB::transaction(function () use ($asset, $data, $requireApproval) { $movement = AssetMovement::create([ 'asset_id' => $asset->id, 'from_location_id' => $asset->asset_location_id, 'to_location_id' => $data['to_location_id'] ?? null, 'from_department_id' => $asset->department_id, 'to_department_id' => $data['to_department_id'] ?? null, 'from_asset_user_id' => $asset->asset_user_id, 'to_asset_user_id' => $data['to_asset_user_id'] ?? null, 'notes' => $data['notes'] ?? null, 'moved_by' => auth()->id(), 'performed_at' => $data['performed_at'] ?? now(), 'status' => $requireApproval ? 'pending' : 'approved', 'requested_by' => auth()->id(), ]); if (!$requireApproval) { $asset->update([ 'asset_location_id' => $data['to_location_id'] ?? $asset->asset_location_id, 'department_id' => $data['to_department_id'] ?? $asset->department_id, 'asset_user_id' => $data['to_asset_user_id'] ?? $asset->asset_user_id, ]); $movement->update([ 'approved_by' => auth()->id(), 'approved_at' => now(), ]); } else { app(\App\Services\Workflow\ApprovalService::class)->create('movement', $movement); } $this->logHistory($asset, 'movement', 'Perpindahan aset', $movement->toArray()); $this->dispatchWebhook('asset.movement', $asset, $movement->toArray()); return $movement; }); } /** * Catat disposal dan ubah status aset (jika ada status DISPOSED). */ public function dispose(Asset $asset, array $data): AssetDisposal { $requireApproval = config('system.workflow.require_approval.disposal', false); return DB::transaction(function () use ($asset, $data, $requireApproval) { $disposal = AssetDisposal::create([ 'asset_id' => $asset->id, 'reason' => $data['reason'] ?? null, 'notes' => $data['notes'] ?? null, 'disposed_by' => $requireApproval ? null : auth()->id(), 'disposed_at' => $requireApproval ? null : ($data['disposed_at'] ?? now()), 'previous_status_id' => $asset->asset_status_id, 'previous_location_id' => $asset->asset_location_id, 'previous_department_id' => $asset->department_id, 'status' => $requireApproval ? 'pending' : 'approved', 'requested_by' => auth()->id(), ]); if (!$requireApproval) { $disposedStatus = AssetStatus::where('code', 'DISPOSED')->first(); $asset->update([ 'asset_status_id' => $disposedStatus?->id ?? $asset->asset_status_id, ]); $disposal->update([ 'approved_by' => auth()->id(), 'approved_at' => now(), ]); } else { app(\App\Services\Workflow\ApprovalService::class)->create('disposal', $disposal); } $this->logHistory($asset, 'disposal', 'Aset didispose', $disposal->toArray()); $this->dispatchWebhook('asset.disposal', $asset, $disposal->toArray()); return $disposal; }); } /** * Reverse disposal dan kembalikan status/lokasi/departemen sebelumnya. */ public function reverseDisposal(AssetDisposal $disposal, ?string $notes = null): Asset { return DB::transaction(function () use ($disposal, $notes) { $asset = $disposal->asset; $asset->update([ 'asset_status_id' => $disposal->previous_status_id, 'asset_location_id' => $disposal->previous_location_id, 'department_id' => $disposal->previous_department_id, ]); $disposal->update([ 'reversed_at' => now(), 'reversed_by' => auth()->id(), 'reversed_notes' => $notes, 'status' => 'reversed', ]); $this->logHistory($asset, 'reverse_disposal', 'Aset di-restore dari disposal', [ 'disposal_id' => $disposal->id, 'notes' => $notes, ]); $this->dispatchWebhook('asset.reverse_disposal', $asset, ['disposal_id' => $disposal->id, 'notes' => $notes]); return $asset; }); } /** * Catat audit aset untuk memastikan kesesuaian fisik vs sistem. */ public function audit(Asset $asset, array $data): AssetAudit { return DB::transaction(function () use ($asset, $data) { $audit = AssetAudit::create([ 'asset_id' => $asset->id, 'status' => $data['status'], 'notes' => $data['notes'] ?? null, 'audited_by' => auth()->id(), 'audited_at' => $data['audited_at'] ?? now(), 'location_id' => $data['location_id'] ?? $asset->asset_location_id, ]); $this->logHistory($asset, 'audit', 'Audit aset', $audit->toArray()); $this->dispatchWebhook('asset.audit', $asset, $audit->toArray()); return $audit; }); } /** * Catat maintenance aset; jika butuh persetujuan, status pending. */ public function maintenance(Asset $asset, array $data): AssetMaintenance { $requireApproval = config('system.workflow.require_approval.maintenance', false); return DB::transaction(function () use ($asset, $data, $requireApproval) { $maintenance = AssetMaintenance::create([ 'asset_id' => $asset->id, 'performed_at' => $data['performed_at'] ?? now(), 'description' => $data['description'] ?? null, 'vendor' => $data['vendor'] ?? null, 'cost' => $data['cost'] ?? null, // Flow: pending (menunggu approval) -> planned (approved, belum mulai) -> in_progress -> completed 'status' => $requireApproval ? 'pending' : 'planned', 'notes' => $data['notes'] ?? null, 'requested_by' => auth()->id(), 'approved_by' => $requireApproval ? null : auth()->id(), 'approved_at' => $requireApproval ? null : now(), ]); if ($requireApproval) { app(\App\Services\Workflow\ApprovalService::class)->create('maintenance', $maintenance); } $this->logHistory($asset, 'maintenance', 'Perawatan aset', $maintenance->toArray()); $this->dispatchWebhook('asset.maintenance', $asset, $maintenance->toArray()); return $maintenance; }); } private function dispatchWebhook(string $event, Asset $asset, array $payload = []): void { $this->webhooks->send($event, [ 'asset_id' => $asset->id, 'asset_code' => $asset->code ?? null, 'asset_status_id' => $asset->asset_status_id, 'payload' => $payload, ]); } /** * Generate RFID/NFC tag otomatis sesuai setting jika belum diisi. */ private function maybeAutoTag(array &$data, ?Asset $existing = null): void { $code = $data['code'] ?? $existing?->code ?? null; if (!$code) { return; } if ($this->settings->get('asset.rfid_auto_generate', false) && empty($data['rfid_tag'])) { $data['rfid_tag'] = $this->uniqueTag("RFID-{$code}", 'rfid_tag', $existing?->id); } if ($this->settings->get('asset.nfc_auto_generate', false) && empty($data['nfc_tag'])) { $data['nfc_tag'] = $this->uniqueTag("NFC-{$code}", 'nfc_tag', $existing?->id); } } private function uniqueTag(string $base, string $column, ?string $exceptId = null): string { $candidate = $base; $counter = 1; while ( Asset::where($column, $candidate) ->when($exceptId, fn($q) => $q->where('id', '!=', $exceptId)) ->exists() ) { $candidate = "{$base}-" . (++$counter); } return $candidate; } }