From 1d5863efaf56c07318b04d2afaf0f69e0be813ee Mon Sep 17 00:00:00 2001 From: Yoga Pangestu Date: Mon, 27 Jul 2026 23:00:22 +0700 Subject: [PATCH] feat: integrate activity log functionality in ProductPrice model and enhance change formatting in ActivityLogService --- app/Models/ProductPrice.php | 21 +- app/Services/System/ActivityLogService.php | 35 ++- app/Support/ActivityLog/FieldLabel.php | 297 +++++++++++++++++++++ app/Support/ActivityLog/ModelLabel.php | 2 + 4 files changed, 342 insertions(+), 13 deletions(-) create mode 100644 app/Support/ActivityLog/FieldLabel.php diff --git a/app/Models/ProductPrice.php b/app/Models/ProductPrice.php index 5be5527..5626c86 100644 --- a/app/Models/ProductPrice.php +++ b/app/Models/ProductPrice.php @@ -3,19 +3,21 @@ namespace App\Models; use App\Enums\PriceType; +use App\Models\Concerns\InteractsWithActivityLog; use Illuminate\Database\Eloquent\Attributes\Appends; use Illuminate\Database\Eloquent\Attributes\Guarded; use Illuminate\Database\Eloquent\Casts\Attribute; use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Relations\BelongsTo; +use Spatie\Activitylog\Support\LogOptions; #[Guarded(['id'])] #[Appends(['price_formatted', 'price_input'])] class ProductPrice extends Model { // 1. Use Trait - use HasFactory; + use HasFactory, InteractsWithActivityLog; // 2. Casting protected function casts(): array @@ -26,18 +28,31 @@ protected function casts(): array ]; } + public function getActivitylogOptions(): LogOptions + { + return LogOptions::defaults() + ->logUnguarded() + ->logOnlyDirty() + ->dontLogEmptyChanges() + ->logExcept([ + 'id', + 'variant_id', + 'deleted_at', + ]); + } + // 3. Attribute public function priceFormatted(): Attribute { return Attribute::make( - get: fn () => 'Rp '.number_format($this->price, 0, ',', '.'), + get: fn() => 'Rp ' . number_format($this->price, 0, ',', '.'), ); } public function priceInput(): Attribute { return Attribute::make( - get: fn () => (string) $this->price, + get: fn() => (string) $this->price, ); } diff --git a/app/Services/System/ActivityLogService.php b/app/Services/System/ActivityLogService.php index c7be4a1..9ad3c57 100644 --- a/app/Services/System/ActivityLogService.php +++ b/app/Services/System/ActivityLogService.php @@ -4,6 +4,7 @@ use App\Enums\ActivityEventLabel; use App\Models\User; +use App\Support\ActivityLog\FieldLabel; use App\Support\ActivityLog\ModelLabel; use Illuminate\Contracts\Pagination\LengthAwarePaginator; use Illuminate\Database\Eloquent\Builder; @@ -62,18 +63,32 @@ private function formatChanges(?Collection $changes): array $attributes = $changes->get('attributes', []); $old = $changes->get('old', []); - if (! is_array($attributes)) { - return []; - } - $formatted = []; - foreach ($attributes as $field => $newValue) { - $formatted[] = [ - 'field' => (string) $field, - 'old' => is_array($old) ? ($old[$field] ?? null) : null, - 'new' => $newValue, - ]; + if (is_array($attributes) && count($attributes) > 0) { + foreach ($attributes as $field => $newValue) { + if (! FieldLabel::isFormInput((string) $field)) { + continue; + } + + $formatted[] = [ + 'field' => FieldLabel::for((string) $field), + 'old' => is_array($old) ? ($old[$field] ?? null) : null, + 'new' => $newValue, + ]; + } + } elseif (is_array($old) && count($old) > 0) { + foreach ($old as $field => $oldValue) { + if (! FieldLabel::isFormInput((string) $field)) { + continue; + } + + $formatted[] = [ + 'field' => FieldLabel::for((string) $field), + 'old' => $oldValue, + 'new' => null, + ]; + } } return $formatted; diff --git a/app/Support/ActivityLog/FieldLabel.php b/app/Support/ActivityLog/FieldLabel.php new file mode 100644 index 0000000..59ec769 --- /dev/null +++ b/app/Support/ActivityLog/FieldLabel.php @@ -0,0 +1,297 @@ + + */ + private const LABELS = [ + // Common + 'name' => 'Nama', + 'description' => 'Deskripsi', + 'status' => 'Status', + 'amount' => 'Jumlah', + 'quantity' => 'Jumlah', + 'price' => 'Harga', + 'balance' => 'Saldo', + 'discount' => 'Diskon', + 'notes' => 'Keterangan', + 'reason' => 'Alasan', + 'type' => 'Jenis', + 'unit' => 'Satuan', + 'stock' => 'Stok', + 'retail_stock' => 'Stok Ecer', + 'reject_stock' => 'Stok Reject', + + // Product + 'category_id' => 'Kategori', + 'category_ids' => 'Kategori', + 'product_variant_id' => 'Varian Produk', + 'variants' => 'Varian', + 'prices' => 'Harga', + 'is_active' => 'Status Aktif', + + // Order + 'customer_id' => 'Pelanggan', + 'marketing_id' => 'Marketing', + 'channel' => 'Channel', + 'price_type' => 'Tipe Harga', + 'payment_type' => 'Tipe Pembayaran', + 'is_affiliate' => 'Pesanan Afiliasi', + 'tiktok_order_id' => 'ID Pesanan TikTok Shop', + 'shopee_order_id' => 'ID Pesanan Shopee', + 'nego_price' => 'Harga Nego', + 'items' => 'Item', + 'subtotal' => 'Subtotal', + 'total' => 'Total', + 'shipping_cost' => 'Ongkir', + + // Purchase + 'supplier_id' => 'Supplier', + + // Cutting + 'materials' => 'Bahan Baku', + 'results' => 'Hasil Produk', + 'sewing_cost' => 'Jasa Jahit', + 'other_cost' => 'Biaya Lainnya', + 'material_usage' => 'Pemakaian', + 'material_result' => 'Hasil', + 'combination_material_result' => 'Hasil Kombinasi', + 'cutting_result' => 'Hasil', + 'sample' => 'Sample', + 'original_outside_sample' => 'Diluar Sample', + 'product_name' => 'Nama Produk', + + // Employee / HR + 'email' => 'Email', + 'username' => 'Username', + 'full_name' => 'Nama Lengkap', + 'phone_number' => 'Nomor Telepon', + 'gender' => 'Jenis Kelamin', + 'birth_date' => 'Tanggal Lahir', + 'address' => 'Alamat', + 'role' => 'Role', + 'join_date' => 'Tanggal Bergabung', + 'employment_status' => 'Status Kepegawaian', + 'base_salary' => 'Gaji Pokok', + 'start_date' => 'Tanggal Mulai', + 'end_date' => 'Tanggal Selesai', + 'due_date' => 'Jatuh Tempo', + 'opname_date' => 'Tanggal Opname', + 'stock_type' => 'Tipe Stok', + 'physical_stock' => 'Stok Fisik', + + // Finance + 'payment' => 'Pembayaran', + 'paid_amount' => 'Jumlah Bayar', + + // Attendance + 'photo' => 'Foto', + 'latitude' => 'Latitude', + 'longitude' => 'Longitude', + 'check_in' => 'Jam Masuk', + 'check_out' => 'Jam Pulang', + + // System + 'app_name' => 'Nama Aplikasi', + 'about_app' => 'Tentang Aplikasi', + 'phone' => 'Nomor Telepon', + 'logo' => 'Logo', + 'favicon' => 'Favicon', + 'login_cover' => 'Cover Login', + 'permissions' => 'Hak Akses', + 'appearance' => 'Tampilan', + 'hero_image' => 'Foto Hero', + 'about_image' => 'Foto Tentang Kami', + 'gallery_s3_keys' => 'Koleksi Lookbook', + 'profile_s3_key' => 'Foto Profil', + 'profile_photo' => 'Foto Profil', + + // Social media + 'instagram_url' => 'Instagram', + 'facebook_url' => 'Facebook', + 'tiktok_url' => 'TikTok', + + // HR Settings + 'scheduled_check_in_time' => 'Jam Masuk Kerja', + 'scheduled_check_out_time' => 'Jam Pulang Kerja', + 'late_penalty_amount' => 'Denda Keterlambatan', + 'absent_penalty_amount' => 'Denda Bolos', + + // Marketplace fees + 'tiktok_shop_platform_commission' => 'TikTok Shop - Komisi Platform', + 'tiktok_shop_logistics_service_fee' => 'TikTok Shop - Biaya Logistik', + 'tiktok_shop_dynamic_commission' => 'TikTok Shop - Komisi Dinamis', + 'tiktok_shop_order_processing_fee' => 'TikTok Shop - Biaya Proses Pesanan', + 'tiktok_shop_affiliate' => 'TikTok Shop - Komisi Affiliate', + 'tiktok_shop_pre_order_service_fee' => 'TikTok Shop - Biaya Pre Order', + 'shopee_admin_fee' => 'Shopee - Biaya Admin', + 'shopee_program_fee' => 'Shopee - Biaya Program', + 'shopee_shipping_savings' => 'Shopee - Hemat Biaya Kirim', + 'shopee_premium' => 'Shopee - Premi', + 'shopee_service_fee' => 'Shopee - Biaya Layanan', + 'shopee_order_processing_fee' => 'Shopee - Biaya Proses Pesanan', + 'shopee_ams_commission_fee' => 'Shopee - Komisi AMS', + 'shopee_pre_order' => 'Shopee - Pre Order', + 'shopee_live_extra' => 'Shopee - Live Extra', + + // Media + 's3_keys' => 'Foto', + 'remove_media_ids' => 'Media yang Dihapus', + ]; + + /** + * Field names that appear as form inputs in CRUD operations. + * Auto-generated/computed fields (slug, subtotal, total, etc.) are excluded. + * + * @var array + */ + private const FORM_INPUT_FIELDS = [ + // Common + 'name' => true, + 'description' => true, + 'status' => true, + 'amount' => true, + 'quantity' => true, + 'price' => true, + 'balance' => true, + 'discount' => true, + 'notes' => true, + 'reason' => true, + 'type' => true, + 'unit' => true, + 'stock' => true, + 'retail_stock' => true, + 'reject_stock' => true, + + // Product + 'category_id' => true, + 'category_ids' => true, + 'product_variant_id' => true, + 'variants' => true, + 'prices' => true, + 'is_active' => true, + + // Order + 'customer_id' => true, + 'marketing_id' => true, + 'channel' => true, + 'price_type' => true, + 'payment_type' => true, + 'is_affiliate' => true, + 'tiktok_order_id' => true, + 'shopee_order_id' => true, + 'nego_price' => true, + 'items' => true, + 'shipping_cost' => true, + + // Purchase + 'supplier_id' => true, + 'raw_material_id' => true, + 'raw_material_price_id' => true, + + // Cutting + 'materials' => true, + 'results' => true, + 'sewing_cost' => true, + 'other_cost' => true, + 'material_usage' => true, + 'material_result' => true, + 'combination_material_result' => true, + 'cutting_result' => true, + 'sample' => true, + 'original_outside_sample' => true, + 'product_name' => true, + + // Employee / HR + 'email' => true, + 'username' => true, + 'full_name' => true, + 'phone_number' => true, + 'gender' => true, + 'birth_date' => true, + 'address' => true, + 'role' => true, + 'join_date' => true, + 'employment_status' => true, + 'base_salary' => true, + 'start_date' => true, + 'end_date' => true, + 'due_date' => true, + 'opname_date' => true, + 'stock_type' => true, + 'physical_stock' => true, + + // Finance + 'payment' => true, + 'paid_amount' => true, + + // Attendance + 'photo' => true, + 'latitude' => true, + 'longitude' => true, + 'check_in' => true, + 'check_out' => true, + + // System + 'app_name' => true, + 'about_app' => true, + 'phone' => true, + 'logo' => true, + 'favicon' => true, + 'login_cover' => true, + 'permissions' => true, + 'appearance' => true, + 'hero_image' => true, + 'about_image' => true, + 'gallery_s3_keys' => true, + 'profile_s3_key' => true, + 'profile_photo' => true, + 'unit_price' => true, + 'variant' => true, + + // Social media + 'instagram_url' => true, + 'facebook_url' => true, + 'tiktok_url' => true, + + // HR Settings + 'scheduled_check_in_time' => true, + 'scheduled_check_out_time' => true, + 'late_penalty_amount' => true, + 'absent_penalty_amount' => true, + + // Marketplace fees + 'tiktok_shop_platform_commission' => true, + 'tiktok_shop_logistics_service_fee' => true, + 'tiktok_shop_dynamic_commission' => true, + 'tiktok_shop_order_processing_fee' => true, + 'tiktok_shop_affiliate' => true, + 'tiktok_shop_pre_order_service_fee' => true, + 'shopee_admin_fee' => true, + 'shopee_program_fee' => true, + 'shopee_shipping_savings' => true, + 'shopee_premium' => true, + 'shopee_service_fee' => true, + 'shopee_order_processing_fee' => true, + 'shopee_ams_commission_fee' => true, + 'shopee_pre_order' => true, + 'shopee_live_extra' => true, + + // Media + 's3_keys' => true, + 'remove_media_ids' => true, + ]; + + public static function for(string $field): string + { + return self::LABELS[$field] ?? ucfirst(str_replace('_', ' ', $field)); + } + + public static function isFormInput(string $field): bool + { + return isset(self::FORM_INPUT_FIELDS[$field]); + } +} diff --git a/app/Support/ActivityLog/ModelLabel.php b/app/Support/ActivityLog/ModelLabel.php index 81c35b9..919493b 100644 --- a/app/Support/ActivityLog/ModelLabel.php +++ b/app/Support/ActivityLog/ModelLabel.php @@ -20,6 +20,7 @@ use App\Models\PayrollAdjustment; use App\Models\PayrollPeriod; use App\Models\Product; +use App\Models\ProductPrice; use App\Models\ProductVariant; use App\Models\Purchase; use App\Models\PurchaseItem; @@ -58,6 +59,7 @@ class ModelLabel PayrollAdjustment::class => 'Penyesuaian Gaji', PayrollPeriod::class => 'Periode Gaji', Product::class => 'Produk', + ProductPrice::class => 'Harga Produk', ProductVariant::class => 'Varian Produk', Purchase::class => 'Belanja', PurchaseItem::class => 'Item Belanja',