346 lines
13 KiB
PHP
346 lines
13 KiB
PHP
<?php
|
|
|
|
namespace App\Services\Asset;
|
|
|
|
use App\Models\Asset;
|
|
use App\Models\AssetHistory;
|
|
use App\Services\System\SystemSettingService;
|
|
use App\Models\AssetStatus;
|
|
use App\Models\AssetMovement;
|
|
use App\Models\AssetDisposal;
|
|
use App\Models\AssetAudit;
|
|
use App\Models\AssetChangelog;
|
|
use App\Models\AssetMaintenance;
|
|
use App\Services\Logging\ActivityLogger;
|
|
use App\Services\Integration\WebhookDispatcher;
|
|
use Illuminate\Support\Facades\DB;
|
|
use Illuminate\Support\Facades\Storage;
|
|
use Illuminate\Support\Str;
|
|
use SimpleSoftwareIO\QrCode\Facades\QrCode;
|
|
|
|
class AssetService
|
|
{
|
|
public function __construct(
|
|
private readonly SystemSettingService $settings,
|
|
private readonly ActivityLogger $logger,
|
|
private readonly WebhookDispatcher $webhooks
|
|
)
|
|
{
|
|
}
|
|
|
|
/**
|
|
* Buat aset baru dengan kode otomatis berdasarkan setting.
|
|
*/
|
|
public function create(array $data): Asset
|
|
{
|
|
return DB::transaction(function () use ($data) {
|
|
$data['code'] = $this->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;
|
|
}
|
|
}
|