only([ 'q', 'asset_status_id', 'asset_class_id', 'asset_category_id', 'asset_location_id', 'department_id', 'asset_user_id', 'person_in_charge_id', 'warranty_id', 'scope', ]); $assets = Asset::with(['status', 'class', 'category', 'unit', 'department', 'personInCharge', 'user', 'location', 'warranty']) ->forUser($request->user()) ->when($filters['q'] ?? null, function ($query, $q) { $query->where(function ($qBuilder) use ($q) { $qBuilder->where('code', 'like', "%{$q}%") ->orWhere('name', 'like', "%{$q}%") ->orWhere('serial_number', 'like', "%{$q}%"); }); }) ->when($filters['asset_status_id'] ?? null, fn($q, $v) => $q->where('asset_status_id', $v)) ->when($filters['asset_class_id'] ?? null, fn($q, $v) => $q->where('asset_class_id', $v)) ->when($filters['asset_category_id'] ?? null, fn($q, $v) => $q->where('asset_category_id', $v)) ->when($filters['asset_location_id'] ?? null, fn($q, $v) => $q->where('asset_location_id', $v)) ->when($filters['department_id'] ?? null, fn($q, $v) => $q->where('department_id', $v)) ->when($filters['asset_user_id'] ?? null, fn($q, $v) => $q->where('asset_user_id', $v)) ->when($filters['person_in_charge_id'] ?? null, fn($q, $v) => $q->where('person_in_charge_id', $v)) ->when($filters['warranty_id'] ?? null, fn($q, $v) => $q->where('warranty_id', $v)) ->when(($filters['scope'] ?? '') === 'archived', fn($q) => $q->whereNotNull('archived_at')) ->when(($filters['scope'] ?? '') === 'trashed', fn($q) => $q->onlyTrashed()) ->when(($filters['scope'] ?? '') === 'all', fn($q) => $q->withTrashed()) ->when(!in_array($filters['scope'] ?? '', ['archived','trashed','all']), fn($q) => $q->whereNull('archived_at')) ->orderBy('created_at', 'desc') ->paginate(config('system.ui.table_page_size', 15)) ->withQueryString(); return view('assets.index', [ 'assets' => $assets, 'statuses' => AssetStatus::orderBy('name')->get(), 'classes' => AssetClass::orderBy('name')->get(), 'categories' => AssetCategory::orderBy('name')->get(), 'units' => Unit::orderBy('name')->get(), 'departments' => Department::orderBy('name')->get(), 'peopleInCharge' => PersonInCharge::orderBy('name')->get(), 'users' => AssetUser::orderBy('name')->get(), 'locations' => AssetLocation::orderBy('name')->get(), 'warranties' => Warranty::orderBy('duration_months')->get(), 'vendorContracts' => VendorContract::orderBy('vendor_name')->get(), 'filters' => $filters, 'importPreview' => session('import_preview'), ]); } public function store(Request $request): RedirectResponse { $data = $this->validateRequest($request); $asset = $this->assets->create($data); return back()->with('success', "Aset {$asset->code} berhasil ditambahkan."); } public function update(Request $request, Asset $asset): RedirectResponse { abort_unless($asset->isVisibleTo($request->user()), 403); $data = $this->validateRequest($request, $asset->id); $this->assets->update($asset, $data); return back()->with('success', "Aset {$asset->code} berhasil diperbarui."); } public function show(Asset $asset): View { abort_unless($asset->isVisibleTo(request()->user()), 403); $asset->load([ 'status','class','category','unit','department', 'personInCharge','user','location','warranty', 'movements.fromLocation','movements.toLocation','movements.fromDepartment','movements.toDepartment', 'disposals', ]); return view('assets.show', compact('asset')); } public function history(Asset $asset): View { abort_unless($asset->isVisibleTo(request()->user()), 403); $asset->load('histories'); return view('assets.history', compact('asset')); } public function archive(Asset $asset): RedirectResponse { $asset->update([ 'archived_at' => now(), 'archived_by' => auth()->id(), ]); return back()->with('success', "Aset {$asset->code} diarsipkan."); } public function unarchive(Asset $asset): RedirectResponse { $asset->update([ 'archived_at' => null, 'archived_by' => null, ]); return back()->with('success', "Aset {$asset->code} dikembalikan dari arsip."); } public function destroy(Asset $asset): RedirectResponse { $asset->delete(); return back()->with('success', "Aset {$asset->code} dihapus (soft delete)."); } public function restore(string $asset): RedirectResponse { $model = Asset::onlyTrashed()->findOrFail($asset); $model->restore(); return back()->with('success', "Aset {$model->code} berhasil di-restore."); } public function exportCsv(Request $request): JsonResponse|\Symfony\Component\HttpFoundation\BinaryFileResponse { $filters = $request->all(); $assets = Asset::with(['status','category','location']) ->when($filters['scope'] ?? null, function ($q, $scope) { if ($scope === 'archived') $q->whereNotNull('archived_at'); if ($scope === 'trashed') $q->onlyTrashed(); if ($scope === 'all') $q->withTrashed(); }) ->limit(2000) ->get(); $filename = 'assets-export-'.now()->format('Ymd_His').'.csv'; $tmp = tempnam(sys_get_temp_dir(), 'csv'); $fh = fopen($tmp, 'w'); fputcsv($fh, [ 'code','name','serial_number','status','category','location', 'rfid_tag','nfc_tag','is_consumable','quantity','available_quantity','is_pool','archived_at' ]); foreach ($assets as $asset) { fputcsv($fh, [ $asset->code, $asset->name, $asset->serial_number, $asset->status?->name, $asset->category?->name, $asset->location?->name, $asset->rfid_tag, $asset->nfc_tag, $asset->is_consumable ? '1':'0', $asset->quantity, $asset->available_quantity, $asset->is_pool ? '1':'0', $asset->archived_at, ]); } fclose($fh); return response()->download($tmp, $filename)->deleteFileAfterSend(true); } public function import(Request $request): RedirectResponse { $request->validate([ 'file' => ['required','file','mimes:csv,txt'], 'action' => ['nullable','in:preview,import'], ]); $action = $request->input('action', 'preview'); $path = $request->file('file')->getRealPath(); $handle = fopen($path, 'r'); if (!$handle) { return back()->withErrors(['file' => 'File tidak bisa dibaca']); } $header = fgetcsv($handle); $rows = []; $line = 1; $existingCodes = Asset::pluck('id','code')->toArray(); $existingSerials = Asset::pluck('id','serial_number')->filter()->toArray(); $statuses = AssetStatus::pluck('id','name')->toArray(); $categories = AssetCategory::pluck('id','name')->toArray(); $locations = AssetLocation::pluck('id','name')->toArray(); while (($data = fgetcsv($handle)) !== false) { $line++; $row = array_combine($header, $data); if (!$row) { $rows[] = ['line' => $line, 'status' => 'error', 'message' => 'Header tidak sesuai', 'data' => $data]; continue; } $code = $row['code'] ?? null; $serial = $row['serial_number'] ?? null; $name = $row['name'] ?? null; $errors = []; if (!$name) $errors[] = 'Nama wajib'; $statusId = $statuses[$row['status'] ?? ''] ?? null; $categoryId = $categories[$row['category'] ?? ''] ?? null; $locationId = $locations[$row['location'] ?? ''] ?? null; $targetId = $code && isset($existingCodes[$code]) ? $existingCodes[$code] : null; if (!$targetId && $serial && isset($existingSerials[$serial])) { $targetId = $existingSerials[$serial]; } $rows[] = [ 'line' => $line, 'status' => $errors ? 'invalid' : ($targetId ? 'update' : 'create'), 'errors' => $errors, 'payload' => [ 'code' => $code, 'name' => $name, 'serial_number' => $serial, 'asset_status_id' => $statusId, 'asset_category_id' => $categoryId, 'asset_location_id' => $locationId, 'rfid_tag' => $row['rfid_tag'] ?? null, 'nfc_tag' => $row['nfc_tag'] ?? null, 'is_consumable' => !empty($row['is_consumable']), 'quantity' => (int) ($row['quantity'] ?? 1), 'available_quantity' => (int) ($row['available_quantity'] ?? $row['quantity'] ?? 1), 'is_pool' => !empty($row['is_pool']), ], 'target_id' => $targetId, ]; } fclose($handle); if ($action === 'preview') { session()->flash('import_preview', [ 'summary' => [ 'create' => collect($rows)->where('status','create')->count(), 'update' => collect($rows)->where('status','update')->count(), 'invalid' => collect($rows)->where('status','invalid')->count(), ], 'rows' => collect($rows)->take(50)->toArray(), ]); return back()->with('success', 'Preview import siap. Silakan cek tabel preview.'); } // commit import $created = 0; $updated = 0; $invalid = 0; foreach ($rows as $row) { if ($row['status'] === 'invalid') { $invalid++; continue; } $payload = $row['payload']; if ($row['target_id']) { Asset::where('id', $row['target_id'])->update($payload); $updated++; } else { if (!$payload['code']) { $payload['code'] = 'IMP-'.Str::upper(Str::random(6)); } Asset::create($payload + ['qr_token' => Str::uuid()]); $created++; } } return back()->with('success', "Import selesai. Create: {$created}, Update: {$updated}, Invalid: {$invalid}"); } private function validateRequest(Request $request, ?string $assetId = null): array { return $request->validate([ 'name' => ['required', 'string', 'max:255'], 'serial_number' => ['nullable', 'string', 'max:255'], 'description' => ['nullable', 'string'], 'asset_status_id' => ['nullable', 'uuid', 'exists:asset_statuses,id'], 'asset_class_id' => ['nullable', 'uuid', 'exists:asset_classes,id'], 'asset_category_id' => ['nullable', 'uuid', 'exists:asset_categories,id'], 'unit_id' => ['nullable', 'uuid', 'exists:units,id'], 'department_id' => ['nullable', 'uuid', 'exists:departments,id'], 'person_in_charge_id' => ['nullable', 'uuid', 'exists:person_in_charges,id'], 'asset_user_id' => ['nullable', 'uuid', 'exists:asset_users,id'], 'asset_location_id' => ['nullable', 'uuid', 'exists:asset_locations,id'], 'warranty_id' => ['nullable', 'uuid', 'exists:warranties,id'], 'purchase_date' => ['nullable', 'date'], 'warranty_end' => ['nullable', 'date'], 'cost' => ['nullable', 'numeric', 'min:0'], 'depreciation_method' => ['nullable', 'in:straight_line,diminishing'], 'useful_life_months' => ['nullable', 'integer', 'min:1'], 'residual_value' => ['nullable', 'numeric', 'min:0'], 'capex_opex' => ['nullable', 'in:capex,opex'], 'vendor_contract_id' => ['nullable', 'uuid', 'exists:vendor_contracts,id'], 'metadata' => ['nullable', 'array'], ]); } }