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], () => {