diff --git a/app/Services/Admin/Manage/TransactionService.php b/app/Services/Admin/Manage/TransactionService.php index a87c1c1..e868259 100644 --- a/app/Services/Admin/Manage/TransactionService.php +++ b/app/Services/Admin/Manage/TransactionService.php @@ -16,6 +16,7 @@ use App\Models\User; use App\Services\Concerns\HasStockAdjustment; use App\Services\Concerns\HandlesCashTransactions; +use App\Services\Concerns\LogsFormHistory; use App\Services\Concerns\RegistersMedia; use App\Services\NotificationService; use App\Services\S3PresignedService; @@ -25,7 +26,7 @@ class TransactionService { - use HasStockAdjustment, HandlesCashTransactions, RegistersMedia; + use HasStockAdjustment, HandlesCashTransactions, LogsFormHistory, RegistersMedia; private const SELLING_PRICE_MAP = [ PriceType::DISTRIBUTOR->value => PriceType::DISTRIBUTOR, @@ -253,12 +254,16 @@ public function store(array $data): Order url: route('admin.manage.transactions.index', ['highlight' => $order->id]), ); + $this->logCreated($order, 'Transaksi', $this->getOrderLogValues($order)); + return $order; }); } public function update(Order $order, array $data): Order { + $oldValues = $this->getOrderLogValues($order); + $order = DB::transaction(function () use ($order, $data) { $order->load('orderItems'); @@ -360,11 +365,15 @@ public function update(Order $order, array $data): Order url: route('admin.manage.transactions.index', ['highlight' => $order->id]), ); + $this->logUpdated($order, 'Transaksi', $oldValues, $this->getOrderLogValues($order)); + return $order; } public function destroy(Order $order): bool { + $oldValues = $this->getOrderLogValues($order); + $result = DB::transaction(function () use ($order) { $order->load('orderItems'); @@ -397,11 +406,14 @@ public function destroy(Order $order): bool url: route('admin.manage.transactions.index'), ); + $this->logDeleted($order, 'Transaksi', $oldValues); + return $result; } public function updateStatus(Order $order, string $status): Order { + $oldValues = $this->getOrderLogValues($order); $oldStatus = $order->status->value; $order->update(['status' => $status]); @@ -423,6 +435,8 @@ public function updateStatus(Order $order, string $status): Order } } + $this->logUpdated($order, 'Transaksi', $oldValues, $this->getOrderLogValues($order)); + return $order; } @@ -524,4 +538,30 @@ private function isMarketingUser(User $user): bool Role::MARKETING_ONLINE->value, ]); } + + private function getOrderLogValues(Order $order): array + { + $order->load(['customer:id,name', 'marketing:id', 'marketing.userProfile:id,user_id,full_name', 'orderItems.productVariant.product:id,name']); + + return [ + 'No. Transaksi' => $order->order_number, + 'Customer' => $order->customer?->name ?? '-', + 'Marketing' => $order->marketing?->userProfile?->full_name ?? '-', + 'Channel' => $order->channel?->label(), + 'Tipe Harga' => $order->price_type?->label(), + 'Status' => $order->status?->label(), + 'Tipe Pembayaran' => $order->payment_type?->label(), + 'Subtotal' => $this->formatCurrency($order->subtotal), + 'Diskon' => $this->formatCurrency($order->discount), + 'Harga Nego' => $order->nego_price ? $this->formatCurrency($order->nego_price) : null, + 'Total' => $this->formatCurrency($order->total_amount), + 'Catatan' => $order->notes, + 'Item' => $order->orderItems->map(fn ($item) => [ + 'Nama' => $item->productVariant?->product?->name.' - '.$item->productVariant?->name ?? '-', + 'Qty' => $item->quantity, + 'Harga' => $this->formatCurrency($item->unit_price), + 'Subtotal' => $this->formatCurrency($item->subtotal), + ])->toArray(), + ]; + } } diff --git a/app/Services/Admin/Master/CustomerService.php b/app/Services/Admin/Master/CustomerService.php index 8ad6631..db7890c 100644 --- a/app/Services/Admin/Master/CustomerService.php +++ b/app/Services/Admin/Master/CustomerService.php @@ -3,11 +3,14 @@ namespace App\Services\Admin\Master; use App\Models\Customer; +use App\Services\Concerns\LogsFormHistory; use Illuminate\Contracts\Pagination\LengthAwarePaginator; use Illuminate\Database\Eloquent\Collection; class CustomerService { + use LogsFormHistory; + public function paginated(int $perPage = 25, string $search = '', string $sort = 'created_at', string $direction = 'desc', array $filters = []): LengthAwarePaginator { return Customer::query() @@ -24,18 +27,44 @@ public function getAll(): Collection public function store(array $data): Customer { - return Customer::create($data); + $customer = Customer::create($data); + + $this->logCreated($customer, 'Customer', [ + 'Nama' => $customer->name, + 'No. Telepon' => $customer->phone_number, + 'Alamat' => $customer->address, + ]); + + return $customer; } public function update(Customer $customer, array $data): Customer { + $oldValues = [ + 'Nama' => $customer->name, + 'No. Telepon' => $customer->phone_number, + 'Alamat' => $customer->address, + ]; + $customer->update($data); + $this->logUpdated($customer, 'Customer', $oldValues, [ + 'Nama' => $customer->name, + 'No. Telepon' => $customer->phone_number, + 'Alamat' => $customer->address, + ]); + return $customer; } public function destroy(Customer $customer): bool { + $this->logDeleted($customer, 'Customer', [ + 'Nama' => $customer->name, + 'No. Telepon' => $customer->phone_number, + 'Alamat' => $customer->address, + ]); + return $customer->delete(); } } diff --git a/app/Services/Admin/Master/SupplierService.php b/app/Services/Admin/Master/SupplierService.php index 79a742c..d3b8c68 100644 --- a/app/Services/Admin/Master/SupplierService.php +++ b/app/Services/Admin/Master/SupplierService.php @@ -3,11 +3,14 @@ namespace App\Services\Admin\Master; use App\Models\Supplier; +use App\Services\Concerns\LogsFormHistory; use Illuminate\Contracts\Pagination\LengthAwarePaginator; use Illuminate\Database\Eloquent\Collection; class SupplierService { + use LogsFormHistory; + public function paginated(int $perPage = 25, string $search = '', string $sort = 'created_at', string $direction = 'desc', array $filters = []): LengthAwarePaginator { return Supplier::query() @@ -24,18 +27,44 @@ public function getAll(): Collection public function store(array $data): Supplier { - return Supplier::create($data); + $supplier = Supplier::create($data); + + $this->logCreated($supplier, 'Supplier', [ + 'Nama' => $supplier->name, + 'No. Telepon' => $supplier->phone_number, + 'Alamat' => $supplier->address, + ]); + + return $supplier; } public function update(Supplier $supplier, array $data): Supplier { + $oldValues = [ + 'Nama' => $supplier->name, + 'No. Telepon' => $supplier->phone_number, + 'Alamat' => $supplier->address, + ]; + $supplier->update($data); + $this->logUpdated($supplier, 'Supplier', $oldValues, [ + 'Nama' => $supplier->name, + 'No. Telepon' => $supplier->phone_number, + 'Alamat' => $supplier->address, + ]); + return $supplier; } public function destroy(Supplier $supplier): bool { + $this->logDeleted($supplier, 'Supplier', [ + 'Nama' => $supplier->name, + 'No. Telepon' => $supplier->phone_number, + 'Alamat' => $supplier->address, + ]); + return $supplier->delete(); } } diff --git a/app/Services/Concerns/LogsFormHistory.php b/app/Services/Concerns/LogsFormHistory.php index 5c5bee4..bc60f01 100644 --- a/app/Services/Concerns/LogsFormHistory.php +++ b/app/Services/Concerns/LogsFormHistory.php @@ -10,56 +10,191 @@ trait LogsFormHistory { private function logCreated(Model $model, string $module, array $newValues): void { - $this->createFormHistory( - module: $module, - event: 'created', - description: "{$module} ditambahkan", - newValues: $newValues, - ); - } - - private function logUpdated(Model $model, string $module, array $oldValues, array $newValues): void - { - $this->createFormHistory( - module: $module, - event: 'updated', - description: "{$module} diperbarui", - newValues: $newValues, - oldValues: $oldValues, - ); - } - - private function logDeleted(Model $model, string $module, array $oldValues): void - { - $this->createFormHistory( - module: $module, - event: 'deleted', - description: "{$module} dihapus", - oldValues: $oldValues, - ); - } - - private function createFormHistory( - string $module, - string $event, - string $description, - array $newValues = [], - array $oldValues = [], - ): void { - $attributeChanges = array_filter([ - 'new' => $newValues ?: null, - 'old' => $oldValues ?: null, - ]); + $changes = $this->buildCreatedChanges($newValues); FormHistory::create([ 'causer_id' => Auth::id(), 'module' => $module, - 'event' => $event, - 'description' => $description, - 'attribute_changes' => $attributeChanges ?: null, + 'event' => 'created', + 'description' => "{$module} ditambahkan", + 'attribute_changes' => $changes ?: null, ]); } + private function logUpdated(Model $model, string $module, array $oldValues, array $newValues): void + { + $changes = $this->buildUpdatedChanges($oldValues, $newValues); + + FormHistory::create([ + 'causer_id' => Auth::id(), + 'module' => $module, + 'event' => 'updated', + 'description' => "{$module} diperbarui", + 'attribute_changes' => $changes ?: null, + ]); + } + + private function logDeleted(Model $model, string $module, array $oldValues): void + { + $changes = $this->buildDeletedChanges($oldValues); + + FormHistory::create([ + 'causer_id' => Auth::id(), + 'module' => $module, + 'event' => 'deleted', + 'description' => "{$module} dihapus", + 'attribute_changes' => $changes ?: null, + ]); + } + + private function resolveItemName(array $item): string + { + $nameKeys = ['Nama Varian', 'Nama', 'name', 'variant', 'Variant', 'variant_name']; + + foreach ($nameKeys as $nameKey) { + if (! empty($item[$nameKey])) { + return (string) $item[$nameKey]; + } + } + + return '?'; + } + + private function buildCreatedChanges(array $newValues): array + { + $changes = []; + + foreach ($newValues as $key => $value) { + if (is_array($value)) { + foreach ($value as $item) { + if (is_array($item)) { + $itemName = $this->resolveItemName($item); + foreach ($item as $subKey => $subValue) { + $changes["{$key}: {$itemName} — {$subKey}"] = ['new' => $subValue]; + } + } else { + $changes["{$key}: {$item}"] = ['new' => null]; + } + } + } else { + $changes[$key] = ['new' => $value]; + } + } + + return $changes; + } + + private function buildUpdatedChanges(array $oldValues, array $newValues): array + { + $changes = []; + $allKeys = array_unique(array_merge(array_keys($oldValues), array_keys($newValues))); + + foreach ($allKeys as $key) { + $old = $oldValues[$key] ?? null; + $new = $newValues[$key] ?? null; + + if (is_array($old) && is_array($new)) { + $nestedChanges = $this->diffArray($old, $new, $key); + $changes = array_merge($changes, $nestedChanges); + } elseif ($old !== $new) { + $changes[$key] = array_filter([ + 'old' => $old ?? null, + 'new' => $new ?? null, + ]); + } + } + + return $changes; + } + + private function buildDeletedChanges(array $oldValues): array + { + $changes = []; + + foreach ($oldValues as $key => $value) { + if (is_array($value)) { + foreach ($value as $item) { + if (is_array($item)) { + $itemName = $this->resolveItemName($item); + foreach ($item as $subKey => $subValue) { + $changes["{$key}: {$itemName} — {$subKey}"] = ['old' => $subValue]; + } + } else { + $changes["{$key}: {$item}"] = ['old' => null]; + } + } + } else { + $changes[$key] = ['old' => $value]; + } + } + + return $changes; + } + + private function diffArray(array $old, array $new, string $prefix = ''): array + { + $changes = []; + + $isAssociative = ! empty($old) && array_is_list($old) === false; + + if ($isAssociative) { + $allKeys = array_unique(array_merge(array_keys($old), array_keys($new))); + + foreach ($allKeys as $key) { + $fullKey = "{$prefix}.{$key}"; + + if (isset($old[$key]) && is_array($old[$key]) && isset($new[$key]) && is_array($new[$key])) { + $nested = $this->diffArray($old[$key], $new[$key], $fullKey); + $changes = array_merge($changes, $nested); + } else { + $oldVal = $old[$key] ?? null; + $newVal = $new[$key] ?? null; + + if ($oldVal !== $newVal) { + $changes[$fullKey] = array_filter([ + 'old' => $oldVal ?? null, + 'new' => $newVal ?? null, + ]); + } + } + } + } else { + $max = max(count($old), count($new)); + + for ($i = 0; $i < $max; $i++) { + $oldItem = $old[$i] ?? null; + $newItem = $new[$i] ?? null; + + if (is_array($oldItem) && is_array($newItem)) { + $itemName = $this->resolveItemName($newItem); + $itemPrefix = "{$prefix}: {$itemName}"; + $itemKeys = array_unique(array_merge(array_keys($oldItem), array_keys($newItem))); + + foreach ($itemKeys as $subKey) { + $fullKey = "{$itemPrefix} — {$subKey}"; + $oldVal = $oldItem[$subKey] ?? null; + $newVal = $newItem[$subKey] ?? null; + + if ($oldVal !== $newVal) { + $changes[$fullKey] = array_filter([ + 'old' => $oldVal ?? null, + 'new' => $newVal ?? null, + ]); + } + } + } elseif ($oldItem !== $newItem) { + $itemName = $this->resolveItemName(is_array($oldItem) ? $oldItem : (is_array($newItem) ? $newItem : [])); + $changes["{$prefix}: {$itemName}"] = array_filter([ + 'old' => $oldItem, + 'new' => $newItem, + ]); + } + } + } + + return $changes; + } + private function formatCurrency(int $value): string { return 'Rp ' . number_format($value, 0, ',', '.'); diff --git a/resources/js/pages/admin/system/form-histories/attribute-changes-dialog.tsx b/resources/js/pages/admin/system/form-histories/attribute-changes-dialog.tsx index 9807725..62ee4e2 100644 --- a/resources/js/pages/admin/system/form-histories/attribute-changes-dialog.tsx +++ b/resources/js/pages/admin/system/form-histories/attribute-changes-dialog.tsx @@ -33,7 +33,7 @@ const eventLabel: Record = { deleted: 'Dihapus', }; -function renderValue(key: string, value: unknown): React.ReactNode { +function renderValue(value: unknown): React.ReactNode { if (value === null || value === undefined) { return -; } @@ -47,10 +47,6 @@ function renderValue(key: string, value: unknown): React.ReactNode { return -; } - if (typeof value[0] === 'object' && value[0] !== null) { - return ; - } - return value.join(', '); } @@ -61,41 +57,18 @@ function renderValue(key: string, value: unknown): React.ReactNode { return String(value); } -function RenderObjectArray({ items }: { items: Record[] }) { - if (!items.length) { - return -; +function flattenChanges(attributeChanges: Record): { field: string; old?: unknown; new?: unknown }[] { + const entries: { field: string; old?: unknown; new?: unknown }[] = []; + + for (const [key, change] of Object.entries(attributeChanges)) { + entries.push({ + field: key, + old: change.old, + new: change.new, + }); } - const keys = Object.keys(items[0]); - - return ( -
-
- - - - {keys.map((key) => ( - - {key} - - ))} - - - - {items.map((item, index) => ( - - {keys.map((key) => ( - - {renderValue(key, item[key])} - - ))} - - ))} - -
-
-
- ); + return entries; } export function AttributeChangesDialog({ open, onOpenChange, item }: AttributeChangesDialogProps) { @@ -104,8 +77,8 @@ export function AttributeChangesDialog({ open, onOpenChange, item }: AttributeCh } const changes = item.attribute_changes; - const hasNew = changes?.new && Object.keys(changes.new).length > 0; - const hasOld = changes?.old && Object.keys(changes.old).length > 0; + const hasChanges = changes && Object.keys(changes).length > 0; + const flattened = hasChanges ? flattenChanges(changes) : []; return ( @@ -125,39 +98,50 @@ export function AttributeChangesDialog({ open, onOpenChange, item }: AttributeCh
{item.formatted_created_at}
- {hasNew && ( -
-

Nilai Baru

-
-
- {Object.entries(changes!.new!).map(([key, value]) => ( -
-
{key}
-
{renderValue(key, value)}
-
- ))} -
+ {hasChanges && ( +
+
+ + + + Field + Lama + Baru + + + + {flattened.map((row) => ( + + + {row.field} + + + {row.old !== undefined ? ( + + {renderValue(row.old)} + + ) : ( + - + )} + + + {row.new !== undefined ? ( + + {renderValue(row.new)} + + ) : ( + - + )} + + + ))} + +
)} - {hasOld && ( -
-

Nilai Lama

-
-
- {Object.entries(changes!.old!).map(([key, value]) => ( -
-
{key}
-
{renderValue(key, value)}
-
- ))} -
-
-
- )} - - {!hasNew && !hasOld && ( + {!hasChanges && (

Tidak ada perubahan data.

diff --git a/resources/js/pages/admin/system/form-histories/columns.tsx b/resources/js/pages/admin/system/form-histories/columns.tsx index 6b9587a..c988f41 100644 --- a/resources/js/pages/admin/system/form-histories/columns.tsx +++ b/resources/js/pages/admin/system/form-histories/columns.tsx @@ -9,10 +9,7 @@ export type FormHistory = { module: string; event: string; description: string; - attribute_changes: { - new?: Record; - old?: Record; - } | null; + attribute_changes: Record | null; created_at: string; formatted_created_at: string; causer: {