refactor: reorganize model structure and enhance attribute handling across multiple models, including improved casting, scope definitions, and attribute formatting
This commit is contained in:
parent
b43835335a
commit
393c36e932
104
BEST_PRACTICE.md
104
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);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@ -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');
|
||||
}
|
||||
}
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@ -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');
|
||||
}
|
||||
}
|
||||
|
||||
@ -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'),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@ -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');
|
||||
}
|
||||
}
|
||||
|
||||
@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@ -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');
|
||||
}
|
||||
}
|
||||
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@ -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');
|
||||
}
|
||||
}
|
||||
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@ -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');
|
||||
}
|
||||
}
|
||||
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@ -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');
|
||||
}
|
||||
}
|
||||
|
||||
@ -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');
|
||||
}
|
||||
}
|
||||
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@ -9,6 +9,7 @@
|
||||
#[Guarded(['id'])]
|
||||
class PushSubscription extends Model
|
||||
{
|
||||
// 1. Relation
|
||||
public function user(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(User::class);
|
||||
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@ -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();
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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');
|
||||
}
|
||||
}
|
||||
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@ -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();
|
||||
|
||||
@ -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),
|
||||
];
|
||||
|
||||
@ -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">
|
||||
|
||||
@ -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',
|
||||
|
||||
Loading…
Reference in New Issue
Block a user