refactor: reorganize model structure and enhance attribute handling across multiple models, including improved casting, scope definitions, and attribute formatting

This commit is contained in:
Yoga Pangestu 2026-06-28 14:47:19 +07:00
parent b43835335a
commit 393c36e932
43 changed files with 1170 additions and 737 deletions

View File

@ -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);
}
}
```

View File

@ -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);
}
}

View File

@ -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);
}
}

View File

@ -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();
}
}

View File

@ -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');
}
}

View File

@ -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);

View File

@ -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);
}
}

View File

@ -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);
}
}

View File

@ -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);

View File

@ -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);
}
}

View File

@ -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);
}
}

View File

@ -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');
}
}

View File

@ -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'),
);
}
}

View File

@ -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');
}
}

View File

@ -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();
}
}

View File

@ -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');
}
}

View File

@ -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);
}
}

View File

@ -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');
}
}

View File

@ -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);
}
}

View File

@ -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');
}
}

View File

@ -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);
}
}

View File

@ -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);
}
}

View File

@ -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);
}
}

View File

@ -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);
}
}

View File

@ -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');
}
}

View File

@ -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');
}
}

View File

@ -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);
}
}

View File

@ -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);
}
}

View File

@ -9,6 +9,7 @@
#[Guarded(['id'])]
class PushSubscription extends Model
{
// 1. Relation
public function user(): BelongsTo
{
return $this->belongsTo(User::class);

View File

@ -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);
}
}

View File

@ -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);
}
}

View File

@ -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();

View File

@ -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);

View File

@ -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');
}
}

View File

@ -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);
}
}

View File

@ -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);

View File

@ -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();
}
}

View File

@ -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);
}
}

View File

@ -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);
}
}

View File

@ -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();

View File

@ -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),
];

View File

@ -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], () => {
<CardTitle>Pendapatan</CardTitle>
</div>
<div class="flex">
<button v-for="chart in ['total', 'net', 'potongan'] as const" :key="chart"
<button v-for="chart in ['total', 'net', 'deduction'] as const" :key="chart"
:data-active="activeRevenueChart === chart"
class="data-[active=true]:bg-muted/50 flex flex-1 flex-col justify-center gap-1 border-t px-6 py-4 text-left even:border-l sm:border-t-0 sm:border-l sm:px-8 sm:py-6"
@click="activeRevenueChart = chart">

View File

@ -46,7 +46,7 @@ interface DashboardProps {
total_revenue: number;
total_discount: number;
total_marketplace_fees: number;
total_potongan: number;
total_deduction: number;
total_orders: number;
avg_order: number;
};
@ -289,12 +289,12 @@ const marketingChartConfig = computed(() => {
label: 'Bersih',
value: 'Rp' + formatRupiah(
revenueSummary.total_revenue -
revenueSummary.total_potongan,
revenueSummary.total_deduction,
),
},
{
label: 'Potongan',
value: 'Rp' + formatRupiah(revenueSummary.total_potongan),
value: 'Rp' + formatRupiah(revenueSummary.total_deduction),
},
{
label: 'Diskon',