From 393c36e9326bf9ead6ea093a4bc9288bc8cdefde Mon Sep 17 00:00:00 2001 From: Yoga Pangestu Date: Sun, 28 Jun 2026 14:47:19 +0700 Subject: [PATCH] refactor: reorganize model structure and enhance attribute handling across multiple models, including improved casting, scope definitions, and attribute formatting --- BEST_PRACTICE.md | 104 +++++++++-- app/Models/Attendance.php | 33 ++-- app/Models/CashAccount.php | 14 +- app/Models/CashTransaction.php | 113 +++++++----- app/Models/Category.php | 13 +- app/Models/Customer.php | 2 + app/Models/Cutting.php | 90 ++++----- app/Models/CuttingMaterial.php | 35 ++-- app/Models/CuttingResult.php | 5 +- app/Models/CuttingResultPrice.php | 38 ++-- app/Models/Employee.php | 62 ++++--- app/Models/EmployeeAdvance.php | 128 +++++++------ app/Models/EmployeeAdvancePayment.php | 43 +++-- app/Models/Expense.php | 25 ++- app/Models/HomepageConfiguration.php | 14 +- app/Models/LeaveRequest.php | 54 +++--- app/Models/Notification.php | 21 ++- app/Models/Order.php | 222 ++++++++++++++++++----- app/Models/OrderItem.php | 40 ++-- app/Models/OwnerVerificationRequest.php | 98 +++++----- app/Models/Payroll.php | 90 +++++---- app/Models/PayrollAdjustment.php | 40 ++-- app/Models/PayrollPeriod.php | 33 +++- app/Models/Product.php | 60 +++--- app/Models/ProductPrice.php | 16 +- app/Models/ProductVariant.php | 36 ++-- app/Models/Purchase.php | 65 ++++--- app/Models/PurchaseItem.php | 38 ++-- app/Models/PushSubscription.php | 1 + app/Models/RawMaterial.php | 46 +++-- app/Models/RawMaterialPrice.php | 37 ++-- app/Models/Rejection.php | 2 + app/Models/StockEcerHistory.php | 12 +- app/Models/StokOpname.php | 65 ++++--- app/Models/StokOpnameItem.php | 17 +- app/Models/Supplier.php | 2 + app/Models/SystemConfiguration.php | 14 +- app/Models/User.php | 74 ++++---- app/Models/UserProfile.php | 41 +++-- app/Services/System/AnalysisService.php | 14 +- app/Services/System/DashboardService.php | 2 +- resources/js/pages/admin/Analysis.vue | 42 ++--- resources/js/pages/admin/Dashboard.vue | 6 +- 43 files changed, 1170 insertions(+), 737 deletions(-) diff --git a/BEST_PRACTICE.md b/BEST_PRACTICE.md index 48b519b..c8135f1 100644 --- a/BEST_PRACTICE.md +++ b/BEST_PRACTICE.md @@ -20,23 +20,77 @@ ### Model Eloquent Setiap model harus disusun mengikuti urutan struktur berikut dari atas ke bawah: 1. **Use Trait** (misal: `use HasFactory, SoftDeletes;`). 2. **Casting** (metode `casts()`). - 3. **Relasi (Relationships)** (diurutkan secara alfabetis berdasarkan nama method relasi). - 4. **Attribute (Accessors / Appends)** (misal format harga, format tanggal, dll. Harus dimasukkan dalam array `$appends` di atas class). - 5. **Scope** (metode local scope dengan PHP attribute `#[Scope]`). - 6. **Method Lainnya** (helper method, logic bisnis, dll.). + 3. **Scope** (metode local scope dengan PHP attribute `#[Scope]`). + 4. **Attribute** (Accessors / Appends - misal format harga, format tanggal, dll. Harus dimasukkan dalam array `$appends` di atas class). + 5. **Method Lainnya** (helper method, logic bisnis, dll.). + 6. **Relasi (Relationships)** (diurutkan secara alfabetis berdasarkan nama method relasi). + +- **Pengelompokan Scope:** + - Scope dikelompokkan berdasarkan kolom yang di-query. + - Urutkan berdasarkan nama kolom (abjad), lalu di dalam setiap Column Group, urutkan lagi berdasarkan nama method (abjad). + - **Contoh:** Jika ada kolom `is_active` dan `status`: + ```php + // Column group: is_active (first alphabetical) + #[Scope] + public function active(Builder $query): void + { + $query->where('is_active', true); + } + + #[Scope] + public function inactive(Builder $query): void + { + $query->where('is_active', false); + } + + // Column group: status (second alphabetical) + #[Scope] + public function completed(Builder $query): void + { + $query->where('status', OrderStatus::COMPLETED); + } + + #[Scope] + public function pending(Builder $query): void + { + $query->where('status', OrderStatus::PENDING); + } + ``` - **Hubungan Timbal Balik (2-Way Relations):** - Pastikan setiap relasi ditulis secara 2 arah (bi-directional). - Jika suatu model memiliki `belongsTo` ke model lain, pastikan model lain tersebut juga mendefinisikan relasi kebalikannya (`hasMany` atau `hasOne`). -- **Casting & Formatting Attribute:** - - Lakukan casting secara tepat pada kolom yang memerlukan tipe data khusus (seperti Boolean, Integer, Datetime, atau Enum). - - Jika suatu kolom perlu diformat (misalnya konversi harga ke format Rupiah atau format tanggal lokal), buatlah accessor menggunakan class `Attribute` (Eloquent Attribute) dan tambahkan nama atribut tersebut ke properti `$appends` model. +- **Casting:** + - Lakukan casting secara tepat pada kolom yang memerlukan tipe data khusus. + - Tipe data yang perlu di-cast: `enum`, `boolean`, `integer`, `decimal`, `date`, `datetime`, `array`, `json`, dll. + - **Jangan** cast kolom relasi (foreign key). + - **Contoh:** + ```php + protected function casts(): array + { + return [ + 'status' => OrderStatus::class, // enum + 'is_active' => 'boolean', // boolean + 'amount' => 'integer', // integer + 'price' => 'decimal:2', // decimal + 'birth_date' => 'date', // date + 'created_at' => 'datetime', // datetime + 'settings' => 'array', // array/json + ]; + } + ``` -- **Query Scopes:** - - Jika model memiliki opsi/status tertentu, buatlah local scope agar kueri dapat digunakan kembali (reusable). Gunakan PHP attribute `#[Scope]` di atas metode scope. +- **Scope untuk Kolom Opsi:** + - Semua kolom yang memiliki opsi/status tertentu (seperti `status`, `is_active`, `type`, dll.) **wajib** memiliki scope. + - Gunakan PHP attribute `#[Scope]` di atas metode scope. -- **Contoh:** +- **Attribute untuk Format:** + - Kolom yang akan diformat (misalnya konversi harga ke format Rupiah atau format tanggal lokal) **wajib** menggunakan accessor dengan class `Attribute`. + - Nama atribut harus dimasukkan dalam properti `$appends` di atas class. + - **Contoh:** `amount` → `amount_formatted`, `status` → `status_label`. + +- **Contoh Lengkap:** ```php namespace App\Models; @@ -68,15 +122,17 @@ ### Model Eloquent ]; } - // 3. Relasi (Urut Abjad) - public function customer(): BelongsTo + // 3. Scope (grouped by column, then alphabetical) + #[Scope] + public function completed(Builder $query): void { - return $this->belongsTo(Customer::class); + $query->where('status', OrderStatus::COMPLETED); } - public function items(): HasMany + #[Scope] + public function pending(Builder $query): void { - return $this->hasMany(OrderItem::class); + $query->where('status', OrderStatus::PENDING); } // 4. Attribute @@ -94,11 +150,21 @@ ### Model Eloquent ); } - // 5. Scope - #[Scope] - public function pending(Builder $query): void + // 5. Method Lainnya + public static function mediaModuleName(): string { - $query->where('status', OrderStatus::PENDING->value); + return 'order'; + } + + // 6. Relation + public function customer(): BelongsTo + { + return $this->belongsTo(Customer::class); + } + + public function items(): HasMany + { + return $this->hasMany(OrderItem::class); } } ``` diff --git a/app/Models/Attendance.php b/app/Models/Attendance.php index 415f7ec..96e2c2a 100644 --- a/app/Models/Attendance.php +++ b/app/Models/Attendance.php @@ -17,42 +17,35 @@ #[Guarded(['id'])] #[Appends([ - 'check_in_at_formatted', - 'check_out_at_formatted', 'attendance_date_formatted', - 'work_duration_formatted', + 'check_in_at_formatted', 'check_in_photo_url', + 'check_out_at_formatted', 'check_out_photo_url', 'employee_name', + 'work_duration_formatted', ])] class Attendance extends Model implements HasMedia { + // 1. Use Trait use HasFactory, HasModuleMedia, InteractsWithActivityLog; + // 2. Casting protected function casts(): array { return [ 'attendance_date' => 'date', 'check_in_at' => 'datetime', - 'check_out_at' => 'datetime', 'check_in_latitude' => 'decimal:7', 'check_in_longitude' => 'decimal:7', + 'check_out_at' => 'datetime', 'check_out_latitude' => 'decimal:7', 'check_out_longitude' => 'decimal:7', 'work_duration_minutes' => 'integer', ]; } - public function employee(): BelongsTo - { - return $this->belongsTo(Employee::class); - } - - public function payrollAdjustments(): HasMany - { - return $this->hasMany(PayrollAdjustment::class); - } - + // 3. Attribute public function attendanceDateFormatted(): Attribute { return Attribute::make( @@ -123,6 +116,7 @@ public function workDurationFormatted(): Attribute ); } + // 4. Other Methods public static function mediaModuleName(): string { return 'attendance'; @@ -133,4 +127,15 @@ public function registerMediaCollections(): void $this->addMediaCollection('checkin')->singleFile(); $this->addMediaCollection('checkout')->singleFile(); } + + // 5. Relation + public function employee(): BelongsTo + { + return $this->belongsTo(Employee::class); + } + + public function payrollAdjustments(): HasMany + { + return $this->hasMany(PayrollAdjustment::class); + } } diff --git a/app/Models/CashAccount.php b/app/Models/CashAccount.php index 6dd494b..e472b12 100644 --- a/app/Models/CashAccount.php +++ b/app/Models/CashAccount.php @@ -14,8 +14,10 @@ #[Appends(['balance_formatted'])] class CashAccount extends Model { + // 1. Use Trait use HasFactory, InteractsWithActivityLog; + // 2. Casting protected function casts(): array { return [ @@ -23,15 +25,17 @@ protected function casts(): array ]; } - public function transactions(): HasMany - { - return $this->hasMany(CashTransaction::class); - } - + // 3. Attribute public function balanceFormatted(): Attribute { return Attribute::make( get: fn () => 'Rp '.number_format($this->balance, 0, ',', '.'), ); } + + // 4. Relation + public function transactions(): HasMany + { + return $this->hasMany(CashTransaction::class); + } } diff --git a/app/Models/CashTransaction.php b/app/Models/CashTransaction.php index 27d4aa0..f94d6b3 100644 --- a/app/Models/CashTransaction.php +++ b/app/Models/CashTransaction.php @@ -7,6 +7,8 @@ use App\Models\Concerns\InteractsWithActivityLog; use Illuminate\Database\Eloquent\Attributes\Appends; use Illuminate\Database\Eloquent\Attributes\Guarded; +use Illuminate\Database\Eloquent\Attributes\Scope; +use Illuminate\Database\Eloquent\Builder; use Illuminate\Database\Eloquent\Casts\Attribute; use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Model; @@ -20,45 +22,42 @@ #[Appends([ 'amount_formatted', 'balance_after_formatted', - 'reference_label', - 'is_incoming', - 'source_badge_class', 'created_at_formatted', 'created_by_name', + 'is_incoming', + 'reference_label', + 'source_badge_class', ])] class CashTransaction extends Model implements HasMedia { + // 1. Use Trait use HasFactory, HasModuleMedia, InteractsWithActivityLog, SoftDeletes; + // 2. Casting protected function casts(): array { return [ - 'type' => CashTransactionType::class, 'amount' => 'integer', 'balance_after' => 'integer', + 'type' => CashTransactionType::class, ]; } - public function cashAccount(): BelongsTo + // 3. Scope (grouped by column, then alphabetical) + // Column Group: type + #[Scope] + protected function deposit(Builder $query): void { - return $this->belongsTo(CashAccount::class); + $query->where('type', CashTransactionType::DEPOSIT); } - public function createdBy(): BelongsTo + #[Scope] + protected function withdrawal(Builder $query): void { - return $this->belongsTo(User::class, 'created_by_id'); - } - - public function order(): HasOne - { - return $this->hasOne(Order::class); - } - - public function reference(): MorphTo - { - return $this->morphTo(); + $query->where('type', CashTransactionType::WITHDRAWAL); } + // 4. Attribute public function amountFormatted(): Attribute { return Attribute::make( @@ -96,20 +95,6 @@ public function isIncoming(): Attribute ); } - public function sourceBadgeClass(): Attribute - { - return Attribute::make( - get: fn () => match ($this->reference_type) { - null => 'bg-blue-100 text-blue-700 hover:bg-blue-100', - Expense::class => 'bg-red-100 text-red-700 hover:bg-red-100', - EmployeeAdvance::class => 'bg-amber-100 text-amber-700 hover:bg-amber-100', - Payroll::class => 'bg-green-100 text-green-700 hover:bg-green-100', - Order::class => 'bg-purple-100 text-purple-700 hover:bg-purple-100', - default => 'bg-gray-100 text-gray-700 hover:bg-gray-100', - }, - ); - } - public function referenceLabel(): Attribute { return Attribute::make( @@ -127,10 +112,24 @@ public function referenceLabel(): Attribute ); } - public function isEmployeeAdvanceRepayment(): bool + public function sourceBadgeClass(): Attribute { - return $this->reference instanceof EmployeeAdvance - && $this->reference->repayment_cash_transaction_id === $this->id; + return Attribute::make( + get: fn () => match ($this->reference_type) { + null => 'bg-blue-100 text-blue-700 hover:bg-blue-100', + Expense::class => 'bg-red-100 text-red-700 hover:bg-red-100', + EmployeeAdvance::class => 'bg-amber-100 text-amber-700 hover:bg-amber-100', + Payroll::class => 'bg-green-100 text-green-700 hover:bg-green-100', + Order::class => 'bg-purple-100 text-purple-700 hover:bg-purple-100', + default => 'bg-gray-100 text-gray-700 hover:bg-gray-100', + }, + ); + } + + // 5. Other Methods + public function getEmployeeAdvanceLabel(): string + { + return $this->isEmployeeAdvanceRepayment() ? 'Pelunasan Kasbon' : 'Pencairan Kasbon'; } public function getManualTransactionLabel(): string @@ -138,19 +137,15 @@ public function getManualTransactionLabel(): string return $this->type === CashTransactionType::WITHDRAWAL ? 'Tarik Kas' : 'Setor Kas'; } - public function getEmployeeAdvanceLabel(): string + public function isEmployeeAdvanceRepayment(): bool { - return $this->isEmployeeAdvanceRepayment() ? 'Pelunasan Kasbon' : 'Pencairan Kasbon'; + return $this->reference instanceof EmployeeAdvance + && $this->reference->repayment_cash_transaction_id === $this->id; } - public static function mediaModuleName(): string + public static function isIncomingTransaction(self $transaction): bool { - return 'cash'; - } - - public function registerMediaCollections(): void - { - $this->addMediaCollection('photos'); + return (bool) $transaction->is_incoming; } public static function labelForReferenceType(?string $referenceType): string @@ -168,8 +163,34 @@ public static function labelForReferenceType(?string $referenceType): string }; } - public static function isIncomingTransaction(self $transaction): bool + public static function mediaModuleName(): string { - return (bool) $transaction->is_incoming; + return 'cash'; + } + + public function registerMediaCollections(): void + { + $this->addMediaCollection('photos'); + } + + // 6. Relation + public function cashAccount(): BelongsTo + { + return $this->belongsTo(CashAccount::class); + } + + public function createdBy(): BelongsTo + { + return $this->belongsTo(User::class, 'created_by_id'); + } + + public function order(): HasOne + { + return $this->hasOne(Order::class); + } + + public function reference(): MorphTo + { + return $this->morphTo(); } } diff --git a/app/Models/Category.php b/app/Models/Category.php index ef8a375..a168644 100644 --- a/app/Models/Category.php +++ b/app/Models/Category.php @@ -15,15 +15,18 @@ #[Sluggable(from: 'name', to: 'slug')] class Category extends Model { + // 1. Use Trait use HasFactory, InteractsWithActivityLog, SoftDeletes; - public function products(): BelongsToMany - { - return $this->belongsToMany(Product::class, 'product_categories'); - } - + // 2. Other Methods public static function getActiveWithProducts(): Collection { return self::whereHas('products')->get(['id', 'name', 'slug']); } + + // 3. Relation + public function products(): BelongsToMany + { + return $this->belongsToMany(Product::class, 'product_categories'); + } } diff --git a/app/Models/Customer.php b/app/Models/Customer.php index 31593cf..61a5be8 100644 --- a/app/Models/Customer.php +++ b/app/Models/Customer.php @@ -12,8 +12,10 @@ #[Guarded(['id'])] class Customer extends Model { + // 1. Use Trait use HasFactory, InteractsWithActivityLog, SoftDeletes; + // 2. Relation public function orders(): HasMany { return $this->hasMany(Order::class); diff --git a/app/Models/Cutting.php b/app/Models/Cutting.php index 20f5e90..0a7a823 100644 --- a/app/Models/Cutting.php +++ b/app/Models/Cutting.php @@ -18,24 +18,68 @@ #[Guarded(['id'])] #[Appends([ - 'status_label', 'created_at_formatted', + 'status_label', ])] class Cutting extends Model { + // 1. Use Trait use HasFactory, HasRejection, InteractsWithActivityLog, SoftDeletes; + // 2. Casting protected function casts(): array { return [ + 'cost_per_unit' => 'integer', + 'other_cost' => 'integer', + 'sewing_cost' => 'integer', 'status' => CuttingStatus::class, 'total_material_cost' => 'integer', - 'sewing_cost' => 'integer', - 'other_cost' => 'integer', - 'cost_per_unit' => 'integer', ]; } + // 3. Scope (grouped by column, then alphabetical) + // Column Group: status + #[Scope] + protected function completed(Builder $query): void + { + $query->where('status', CuttingStatus::COMPLETED); + } + + #[Scope] + protected function inProgress(Builder $query): void + { + $query->where('status', CuttingStatus::IN_PROGRESS); + } + + #[Scope] + protected function pendingVerification(Builder $query): void + { + $query->where('status', CuttingStatus::PENDING_VERIFICATION); + } + + #[Scope] + protected function verified(Builder $query): void + { + $query->where('status', CuttingStatus::VERIFIED); + } + + // 4. Attribute + public function createdAtFormatted(): Attribute + { + return Attribute::make( + get: fn () => $this->created_at?->translatedFormat('l, d F Y H:i'), + ); + } + + public function statusLabel(): Attribute + { + return Attribute::make( + get: fn () => $this->status->label(), + ); + } + + // 5. Relation public function createdBy(): BelongsTo { return $this->belongsTo(User::class, 'created_by_id'); @@ -60,42 +104,4 @@ public function submittedBy(): BelongsTo { return $this->belongsTo(User::class, 'submitted_by_id'); } - - public function createdAtFormatted(): Attribute - { - return Attribute::make( - get: fn () => $this->created_at?->translatedFormat('l, d F Y H:i'), - ); - } - - public function statusLabel(): Attribute - { - return Attribute::make( - get: fn () => $this->status->label(), - ); - } - - #[Scope] - public function inProgress(Builder $query): void - { - $query->where('status', CuttingStatus::IN_PROGRESS); - } - - #[Scope] - public function completed(Builder $query): void - { - $query->where('status', CuttingStatus::COMPLETED); - } - - #[Scope] - public function pendingVerification(Builder $query): void - { - $query->where('status', CuttingStatus::PENDING_VERIFICATION); - } - - #[Scope] - public function verified(Builder $query): void - { - $query->where('status', CuttingStatus::VERIFIED); - } } diff --git a/app/Models/CuttingMaterial.php b/app/Models/CuttingMaterial.php index 9e87921..2446c30 100644 --- a/app/Models/CuttingMaterial.php +++ b/app/Models/CuttingMaterial.php @@ -19,8 +19,10 @@ ])] class CuttingMaterial extends Model { + // 1. Use Trait use HasFactory, InteractsWithActivityLog, SoftDeletes; + // 2. Casting protected function casts(): array { return [ @@ -28,21 +30,7 @@ protected function casts(): array ]; } - public function cutting(): BelongsTo - { - return $this->belongsTo(Cutting::class); - } - - public function rawMaterialPrice(): BelongsTo - { - return $this->belongsTo(RawMaterialPrice::class); - } - - public function user(): BelongsTo - { - return $this->belongsTo(User::class); - } - + // 3. Attribute public function materialUsageFormatted(): Attribute { return Attribute::make( @@ -64,6 +52,7 @@ public function unitAbbreviation(): Attribute ); } + // 4. Other Methods public function materialCost(): int { $price = $this->rawMaterialPrice; @@ -86,4 +75,20 @@ private function formatQuantityInput(float|string|null $value): string { return rtrim(rtrim(number_format((float) $value, 4, '.', ''), '0'), '.'); } + + // 5. Relation + public function cutting(): BelongsTo + { + return $this->belongsTo(Cutting::class); + } + + public function rawMaterialPrice(): BelongsTo + { + return $this->belongsTo(RawMaterialPrice::class); + } + + public function user(): BelongsTo + { + return $this->belongsTo(User::class); + } } diff --git a/app/Models/CuttingResult.php b/app/Models/CuttingResult.php index 05de9bc..6493c85 100644 --- a/app/Models/CuttingResult.php +++ b/app/Models/CuttingResult.php @@ -12,17 +12,20 @@ #[Guarded(['id'])] class CuttingResult extends Model { + // 1. Use Trait use HasFactory, InteractsWithActivityLog, SoftDeletes; + // 2. Casting protected function casts(): array { return [ 'cutting_result' => 'integer', - 'sampel' => 'integer', 'hasil_cutting_diluar_sampel' => 'integer', + 'sampel' => 'integer', ]; } + // 3. Relation public function cutting(): BelongsTo { return $this->belongsTo(Cutting::class); diff --git a/app/Models/CuttingResultPrice.php b/app/Models/CuttingResultPrice.php index e62bd05..932332c 100644 --- a/app/Models/CuttingResultPrice.php +++ b/app/Models/CuttingResultPrice.php @@ -12,28 +12,28 @@ use Illuminate\Database\Eloquent\Relations\BelongsTo; #[Guarded(['id'])] -#[Appends(['price_formatted', 'cost_per_unit_formatted', 'type_label'])] +#[Appends(['cost_per_unit_formatted', 'price_formatted', 'type_label'])] class CuttingResultPrice extends Model { + // 1. Use Trait use HasFactory, InteractsWithActivityLog; + // 2. Casting protected function casts(): array { return [ - 'price_type' => PriceType::class, - 'price' => 'integer', 'cost_per_unit' => 'integer', + 'price' => 'integer', + 'price_type' => PriceType::class, ]; } - public function cutting(): BelongsTo + // 3. Attribute + public function costPerUnitFormatted(): Attribute { - return $this->belongsTo(Cutting::class); - } - - public function productVariant(): BelongsTo - { - return $this->belongsTo(ProductVariant::class); + return Attribute::make( + get: fn () => 'Rp '.number_format($this->cost_per_unit, 0, ',', '.'), + ); } public function priceFormatted(): Attribute @@ -43,17 +43,21 @@ public function priceFormatted(): Attribute ); } - public function costPerUnitFormatted(): Attribute - { - return Attribute::make( - get: fn () => 'Rp '.number_format($this->cost_per_unit, 0, ',', '.'), - ); - } - public function typeLabel(): Attribute { return Attribute::make( get: fn () => $this->price_type->label(), ); } + + // 4. Relation + public function cutting(): BelongsTo + { + return $this->belongsTo(Cutting::class); + } + + public function productVariant(): BelongsTo + { + return $this->belongsTo(ProductVariant::class); + } } diff --git a/app/Models/Employee.php b/app/Models/Employee.php index d741008..9d97c20 100644 --- a/app/Models/Employee.php +++ b/app/Models/Employee.php @@ -17,46 +17,50 @@ use Illuminate\Database\Eloquent\SoftDeletes; #[Guarded(['id'])] -#[Appends(['base_salary_formatted', 'join_date_formatted', 'resign_date_formatted', 'join_date_input', 'employment_status_label'])] +#[Appends(['base_salary_formatted', 'employment_status_label', 'join_date_formatted', 'join_date_input', 'resign_date_formatted'])] class Employee extends Model { + // 1. Use Trait use HasFactory, InteractsWithActivityLog, SoftDeletes; + // 2. Casting protected function casts(): array { return [ + 'base_salary' => 'integer', + 'employment_status' => EmploymentStatus::class, 'join_date' => 'date', 'resign_date' => 'date', - 'employment_status' => EmploymentStatus::class, - 'base_salary' => 'integer', ]; } - public function advances(): HasMany + // 3. Scope (grouped by column, then alphabetical) + // Column Group: employment_status + #[Scope] + protected function contract(Builder $query): void { - return $this->hasMany(EmployeeAdvance::class); + $query->where('employment_status', EmploymentStatus::CONTRACT); } - public function attendances(): HasMany + #[Scope] + protected function fullTime(Builder $query): void { - return $this->hasMany(Attendance::class); + $query->where('employment_status', EmploymentStatus::FULL_TIME); } - public function leaveRequests(): HasMany + #[Scope] + protected function partTime(Builder $query): void { - return $this->hasMany(LeaveRequest::class); + $query->where('employment_status', EmploymentStatus::PART_TIME); } - public function payrolls(): HasMany + #[Scope] + protected function temporary(Builder $query): void { - return $this->hasMany(Payroll::class); - } - - public function user(): BelongsTo - { - return $this->belongsTo(User::class); + $query->where('employment_status', EmploymentStatus::TEMPORARY); } + // 4. Attribute public function baseSalaryFormatted(): Attribute { return Attribute::make( @@ -92,27 +96,29 @@ public function resignDateFormatted(): Attribute ); } - #[Scope] - public function contract(Builder $query): void + // 5. Relation + public function advances(): HasMany { - $query->where('employment_status', EmploymentStatus::CONTRACT->value); + return $this->hasMany(EmployeeAdvance::class); } - #[Scope] - public function fullTime(Builder $query): void + public function attendances(): HasMany { - $query->where('employment_status', EmploymentStatus::FULL_TIME->value); + return $this->hasMany(Attendance::class); } - #[Scope] - public function partTime(Builder $query): void + public function leaveRequests(): HasMany { - $query->where('employment_status', EmploymentStatus::PART_TIME->value); + return $this->hasMany(LeaveRequest::class); } - #[Scope] - public function temporary(Builder $query): void + public function payrolls(): HasMany { - $query->where('employment_status', EmploymentStatus::TEMPORARY->value); + return $this->hasMany(Payroll::class); + } + + public function user(): BelongsTo + { + return $this->belongsTo(User::class); } } diff --git a/app/Models/EmployeeAdvance.php b/app/Models/EmployeeAdvance.php index 3ff4956..5232ee2 100644 --- a/app/Models/EmployeeAdvance.php +++ b/app/Models/EmployeeAdvance.php @@ -18,65 +18,70 @@ #[Guarded(['id'])] #[Appends([ 'amount_formatted', - 'paid_amount_formatted', - 'remaining_amount', - 'remaining_amount_formatted', + 'can_pay', + 'can_verify', + 'created_at_formatted', 'due_date_formatted', 'due_date_input', - 'status_label', 'employee_name', - 'rejection_reason', - 'created_at_formatted', 'is_editable', - 'can_verify', - 'can_pay', + 'paid_amount_formatted', + 'rejection_reason', + 'remaining_amount', + 'remaining_amount_formatted', + 'status_label', ])] class EmployeeAdvance extends Model { + // 1. Use Trait use HasFactory, HasRejection, InteractsWithActivityLog; + // 2. Casting protected function casts(): array { return [ 'amount' => 'integer', - 'paid_amount' => 'integer', 'due_date' => 'date', + 'paid_amount' => 'integer', + 'paid_at' => 'datetime', 'status' => EmployeeAdvanceStatus::class, 'verified_at' => 'datetime', - 'paid_at' => 'datetime', ]; } - public function cashTransaction(): BelongsTo + // 3. Scope (grouped by column, then alphabetical) + // Column Group: status + #[Scope] + protected function approved(Builder $query): void { - return $this->belongsTo(CashTransaction::class); + $query->where('status', EmployeeAdvanceStatus::APPROVED); } - public function employee(): BelongsTo + #[Scope] + protected function paid(Builder $query): void { - return $this->belongsTo(Employee::class); + $query->where('status', EmployeeAdvanceStatus::PAID); } - public function paidBy(): BelongsTo + #[Scope] + protected function partiallyPaid(Builder $query): void { - return $this->belongsTo(User::class, 'paid_by_id'); + $query->where('status', EmployeeAdvanceStatus::PARTIALLY_PAID); } - public function repaymentCashTransaction(): BelongsTo + #[Scope] + protected function pending(Builder $query): void { - return $this->belongsTo(CashTransaction::class, 'repayment_cash_transaction_id'); + $query->where('status', EmployeeAdvanceStatus::PENDING); } - public function verifiedBy(): BelongsTo + #[Scope] + protected function rejected(Builder $query): void { - return $this->belongsTo(User::class, 'verified_by_id'); - } - - public function payments(): HasMany - { - return $this->hasMany(EmployeeAdvancePayment::class)->latest('paid_at'); + $query->where('status', EmployeeAdvanceStatus::REJECTED); } + // 4. Attribute public function amountFormatted(): Attribute { return Attribute::make( @@ -84,27 +89,6 @@ public function amountFormatted(): Attribute ); } - public function paidAmountFormatted(): Attribute - { - return Attribute::make( - get: fn () => 'Rp '.number_format($this->paid_amount, 0, ',', '.'), - ); - } - - public function remainingAmount(): Attribute - { - return Attribute::make( - get: fn () => $this->amount - $this->paid_amount, - ); - } - - public function remainingAmountFormatted(): Attribute - { - return Attribute::make( - get: fn () => 'Rp '.number_format($this->remaining_amount, 0, ',', '.'), - ); - } - public function canPay(): Attribute { return Attribute::make( @@ -155,6 +139,13 @@ public function isEditable(): Attribute ); } + public function paidAmountFormatted(): Attribute + { + return Attribute::make( + get: fn () => 'Rp '.number_format($this->paid_amount, 0, ',', '.'), + ); + } + public function rejectionReason(): Attribute { return Attribute::make( @@ -162,6 +153,20 @@ public function rejectionReason(): Attribute ); } + public function remainingAmount(): Attribute + { + return Attribute::make( + get: fn () => $this->amount - $this->paid_amount, + ); + } + + public function remainingAmountFormatted(): Attribute + { + return Attribute::make( + get: fn () => 'Rp '.number_format($this->remaining_amount, 0, ',', '.'), + ); + } + public function statusLabel(): Attribute { return Attribute::make( @@ -169,21 +174,34 @@ public function statusLabel(): Attribute ); } - #[Scope] - public function pending(Builder $query): void + // 5. Relation + public function cashTransaction(): BelongsTo { - $query->where('status', EmployeeAdvanceStatus::PENDING); + return $this->belongsTo(CashTransaction::class); } - #[Scope] - public function approved(Builder $query): void + public function employee(): BelongsTo { - $query->where('status', EmployeeAdvanceStatus::APPROVED); + return $this->belongsTo(Employee::class); } - #[Scope] - public function paid(Builder $query): void + public function paidBy(): BelongsTo { - $query->where('status', EmployeeAdvanceStatus::PAID); + return $this->belongsTo(User::class, 'paid_by_id'); + } + + public function payments(): HasMany + { + return $this->hasMany(EmployeeAdvancePayment::class)->latest('paid_at'); + } + + public function repaymentCashTransaction(): BelongsTo + { + return $this->belongsTo(CashTransaction::class, 'repayment_cash_transaction_id'); + } + + public function verifiedBy(): BelongsTo + { + return $this->belongsTo(User::class, 'verified_by_id'); } } diff --git a/app/Models/EmployeeAdvancePayment.php b/app/Models/EmployeeAdvancePayment.php index 4a35646..c74e67c 100644 --- a/app/Models/EmployeeAdvancePayment.php +++ b/app/Models/EmployeeAdvancePayment.php @@ -4,6 +4,7 @@ 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; @@ -15,8 +16,10 @@ ])] class EmployeeAdvancePayment extends Model { + // 1. Use Trait use HasFactory; + // 2. Casting protected function casts(): array { return [ @@ -25,6 +28,27 @@ protected function casts(): array ]; } + // 3. Attribute + public function amountFormatted(): Attribute + { + return Attribute::make( + get: fn () => 'Rp '.number_format($this->amount, 0, ',', '.'), + ); + } + + public function paidAtFormatted(): Attribute + { + return Attribute::make( + get: fn () => $this->paid_at?->translatedFormat('l, d F Y H:i'), + ); + } + + // 4. Relation + public function cashTransaction(): BelongsTo + { + return $this->belongsTo(CashTransaction::class); + } + public function employeeAdvance(): BelongsTo { return $this->belongsTo(EmployeeAdvance::class); @@ -34,23 +58,4 @@ public function paidBy(): BelongsTo { return $this->belongsTo(User::class, 'paid_by_id'); } - - public function cashTransaction(): BelongsTo - { - return $this->belongsTo(CashTransaction::class); - } - - public function amountFormatted(): \Illuminate\Database\Eloquent\Casts\Attribute - { - return \Illuminate\Database\Eloquent\Casts\Attribute::make( - get: fn () => 'Rp '.number_format($this->amount, 0, ',', '.'), - ); - } - - public function paidAtFormatted(): \Illuminate\Database\Eloquent\Casts\Attribute - { - return \Illuminate\Database\Eloquent\Casts\Attribute::make( - get: fn () => $this->paid_at?->translatedFormat('l, d F Y H:i'), - ); - } } diff --git a/app/Models/Expense.php b/app/Models/Expense.php index e156cd4..bbb12e4 100644 --- a/app/Models/Expense.php +++ b/app/Models/Expense.php @@ -17,8 +17,10 @@ #[Appends(['amount_formatted', 'created_at_formatted', 'created_by_name'])] class Expense extends Model implements HasMedia { + // 1. Use Trait use HasFactory, HasModuleMedia, InteractsWithActivityLog, SoftDeletes; + // 2. Casting protected function casts(): array { return [ @@ -26,16 +28,7 @@ protected function casts(): array ]; } - public function cashTransaction(): BelongsTo - { - return $this->belongsTo(CashTransaction::class); - } - - public function createdBy(): BelongsTo - { - return $this->belongsTo(User::class, 'created_by_id'); - } - + // 3. Attribute public function amountFormatted(): Attribute { return Attribute::make( @@ -57,6 +50,7 @@ public function createdByName(): Attribute ); } + // 4. Other Methods public static function mediaModuleName(): string { return 'expense'; @@ -66,4 +60,15 @@ public function registerMediaCollections(): void { $this->addMediaCollection('photos'); } + + // 5. Relation + public function cashTransaction(): BelongsTo + { + return $this->belongsTo(CashTransaction::class); + } + + public function createdBy(): BelongsTo + { + return $this->belongsTo(User::class, 'created_by_id'); + } } diff --git a/app/Models/HomepageConfiguration.php b/app/Models/HomepageConfiguration.php index 02ce091..8bcd254 100644 --- a/app/Models/HomepageConfiguration.php +++ b/app/Models/HomepageConfiguration.php @@ -12,22 +12,24 @@ #[Guarded(['id'])] class HomepageConfiguration extends Model implements HasMedia { + // 1. Use Trait use HasFactory, HasModuleMedia, InteractsWithActivityLog; + // 2. Other Methods + public static function instance(): self + { + return static::query()->firstOrCreate(['id' => 1]); + } + public static function mediaModuleName(): string { return 'homepage'; } - public static function instance(): self - { - return static::query()->firstOrCreate(['id' => 1]); - } - public function registerMediaCollections(): void { - $this->addMediaCollection('hero_image')->singleFile(); $this->addMediaCollection('about_image')->singleFile(); $this->addMediaCollection('gallery'); + $this->addMediaCollection('hero_image')->singleFile(); } } diff --git a/app/Models/LeaveRequest.php b/app/Models/LeaveRequest.php index 2df9850..f781edc 100644 --- a/app/Models/LeaveRequest.php +++ b/app/Models/LeaveRequest.php @@ -16,42 +16,55 @@ #[Guarded(['id'])] #[Appends([ - 'start_date_formatted', - 'end_date_formatted', - 'start_date_input', - 'end_date_input', - 'status_label', - 'employee_name', - 'rejection_reason', - 'created_at_formatted', - 'is_editable', 'can_verify', + 'created_at_formatted', + 'employee_name', + 'end_date_formatted', + 'end_date_input', + 'is_editable', + 'rejection_reason', + 'start_date_formatted', + 'start_date_input', + 'status_label', ])] class LeaveRequest extends Model { + // 1. Use Trait use HasFactory, HasRejection, InteractsWithActivityLog; + // 2. Casting protected function casts(): array { return [ - 'start_date' => 'date', 'end_date' => 'date', - 'total_days' => 'integer', + 'start_date' => 'date', 'status' => LeaveRequestStatus::class, + 'total_days' => 'integer', 'verified_at' => 'datetime', ]; } - public function employee(): BelongsTo + // 3. Scope (grouped by column, then alphabetical) + // Column Group: status + #[Scope] + protected function approved(Builder $query): void { - return $this->belongsTo(Employee::class); + $query->where('status', LeaveRequestStatus::APPROVED); } - public function verifiedBy(): BelongsTo + #[Scope] + protected function pending(Builder $query): void { - return $this->belongsTo(User::class, 'verified_by_id'); + $query->where('status', LeaveRequestStatus::PENDING); } + #[Scope] + protected function rejected(Builder $query): void + { + $query->where('status', LeaveRequestStatus::REJECTED); + } + + // 4. Attribute public function canVerify(): Attribute { return Attribute::make( @@ -123,15 +136,14 @@ public function statusLabel(): Attribute ); } - #[Scope] - public function pending(Builder $query): void + // 5. Relation + public function employee(): BelongsTo { - $query->where('status', LeaveRequestStatus::PENDING); + return $this->belongsTo(Employee::class); } - #[Scope] - public function approved(Builder $query): void + public function verifiedBy(): BelongsTo { - $query->where('status', LeaveRequestStatus::APPROVED); + return $this->belongsTo(User::class, 'verified_by_id'); } } diff --git a/app/Models/Notification.php b/app/Models/Notification.php index 288a06a..dc816e2 100644 --- a/app/Models/Notification.php +++ b/app/Models/Notification.php @@ -11,6 +11,7 @@ #[Guarded(['id'])] class Notification extends Model { + // 1. Casting protected function casts(): array { return [ @@ -19,11 +20,21 @@ protected function casts(): array ]; } - public function user(): BelongsTo + // 2. Scope (grouped by column, then alphabetical) + // Column Group: is_read + #[Scope] + protected function read(Builder $query): void { - return $this->belongsTo(User::class); + $query->where('is_read', true); } + #[Scope] + protected function unread(Builder $query): void + { + $query->where('is_read', false); + } + + // 3. Other Methods public function markAsRead(): void { if (! $this->is_read) { @@ -31,9 +42,9 @@ public function markAsRead(): void } } - #[Scope] - public function unread(Builder $query): void + // 4. Relation + public function user(): BelongsTo { - $query->where('is_read', false); + return $this->belongsTo(User::class); } } diff --git a/app/Models/Order.php b/app/Models/Order.php index c87b235..31451c7 100644 --- a/app/Models/Order.php +++ b/app/Models/Order.php @@ -22,63 +22,172 @@ #[Guarded(['id'])] #[Appends([ - 'subtotal_formatted', - 'discount_formatted', - 'nego_price_formatted', - 'total_amount_formatted', - 'created_at_formatted', 'channel_label', + 'creator_name', + 'discount_formatted', + 'marketing_name', + 'nego_price_formatted', 'payment_type_label', 'price_type_label', 'status_label', - 'creator_name', - 'marketing_name', + 'subtotal_formatted', + 'total_amount_formatted', + 'created_at_formatted', ])] class Order extends Model implements HasMedia { + // 1. Use Trait use HasFactory, HasModuleMedia, InteractsWithActivityLog, SoftDeletes; + // 2. Casting protected function casts(): array { return [ 'channel' => OrderChannel::class, - 'price_type' => PriceType::class, - 'payment_type' => PaymentType::class, 'is_affiliate' => 'boolean', + 'marketplace_settings_snapshot' => 'array', + 'nego_price' => 'integer', + 'payment_type' => PaymentType::class, + 'price_type' => PriceType::class, 'status' => OrderStatus::class, 'subtotal' => 'integer', - 'discount' => 'integer', - 'nego_price' => 'integer', - 'marketplace_settings_snapshot' => 'array', 'total_amount' => 'integer', ]; } - public function cashTransaction(): BelongsTo + // 3. Scope (grouped by column, then alphabetical) + // Column Group: channel + #[Scope] + protected function shopee(Builder $query): void { - return $this->belongsTo(CashTransaction::class); + $query->where('channel', OrderChannel::SHOPEE); } - public function createdBy(): BelongsTo + #[Scope] + protected function store(Builder $query): void { - return $this->belongsTo(User::class, 'created_by_id'); + $query->where('channel', OrderChannel::STORE); } - public function customer(): BelongsTo + #[Scope] + protected function tiktok(Builder $query): void { - return $this->belongsTo(Customer::class); + $query->where('channel', OrderChannel::TIKTOK); } - public function items(): HasMany + // Column Group: is_affiliate + #[Scope] + protected function affiliate(Builder $query): void { - return $this->hasMany(OrderItem::class); + $query->where('is_affiliate', true); } - public function marketing(): BelongsTo + #[Scope] + protected function nonAffiliate(Builder $query): void { - return $this->belongsTo(User::class, 'marketing_id'); + $query->where('is_affiliate', false); } + // Column Group: payment_type + #[Scope] + protected function cash(Builder $query): void + { + $query->where('payment_type', PaymentType::CASH); + } + + #[Scope] + protected function marketplace(Builder $query): void + { + $query->where('payment_type', PaymentType::MARKETPLACE); + } + + #[Scope] + protected function qris(Builder $query): void + { + $query->where('payment_type', PaymentType::QRIS); + } + + #[Scope] + protected function transfer(Builder $query): void + { + $query->where('payment_type', PaymentType::TRANSFER); + } + + // Column Group: price_type + #[Scope] + protected function agent(Builder $query): void + { + $query->where('price_type', PriceType::AGENT); + } + + #[Scope] + protected function distributor(Builder $query): void + { + $query->where('price_type', PriceType::DISTRIBUTOR); + } + + #[Scope] + protected function ecer(Builder $query): void + { + $query->where('price_type', PriceType::ECER); + } + + #[Scope] + protected function grosir(Builder $query): void + { + $query->where('price_type', PriceType::GROSIR); + } + + #[Scope] + protected function hargaModal(Builder $query): void + { + $query->where('price_type', PriceType::HARGA_MODAL); + } + + #[Scope] + protected function shopeePrice(Builder $query): void + { + $query->where('price_type', PriceType::SHOPEE); + } + + #[Scope] + protected function subAgent(Builder $query): void + { + $query->where('price_type', PriceType::SUB_AGENT); + } + + #[Scope] + protected function tiktokPrice(Builder $query): void + { + $query->where('price_type', PriceType::TIKTOK); + } + + // Column Group: status + #[Scope] + protected function cancelled(Builder $query): void + { + $query->where('status', OrderStatus::CANCELLED); + } + + #[Scope] + protected function completed(Builder $query): void + { + $query->where('status', OrderStatus::COMPLETED); + } + + #[Scope] + protected function pending(Builder $query): void + { + $query->where('status', OrderStatus::PENDING); + } + + #[Scope] + protected function processing(Builder $query): void + { + $query->where('status', OrderStatus::PROCESSING); + } + + // 4. Attribute public function channelLabel(): Attribute { return Attribute::make( @@ -93,6 +202,13 @@ public function createdAtFormatted(): Attribute ); } + public function creatorName(): Attribute + { + return Attribute::make( + get: fn () => $this->createdBy?->profile?->full_name ?? $this->createdBy?->username ?? '-', + ); + } + public function discountFormatted(): Attribute { return Attribute::make( @@ -100,6 +216,13 @@ public function discountFormatted(): Attribute ); } + public function marketingName(): Attribute + { + return Attribute::make( + get: fn () => $this->marketing?->profile?->full_name ?? $this->marketing?->username ?? '-', + ); + } + public function negoPriceFormatted(): Attribute { return Attribute::make( @@ -107,13 +230,6 @@ public function negoPriceFormatted(): Attribute ); } - public function totalAmountFormatted(): Attribute - { - return Attribute::make( - get: fn () => 'Rp '.number_format($this->total_amount, 0, ',', '.'), - ); - } - public function paymentTypeLabel(): Attribute { return Attribute::make( @@ -135,20 +251,6 @@ public function statusLabel(): Attribute ); } - public function creatorName(): Attribute - { - return Attribute::make( - get: fn () => $this->createdBy?->profile?->full_name ?? $this->createdBy?->username ?? '-', - ); - } - - public function marketingName(): Attribute - { - return Attribute::make( - get: fn () => $this->marketing?->profile?->full_name ?? $this->marketing?->username ?? '-', - ); - } - public function subtotalFormatted(): Attribute { return Attribute::make( @@ -156,6 +258,14 @@ public function subtotalFormatted(): Attribute ); } + public function totalAmountFormatted(): Attribute + { + return Attribute::make( + get: fn () => 'Rp '.number_format($this->total_amount, 0, ',', '.'), + ); + } + + // 5. Other Methods public static function mediaModuleName(): string { return 'order'; @@ -166,9 +276,29 @@ public function registerMediaCollections(): void $this->addMediaCollection('photos'); } - #[Scope] - public function completed(Builder $query): void + // 6. Relation + public function cashTransaction(): BelongsTo { - $query->where('status', OrderStatus::COMPLETED); + return $this->belongsTo(CashTransaction::class); + } + + public function createdBy(): BelongsTo + { + return $this->belongsTo(User::class, 'created_by_id'); + } + + public function customer(): BelongsTo + { + return $this->belongsTo(Customer::class); + } + + public function items(): HasMany + { + return $this->hasMany(OrderItem::class); + } + + public function marketing(): BelongsTo + { + return $this->belongsTo(User::class, 'marketing_id'); } } diff --git a/app/Models/OrderItem.php b/app/Models/OrderItem.php index fd033fb..d1e7b25 100644 --- a/app/Models/OrderItem.php +++ b/app/Models/OrderItem.php @@ -16,38 +16,26 @@ #[Appends([ 'quantity_formatted', 'quantity_input', - 'unit_price_formatted', 'subtotal_formatted', + 'unit_price_formatted', ])] class OrderItem extends Model { + // 1. Use Trait use HasFactory, InteractsWithActivityLog, SoftDeletes; + // 2. Casting protected function casts(): array { return [ - 'stock_quality' => ProductStockQuality::class, 'quantity' => 'integer', - 'unit_price' => 'integer', + 'stock_quality' => ProductStockQuality::class, 'subtotal' => 'integer', + 'unit_price' => 'integer', ]; } - public function order(): BelongsTo - { - return $this->belongsTo(Order::class); - } - - public function productVariant(): BelongsTo - { - return $this->belongsTo(ProductVariant::class); - } - - public function user(): BelongsTo - { - return $this->belongsTo(User::class); - } - + // 3. Attribute public function quantityFormatted(): Attribute { return Attribute::make( @@ -75,4 +63,20 @@ public function unitPriceFormatted(): Attribute get: fn () => 'Rp '.number_format($this->unit_price, 0, ',', '.'), ); } + + // 4. Relation + public function order(): BelongsTo + { + return $this->belongsTo(Order::class); + } + + public function productVariant(): BelongsTo + { + return $this->belongsTo(ProductVariant::class); + } + + public function user(): BelongsTo + { + return $this->belongsTo(User::class); + } } diff --git a/app/Models/OwnerVerificationRequest.php b/app/Models/OwnerVerificationRequest.php index ce688fc..2b0c7cd 100644 --- a/app/Models/OwnerVerificationRequest.php +++ b/app/Models/OwnerVerificationRequest.php @@ -23,43 +23,68 @@ #[Guarded(['id'])] #[Appends([ 'action_label', + 'created_at_formatted', + 'is_pending', 'status_label', 'subject_label', 'submitted_by_name', - 'verified_by_name', - 'created_at_formatted', 'verified_at_formatted', - 'is_pending', + 'verified_by_name', ])] class OwnerVerificationRequest extends Model implements HasMedia { + // 1. Use Trait use HasFactory, HasModuleMedia, HasRejection, InteractsWithActivityLog; + // 2. Casting protected function casts(): array { return [ 'action' => OwnerVerificationAction::class, - 'status' => OwnerVerificationStatus::class, 'payload' => 'array', + 'status' => OwnerVerificationStatus::class, 'verified_at' => 'datetime', ]; } - public function subject(): MorphTo + // 3. Scope (grouped by column, then alphabetical) + // Column Group: status + #[Scope] + protected function approved(Builder $query): void { - return $this->morphTo(); + $query->where('status', OwnerVerificationStatus::APPROVED); } - public function submittedBy(): BelongsTo + #[Scope] + protected function pending(Builder $query): void { - return $this->belongsTo(User::class, 'submitted_by_id'); + $query->where('status', OwnerVerificationStatus::PENDING); } - public function verifiedBy(): BelongsTo + #[Scope] + protected function rejected(Builder $query): void { - return $this->belongsTo(User::class, 'verified_by_id'); + $query->where('status', OwnerVerificationStatus::REJECTED); } + // Scope lainnya + #[Scope] + protected function forSubmitter(Builder $query, User $user): void + { + $query->where('submitted_by_id', $user->id); + } + + #[Scope] + protected function visibleTo(Builder $query, User $user): void + { + if ($user->can(Permission::OWNER_VERIFICATIONS_VERIFY->value)) { + return; + } + + $query->forSubmitter($user); + } + + // 4. Attribute public function actionLabel(): Attribute { return Attribute::make( @@ -119,38 +144,10 @@ public function verifiedByName(): Attribute ); } - #[Scope] - public function approved(Builder $query): void + // 5. Other Methods + public static function mediaModuleName(): string { - $query->where('status', OwnerVerificationStatus::APPROVED); - } - - #[Scope] - public function pending(Builder $query): void - { - $query->where('status', OwnerVerificationStatus::PENDING); - } - - #[Scope] - public function rejected(Builder $query): void - { - $query->where('status', OwnerVerificationStatus::REJECTED); - } - - #[Scope] - public function forSubmitter(Builder $query, User $user): void - { - $query->where('submitted_by_id', $user->id); - } - - #[Scope] - public function visibleTo(Builder $query, User $user): void - { - if ($user->can(Permission::OWNER_VERIFICATIONS_VERIFY->value)) { - return; - } - - $query->forSubmitter($user); + return 'owner_verification_request'; } public function pendingToggleIsActive(): ?bool @@ -169,9 +166,9 @@ public function pendingToggleIsActive(): ?bool return (bool) $new['is_active']; } - public static function mediaModuleName(): string + public function priceImageCollection(int $index): string { - return 'owner_verification_request'; + return "price_images_{$index}"; } public function variantImageCollection(int $index): string @@ -179,8 +176,19 @@ public function variantImageCollection(int $index): string return "variant_images_{$index}"; } - public function priceImageCollection(int $index): string + // 6. Relation + public function subject(): MorphTo { - return "price_images_{$index}"; + return $this->morphTo(); + } + + public function submittedBy(): BelongsTo + { + return $this->belongsTo(User::class, 'submitted_by_id'); + } + + public function verifiedBy(): BelongsTo + { + return $this->belongsTo(User::class, 'verified_by_id'); } } diff --git a/app/Models/Payroll.php b/app/Models/Payroll.php index 91fc447..487bd1f 100644 --- a/app/Models/Payroll.php +++ b/app/Models/Payroll.php @@ -2,12 +2,13 @@ namespace App\Models; -use App\Enums\EmployeeAdvanceStatus; use App\Enums\PayrollAdjustmentType; use App\Enums\PayrollStatus; use App\Models\Concerns\InteractsWithActivityLog; use Illuminate\Database\Eloquent\Attributes\Appends; use Illuminate\Database\Eloquent\Attributes\Guarded; +use Illuminate\Database\Eloquent\Attributes\Scope; +use Illuminate\Database\Eloquent\Builder; use Illuminate\Database\Eloquent\Casts\Attribute; use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Model; @@ -18,54 +19,46 @@ #[Appends([ 'base_salary_formatted', 'bonus_amount_formatted', + 'can_adjust', 'deduction_amount_formatted', - 'total_amount_formatted', - 'status_label', 'employee_name', 'paid_at_formatted', - 'can_adjust', + 'status_label', + 'total_amount_formatted', ])] class Payroll extends Model { + // 1. Use Trait use HasFactory, InteractsWithActivityLog; + // 2. Casting protected function casts(): array { return [ 'base_salary' => 'integer', 'bonus_amount' => 'integer', 'deduction_amount' => 'integer', - 'total_amount' => 'integer', - 'status' => PayrollStatus::class, 'paid_at' => 'datetime', + 'status' => PayrollStatus::class, + 'total_amount' => 'integer', ]; } - public function adjustments(): HasMany + // 3. Scope (grouped by column, then alphabetical) + // Column Group: status + #[Scope] + protected function paid(Builder $query): void { - return $this->hasMany(PayrollAdjustment::class); + $query->where('status', PayrollStatus::PAID); } - public function cashTransaction(): BelongsTo + #[Scope] + protected function unpaid(Builder $query): void { - return $this->belongsTo(CashTransaction::class); - } - - public function employee(): BelongsTo - { - return $this->belongsTo(Employee::class); - } - - public function paidBy(): BelongsTo - { - return $this->belongsTo(User::class, 'paid_by_id'); - } - - public function payrollPeriod(): BelongsTo - { - return $this->belongsTo(PayrollPeriod::class); + $query->where('status', PayrollStatus::UNPAID); } + // 4. Attribute public function baseSalaryFormatted(): Attribute { return Attribute::make( @@ -104,13 +97,6 @@ public function employeeName(): Attribute ); } - public function totalAmountFormatted(): Attribute - { - return Attribute::make( - get: fn () => 'Rp '.number_format($this->total_amount, 0, ',', '.'), - ); - } - public function paidAtFormatted(): Attribute { return Attribute::make( @@ -125,7 +111,15 @@ public function statusLabel(): Attribute ); } - public function calculateKasbonDeduction(): int + public function totalAmountFormatted(): Attribute + { + return Attribute::make( + get: fn () => 'Rp '.number_format($this->total_amount, 0, ',', '.'), + ); + } + + // 5. Other Methods + public function calculateAdvanceDeduction(): int { $outstanding = (int) EmployeeAdvance::query() ->where('employee_id', $this->employee_id) @@ -148,10 +142,36 @@ public function recalculateAmounts(): void ->where('type', PayrollAdjustmentType::DEDUCTION) ->sum('amount'); - $kasbonDeduction = $this->calculateKasbonDeduction(); + $advanceDeduction = $this->calculateAdvanceDeduction(); $this->bonus_amount = $bonusAmount; - $this->deduction_amount = $kasbonDeduction + $manualDeduction; + $this->deduction_amount = $advanceDeduction + $manualDeduction; $this->total_amount = max(0, (int) $this->base_salary + $bonusAmount - $this->deduction_amount); } + + // 6. Relation + public function adjustments(): HasMany + { + return $this->hasMany(PayrollAdjustment::class); + } + + public function cashTransaction(): BelongsTo + { + return $this->belongsTo(CashTransaction::class); + } + + public function employee(): BelongsTo + { + return $this->belongsTo(Employee::class); + } + + public function paidBy(): BelongsTo + { + return $this->belongsTo(User::class, 'paid_by_id'); + } + + public function payrollPeriod(): BelongsTo + { + return $this->belongsTo(PayrollPeriod::class); + } } diff --git a/app/Models/PayrollAdjustment.php b/app/Models/PayrollAdjustment.php index 720e30a..d0a16d8 100644 --- a/app/Models/PayrollAdjustment.php +++ b/app/Models/PayrollAdjustment.php @@ -14,37 +14,39 @@ use Illuminate\Database\Eloquent\Relations\BelongsTo; #[Guarded(['id'])] -#[Appends(['amount_formatted', 'type_label', 'created_at_formatted', 'created_by_name'])] +#[Appends(['amount_formatted', 'created_at_formatted', 'created_by_name', 'type_label'])] class PayrollAdjustment extends Model { + // 1. Use Trait use HasFactory, InteractsWithActivityLog; public $timestamps = false; + // 2. Casting protected function casts(): array { return [ - 'type' => PayrollAdjustmentType::class, 'amount' => 'integer', 'created_at' => 'datetime', + 'type' => PayrollAdjustmentType::class, ]; } - public function attendance(): BelongsTo + // 3. Scope (grouped by column, then alphabetical) + // Column Group: type + #[Scope] + protected function bonus(Builder $query): void { - return $this->belongsTo(Attendance::class); + $query->where('type', PayrollAdjustmentType::BONUS); } - public function createdBy(): BelongsTo + #[Scope] + protected function deduction(Builder $query): void { - return $this->belongsTo(User::class, 'created_by_id'); - } - - public function payroll(): BelongsTo - { - return $this->belongsTo(Payroll::class); + $query->where('type', PayrollAdjustmentType::DEDUCTION); } + // 4. Attribute public function amountFormatted(): Attribute { return Attribute::make( @@ -73,15 +75,19 @@ public function typeLabel(): Attribute ); } - #[Scope] - public function deduction(Builder $query): void + // 5. Relation + public function attendance(): BelongsTo { - $query->where('type', PayrollAdjustmentType::DEDUCTION); + return $this->belongsTo(Attendance::class); } - #[Scope] - public function bonus(Builder $query): void + public function createdBy(): BelongsTo { - $query->where('type', PayrollAdjustmentType::BONUS); + return $this->belongsTo(User::class, 'created_by_id'); + } + + public function payroll(): BelongsTo + { + return $this->belongsTo(Payroll::class); } } diff --git a/app/Models/PayrollPeriod.php b/app/Models/PayrollPeriod.php index cf09ee4..7a52998 100644 --- a/app/Models/PayrollPeriod.php +++ b/app/Models/PayrollPeriod.php @@ -8,38 +8,45 @@ use Illuminate\Database\Eloquent\Attributes\Appends; use Illuminate\Database\Eloquent\Attributes\Guarded; use Illuminate\Database\Eloquent\Attributes\Scope; +use Illuminate\Database\Eloquent\Builder; use Illuminate\Database\Eloquent\Casts\Attribute; use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Relations\BelongsTo; use Illuminate\Database\Eloquent\Relations\HasMany; use Illuminate\Database\Eloquent\SoftDeletes; -use Illuminate\Database\Query\Builder; #[Guarded(['id'])] -#[Appends(['period_label', 'status_label', 'closed_at_formatted'])] +#[Appends(['closed_at_formatted', 'period_label', 'status_label'])] class PayrollPeriod extends Model { + // 1. Use Trait use HasFactory, InteractsWithActivityLog, SoftDeletes; + // 2. Casting protected function casts(): array { return [ - 'status' => PayrollPeriodStatus::class, 'closed_at' => 'datetime', + 'status' => PayrollPeriodStatus::class, ]; } - public function closedBy(): BelongsTo + // 3. Scope (grouped by column, then alphabetical) + // Column Group: status + #[Scope] + protected function closed(Builder $query): void { - return $this->belongsTo(User::class, 'closed_by_id'); + $query->where('status', PayrollPeriodStatus::CLOSED); } - public function payrolls(): HasMany + #[Scope] + protected function open(Builder $query): void { - return $this->hasMany(Payroll::class); + $query->where('status', PayrollPeriodStatus::OPEN); } + // 4. Attribute public function closedAtFormatted(): Attribute { return Attribute::make( @@ -61,14 +68,20 @@ public function statusLabel(): Attribute ); } + // 5. Other Methods public function isOpen(): bool { return $this->status === PayrollPeriodStatus::OPEN; } - #[Scope] - public function open(Builder $query): void + // 6. Relation + public function closedBy(): BelongsTo { - $query->where('status', PayrollPeriodStatus::OPEN); + return $this->belongsTo(User::class, 'closed_by_id'); + } + + public function payrolls(): HasMany + { + return $this->hasMany(Payroll::class); } } diff --git a/app/Models/Product.php b/app/Models/Product.php index 815227a..ade2419 100644 --- a/app/Models/Product.php +++ b/app/Models/Product.php @@ -26,8 +26,10 @@ #[Sluggable(from: 'name', to: 'slug')] class Product extends Model { + // 1. Use Trait use HasFactory, HasPendingOwnerVerification, InteractsWithActivityLog, SoftDeletes; + // 2. Casting protected function casts(): array { return [ @@ -35,16 +37,40 @@ protected function casts(): array ]; } + // 3. Scope (grouped by column, then alphabetical) + // Column Group: is_active + #[Scope] + protected function active(Builder $query): void + { + $query->where('is_active', true); + } + + #[Scope] + protected function inactive(Builder $query): void + { + $query->where('is_active', false); + } + + // 4. Other Methods + public static function getActiveWithVariantsAndCategories(): Collection + { + return self::query() + ->active() + ->with([ + 'categories', + 'variants' => fn ($query) => $query + ->with('media') + ->orderBy('created_at'), + ]) + ->get(); + } + + // 5. Relation public function categories(): BelongsToMany { return $this->belongsToMany(Category::class, 'product_categories'); } - public function variants(): HasMany - { - return $this->hasMany(ProductVariant::class); - } - public function ownerVerificationRequests(): MorphMany { return $this->morphMany(OwnerVerificationRequest::class, 'subject'); @@ -57,28 +83,8 @@ public function pendingOwnerVerificationRequest(): MorphOne ->latestOfMany(); } - #[Scope] - public function active(Builder $query): void + public function variants(): HasMany { - $query->where('is_active', true); - } - - #[Scope] - public function inactive(Builder $query): void - { - $query->where('is_active', false); - } - - public static function getActiveWithVariantsAndCategories(): Collection - { - return self::query() - ->active() - ->with([ - 'categories', - 'variants' => fn ($query) => $query - ->with('media') - ->orderBy('created_at'), - ]) - ->get(); + return $this->hasMany(ProductVariant::class); } } diff --git a/app/Models/ProductPrice.php b/app/Models/ProductPrice.php index d3cfdf2..a4c01f0 100644 --- a/app/Models/ProductPrice.php +++ b/app/Models/ProductPrice.php @@ -14,21 +14,19 @@ #[Appends(['price_formatted', 'price_input'])] class ProductPrice extends Model { + // 1. Use Trait use HasFactory; + // 2. Casting protected function casts(): array { return [ - 'type' => PriceType::class, 'price' => 'integer', + 'type' => PriceType::class, ]; } - public function variant(): BelongsTo - { - return $this->belongsTo(ProductVariant::class, 'variant_id'); - } - + // 3. Attribute public function priceFormatted(): Attribute { return Attribute::make( @@ -42,4 +40,10 @@ public function priceInput(): Attribute get: fn () => (string) $this->price, ); } + + // 4. Relation + public function variant(): BelongsTo + { + return $this->belongsTo(ProductVariant::class, 'variant_id'); + } } diff --git a/app/Models/ProductVariant.php b/app/Models/ProductVariant.php index c81d673..6dcba3a 100644 --- a/app/Models/ProductVariant.php +++ b/app/Models/ProductVariant.php @@ -15,19 +15,38 @@ #[Guarded(['id'])] class ProductVariant extends Model implements HasMedia { + // 1. Use Trait use HasFactory, HasModuleMedia, InteractsWithActivityLog, SoftDeletes; private const MIN_STOCK = 5; + // 2. Casting protected function casts(): array { return [ - 'stock' => 'integer', 'reject_stock' => 'integer', + 'stock' => 'integer', 'stock_ecer' => 'integer', ]; } + // 3. Other Methods + public static function mediaModuleName(): string + { + return 'product'; + } + + public static function minStock(): int + { + return self::MIN_STOCK; + } + + public function registerMediaCollections(): void + { + $this->addMediaCollection('images'); + } + + // 4. Relation public function cuttingResultPrices(): HasMany { return $this->hasMany(CuttingResultPrice::class); @@ -52,19 +71,4 @@ public function product(): BelongsTo { return $this->belongsTo(Product::class); } - - public static function minStock(): int - { - return self::MIN_STOCK; - } - - public static function mediaModuleName(): string - { - return 'product'; - } - - public function registerMediaCollections(): void - { - $this->addMediaCollection('images'); - } } diff --git a/app/Models/Purchase.php b/app/Models/Purchase.php index 92d8435..067d694 100644 --- a/app/Models/Purchase.php +++ b/app/Models/Purchase.php @@ -20,53 +20,29 @@ #[Guarded(['id'])] #[Appends([ - 'subtotal_formatted', + 'created_at_formatted', 'discount_formatted', 'shipping_cost_formatted', + 'subtotal_formatted', 'total_formatted', - 'created_at_formatted', ])] class Purchase extends Model implements HasMedia { + // 1. Use Trait use HasFactory, HasModuleMedia, HasPendingOwnerVerification, InteractsWithActivityLog, SoftDeletes; + // 2. Casting protected function casts(): array { return [ - 'subtotal' => 'integer', 'discount' => 'integer', 'shipping_cost' => 'integer', + 'subtotal' => 'integer', 'total' => 'integer', ]; } - public function createdBy(): BelongsTo - { - return $this->belongsTo(User::class, 'created_by_id'); - } - - public function items(): HasMany - { - return $this->hasMany(PurchaseItem::class); - } - - public function ownerVerificationRequests(): MorphMany - { - return $this->morphMany(OwnerVerificationRequest::class, 'subject'); - } - - public function pendingOwnerVerificationRequest(): MorphOne - { - return $this->morphOne(OwnerVerificationRequest::class, 'subject') - ->where('status', OwnerVerificationStatus::PENDING) - ->latestOfMany(); - } - - public function supplier(): BelongsTo - { - return $this->belongsTo(Supplier::class); - } - + // 3. Attribute public function createdAtFormatted(): Attribute { return Attribute::make( @@ -102,6 +78,7 @@ public function totalFormatted(): Attribute ); } + // 4. Other Methods public static function mediaModuleName(): string { return 'purchase'; @@ -111,4 +88,32 @@ public function registerMediaCollections(): void { $this->addMediaCollection('photos'); } + + // 5. Relation + public function createdBy(): BelongsTo + { + return $this->belongsTo(User::class, 'created_by_id'); + } + + public function items(): HasMany + { + return $this->hasMany(PurchaseItem::class); + } + + public function ownerVerificationRequests(): MorphMany + { + return $this->morphMany(OwnerVerificationRequest::class, 'subject'); + } + + public function pendingOwnerVerificationRequest(): MorphOne + { + return $this->morphOne(OwnerVerificationRequest::class, 'subject') + ->where('status', OwnerVerificationStatus::PENDING) + ->latestOfMany(); + } + + public function supplier(): BelongsTo + { + return $this->belongsTo(Supplier::class); + } } diff --git a/app/Models/PurchaseItem.php b/app/Models/PurchaseItem.php index ea9c0b3..b1ce55d 100644 --- a/app/Models/PurchaseItem.php +++ b/app/Models/PurchaseItem.php @@ -15,38 +15,26 @@ #[Appends([ 'quantity_formatted', 'quantity_input', - 'unit_price_formatted', 'subtotal_formatted', 'unit_abbreviation', + 'unit_price_formatted', ])] class PurchaseItem extends Model { + // 1. Use Trait use HasFactory, InteractsWithActivityLog, SoftDeletes; + // 2. Casting protected function casts(): array { return [ 'quantity' => 'decimal:4', - 'unit_price' => 'integer', 'subtotal' => 'integer', + 'unit_price' => 'integer', ]; } - public function user(): BelongsTo - { - return $this->belongsTo(User::class); - } - - public function purchase(): BelongsTo - { - return $this->belongsTo(Purchase::class); - } - - public function rawMaterialPrice(): BelongsTo - { - return $this->belongsTo(RawMaterialPrice::class); - } - + // 3. Attribute public function quantityFormatted(): Attribute { return Attribute::make( @@ -85,4 +73,20 @@ public function unitPriceFormatted(): Attribute get: fn () => 'Rp '.number_format($this->unit_price, 0, ',', '.'), ); } + + // 4. Relation + public function purchase(): BelongsTo + { + return $this->belongsTo(Purchase::class); + } + + public function rawMaterialPrice(): BelongsTo + { + return $this->belongsTo(RawMaterialPrice::class); + } + + public function user(): BelongsTo + { + return $this->belongsTo(User::class); + } } diff --git a/app/Models/PushSubscription.php b/app/Models/PushSubscription.php index 0f33a33..0a48ab2 100644 --- a/app/Models/PushSubscription.php +++ b/app/Models/PushSubscription.php @@ -9,6 +9,7 @@ #[Guarded(['id'])] class PushSubscription extends Model { + // 1. Relation public function user(): BelongsTo { return $this->belongsTo(User::class); diff --git a/app/Models/RawMaterial.php b/app/Models/RawMaterial.php index 940db1f..74cfafd 100644 --- a/app/Models/RawMaterial.php +++ b/app/Models/RawMaterial.php @@ -19,11 +19,13 @@ use Illuminate\Database\Eloquent\SoftDeletes; #[Guarded(['id'])] -#[Appends(['unit_label', 'unit_abbreviation'])] +#[Appends(['unit_abbreviation', 'unit_label'])] class RawMaterial extends Model { + // 1. Use Trait use HasFactory, HasPendingOwnerVerification, InteractsWithActivityLog, SoftDeletes; + // 2. Casting protected function casts(): array { return [ @@ -32,35 +34,21 @@ protected function casts(): array ]; } + // 3. Scope (grouped by column, then alphabetical) + // Column Group: is_active #[Scope] - public function active(Builder $query): void + protected function active(Builder $query): void { $query->where('is_active', true); } #[Scope] - public function inactive(Builder $query): void + protected function inactive(Builder $query): void { $query->where('is_active', false); } - public function prices(): HasMany - { - return $this->hasMany(RawMaterialPrice::class); - } - - public function ownerVerificationRequests(): MorphMany - { - return $this->morphMany(OwnerVerificationRequest::class, 'subject'); - } - - public function pendingOwnerVerificationRequest(): MorphOne - { - return $this->morphOne(OwnerVerificationRequest::class, 'subject') - ->where('status', OwnerVerificationStatus::PENDING) - ->latestOfMany(); - } - + // 4. Attribute public function unitAbbreviation(): Attribute { return Attribute::make( @@ -74,4 +62,22 @@ public function unitLabel(): Attribute get: fn () => $this->unit->label(), ); } + + // 5. Relation + public function ownerVerificationRequests(): MorphMany + { + return $this->morphMany(OwnerVerificationRequest::class, 'subject'); + } + + public function pendingOwnerVerificationRequest(): MorphOne + { + return $this->morphOne(OwnerVerificationRequest::class, 'subject') + ->where('status', OwnerVerificationStatus::PENDING) + ->latestOfMany(); + } + + public function prices(): HasMany + { + return $this->hasMany(RawMaterialPrice::class); + } } diff --git a/app/Models/RawMaterialPrice.php b/app/Models/RawMaterialPrice.php index 1d8ef94..aeeb738 100644 --- a/app/Models/RawMaterialPrice.php +++ b/app/Models/RawMaterialPrice.php @@ -15,11 +15,13 @@ use Spatie\MediaLibrary\HasMedia; #[Guarded(['id'])] -#[Appends(['price_formatted', 'stock_formatted', 'price_input', 'stock_input'])] +#[Appends(['price_formatted', 'price_input', 'stock_formatted', 'stock_input'])] class RawMaterialPrice extends Model implements HasMedia { + // 1. Use Trait use HasFactory, HasModuleMedia, InteractsWithActivityLog, SoftDeletes; + // 2. Casting protected function casts(): array { return [ @@ -28,21 +30,7 @@ protected function casts(): array ]; } - public function cuttingMaterials(): HasMany - { - return $this->hasMany(CuttingMaterial::class); - } - - public function purchaseItems(): HasMany - { - return $this->hasMany(PurchaseItem::class); - } - - public function rawMaterial(): BelongsTo - { - return $this->belongsTo(RawMaterial::class); - } - + // 3. Attribute public function priceFormatted(): Attribute { return Attribute::make( @@ -75,6 +63,7 @@ public function stockInput(): Attribute ); } + // 4. Other Methods public static function mediaModuleName(): string { return 'raw-material'; @@ -84,4 +73,20 @@ public function registerMediaCollections(): void { $this->addMediaCollection('images'); } + + // 5. Relation + public function cuttingMaterials(): HasMany + { + return $this->hasMany(CuttingMaterial::class); + } + + public function purchaseItems(): HasMany + { + return $this->hasMany(PurchaseItem::class); + } + + public function rawMaterial(): BelongsTo + { + return $this->belongsTo(RawMaterial::class); + } } diff --git a/app/Models/Rejection.php b/app/Models/Rejection.php index 5b7380b..ff1cd18 100644 --- a/app/Models/Rejection.php +++ b/app/Models/Rejection.php @@ -12,8 +12,10 @@ #[Guarded(['id'])] class Rejection extends Model { + // 1. Use Trait use HasFactory, InteractsWithActivityLog; + // 2. Relation public function rejectable(): MorphTo { return $this->morphTo(); diff --git a/app/Models/StockEcerHistory.php b/app/Models/StockEcerHistory.php index b41133e..073cf3c 100644 --- a/app/Models/StockEcerHistory.php +++ b/app/Models/StockEcerHistory.php @@ -13,18 +13,20 @@ class StockEcerHistory extends Model public $timestamps = false; + // 1. Casting protected function casts(): array { return [ - 'quantity' => 'integer', - 'stock_before' => 'integer', - 'stock_ecer_before' => 'integer', - 'stock_after' => 'integer', - 'stock_ecer_after' => 'integer', 'created_at' => 'datetime', + 'quantity' => 'integer', + 'stock_after' => 'integer', + 'stock_before' => 'integer', + 'stock_ecer_after' => 'integer', + 'stock_ecer_before' => 'integer', ]; } + // 2. Relation public function productVariant(): BelongsTo { return $this->belongsTo(ProductVariant::class); diff --git a/app/Models/StokOpname.php b/app/Models/StokOpname.php index ca47235..f47b9ec 100644 --- a/app/Models/StokOpname.php +++ b/app/Models/StokOpname.php @@ -6,6 +6,8 @@ use App\Models\Concerns\InteractsWithActivityLog; use Illuminate\Database\Eloquent\Attributes\Appends; use Illuminate\Database\Eloquent\Attributes\Guarded; +use Illuminate\Database\Eloquent\Attributes\Scope; +use Illuminate\Database\Eloquent\Builder; use Illuminate\Database\Eloquent\Casts\Attribute; use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Model; @@ -14,11 +16,13 @@ use Illuminate\Database\Eloquent\SoftDeletes; #[Guarded(['id'])] -#[Appends(['status_label', 'opname_date_formatted', 'created_at_formatted', 'created_by_name'])] +#[Appends(['created_at_formatted', 'created_by_name', 'opname_date_formatted', 'status_label'])] class StokOpname extends Model { + // 1. Use Trait use HasFactory, InteractsWithActivityLog, SoftDeletes; + // 2. Casting protected function casts(): array { return [ @@ -27,35 +31,33 @@ protected function casts(): array ]; } - public function items(): HasMany + // 3. Scope (grouped by column, then alphabetical) + // Column Group: status + #[Scope] + protected function draft(Builder $query): void { - return $this->hasMany(StokOpnameItem::class); + $query->where('status', StokOpnameStatus::DRAFT); } - public function createdBy(): BelongsTo + #[Scope] + protected function pending(Builder $query): void { - return $this->belongsTo(User::class, 'created_by_id'); + $query->where('status', StokOpnameStatus::PENDING); } - public function verifiedBy(): BelongsTo + #[Scope] + protected function rejected(Builder $query): void { - return $this->belongsTo(User::class, 'verified_by_id'); + $query->where('status', StokOpnameStatus::REJECTED); } - public function statusLabel(): Attribute + #[Scope] + protected function verified(Builder $query): void { - return Attribute::make( - get: fn () => $this->status->label(), - ); - } - - public function opnameDateFormatted(): Attribute - { - return Attribute::make( - get: fn () => $this->opname_date?->translatedFormat('l, d F Y'), - ); + $query->where('status', StokOpnameStatus::VERIFIED); } + // 4. Attribute public function createdAtFormatted(): Attribute { return Attribute::make( @@ -70,18 +72,33 @@ public function createdByName(): Attribute ); } - public function scopeDraft($query) + public function opnameDateFormatted(): Attribute { - return $query->where('status', StokOpnameStatus::DRAFT); + return Attribute::make( + get: fn () => $this->opname_date?->translatedFormat('l, d F Y'), + ); } - public function scopePending($query) + public function statusLabel(): Attribute { - return $query->where('status', StokOpnameStatus::PENDING); + return Attribute::make( + get: fn () => $this->status->label(), + ); } - public function scopeVerified($query) + // 5. Relation + public function createdBy(): BelongsTo { - return $query->where('status', StokOpnameStatus::VERIFIED); + return $this->belongsTo(User::class, 'created_by_id'); + } + + public function items(): HasMany + { + return $this->hasMany(StokOpnameItem::class); + } + + public function verifiedBy(): BelongsTo + { + return $this->belongsTo(User::class, 'verified_by_id'); } } diff --git a/app/Models/StokOpnameItem.php b/app/Models/StokOpnameItem.php index 6198e94..6011946 100644 --- a/app/Models/StokOpnameItem.php +++ b/app/Models/StokOpnameItem.php @@ -11,24 +11,27 @@ #[Guarded(['id'])] class StokOpnameItem extends Model { + // 1. Use Trait use HasFactory, InteractsWithActivityLog; + // 2. Casting protected function casts(): array { return [ - 'system_stock' => 'integer', - 'physical_stock' => 'integer', 'difference' => 'integer', + 'physical_stock' => 'integer', + 'system_stock' => 'integer', ]; } + // 3. Relation + public function productVariant(): BelongsTo + { + return $this->belongsTo(ProductVariant::class); + } + public function stokOpname(): BelongsTo { return $this->belongsTo(StokOpname::class); } - - public function productVariant(): BelongsTo - { - return $this->belongsTo(ProductVariant::class); - } } diff --git a/app/Models/Supplier.php b/app/Models/Supplier.php index 6e3ef48..68a1e72 100644 --- a/app/Models/Supplier.php +++ b/app/Models/Supplier.php @@ -12,8 +12,10 @@ #[Guarded(['id'])] class Supplier extends Model { + // 1. Use Trait use HasFactory, InteractsWithActivityLog, SoftDeletes; + // 2. Relation public function purchases(): HasMany { return $this->hasMany(Purchase::class); diff --git a/app/Models/SystemConfiguration.php b/app/Models/SystemConfiguration.php index b96dcba..dc9dd59 100644 --- a/app/Models/SystemConfiguration.php +++ b/app/Models/SystemConfiguration.php @@ -12,22 +12,24 @@ #[Guarded(['id'])] class SystemConfiguration extends Model implements HasMedia { + // 1. Use Trait use HasFactory, HasModuleMedia, InteractsWithActivityLog; + // 2. Other Methods + public static function instance(): self + { + return static::query()->firstOrCreate(['id' => 1]); + } + public static function mediaModuleName(): string { return 'setting'; } - public static function instance(): self - { - return static::query()->firstOrCreate(['id' => 1]); - } - public function registerMediaCollections(): void { - $this->addMediaCollection('logo')->singleFile(); $this->addMediaCollection('favicon')->singleFile(); $this->addMediaCollection('login_cover')->singleFile(); + $this->addMediaCollection('logo')->singleFile(); } } diff --git a/app/Models/User.php b/app/Models/User.php index 516f6bf..80753fd 100644 --- a/app/Models/User.php +++ b/app/Models/User.php @@ -24,8 +24,10 @@ #[Appends(['role_label'])] class User extends Authenticatable { + // 1. Use Trait use CausesActivity, HasFactory, HasRoles, InteractsWithActivityLog, Notifiable, SoftDeletes; + // 2. Casting protected function casts(): array { return [ @@ -35,6 +37,39 @@ protected function casts(): array ]; } + // 3. Scope (grouped by column, then alphabetical) + // Column Group: is_active + #[Scope] + protected function active(Builder $query): void + { + $query->where('is_active', true); + } + + #[Scope] + protected function inactive(Builder $query): void + { + $query->where('is_active', false); + } + + // 4. Attribute + public function roleLabel(): Attribute + { + return Attribute::make( + get: function () { + $role = $this->roles->first(); + + if (! $role) { + return null; + } + + $enum = Role::tryFrom($role->name); + + return $enum ? $enum->label() : ucwords(str_replace('-', ' ', $role->name)); + }, + ); + } + + // 5. Relation public function cashTransactions(): HasMany { return $this->hasMany(CashTransaction::class, 'created_by_id'); @@ -80,6 +115,11 @@ public function marketingOrders(): HasMany return $this->hasMany(Order::class, 'marketing_id'); } + public function notifications(): HasMany + { + return $this->hasMany(Notification::class); + } + public function orderItems(): HasMany { return $this->hasMany(OrderItem::class); @@ -105,11 +145,6 @@ public function purchases(): HasMany return $this->hasMany(Purchase::class, 'created_by_id'); } - public function notifications(): HasMany - { - return $this->hasMany(Notification::class); - } - public function pushSubscriptions(): HasMany { return $this->hasMany(PushSubscription::class); @@ -124,33 +159,4 @@ public function verifiedLeaveRequests(): HasMany { return $this->hasMany(LeaveRequest::class, 'verified_by_id'); } - - public function roleLabel(): Attribute - { - return Attribute::make( - get: function () { - $role = $this->roles->first(); - - if (! $role) { - return null; - } - - $enum = Role::tryFrom($role->name); - - return $enum ? $enum->label() : ucwords(str_replace('-', ' ', $role->name)); - }, - ); - } - - #[Scope] - public function active(Builder $query): void - { - $query->where('is_active', true); - } - - #[Scope] - public function inactive(Builder $query): void - { - $query->where('is_active', false); - } } diff --git a/app/Models/UserProfile.php b/app/Models/UserProfile.php index d87acff..5f23a1f 100644 --- a/app/Models/UserProfile.php +++ b/app/Models/UserProfile.php @@ -19,35 +19,23 @@ #[Appends(['birth_date_formatted', 'birth_date_input', 'gender_label', 'profile_photo_url'])] class UserProfile extends Model implements HasMedia { + // 1. Use Trait use HasFactory, HasModuleMedia, InteractsWithActivityLog, SoftDeletes; - public static function mediaModuleName(): string - { - return 'user-profile'; - } - - public function registerMediaCollections(): void - { - $this->addMediaCollection('profile_photo')->singleFile(); - } - + // 2. Casting protected function casts(): array { return [ - 'gender' => Gender::class, 'birth_date' => 'date', + 'gender' => Gender::class, ]; } - public function user(): BelongsTo - { - return $this->belongsTo(User::class); - } - + // 3. Attribute public function birthDateFormatted(): Attribute { return Attribute::make( - get: fn () => Carbon::parse($this->birth_date)->translatedFormat('l, d F Y'), + get: fn () => $this->birth_date ? Carbon::parse($this->birth_date)->translatedFormat('l, d F Y') : '-', ); } @@ -61,7 +49,7 @@ public function birthDateInput(): Attribute public function genderLabel(): Attribute { return Attribute::make( - get: fn () => $this->gender?->label(), + get: fn () => $this->gender?->label() ?? '-', ); } @@ -71,4 +59,21 @@ public function profilePhotoUrl(): Attribute get: fn () => $this->getFirstMediaUrl('profile_photo') ?: null, ); } + + // 4. Other Methods + public static function mediaModuleName(): string + { + return 'user-profile'; + } + + public function registerMediaCollections(): void + { + $this->addMediaCollection('profile_photo')->singleFile(); + } + + // 5. Relation + public function user(): BelongsTo + { + return $this->belongsTo(User::class); + } } diff --git a/app/Services/System/AnalysisService.php b/app/Services/System/AnalysisService.php index fb85a78..9a529ed 100644 --- a/app/Services/System/AnalysisService.php +++ b/app/Services/System/AnalysisService.php @@ -202,7 +202,7 @@ public function getRevenueSummary(?Carbon $startDate = null, ?Carbon $endDate = 'total_revenue' => (int) ($revenueSummary->total_revenue ?? 0), 'total_discount' => (int) ($revenueSummary->total_discount ?? 0), 'total_marketplace_fees' => $totalMarketplaceFees, - 'total_potongan' => (int) ($revenueSummary->total_discount ?? 0) + $totalMarketplaceFees, + 'total_deduction' => (int) ($revenueSummary->total_discount ?? 0) + $totalMarketplaceFees, 'total_orders' => (int) ($revenueSummary->total_orders ?? 0), 'avg_order' => (int) ($revenueSummary->avg_order ?? 0), ]; @@ -259,13 +259,13 @@ public function getMonthlyRevenue(?Carbon $startDate = null, ?Carbon $endDate = $revenue = $monthlyData->firstWhere('month_key', $key); $fees = $monthlyFees->get($key, 0); $discount = (int) ($revenue->total_discount ?? 0); - $potongan = $discount + $fees; + $deduction = $discount + $fees; $result[] = [ 'month' => $monthLabel, 'total' => (int) ($revenue->total_revenue ?? 0), - 'net' => (int) ($revenue->total_revenue ?? 0) - $potongan, - 'potongan' => $potongan, + 'net' => (int) ($revenue->total_revenue ?? 0) - $deduction, + 'deduction' => $deduction, ]; $current->addMonth(); @@ -360,9 +360,9 @@ public function getMonthlyExpense(?Carbon $startDate = null, ?Carbon $endDate = $result[] = [ 'month' => $monthLabel, 'total' => $purchaseAmount + $expenseAmount + $advanceAmount, - 'belanja' => $purchaseAmount, - 'pengeluaran' => $expenseAmount, - 'kasbon' => $advanceAmount, + 'purchase' => $purchaseAmount, + 'expense' => $expenseAmount, + 'advance' => $advanceAmount, ]; $current->addMonth(); diff --git a/app/Services/System/DashboardService.php b/app/Services/System/DashboardService.php index 929ebf9..0b4f946 100644 --- a/app/Services/System/DashboardService.php +++ b/app/Services/System/DashboardService.php @@ -136,7 +136,7 @@ public function getRevenueSummary(): array 'total_revenue' => (int) ($revenueSummary->total_revenue ?? 0), 'total_discount' => (int) ($revenueSummary->total_discount ?? 0), 'total_marketplace_fees' => $totalMarketplaceFees, - 'total_potongan' => (int) ($revenueSummary->total_discount ?? 0) + $totalMarketplaceFees, + 'total_deduction' => (int) ($revenueSummary->total_discount ?? 0) + $totalMarketplaceFees, 'total_orders' => (int) ($revenueSummary->total_orders ?? 0), 'avg_order' => (int) ($revenueSummary->avg_order ?? 0), ]; diff --git a/resources/js/pages/admin/Analysis.vue b/resources/js/pages/admin/Analysis.vue index 3bdeee6..b6788ad 100644 --- a/resources/js/pages/admin/Analysis.vue +++ b/resources/js/pages/admin/Analysis.vue @@ -70,7 +70,7 @@ const props = defineProps<{ total_revenue: number; total_discount: number; total_marketplace_fees: number; - total_potongan: number; + total_deduction: number; total_orders: number; avg_order: number; }; @@ -78,7 +78,7 @@ const props = defineProps<{ month: string; total: number; net: number; - potongan: number; + deduction: number; }>; expenseSummary: { total: number; @@ -89,9 +89,9 @@ const props = defineProps<{ monthlyExpense: Array<{ month: string; total: number; - belanja: number; - pengeluaran: number; - kasbon: number; + purchase: number; + expense: number; + advance: number; }>; busyHours: Array<{ hour: string; @@ -166,7 +166,7 @@ function onPresetChange(value: any) { } // Revenue Chart -type MonthlyRevenueData = { month: string; total: number; net: number; potongan: number }; +type MonthlyRevenueData = { month: string; total: number; net: number; deduction: number }; const revenueChartConfig = { total: { @@ -177,55 +177,55 @@ const revenueChartConfig = { label: 'Bersih', color: 'var(--chart-2)', }, - potongan: { + deduction: { label: 'Potongan', color: 'var(--chart-3)', }, } satisfies ChartConfig; -const activeRevenueChart = ref<'total' | 'net' | 'potongan'>('total'); +const activeRevenueChart = ref<'total' | 'net' | 'deduction'>('total'); const revenueTotals = computed(() => ({ total: props.revenueSummary.total_revenue, - net: props.revenueSummary.total_revenue - props.revenueSummary.total_potongan, - potongan: props.revenueSummary.total_potongan, + net: props.revenueSummary.total_revenue - props.revenueSummary.total_deduction, + deduction: props.revenueSummary.total_deduction, })); // Expense Chart -type MonthlyExpenseData = { month: string; total: number; belanja: number; pengeluaran: number; kasbon: number }; +type MonthlyExpenseData = { month: string; total: number; purchase: number; expense: number; advance: number }; const expenseChartConfig = { total: { label: 'Total', color: 'var(--chart-1)', }, - belanja: { + purchase: { label: 'Belanja', color: 'var(--chart-2)', }, - pengeluaran: { + expense: { label: 'Pengeluaran', color: 'var(--chart-3)', }, - kasbon: { + advance: { label: 'Kasbon', color: 'var(--chart-4)', }, } satisfies ChartConfig; -const activeExpenseChart = ref<'total' | 'belanja' | 'pengeluaran' | 'kasbon'>('total'); +const activeExpenseChart = ref<'total' | 'purchase' | 'expense' | 'advance'>('total'); const expenseTotals = computed(() => ({ total: props.expenseSummary.total, - belanja: props.expenseSummary.purchase_total, - pengeluaran: props.expenseSummary.expense_total, - kasbon: props.expenseSummary.advance_total, + purchase: props.expenseSummary.purchase_total, + expense: props.expenseSummary.expense_total, + advance: props.expenseSummary.advance_total, })); const visibleExpenseCharts = computed(() => { - const charts = ['total', 'belanja', 'pengeluaran', 'kasbon'] as const; + const charts = ['total', 'purchase', 'expense', 'advance'] as const; if (hasRole('admin-toko')) { - return charts.filter((c) => c !== 'belanja'); + return charts.filter((c) => c !== 'purchase'); } return charts; }); @@ -451,7 +451,7 @@ watch([startDate, endDate], () => { Pendapatan
-