feat: enhance form history logging by restructuring attribute changes and improving data handling in LogsFormHistory and related components
This commit is contained in:
parent
3e715518fb
commit
4d94fbe7c6
@ -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(),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@ -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, ',', '.');
|
||||
|
||||
@ -33,7 +33,7 @@ const eventLabel: Record<string, string> = {
|
||||
deleted: 'Dihapus',
|
||||
};
|
||||
|
||||
function renderValue(key: string, value: unknown): React.ReactNode {
|
||||
function renderValue(value: unknown): React.ReactNode {
|
||||
if (value === null || value === undefined) {
|
||||
return <span className="text-muted-foreground">-</span>;
|
||||
}
|
||||
@ -47,10 +47,6 @@ function renderValue(key: string, value: unknown): React.ReactNode {
|
||||
return <span className="text-muted-foreground">-</span>;
|
||||
}
|
||||
|
||||
if (typeof value[0] === 'object' && value[0] !== null) {
|
||||
return <RenderObjectArray items={value} />;
|
||||
}
|
||||
|
||||
return value.join(', ');
|
||||
}
|
||||
|
||||
@ -61,41 +57,18 @@ function renderValue(key: string, value: unknown): React.ReactNode {
|
||||
return String(value);
|
||||
}
|
||||
|
||||
function RenderObjectArray({ items }: { items: Record<string, unknown>[] }) {
|
||||
if (!items.length) {
|
||||
return <span className="text-muted-foreground">-</span>;
|
||||
function flattenChanges(attributeChanges: Record<string, { old?: unknown; new?: unknown }>): { 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 (
|
||||
<div className="rounded-md border">
|
||||
<div className="max-h-[300px] overflow-auto">
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
{keys.map((key) => (
|
||||
<TableHead key={key} className="h-8 text-xs">
|
||||
{key}
|
||||
</TableHead>
|
||||
))}
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{items.map((item, index) => (
|
||||
<TableRow key={index}>
|
||||
{keys.map((key) => (
|
||||
<TableCell key={key} className="py-1.5 text-xs">
|
||||
{renderValue(key, item[key])}
|
||||
</TableCell>
|
||||
))}
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
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 (
|
||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||
@ -125,39 +98,50 @@ export function AttributeChangesDialog({ open, onOpenChange, item }: AttributeCh
|
||||
<div>{item.formatted_created_at}</div>
|
||||
</div>
|
||||
|
||||
{hasNew && (
|
||||
<div className="space-y-2">
|
||||
<h4 className="text-sm font-medium">Nilai Baru</h4>
|
||||
<div className="rounded-md border p-3 bg-muted/50">
|
||||
<dl className="space-y-2">
|
||||
{Object.entries(changes!.new!).map(([key, value]) => (
|
||||
<div key={key} className="flex flex-col">
|
||||
<dt className="text-xs text-muted-foreground">{key}</dt>
|
||||
<dd className="text-sm font-medium">{renderValue(key, value)}</dd>
|
||||
</div>
|
||||
))}
|
||||
</dl>
|
||||
{hasChanges && (
|
||||
<div className="rounded-md border">
|
||||
<div className="max-h-[300px] overflow-auto">
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead className="h-8 text-xs w-[30%]">Field</TableHead>
|
||||
<TableHead className="h-8 text-xs w-[35%]">Lama</TableHead>
|
||||
<TableHead className="h-8 text-xs w-[35%]">Baru</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{flattened.map((row) => (
|
||||
<TableRow key={row.field}>
|
||||
<TableCell className="py-1.5 text-xs font-medium">
|
||||
{row.field}
|
||||
</TableCell>
|
||||
<TableCell className="py-1.5 text-xs">
|
||||
{row.old !== undefined ? (
|
||||
<span className="text-red-600 dark:text-red-400">
|
||||
{renderValue(row.old)}
|
||||
</span>
|
||||
) : (
|
||||
<span className="text-muted-foreground">-</span>
|
||||
)}
|
||||
</TableCell>
|
||||
<TableCell className="py-1.5 text-xs">
|
||||
{row.new !== undefined ? (
|
||||
<span className="text-green-600 dark:text-green-400">
|
||||
{renderValue(row.new)}
|
||||
</span>
|
||||
) : (
|
||||
<span className="text-muted-foreground">-</span>
|
||||
)}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{hasOld && (
|
||||
<div className="space-y-2">
|
||||
<h4 className="text-sm font-medium">Nilai Lama</h4>
|
||||
<div className="rounded-md border p-3 bg-muted/50">
|
||||
<dl className="space-y-2">
|
||||
{Object.entries(changes!.old!).map(([key, value]) => (
|
||||
<div key={key} className="flex flex-col">
|
||||
<dt className="text-xs text-muted-foreground">{key}</dt>
|
||||
<dd className="text-sm font-medium">{renderValue(key, value)}</dd>
|
||||
</div>
|
||||
))}
|
||||
</dl>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{!hasNew && !hasOld && (
|
||||
{!hasChanges && (
|
||||
<p className="text-sm text-muted-foreground text-center py-4">
|
||||
Tidak ada perubahan data.
|
||||
</p>
|
||||
|
||||
@ -9,10 +9,7 @@ export type FormHistory = {
|
||||
module: string;
|
||||
event: string;
|
||||
description: string;
|
||||
attribute_changes: {
|
||||
new?: Record<string, unknown>;
|
||||
old?: Record<string, unknown>;
|
||||
} | null;
|
||||
attribute_changes: Record<string, { old?: unknown; new?: unknown }> | null;
|
||||
created_at: string;
|
||||
formatted_created_at: string;
|
||||
causer: {
|
||||
|
||||
Loading…
Reference in New Issue
Block a user