refactor: streamline enum management and enhance role assignment logic in EmployeeController for improved maintainability
This commit is contained in:
parent
7508227768
commit
27c5b5fffa
@ -3,7 +3,6 @@
|
|||||||
namespace App\Console\Commands;
|
namespace App\Console\Commands;
|
||||||
|
|
||||||
use App\Enums\PayrollAdjustmentType;
|
use App\Enums\PayrollAdjustmentType;
|
||||||
use App\Enums\PayrollPeriodStatus;
|
|
||||||
use App\Models\Attendance;
|
use App\Models\Attendance;
|
||||||
use App\Models\Employee;
|
use App\Models\Employee;
|
||||||
use App\Models\LeaveRequest;
|
use App\Models\LeaveRequest;
|
||||||
|
|||||||
@ -2,8 +2,12 @@
|
|||||||
|
|
||||||
namespace App\Enums;
|
namespace App\Enums;
|
||||||
|
|
||||||
|
use App\Traits\ProvidesEnumOptions;
|
||||||
|
|
||||||
enum ActivityEventLabel: string
|
enum ActivityEventLabel: string
|
||||||
{
|
{
|
||||||
|
use ProvidesEnumOptions;
|
||||||
|
|
||||||
case CREATED = 'created';
|
case CREATED = 'created';
|
||||||
case UPDATED = 'updated';
|
case UPDATED = 'updated';
|
||||||
case DELETED = 'deleted';
|
case DELETED = 'deleted';
|
||||||
@ -20,26 +24,4 @@ public function label(): string
|
|||||||
self::LOGOUT => 'Keluar',
|
self::LOGOUT => 'Keluar',
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
public static function labelFor(?string $event): string
|
|
||||||
{
|
|
||||||
if ($event === null || $event === '') {
|
|
||||||
return '-';
|
|
||||||
}
|
|
||||||
|
|
||||||
return self::tryFrom($event)?->label() ?? ucfirst($event);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @return list<array{value: string, label: string}>
|
|
||||||
*/
|
|
||||||
public static function selectOptions(): array
|
|
||||||
{
|
|
||||||
return collect(self::cases())
|
|
||||||
->map(fn (self $event) => [
|
|
||||||
'value' => $event->value,
|
|
||||||
'label' => $event->label(),
|
|
||||||
])
|
|
||||||
->all();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -3,7 +3,6 @@
|
|||||||
namespace App\Enums;
|
namespace App\Enums;
|
||||||
|
|
||||||
use App\Traits\ProvidesEnumOptions;
|
use App\Traits\ProvidesEnumOptions;
|
||||||
use InvalidArgumentException;
|
|
||||||
|
|
||||||
enum CuttingStatus: string
|
enum CuttingStatus: string
|
||||||
{
|
{
|
||||||
@ -25,62 +24,4 @@ public function label(): string
|
|||||||
self::REJECTED => 'Ditolak',
|
self::REJECTED => 'Ditolak',
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
public function isEditable(): bool
|
|
||||||
{
|
|
||||||
return in_array($this, [self::IN_PROGRESS, self::REJECTED], true);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function canTransitionTo(self $status): bool
|
|
||||||
{
|
|
||||||
return match ($this) {
|
|
||||||
self::IN_PROGRESS => $status === self::COMPLETED,
|
|
||||||
self::COMPLETED => in_array($status, [self::PENDING_VERIFICATION, self::REJECTED, self::IN_PROGRESS], true),
|
|
||||||
self::PENDING_VERIFICATION => in_array($status, [self::VERIFIED, self::REJECTED, self::COMPLETED], true),
|
|
||||||
self::REJECTED => $status === self::IN_PROGRESS,
|
|
||||||
default => false,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
public function transitionPermission(): Permission
|
|
||||||
{
|
|
||||||
return match ($this) {
|
|
||||||
self::COMPLETED => Permission::CUTTINGS_COMPLETE,
|
|
||||||
self::PENDING_VERIFICATION => Permission::CUTTINGS_VERIFY,
|
|
||||||
self::VERIFIED => Permission::CUTTINGS_VERIFY,
|
|
||||||
self::REJECTED => Permission::CUTTINGS_REJECT,
|
|
||||||
self::IN_PROGRESS => Permission::CUTTINGS_UPDATE,
|
|
||||||
default => throw new InvalidArgumentException('Status tidak mendukung transisi.'),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @return list<array{status: string, label: string, destructive: bool, permission: string, icon_only: bool}>
|
|
||||||
*/
|
|
||||||
public function availableActions(): array
|
|
||||||
{
|
|
||||||
return match ($this) {
|
|
||||||
self::IN_PROGRESS => [
|
|
||||||
[
|
|
||||||
'status' => self::COMPLETED->value,
|
|
||||||
'label' => 'Selesai',
|
|
||||||
'destructive' => false,
|
|
||||||
'permission' => Permission::CUTTINGS_COMPLETE->value,
|
|
||||||
'icon_only' => false,
|
|
||||||
],
|
|
||||||
],
|
|
||||||
self::COMPLETED => [],
|
|
||||||
self::PENDING_VERIFICATION => [],
|
|
||||||
self::REJECTED => [
|
|
||||||
[
|
|
||||||
'status' => self::IN_PROGRESS->value,
|
|
||||||
'label' => 'Kembalikan ke Proses',
|
|
||||||
'destructive' => false,
|
|
||||||
'permission' => Permission::CUTTINGS_UPDATE->value,
|
|
||||||
'icon_only' => false,
|
|
||||||
],
|
|
||||||
],
|
|
||||||
default => [],
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -20,13 +20,4 @@ public function label(): string
|
|||||||
self::TIKTOK => 'TikTok',
|
self::TIKTOK => 'TikTok',
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
public function defaultPriceType(): ?PriceType
|
|
||||||
{
|
|
||||||
return match ($this) {
|
|
||||||
self::STORE => null,
|
|
||||||
self::SHOPEE => PriceType::SHOPEE,
|
|
||||||
self::TIKTOK => PriceType::TIKTOK,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -3,7 +3,6 @@
|
|||||||
namespace App\Enums;
|
namespace App\Enums;
|
||||||
|
|
||||||
use App\Traits\ProvidesEnumOptions;
|
use App\Traits\ProvidesEnumOptions;
|
||||||
use InvalidArgumentException;
|
|
||||||
|
|
||||||
enum OrderStatus: string
|
enum OrderStatus: string
|
||||||
{
|
{
|
||||||
@ -23,70 +22,4 @@ public function label(): string
|
|||||||
self::CANCELLED => 'Dibatalkan',
|
self::CANCELLED => 'Dibatalkan',
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
public function isEditable(): bool
|
|
||||||
{
|
|
||||||
return in_array($this, [self::PENDING, self::PROCESSING], true);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function canTransitionTo(self $status): bool
|
|
||||||
{
|
|
||||||
return match ($this) {
|
|
||||||
self::PENDING => in_array($status, [self::PROCESSING, self::CANCELLED], true),
|
|
||||||
self::PROCESSING => in_array($status, [self::COMPLETED, self::CANCELLED], true),
|
|
||||||
self::COMPLETED, self::CANCELLED => false,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
public function transitionPermission(): Permission
|
|
||||||
{
|
|
||||||
return match ($this) {
|
|
||||||
self::PROCESSING => Permission::ORDERS_SEND,
|
|
||||||
self::COMPLETED => Permission::ORDERS_COMPLETE,
|
|
||||||
self::CANCELLED => Permission::ORDERS_CANCEL,
|
|
||||||
default => throw new InvalidArgumentException('Status tidak mendukung transisi.'),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @return list<array{status: string, label: string, destructive: bool, permission: string, icon_only: bool}>
|
|
||||||
*/
|
|
||||||
public function availableActions(): array
|
|
||||||
{
|
|
||||||
return match ($this) {
|
|
||||||
self::PENDING => [
|
|
||||||
[
|
|
||||||
'status' => self::PROCESSING->value,
|
|
||||||
'label' => 'Kirim',
|
|
||||||
'destructive' => false,
|
|
||||||
'permission' => Permission::ORDERS_SEND->value,
|
|
||||||
'icon_only' => true,
|
|
||||||
],
|
|
||||||
[
|
|
||||||
'status' => self::CANCELLED->value,
|
|
||||||
'label' => 'Batalkan',
|
|
||||||
'destructive' => true,
|
|
||||||
'permission' => Permission::ORDERS_CANCEL->value,
|
|
||||||
'icon_only' => true,
|
|
||||||
],
|
|
||||||
],
|
|
||||||
self::PROCESSING => [
|
|
||||||
[
|
|
||||||
'status' => self::COMPLETED->value,
|
|
||||||
'label' => 'Selesai',
|
|
||||||
'destructive' => false,
|
|
||||||
'permission' => Permission::ORDERS_COMPLETE->value,
|
|
||||||
'icon_only' => true,
|
|
||||||
],
|
|
||||||
[
|
|
||||||
'status' => self::CANCELLED->value,
|
|
||||||
'label' => 'Batalkan',
|
|
||||||
'destructive' => true,
|
|
||||||
'permission' => Permission::ORDERS_CANCEL->value,
|
|
||||||
'icon_only' => true,
|
|
||||||
],
|
|
||||||
],
|
|
||||||
default => [],
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -222,57 +222,4 @@ public function label(): string
|
|||||||
self::ROLES_DELETE => 'Hapus Role',
|
self::ROLES_DELETE => 'Hapus Role',
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
public function group(): string
|
|
||||||
{
|
|
||||||
return match ($this) {
|
|
||||||
self::DASHBOARD_VIEW => 'Umum',
|
|
||||||
self::EMPLOYEES_VIEW, self::EMPLOYEES_CREATE, self::EMPLOYEES_UPDATE,
|
|
||||||
self::EMPLOYEES_DELETE, self::EMPLOYEES_RESET_PASSWORD, self::EMPLOYEES_TOGGLE_STATUS => 'Pegawai',
|
|
||||||
self::ATTENDANCES_VIEW, self::ATTENDANCES_CREATE, self::ATTENDANCES_DELETE,
|
|
||||||
self::ATTENDANCES_MANAGE => 'Presensi',
|
|
||||||
self::LEAVE_REQUESTS_VIEW, self::LEAVE_REQUESTS_CREATE, self::LEAVE_REQUESTS_UPDATE,
|
|
||||||
self::LEAVE_REQUESTS_DELETE, self::LEAVE_REQUESTS_VERIFY => 'Cuti',
|
|
||||||
self::CATEGORIES_VIEW, self::CATEGORIES_CREATE, self::CATEGORIES_UPDATE,
|
|
||||||
self::CATEGORIES_DELETE => 'Kategori',
|
|
||||||
self::SUPPLIERS_VIEW, self::SUPPLIERS_CREATE, self::SUPPLIERS_UPDATE,
|
|
||||||
self::SUPPLIERS_DELETE => 'Supplier',
|
|
||||||
self::CUSTOMERS_VIEW, self::CUSTOMERS_CREATE, self::CUSTOMERS_UPDATE,
|
|
||||||
self::CUSTOMERS_DELETE => 'Pelanggan',
|
|
||||||
self::PRODUCTS_VIEW, self::PRODUCTS_CREATE, self::PRODUCTS_UPDATE,
|
|
||||||
self::PRODUCTS_DELETE, self::PRODUCTS_TOGGLE_STATUS => 'Produk',
|
|
||||||
self::RAW_MATERIALS_VIEW, self::RAW_MATERIALS_CREATE, self::RAW_MATERIALS_UPDATE,
|
|
||||||
self::RAW_MATERIALS_DELETE, self::RAW_MATERIALS_TOGGLE_STATUS => 'Bahan Baku',
|
|
||||||
self::PURCHASES_VIEW, self::PURCHASES_CREATE, self::PURCHASES_UPDATE,
|
|
||||||
self::PURCHASES_DELETE => 'Belanja',
|
|
||||||
self::ORDERS_VIEW, self::ORDERS_CREATE, self::ORDERS_UPDATE,
|
|
||||||
self::ORDERS_DELETE, self::ORDERS_SEND, self::ORDERS_COMPLETE,
|
|
||||||
self::ORDERS_CANCEL => 'Pesanan',
|
|
||||||
self::CUTTINGS_VIEW, self::CUTTINGS_CREATE, self::CUTTINGS_UPDATE,
|
|
||||||
self::CUTTINGS_DELETE, self::CUTTINGS_COMPLETE, self::CUTTINGS_VERIFY,
|
|
||||||
self::CUTTINGS_REJECT => 'Cutting',
|
|
||||||
self::OWNER_VERIFICATIONS_VIEW, self::OWNER_VERIFICATIONS_VERIFY,
|
|
||||||
self::OWNER_VERIFICATIONS_REJECT => 'Verifikasi Owner',
|
|
||||||
self::STOCKS_VIEW => 'Stok',
|
|
||||||
self::CASH_VIEW, self::CASH_DEPOSIT, self::CASH_WITHDRAW, self::CASH_UPDATE, self::CASH_DELETE => 'Kas',
|
|
||||||
self::EXPENSES_VIEW, self::EXPENSES_CREATE, self::EXPENSES_UPDATE,
|
|
||||||
self::EXPENSES_DELETE => 'Pengeluaran',
|
|
||||||
self::EMPLOYEE_ADVANCES_VIEW, self::EMPLOYEE_ADVANCES_CREATE, self::EMPLOYEE_ADVANCES_UPDATE,
|
|
||||||
self::EMPLOYEE_ADVANCES_DELETE, self::EMPLOYEE_ADVANCES_VERIFY,
|
|
||||||
self::EMPLOYEE_ADVANCES_PAY => 'Kasbon',
|
|
||||||
self::PAYROLL_VIEW, self::PAYROLL_ADJUST => 'Gaji',
|
|
||||||
self::SETTINGS_VIEW, self::SETTINGS_UPDATE => 'Pengaturan Aplikasi',
|
|
||||||
self::ACTIVITY_LOGS_VIEW => 'Log Aktivitas',
|
|
||||||
self::ROLES_VIEW, self::ROLES_CREATE, self::ROLES_UPDATE,
|
|
||||||
self::ROLES_DELETE => 'Role & Permission',
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @return list<string>
|
|
||||||
*/
|
|
||||||
public static function values(): array
|
|
||||||
{
|
|
||||||
return array_column(self::cases(), 'value');
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -30,12 +30,4 @@ public function label(): string
|
|||||||
self::HARGA_MODAL => 'Harga Modal',
|
self::HARGA_MODAL => 'Harga Modal',
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* @return list<string>
|
|
||||||
*/
|
|
||||||
public static function values(): array
|
|
||||||
{
|
|
||||||
return array_column(self::cases(), 'value');
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -18,12 +18,4 @@ public function label(): string
|
|||||||
self::REJECT => 'Reject',
|
self::REJECT => 'Reject',
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
public function stockColumn(): string
|
|
||||||
{
|
|
||||||
return match ($this) {
|
|
||||||
self::GOOD => 'stock',
|
|
||||||
self::REJECT => 'reject_stock',
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -12,16 +12,6 @@ enum RawMaterialUnit: string
|
|||||||
case METER = 'meter';
|
case METER = 'meter';
|
||||||
case KILOGRAM = 'kilogram';
|
case KILOGRAM = 'kilogram';
|
||||||
|
|
||||||
private const MIN_STOCK_YARD = 20;
|
|
||||||
|
|
||||||
private const MIN_STOCK_METER = 10;
|
|
||||||
|
|
||||||
private const MIN_STOCK_KILOGRAM = 5;
|
|
||||||
|
|
||||||
private const CM_PER_YARD = 91.44;
|
|
||||||
|
|
||||||
private const CM_PER_METER = 100.0;
|
|
||||||
|
|
||||||
public function label(): string
|
public function label(): string
|
||||||
{
|
{
|
||||||
return match ($this) {
|
return match ($this) {
|
||||||
@ -39,71 +29,4 @@ public function abbreviation(): string
|
|||||||
self::KILOGRAM => 'kg',
|
self::KILOGRAM => 'kg',
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* @return list<string>
|
|
||||||
*/
|
|
||||||
public static function values(): array
|
|
||||||
{
|
|
||||||
return array_column(self::cases(), 'value');
|
|
||||||
}
|
|
||||||
|
|
||||||
public function minStock(): float
|
|
||||||
{
|
|
||||||
return match ($this) {
|
|
||||||
self::YARD => self::MIN_STOCK_YARD,
|
|
||||||
self::METER => self::MIN_STOCK_METER,
|
|
||||||
self::KILOGRAM => self::MIN_STOCK_KILOGRAM,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
public function usesLengthUnit(): bool
|
|
||||||
{
|
|
||||||
return match ($this) {
|
|
||||||
self::YARD, self::METER => true,
|
|
||||||
self::KILOGRAM => false,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
public function cmPerUnit(): ?float
|
|
||||||
{
|
|
||||||
return match ($this) {
|
|
||||||
self::YARD => self::CM_PER_YARD,
|
|
||||||
self::METER => self::CM_PER_METER,
|
|
||||||
self::KILOGRAM => null,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
public function toCm(float $value): ?float
|
|
||||||
{
|
|
||||||
$cmPerUnit = $this->cmPerUnit();
|
|
||||||
|
|
||||||
if ($cmPerUnit === null) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return $value * $cmPerUnit;
|
|
||||||
}
|
|
||||||
|
|
||||||
public function fromCm(float $cm): float
|
|
||||||
{
|
|
||||||
$cmPerUnit = $this->cmPerUnit();
|
|
||||||
|
|
||||||
if ($cmPerUnit === null || $cmPerUnit <= 0) {
|
|
||||||
return $cm;
|
|
||||||
}
|
|
||||||
|
|
||||||
return $cm / $cmPerUnit;
|
|
||||||
}
|
|
||||||
|
|
||||||
public function pricePerCm(int $pricePerUnit): ?float
|
|
||||||
{
|
|
||||||
$cmPerUnit = $this->cmPerUnit();
|
|
||||||
|
|
||||||
if ($cmPerUnit === null || $cmPerUnit <= 0) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return $pricePerUnit / $cmPerUnit;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -30,302 +30,4 @@ public function label(): string
|
|||||||
self::NON_OPERATOR => 'Non Operator',
|
self::NON_OPERATOR => 'Non Operator',
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* @return list<Permission>
|
|
||||||
*/
|
|
||||||
public function permissions(): array
|
|
||||||
{
|
|
||||||
return match ($this) {
|
|
||||||
self::DEVELOPER, self::OWNER => array_values(array_filter(
|
|
||||||
Permission::cases(),
|
|
||||||
fn (Permission $permission) => ! in_array($permission, [
|
|
||||||
Permission::ATTENDANCES_CREATE,
|
|
||||||
Permission::ATTENDANCES_DELETE,
|
|
||||||
], true)
|
|
||||||
)),
|
|
||||||
|
|
||||||
self::DIREKTUR => [
|
|
||||||
Permission::DASHBOARD_VIEW,
|
|
||||||
|
|
||||||
Permission::EMPLOYEES_VIEW,
|
|
||||||
|
|
||||||
Permission::STOCKS_VIEW,
|
|
||||||
|
|
||||||
Permission::ATTENDANCES_VIEW,
|
|
||||||
Permission::ATTENDANCES_CREATE,
|
|
||||||
Permission::ATTENDANCES_DELETE,
|
|
||||||
Permission::ATTENDANCES_MANAGE,
|
|
||||||
|
|
||||||
Permission::LEAVE_REQUESTS_VIEW,
|
|
||||||
Permission::LEAVE_REQUESTS_CREATE,
|
|
||||||
Permission::LEAVE_REQUESTS_UPDATE,
|
|
||||||
Permission::LEAVE_REQUESTS_DELETE,
|
|
||||||
|
|
||||||
Permission::CATEGORIES_VIEW,
|
|
||||||
|
|
||||||
Permission::SUPPLIERS_VIEW,
|
|
||||||
|
|
||||||
Permission::CUSTOMERS_VIEW,
|
|
||||||
|
|
||||||
Permission::PRODUCTS_VIEW,
|
|
||||||
|
|
||||||
Permission::RAW_MATERIALS_VIEW,
|
|
||||||
|
|
||||||
Permission::PURCHASES_VIEW,
|
|
||||||
|
|
||||||
Permission::ORDERS_VIEW,
|
|
||||||
|
|
||||||
Permission::CUTTINGS_VIEW,
|
|
||||||
|
|
||||||
Permission::CASH_VIEW,
|
|
||||||
|
|
||||||
Permission::EXPENSES_VIEW,
|
|
||||||
|
|
||||||
Permission::EMPLOYEE_ADVANCES_VIEW,
|
|
||||||
Permission::EMPLOYEE_ADVANCES_CREATE,
|
|
||||||
Permission::EMPLOYEE_ADVANCES_UPDATE,
|
|
||||||
Permission::EMPLOYEE_ADVANCES_DELETE,
|
|
||||||
Permission::EMPLOYEE_ADVANCES_PAY,
|
|
||||||
|
|
||||||
Permission::PAYROLL_VIEW,
|
|
||||||
],
|
|
||||||
|
|
||||||
self::ADMIN_TOKO => [
|
|
||||||
Permission::DASHBOARD_VIEW,
|
|
||||||
|
|
||||||
Permission::STOCKS_VIEW,
|
|
||||||
|
|
||||||
Permission::CUTTINGS_VERIFY,
|
|
||||||
|
|
||||||
Permission::ATTENDANCES_VIEW,
|
|
||||||
Permission::ATTENDANCES_CREATE,
|
|
||||||
Permission::ATTENDANCES_DELETE,
|
|
||||||
|
|
||||||
Permission::LEAVE_REQUESTS_VIEW,
|
|
||||||
Permission::LEAVE_REQUESTS_CREATE,
|
|
||||||
Permission::LEAVE_REQUESTS_UPDATE,
|
|
||||||
Permission::LEAVE_REQUESTS_DELETE,
|
|
||||||
|
|
||||||
Permission::CATEGORIES_VIEW,
|
|
||||||
Permission::CATEGORIES_CREATE,
|
|
||||||
Permission::CATEGORIES_UPDATE,
|
|
||||||
Permission::CATEGORIES_DELETE,
|
|
||||||
|
|
||||||
Permission::SUPPLIERS_VIEW,
|
|
||||||
Permission::SUPPLIERS_CREATE,
|
|
||||||
Permission::SUPPLIERS_UPDATE,
|
|
||||||
Permission::SUPPLIERS_DELETE,
|
|
||||||
|
|
||||||
Permission::CUSTOMERS_VIEW,
|
|
||||||
Permission::CUSTOMERS_CREATE,
|
|
||||||
Permission::CUSTOMERS_UPDATE,
|
|
||||||
Permission::CUSTOMERS_DELETE,
|
|
||||||
|
|
||||||
Permission::PRODUCTS_VIEW,
|
|
||||||
Permission::PRODUCTS_CREATE,
|
|
||||||
Permission::PRODUCTS_UPDATE,
|
|
||||||
Permission::PRODUCTS_DELETE,
|
|
||||||
Permission::PRODUCTS_TOGGLE_STATUS,
|
|
||||||
|
|
||||||
Permission::ORDERS_VIEW,
|
|
||||||
Permission::ORDERS_CREATE,
|
|
||||||
Permission::ORDERS_UPDATE,
|
|
||||||
Permission::ORDERS_DELETE,
|
|
||||||
Permission::ORDERS_SEND,
|
|
||||||
Permission::ORDERS_COMPLETE,
|
|
||||||
Permission::ORDERS_CANCEL,
|
|
||||||
|
|
||||||
Permission::CASH_VIEW,
|
|
||||||
Permission::CASH_DEPOSIT,
|
|
||||||
Permission::CASH_WITHDRAW,
|
|
||||||
Permission::CASH_UPDATE,
|
|
||||||
Permission::CASH_DELETE,
|
|
||||||
|
|
||||||
Permission::EXPENSES_VIEW,
|
|
||||||
Permission::EXPENSES_CREATE,
|
|
||||||
Permission::EXPENSES_UPDATE,
|
|
||||||
Permission::EXPENSES_DELETE,
|
|
||||||
|
|
||||||
Permission::EMPLOYEE_ADVANCES_VIEW,
|
|
||||||
Permission::EMPLOYEE_ADVANCES_CREATE,
|
|
||||||
Permission::EMPLOYEE_ADVANCES_UPDATE,
|
|
||||||
Permission::EMPLOYEE_ADVANCES_DELETE,
|
|
||||||
Permission::EMPLOYEE_ADVANCES_PAY,
|
|
||||||
|
|
||||||
Permission::PAYROLL_VIEW,
|
|
||||||
Permission::PAYROLL_ADJUST,
|
|
||||||
],
|
|
||||||
|
|
||||||
self::CASHIER => [
|
|
||||||
Permission::DASHBOARD_VIEW,
|
|
||||||
|
|
||||||
Permission::ATTENDANCES_VIEW,
|
|
||||||
Permission::ATTENDANCES_CREATE,
|
|
||||||
Permission::ATTENDANCES_DELETE,
|
|
||||||
|
|
||||||
Permission::LEAVE_REQUESTS_VIEW,
|
|
||||||
Permission::LEAVE_REQUESTS_CREATE,
|
|
||||||
Permission::LEAVE_REQUESTS_UPDATE,
|
|
||||||
Permission::LEAVE_REQUESTS_DELETE,
|
|
||||||
|
|
||||||
Permission::CUSTOMERS_VIEW,
|
|
||||||
Permission::CUSTOMERS_CREATE,
|
|
||||||
Permission::CUSTOMERS_UPDATE,
|
|
||||||
Permission::CUSTOMERS_DELETE,
|
|
||||||
|
|
||||||
Permission::PRODUCTS_VIEW,
|
|
||||||
|
|
||||||
Permission::ORDERS_VIEW,
|
|
||||||
Permission::ORDERS_CREATE,
|
|
||||||
Permission::ORDERS_UPDATE,
|
|
||||||
Permission::ORDERS_DELETE,
|
|
||||||
Permission::ORDERS_SEND,
|
|
||||||
Permission::ORDERS_COMPLETE,
|
|
||||||
Permission::ORDERS_CANCEL,
|
|
||||||
|
|
||||||
Permission::EMPLOYEE_ADVANCES_VIEW,
|
|
||||||
Permission::EMPLOYEE_ADVANCES_CREATE,
|
|
||||||
Permission::EMPLOYEE_ADVANCES_UPDATE,
|
|
||||||
Permission::EMPLOYEE_ADVANCES_DELETE,
|
|
||||||
Permission::EMPLOYEE_ADVANCES_PAY,
|
|
||||||
|
|
||||||
Permission::PAYROLL_VIEW,
|
|
||||||
Permission::PAYROLL_ADJUST,
|
|
||||||
],
|
|
||||||
|
|
||||||
self::ADMIN_BAHAN_BAKU => [
|
|
||||||
Permission::DASHBOARD_VIEW,
|
|
||||||
|
|
||||||
Permission::ATTENDANCES_VIEW,
|
|
||||||
Permission::ATTENDANCES_CREATE,
|
|
||||||
|
|
||||||
Permission::LEAVE_REQUESTS_VIEW,
|
|
||||||
Permission::LEAVE_REQUESTS_CREATE,
|
|
||||||
Permission::LEAVE_REQUESTS_UPDATE,
|
|
||||||
Permission::LEAVE_REQUESTS_DELETE,
|
|
||||||
|
|
||||||
Permission::SUPPLIERS_VIEW,
|
|
||||||
Permission::SUPPLIERS_CREATE,
|
|
||||||
Permission::SUPPLIERS_UPDATE,
|
|
||||||
Permission::SUPPLIERS_DELETE,
|
|
||||||
|
|
||||||
Permission::RAW_MATERIALS_VIEW,
|
|
||||||
Permission::RAW_MATERIALS_CREATE,
|
|
||||||
Permission::RAW_MATERIALS_UPDATE,
|
|
||||||
Permission::RAW_MATERIALS_DELETE,
|
|
||||||
Permission::RAW_MATERIALS_TOGGLE_STATUS,
|
|
||||||
|
|
||||||
Permission::PRODUCTS_VIEW,
|
|
||||||
|
|
||||||
Permission::CUTTINGS_VIEW,
|
|
||||||
Permission::CUTTINGS_CREATE,
|
|
||||||
Permission::CUTTINGS_UPDATE,
|
|
||||||
Permission::CUTTINGS_DELETE,
|
|
||||||
Permission::CUTTINGS_COMPLETE,
|
|
||||||
Permission::CUTTINGS_VERIFY,
|
|
||||||
|
|
||||||
Permission::PURCHASES_VIEW,
|
|
||||||
Permission::PURCHASES_CREATE,
|
|
||||||
Permission::PURCHASES_UPDATE,
|
|
||||||
Permission::PURCHASES_DELETE,
|
|
||||||
|
|
||||||
Permission::EMPLOYEE_ADVANCES_VIEW,
|
|
||||||
Permission::EMPLOYEE_ADVANCES_CREATE,
|
|
||||||
Permission::EMPLOYEE_ADVANCES_UPDATE,
|
|
||||||
Permission::EMPLOYEE_ADVANCES_DELETE,
|
|
||||||
Permission::EMPLOYEE_ADVANCES_PAY,
|
|
||||||
|
|
||||||
Permission::PAYROLL_VIEW,
|
|
||||||
],
|
|
||||||
|
|
||||||
self::MARKETING => [
|
|
||||||
Permission::DASHBOARD_VIEW,
|
|
||||||
|
|
||||||
Permission::ATTENDANCES_VIEW,
|
|
||||||
Permission::ATTENDANCES_CREATE,
|
|
||||||
|
|
||||||
Permission::LEAVE_REQUESTS_VIEW,
|
|
||||||
Permission::LEAVE_REQUESTS_CREATE,
|
|
||||||
Permission::LEAVE_REQUESTS_UPDATE,
|
|
||||||
Permission::LEAVE_REQUESTS_DELETE,
|
|
||||||
|
|
||||||
Permission::CUSTOMERS_VIEW,
|
|
||||||
|
|
||||||
Permission::PRODUCTS_VIEW,
|
|
||||||
|
|
||||||
Permission::ORDERS_VIEW,
|
|
||||||
|
|
||||||
Permission::EMPLOYEE_ADVANCES_VIEW,
|
|
||||||
Permission::EMPLOYEE_ADVANCES_CREATE,
|
|
||||||
Permission::EMPLOYEE_ADVANCES_UPDATE,
|
|
||||||
Permission::EMPLOYEE_ADVANCES_DELETE,
|
|
||||||
Permission::EMPLOYEE_ADVANCES_PAY,
|
|
||||||
|
|
||||||
Permission::PAYROLL_VIEW,
|
|
||||||
],
|
|
||||||
|
|
||||||
self::NON_OPERATOR => [
|
|
||||||
Permission::DASHBOARD_VIEW,
|
|
||||||
|
|
||||||
Permission::DASHBOARD_VIEW,
|
|
||||||
|
|
||||||
Permission::ATTENDANCES_VIEW,
|
|
||||||
Permission::ATTENDANCES_CREATE,
|
|
||||||
|
|
||||||
Permission::LEAVE_REQUESTS_VIEW,
|
|
||||||
Permission::LEAVE_REQUESTS_CREATE,
|
|
||||||
Permission::LEAVE_REQUESTS_UPDATE,
|
|
||||||
Permission::LEAVE_REQUESTS_DELETE,
|
|
||||||
|
|
||||||
Permission::EMPLOYEE_ADVANCES_VIEW,
|
|
||||||
Permission::EMPLOYEE_ADVANCES_CREATE,
|
|
||||||
Permission::EMPLOYEE_ADVANCES_UPDATE,
|
|
||||||
Permission::EMPLOYEE_ADVANCES_DELETE,
|
|
||||||
Permission::EMPLOYEE_ADVANCES_PAY,
|
|
||||||
|
|
||||||
Permission::PAYROLL_VIEW,
|
|
||||||
],
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
public function isAssignable(): bool
|
|
||||||
{
|
|
||||||
return $this !== self::DEVELOPER;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @return list<array{value: string, label: string}>
|
|
||||||
*/
|
|
||||||
public static function assignableSelectOptions(): array
|
|
||||||
{
|
|
||||||
return collect(self::cases())
|
|
||||||
->filter(fn (self $role) => $role->isAssignable())
|
|
||||||
->map(fn (self $role) => [
|
|
||||||
'value' => $role->value,
|
|
||||||
'label' => $role->label(),
|
|
||||||
])
|
|
||||||
->values()
|
|
||||||
->all();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @return list<string>
|
|
||||||
*/
|
|
||||||
public static function assignableValues(): array
|
|
||||||
{
|
|
||||||
return collect(self::cases())
|
|
||||||
->filter(fn (self $role) => $role->isAssignable())
|
|
||||||
->map(fn (self $role) => $role->value)
|
|
||||||
->values()
|
|
||||||
->all();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @return list<string>
|
|
||||||
*/
|
|
||||||
public static function values(): array
|
|
||||||
{
|
|
||||||
return array_column(self::cases(), 'value');
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -25,6 +25,20 @@ public function __construct(
|
|||||||
private readonly EmployeeService $employeeService,
|
private readonly EmployeeService $employeeService,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
|
private function assignableRoleOptions(): array
|
||||||
|
{
|
||||||
|
$assignable = config('roles.assignable', []);
|
||||||
|
|
||||||
|
return collect(Role::cases())
|
||||||
|
->filter(fn (Role $role) => in_array($role->value, $assignable, true))
|
||||||
|
->map(fn (Role $role) => [
|
||||||
|
'value' => $role->value,
|
||||||
|
'label' => $role->label(),
|
||||||
|
])
|
||||||
|
->values()
|
||||||
|
->all();
|
||||||
|
}
|
||||||
|
|
||||||
public function index(Request $request): Response
|
public function index(Request $request): Response
|
||||||
{
|
{
|
||||||
$tableQuery = $this->parseDataTableQuery($request);
|
$tableQuery = $this->parseDataTableQuery($request);
|
||||||
@ -47,7 +61,7 @@ public function index(Request $request): Response
|
|||||||
'employment_status' => $employmentStatus,
|
'employment_status' => $employmentStatus,
|
||||||
'is_active' => $isActive,
|
'is_active' => $isActive,
|
||||||
]),
|
]),
|
||||||
'roles' => Role::assignableSelectOptions(),
|
'roles' => $this->assignableRoleOptions(),
|
||||||
'genders' => Gender::selectOptions(),
|
'genders' => Gender::selectOptions(),
|
||||||
'employmentStatuses' => EmploymentStatus::selectOptions(),
|
'employmentStatuses' => EmploymentStatus::selectOptions(),
|
||||||
]);
|
]);
|
||||||
@ -58,7 +72,7 @@ public function create(): Response
|
|||||||
return Inertia::render('admin/hr/employees/Create', [
|
return Inertia::render('admin/hr/employees/Create', [
|
||||||
'genders' => Gender::selectOptions(),
|
'genders' => Gender::selectOptions(),
|
||||||
'employmentStatuses' => EmploymentStatus::selectOptions(),
|
'employmentStatuses' => EmploymentStatus::selectOptions(),
|
||||||
'roles' => Role::assignableSelectOptions(),
|
'roles' => $this->assignableRoleOptions(),
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -78,7 +92,7 @@ public function edit(User $user): Response
|
|||||||
return Inertia::render('admin/hr/employees/Edit', [
|
return Inertia::render('admin/hr/employees/Edit', [
|
||||||
'genders' => Gender::selectOptions(),
|
'genders' => Gender::selectOptions(),
|
||||||
'employmentStatuses' => EmploymentStatus::selectOptions(),
|
'employmentStatuses' => EmploymentStatus::selectOptions(),
|
||||||
'roles' => Role::assignableSelectOptions(),
|
'roles' => $this->assignableRoleOptions(),
|
||||||
'employee' => $user,
|
'employee' => $user,
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
@ -94,20 +108,20 @@ public function update(EmployeeRequest $request, User $user): RedirectResponse
|
|||||||
|
|
||||||
public function toggleStatus(ToggleStatusRequest $request, User $user): RedirectResponse
|
public function toggleStatus(ToggleStatusRequest $request, User $user): RedirectResponse
|
||||||
{
|
{
|
||||||
$this->employeeService->toggleStatus($user, $request->validated());
|
$this->employeeService->toggleStatus($user);
|
||||||
|
|
||||||
$this->flashStatusUpdated('pegawai');
|
$this->flashSuccess('Status pegawai berhasil diubah.');
|
||||||
|
|
||||||
return back();
|
return redirect()->route('admin.hr.employees.index');
|
||||||
}
|
}
|
||||||
|
|
||||||
public function resetPassword(User $user): RedirectResponse
|
public function resetPassword(User $user): RedirectResponse
|
||||||
{
|
{
|
||||||
$this->employeeService->resetPassword($user);
|
$this->employeeService->resetPassword($user);
|
||||||
|
|
||||||
$this->flashSuccess('Kata sandi berhasil direset. Pengguna telah logout dari semua sesi.');
|
$this->flashSuccess('Kata sandi pegawai berhasil direset.');
|
||||||
|
|
||||||
return back();
|
return redirect()->route('admin.hr.employees.index');
|
||||||
}
|
}
|
||||||
|
|
||||||
public function destroy(User $user): RedirectResponse
|
public function destroy(User $user): RedirectResponse
|
||||||
|
|||||||
@ -57,7 +57,7 @@ public function store(CuttingRequest $request): RedirectResponse
|
|||||||
|
|
||||||
public function edit(Cutting $cutting): Response|RedirectResponse
|
public function edit(Cutting $cutting): Response|RedirectResponse
|
||||||
{
|
{
|
||||||
if (! $cutting->status->isEditable()) {
|
if (! $this->cuttingService->isEditable($cutting->status)) {
|
||||||
$this->flashError('Proses cutting tidak dapat diubah.');
|
$this->flashError('Proses cutting tidak dapat diubah.');
|
||||||
|
|
||||||
return redirect()->route('admin.manage.cuttings.index');
|
return redirect()->route('admin.manage.cuttings.index');
|
||||||
|
|||||||
@ -75,7 +75,7 @@ public function store(OrderRequest $request): RedirectResponse
|
|||||||
|
|
||||||
public function edit(Order $order): Response|RedirectResponse
|
public function edit(Order $order): Response|RedirectResponse
|
||||||
{
|
{
|
||||||
if (! $order->status->isEditable()) {
|
if (! $this->orderService->isEditable($order->status)) {
|
||||||
$this->flashError('Pesanan tidak dapat diubah.');
|
$this->flashError('Pesanan tidak dapat diubah.');
|
||||||
|
|
||||||
return redirect()->route('admin.manage.orders.index');
|
return redirect()->route('admin.manage.orders.index');
|
||||||
|
|||||||
@ -35,11 +35,13 @@ public function index(Request $request): Response
|
|||||||
|
|
||||||
public function create(): Response
|
public function create(): Response
|
||||||
{
|
{
|
||||||
|
$permissionGroups = config('permissions.groups');
|
||||||
|
|
||||||
$permissions = collect(PermissionEnum::cases())
|
$permissions = collect(PermissionEnum::cases())
|
||||||
->map(fn (PermissionEnum $permission) => [
|
->map(fn (PermissionEnum $permission) => [
|
||||||
'value' => $permission->value,
|
'value' => $permission->value,
|
||||||
'label' => $permission->label(),
|
'label' => $permission->label(),
|
||||||
'group' => $permission->group(),
|
'group' => $permissionGroups[$permission->value] ?? '-',
|
||||||
])
|
])
|
||||||
->values()
|
->values()
|
||||||
->all();
|
->all();
|
||||||
@ -62,11 +64,13 @@ public function edit(Role $role): Response
|
|||||||
{
|
{
|
||||||
$role->load('permissions');
|
$role->load('permissions');
|
||||||
|
|
||||||
|
$permissionGroups = config('permissions.groups');
|
||||||
|
|
||||||
$permissions = collect(PermissionEnum::cases())
|
$permissions = collect(PermissionEnum::cases())
|
||||||
->map(fn (PermissionEnum $permission) => [
|
->map(fn (PermissionEnum $permission) => [
|
||||||
'value' => $permission->value,
|
'value' => $permission->value,
|
||||||
'label' => $permission->label(),
|
'label' => $permission->label(),
|
||||||
'group' => $permission->group(),
|
'group' => $permissionGroups[$permission->value] ?? '-',
|
||||||
])
|
])
|
||||||
->values()
|
->values()
|
||||||
->all();
|
->all();
|
||||||
@ -78,10 +82,10 @@ public function edit(Role $role): Response
|
|||||||
'role' => [
|
'role' => [
|
||||||
'id' => $role->id,
|
'id' => $role->id,
|
||||||
'name' => $role->name,
|
'name' => $role->name,
|
||||||
'permission_names' => $role->permissions->pluck('name')->all(),
|
'permissions' => $role->permissions->pluck('name'),
|
||||||
|
'is_protected' => $isProtected,
|
||||||
],
|
],
|
||||||
'permissions' => $permissions,
|
'permissions' => $permissions,
|
||||||
'isProtected' => $isProtected,
|
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -96,12 +100,9 @@ public function update(RoleRequest $request, Role $role): RedirectResponse
|
|||||||
|
|
||||||
public function destroy(Role $role): RedirectResponse
|
public function destroy(Role $role): RedirectResponse
|
||||||
{
|
{
|
||||||
try {
|
$this->roleService->delete($role);
|
||||||
$this->roleService->delete($role);
|
|
||||||
$this->flashDeleted('Role');
|
$this->flashDeleted('Role');
|
||||||
} catch (\InvalidArgumentException $exception) {
|
|
||||||
$this->flashError($exception->getMessage());
|
|
||||||
}
|
|
||||||
|
|
||||||
return redirect()->route('admin.system.roles.index');
|
return redirect()->route('admin.system.roles.index');
|
||||||
}
|
}
|
||||||
|
|||||||
@ -36,7 +36,7 @@ public function rules(): array
|
|||||||
'gender' => ['nullable', Rule::enum(Gender::class)],
|
'gender' => ['nullable', Rule::enum(Gender::class)],
|
||||||
'birth_date' => ['nullable', 'date', 'before:today'],
|
'birth_date' => ['nullable', 'date', 'before:today'],
|
||||||
'address' => ['nullable', 'string'],
|
'address' => ['nullable', 'string'],
|
||||||
'role' => ['required', Rule::in(Role::assignableValues())],
|
'role' => ['required', Rule::in(config('roles.assignable', []))],
|
||||||
];
|
];
|
||||||
|
|
||||||
if (! $isOwner) {
|
if (! $isOwner) {
|
||||||
@ -61,10 +61,10 @@ public function attributes(): array
|
|||||||
'gender' => 'jenis kelamin',
|
'gender' => 'jenis kelamin',
|
||||||
'birth_date' => 'tanggal lahir',
|
'birth_date' => 'tanggal lahir',
|
||||||
'address' => 'alamat',
|
'address' => 'alamat',
|
||||||
|
'role' => 'role',
|
||||||
'join_date' => 'tanggal bergabung',
|
'join_date' => 'tanggal bergabung',
|
||||||
'employment_status' => 'status kepegawaian',
|
'employment_status' => 'status kepegawaian',
|
||||||
'base_salary' => 'gaji pokok',
|
'base_salary' => 'gaji pokok',
|
||||||
'role' => 'role',
|
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -26,7 +26,10 @@ public function authorize(): bool
|
|||||||
|| false;
|
|| false;
|
||||||
}
|
}
|
||||||
|
|
||||||
return $this->user()?->can($status->transitionPermission()->value) ?? false;
|
$permissions = config('cutting-status.permissions', $status);
|
||||||
|
$permission = $permissions[$status->value] ?? null;
|
||||||
|
|
||||||
|
return $permission ? ($this->user()?->can($permission->value) ?? false) : false;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -73,7 +76,10 @@ public function withValidator(Validator $validator): void
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (! $cutting->status->canTransitionTo($status)) {
|
$transitions = config('cutting-status.transitions', []);
|
||||||
|
$allowedTargets = $transitions[$cutting->status->value] ?? [];
|
||||||
|
|
||||||
|
if (! in_array($status, $allowedTargets, true)) {
|
||||||
$validator->errors()->add('status', 'Status cutting tidak dapat diubah.');
|
$validator->errors()->add('status', 'Status cutting tidak dapat diubah.');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -90,7 +90,7 @@ public function withValidator(Validator $validator): void
|
|||||||
/** @var Order $order */
|
/** @var Order $order */
|
||||||
$order = $this->route('order');
|
$order = $this->route('order');
|
||||||
|
|
||||||
if (! $order->status->isEditable()) {
|
if (! in_array($order->status, config('order-status.editable'), true)) {
|
||||||
$validator->errors()->add('status', 'Pesanan tidak dapat diubah.');
|
$validator->errors()->add('status', 'Pesanan tidak dapat diubah.');
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@ -18,7 +18,10 @@ public function authorize(): bool
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
return $this->user()?->can($status->transitionPermission()->value) ?? false;
|
$permissions = config('order-status.permissions', []);
|
||||||
|
$permission = $permissions[$status->value] ?? null;
|
||||||
|
|
||||||
|
return $permission ? ($this->user()?->can($permission->value) ?? false) : false;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -52,7 +55,10 @@ public function withValidator(Validator $validator): void
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (! $order->status->canTransitionTo($status)) {
|
$transitions = config('order-status.transitions', []);
|
||||||
|
$allowedTargets = $transitions[$order->status->value] ?? [];
|
||||||
|
|
||||||
|
if (! in_array($status, $allowedTargets, true)) {
|
||||||
$validator->errors()->add('status', 'Status pesanan tidak dapat diubah.');
|
$validator->errors()->add('status', 'Status pesanan tidak dapat diubah.');
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@ -3,6 +3,7 @@
|
|||||||
namespace App\Services\Manage;
|
namespace App\Services\Manage;
|
||||||
|
|
||||||
use App\Enums\CuttingStatus;
|
use App\Enums\CuttingStatus;
|
||||||
|
use App\Enums\Permission;
|
||||||
use App\Models\Cutting;
|
use App\Models\Cutting;
|
||||||
use App\Models\CuttingMaterial;
|
use App\Models\CuttingMaterial;
|
||||||
use App\Models\CuttingResult;
|
use App\Models\CuttingResult;
|
||||||
@ -27,6 +28,34 @@ public function __construct(
|
|||||||
private readonly PushNotificationService $pushNotificationService,
|
private readonly PushNotificationService $pushNotificationService,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
|
public function isEditable(CuttingStatus $status): bool
|
||||||
|
{
|
||||||
|
return in_array($status, config('cutting-status.editable'), true);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function canTransitionTo(CuttingStatus $from, CuttingStatus $to): bool
|
||||||
|
{
|
||||||
|
$transitions = config('cutting-status.transitions', []);
|
||||||
|
|
||||||
|
return in_array($to, $transitions[$from->value] ?? [], true);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function transitionPermission(CuttingStatus $status): Permission
|
||||||
|
{
|
||||||
|
$permissions = config('cutting-status.permissions', []);
|
||||||
|
|
||||||
|
return $permissions[$status->value]
|
||||||
|
?? throw new \InvalidArgumentException('Status tidak mendukung transisi.');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return list<array{status: string, label: string, destructive: bool, permission: string, icon_only: bool}>
|
||||||
|
*/
|
||||||
|
public function availableActions(CuttingStatus $status): array
|
||||||
|
{
|
||||||
|
return config('cutting-status.actions.'.$status->value, []);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param array{search: string, sort: string, direction: 'asc'|'desc'} $tableQuery
|
* @param array{search: string, sort: string, direction: 'asc'|'desc'} $tableQuery
|
||||||
*/
|
*/
|
||||||
@ -61,13 +90,13 @@ public function paginateForIndex(array $tableQuery, User $user): LengthAwarePagi
|
|||||||
->paginate(10)
|
->paginate(10)
|
||||||
->withQueryString()
|
->withQueryString()
|
||||||
->through(function (Cutting $cutting) use ($user) {
|
->through(function (Cutting $cutting) use ($user) {
|
||||||
$actions = collect($cutting->status->availableActions())
|
$actions = collect($this->availableActions($cutting->status))
|
||||||
->filter(fn (array $action) => $user->can($action['permission']))
|
->filter(fn (array $action) => $user->can($action['permission']))
|
||||||
->values()
|
->values()
|
||||||
->all();
|
->all();
|
||||||
|
|
||||||
$cutting->setAttribute('available_actions', $actions);
|
$cutting->setAttribute('available_actions', $actions);
|
||||||
$cutting->setAttribute('is_editable', $cutting->status->isEditable());
|
$cutting->setAttribute('is_editable', $this->isEditable($cutting->status));
|
||||||
$this->appendCostPreview($cutting);
|
$this->appendCostPreview($cutting);
|
||||||
|
|
||||||
return $cutting;
|
return $cutting;
|
||||||
@ -91,13 +120,13 @@ public function getInProgressCuttings(User $user): Collection
|
|||||||
->latest()
|
->latest()
|
||||||
->get()
|
->get()
|
||||||
->each(function (Cutting $cutting) use ($user): void {
|
->each(function (Cutting $cutting) use ($user): void {
|
||||||
$actions = collect($cutting->status->availableActions())
|
$actions = collect($this->availableActions($cutting->status))
|
||||||
->filter(fn (array $action) => $user->can($action['permission']))
|
->filter(fn (array $action) => $user->can($action['permission']))
|
||||||
->values()
|
->values()
|
||||||
->all();
|
->all();
|
||||||
|
|
||||||
$cutting->setAttribute('available_actions', $actions);
|
$cutting->setAttribute('available_actions', $actions);
|
||||||
$cutting->setAttribute('is_editable', $cutting->status->isEditable());
|
$cutting->setAttribute('is_editable', $this->isEditable($cutting->status));
|
||||||
$this->appendCostPreview($cutting);
|
$this->appendCostPreview($cutting);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -119,13 +148,13 @@ public function getCompletedCuttings(User $user): Collection
|
|||||||
->latest()
|
->latest()
|
||||||
->get()
|
->get()
|
||||||
->each(function (Cutting $cutting) use ($user): void {
|
->each(function (Cutting $cutting) use ($user): void {
|
||||||
$actions = collect($cutting->status->availableActions())
|
$actions = collect($this->availableActions($cutting->status))
|
||||||
->filter(fn (array $action) => $user->can($action['permission']))
|
->filter(fn (array $action) => $user->can($action['permission']))
|
||||||
->values()
|
->values()
|
||||||
->all();
|
->all();
|
||||||
|
|
||||||
$cutting->setAttribute('available_actions', $actions);
|
$cutting->setAttribute('available_actions', $actions);
|
||||||
$cutting->setAttribute('is_editable', $cutting->status->isEditable());
|
$cutting->setAttribute('is_editable', $this->isEditable($cutting->status));
|
||||||
$this->appendCostPreview($cutting);
|
$this->appendCostPreview($cutting);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -463,7 +492,7 @@ public function create(array $validated, User $user): Cutting
|
|||||||
*/
|
*/
|
||||||
public function update(Cutting $cutting, array $validated): void
|
public function update(Cutting $cutting, array $validated): void
|
||||||
{
|
{
|
||||||
if (! $cutting->status->isEditable()) {
|
if (! $this->isEditable($cutting->status)) {
|
||||||
throw ValidationException::withMessages([
|
throw ValidationException::withMessages([
|
||||||
'status' => 'Proses cutting tidak dapat diubah.',
|
'status' => 'Proses cutting tidak dapat diubah.',
|
||||||
]);
|
]);
|
||||||
@ -557,7 +586,7 @@ public function transitionStatus(
|
|||||||
?array $results = null,
|
?array $results = null,
|
||||||
?array $resultPrices = null,
|
?array $resultPrices = null,
|
||||||
): void {
|
): void {
|
||||||
if (! $cutting->status->canTransitionTo($status)) {
|
if (! $this->canTransitionTo($cutting->status, $status)) {
|
||||||
throw ValidationException::withMessages([
|
throw ValidationException::withMessages([
|
||||||
'status' => 'Status proses cutting tidak dapat diubah.',
|
'status' => 'Status proses cutting tidak dapat diubah.',
|
||||||
]);
|
]);
|
||||||
|
|||||||
@ -5,6 +5,7 @@
|
|||||||
use App\Enums\OrderChannel;
|
use App\Enums\OrderChannel;
|
||||||
use App\Enums\OrderStatus;
|
use App\Enums\OrderStatus;
|
||||||
use App\Enums\PaymentType;
|
use App\Enums\PaymentType;
|
||||||
|
use App\Enums\Permission;
|
||||||
use App\Enums\PriceType;
|
use App\Enums\PriceType;
|
||||||
use App\Enums\ProductStockQuality;
|
use App\Enums\ProductStockQuality;
|
||||||
use App\Models\Customer;
|
use App\Models\Customer;
|
||||||
@ -33,6 +34,51 @@ public function __construct(
|
|||||||
private readonly CuttingResultPriceResolver $cuttingResultPriceResolver,
|
private readonly CuttingResultPriceResolver $cuttingResultPriceResolver,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
|
public function isEditable(OrderStatus $status): bool
|
||||||
|
{
|
||||||
|
return in_array($status, config('order-status.editable'), true);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function canTransitionTo(OrderStatus $from, OrderStatus $to): bool
|
||||||
|
{
|
||||||
|
$transitions = config('order-status.transitions', []);
|
||||||
|
|
||||||
|
return in_array($to, $transitions[$from->value] ?? [], true);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function transitionPermission(OrderStatus $status): Permission
|
||||||
|
{
|
||||||
|
$permissions = config('order-status.permissions', []);
|
||||||
|
|
||||||
|
return $permissions[$status->value]
|
||||||
|
?? throw new \InvalidArgumentException('Status tidak mendukung transisi.');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return list<array{status: string, label: string, destructive: bool, permission: string, icon_only: bool}>
|
||||||
|
*/
|
||||||
|
public function availableActions(OrderStatus $status): array
|
||||||
|
{
|
||||||
|
return config('order-status.actions.'.$status->value, []);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function defaultPriceType(OrderChannel $channel): ?PriceType
|
||||||
|
{
|
||||||
|
return match ($channel) {
|
||||||
|
OrderChannel::STORE => null,
|
||||||
|
OrderChannel::SHOPEE => PriceType::SHOPEE,
|
||||||
|
OrderChannel::TIKTOK => PriceType::TIKTOK,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public function stockColumn(ProductStockQuality $quality): string
|
||||||
|
{
|
||||||
|
return match ($quality) {
|
||||||
|
ProductStockQuality::GOOD => 'stock',
|
||||||
|
ProductStockQuality::REJECT => 'reject_stock',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param array{search: string, sort: string, direction: 'asc'|'desc'} $tableQuery
|
* @param array{search: string, sort: string, direction: 'asc'|'desc'} $tableQuery
|
||||||
*/
|
*/
|
||||||
@ -67,13 +113,13 @@ public function paginateForIndex(array $tableQuery, User $user): LengthAwarePagi
|
|||||||
->paginate(10)
|
->paginate(10)
|
||||||
->withQueryString()
|
->withQueryString()
|
||||||
->through(function (Order $order) use ($user) {
|
->through(function (Order $order) use ($user) {
|
||||||
$actions = collect($order->status->availableActions())
|
$actions = collect($this->availableActions($order->status))
|
||||||
->filter(fn (array $action) => $user->can($action['permission']))
|
->filter(fn (array $action) => $user->can($action['permission']))
|
||||||
->values()
|
->values()
|
||||||
->all();
|
->all();
|
||||||
|
|
||||||
$order->setAttribute('available_actions', $actions);
|
$order->setAttribute('available_actions', $actions);
|
||||||
$order->setAttribute('is_editable', $order->status->isEditable());
|
$order->setAttribute('is_editable', $this->isEditable($order->status));
|
||||||
|
|
||||||
return $order;
|
return $order;
|
||||||
});
|
});
|
||||||
@ -239,12 +285,12 @@ public function findForShow(Order $order): Order
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
$availableActions = collect($order->status->availableActions())
|
$availableActions = collect($this->availableActions($order->status))
|
||||||
->values()
|
->values()
|
||||||
->all();
|
->all();
|
||||||
|
|
||||||
$order->setAttribute('available_actions', $availableActions);
|
$order->setAttribute('available_actions', $availableActions);
|
||||||
$order->setAttribute('is_editable', $order->status->isEditable());
|
$order->setAttribute('is_editable', $this->isEditable($order->status));
|
||||||
|
|
||||||
return $order;
|
return $order;
|
||||||
}
|
}
|
||||||
@ -453,7 +499,7 @@ public function create(array $validated, User $user): Order
|
|||||||
*/
|
*/
|
||||||
public function update(Order $order, array $validated): void
|
public function update(Order $order, array $validated): void
|
||||||
{
|
{
|
||||||
if (! $order->status->isEditable()) {
|
if (! $this->isEditable($order->status)) {
|
||||||
throw ValidationException::withMessages([
|
throw ValidationException::withMessages([
|
||||||
'status' => 'Pesanan tidak dapat diubah.',
|
'status' => 'Pesanan tidak dapat diubah.',
|
||||||
]);
|
]);
|
||||||
@ -534,7 +580,7 @@ public function delete(Order $order): void
|
|||||||
DB::transaction(function () use ($order): void {
|
DB::transaction(function () use ($order): void {
|
||||||
$order->load('items');
|
$order->load('items');
|
||||||
|
|
||||||
if ($order->status->isEditable()) {
|
if ($this->isEditable($order->status)) {
|
||||||
foreach ($order->items as $item) {
|
foreach ($order->items as $item) {
|
||||||
$this->incrementStock($item);
|
$this->incrementStock($item);
|
||||||
}
|
}
|
||||||
@ -558,7 +604,7 @@ public function delete(Order $order): void
|
|||||||
|
|
||||||
public function transitionStatus(Order $order, OrderStatus $status): void
|
public function transitionStatus(Order $order, OrderStatus $status): void
|
||||||
{
|
{
|
||||||
if (! $order->status->canTransitionTo($status)) {
|
if (! $this->canTransitionTo($order->status, $status)) {
|
||||||
throw ValidationException::withMessages([
|
throw ValidationException::withMessages([
|
||||||
'status' => 'Status pesanan tidak dapat diubah.',
|
'status' => 'Status pesanan tidak dapat diubah.',
|
||||||
]);
|
]);
|
||||||
@ -635,7 +681,7 @@ private function resolvePriceType(string $channel, string $priceType): PriceType
|
|||||||
$channelEnum = OrderChannel::from($channel);
|
$channelEnum = OrderChannel::from($channel);
|
||||||
$priceTypeEnum = PriceType::from($priceType);
|
$priceTypeEnum = PriceType::from($priceType);
|
||||||
|
|
||||||
$defaultPriceType = $channelEnum->defaultPriceType();
|
$defaultPriceType = $this->defaultPriceType($channelEnum);
|
||||||
|
|
||||||
if ($defaultPriceType !== null && $priceTypeEnum !== $defaultPriceType) {
|
if ($defaultPriceType !== null && $priceTypeEnum !== $defaultPriceType) {
|
||||||
throw ValidationException::withMessages([
|
throw ValidationException::withMessages([
|
||||||
@ -705,7 +751,7 @@ private function decrementStock(OrderItem $item): void
|
|||||||
|
|
||||||
ProductVariant::query()
|
ProductVariant::query()
|
||||||
->whereKey($item->product_variant_id)
|
->whereKey($item->product_variant_id)
|
||||||
->decrement($stockQuality->stockColumn(), $item->quantity);
|
->decrement($this->stockColumn($stockQuality), $item->quantity);
|
||||||
}
|
}
|
||||||
|
|
||||||
private function incrementStock(OrderItem $item): void
|
private function incrementStock(OrderItem $item): void
|
||||||
@ -714,12 +760,12 @@ private function incrementStock(OrderItem $item): void
|
|||||||
|
|
||||||
ProductVariant::query()
|
ProductVariant::query()
|
||||||
->whereKey($item->product_variant_id)
|
->whereKey($item->product_variant_id)
|
||||||
->increment($stockQuality->stockColumn(), $item->quantity);
|
->increment($this->stockColumn($stockQuality), $item->quantity);
|
||||||
}
|
}
|
||||||
|
|
||||||
private function availableStock(ProductVariant $variant, ProductStockQuality $stockQuality): int
|
private function availableStock(ProductVariant $variant, ProductStockQuality $stockQuality): int
|
||||||
{
|
{
|
||||||
return (int) $variant->{$stockQuality->stockColumn()};
|
return (int) $variant->{$this->stockColumn($stockQuality)};
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@ -42,12 +42,18 @@ public function paginateForIndex(array $tableQuery, string $isActive, string $st
|
|||||||
$query->whereHas('prices', fn (Builder $priceQuery) => $priceQuery->where('stock', '<=', 0));
|
$query->whereHas('prices', fn (Builder $priceQuery) => $priceQuery->where('stock', '<=', 0));
|
||||||
})
|
})
|
||||||
->when($stockStatus === 'low_stock', function (Builder $query): void {
|
->when($stockStatus === 'low_stock', function (Builder $query): void {
|
||||||
$query->whereHas('prices', function (Builder $priceQuery): void {
|
$minStock = [
|
||||||
$priceQuery->where('stock', '>', 0)->where(function (Builder $priceQuery): void {
|
RawMaterialUnit::YARD => 20,
|
||||||
|
RawMaterialUnit::METER => 10,
|
||||||
|
RawMaterialUnit::KILOGRAM => 5,
|
||||||
|
];
|
||||||
|
|
||||||
|
$query->whereHas('prices', function (Builder $priceQuery) use ($minStock): void {
|
||||||
|
$priceQuery->where('stock', '>', 0)->where(function (Builder $priceQuery) use ($minStock): void {
|
||||||
foreach (RawMaterialUnit::cases() as $unit) {
|
foreach (RawMaterialUnit::cases() as $unit) {
|
||||||
$priceQuery->orWhere(function (Builder $priceQuery) use ($unit): void {
|
$priceQuery->orWhere(function (Builder $priceQuery) use ($unit, $minStock): void {
|
||||||
$priceQuery->whereHas('rawMaterial', fn (Builder $rawMaterialQuery) => $rawMaterialQuery->where('unit', $unit))
|
$priceQuery->whereHas('rawMaterial', fn (Builder $rawMaterialQuery) => $rawMaterialQuery->where('unit', $unit))
|
||||||
->where('stock', '<', $unit->minStock());
|
->where('stock', '<', $minStock[$unit]);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@ -22,4 +22,12 @@ public static function selectOptions(): array
|
|||||||
])
|
])
|
||||||
->all();
|
->all();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return list<string>
|
||||||
|
*/
|
||||||
|
public static function values(): array
|
||||||
|
{
|
||||||
|
return array_column(static::cases(), 'value');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -19,7 +19,14 @@ class OrderFactory extends Factory
|
|||||||
public function definition(): array
|
public function definition(): array
|
||||||
{
|
{
|
||||||
$channel = fake()->randomElement(OrderChannel::cases());
|
$channel = fake()->randomElement(OrderChannel::cases());
|
||||||
$priceType = $channel->defaultPriceType() ?? fake()->randomElement(PriceType::cases());
|
|
||||||
|
$defaultPriceType = match ($channel) {
|
||||||
|
OrderChannel::SHOPEE => PriceType::SHOPEE,
|
||||||
|
OrderChannel::TIKTOK => PriceType::TIKTOK,
|
||||||
|
default => null,
|
||||||
|
};
|
||||||
|
|
||||||
|
$priceType = $defaultPriceType ?? fake()->randomElement(PriceType::cases());
|
||||||
$subtotal = fake()->numberBetween(100_000, 5_000_000);
|
$subtotal = fake()->numberBetween(100_000, 5_000_000);
|
||||||
$discount = fake()->numberBetween(0, (int) ($subtotal * 0.15));
|
$discount = fake()->numberBetween(0, (int) ($subtotal * 0.15));
|
||||||
|
|
||||||
|
|||||||
@ -23,11 +23,15 @@ public function run(): void
|
|||||||
|
|
||||||
$registrar->forgetCachedPermissions();
|
$registrar->forgetCachedPermissions();
|
||||||
|
|
||||||
|
$rolePermissions = config('roles.permissions');
|
||||||
|
|
||||||
foreach (RoleEnum::cases() as $role) {
|
foreach (RoleEnum::cases() as $role) {
|
||||||
$roleModel = Role::findOrCreate($role->value, 'web');
|
$roleModel = Role::findOrCreate($role->value, 'web');
|
||||||
|
|
||||||
|
$rolePermissionsList = $rolePermissions[$role->value] ?? [];
|
||||||
|
|
||||||
$roleModel->syncPermissions(
|
$roleModel->syncPermissions(
|
||||||
collect($role->permissions())
|
collect($rolePermissionsList)
|
||||||
->map(fn (PermissionEnum $permission) => $permissions[$permission->value])
|
->map(fn (PermissionEnum $permission) => $permissions[$permission->value])
|
||||||
->all()
|
->all()
|
||||||
);
|
);
|
||||||
|
|||||||
@ -1,11 +1,12 @@
|
|||||||
import type { AppearanceMode } from '@/types/account';
|
import { AppearanceMode } from '@/constants/appearance-mode';
|
||||||
|
import type { AppearanceMode as AppearanceModeType } from '@/types/account';
|
||||||
|
|
||||||
export function applyAppearance(mode: AppearanceMode): void {
|
export function applyAppearance(mode: AppearanceModeType): void {
|
||||||
const root = document.documentElement;
|
const root = document.documentElement;
|
||||||
|
|
||||||
root.classList.remove('dark');
|
root.classList.remove('dark');
|
||||||
|
|
||||||
if (mode === 'dark' || (mode === 'system' && window.matchMedia('(prefers-color-scheme: dark)').matches)) {
|
if (mode === AppearanceMode.DARK || (mode === AppearanceMode.SYSTEM && window.matchMedia('(prefers-color-scheme: dark)').matches)) {
|
||||||
root.classList.add('dark');
|
root.classList.add('dark');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
4
resources/js/constants/active-status.ts
Normal file
4
resources/js/constants/active-status.ts
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
export const ActiveStatus = {
|
||||||
|
ACTIVE: '1',
|
||||||
|
INACTIVE: '0',
|
||||||
|
} as const;
|
||||||
5
resources/js/constants/appearance-mode.ts
Normal file
5
resources/js/constants/appearance-mode.ts
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
export const AppearanceMode = {
|
||||||
|
LIGHT: 'light',
|
||||||
|
DARK: 'dark',
|
||||||
|
SYSTEM: 'system',
|
||||||
|
} as const;
|
||||||
7
resources/js/constants/cash-reference-type.ts
Normal file
7
resources/js/constants/cash-reference-type.ts
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
export const CashReferenceType = {
|
||||||
|
MANUAL: 'manual',
|
||||||
|
EXPENSE: 'App\\Models\\Expense',
|
||||||
|
EMPLOYEE_ADVANCE: 'App\\Models\\EmployeeAdvance',
|
||||||
|
PAYROLL: 'App\\Models\\Payroll',
|
||||||
|
ORDER: 'App\\Models\\Order',
|
||||||
|
} as const;
|
||||||
4
resources/js/constants/cash-transaction-type.ts
Normal file
4
resources/js/constants/cash-transaction-type.ts
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
export const CashTransactionType = {
|
||||||
|
DEPOSIT: 'deposit',
|
||||||
|
WITHDRAWAL: 'withdrawal',
|
||||||
|
} as const;
|
||||||
6
resources/js/constants/catalog-sort-option.ts
Normal file
6
resources/js/constants/catalog-sort-option.ts
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
export const CatalogSortOption = {
|
||||||
|
DEFAULT: 'default',
|
||||||
|
PRICE_ASC: 'price_asc',
|
||||||
|
PRICE_DESC: 'price_desc',
|
||||||
|
NAME_ASC: 'name_asc',
|
||||||
|
} as const;
|
||||||
6
resources/js/constants/cutting-status.ts
Normal file
6
resources/js/constants/cutting-status.ts
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
export const CuttingStatus = {
|
||||||
|
IN_PROGRESS: 'in_progress',
|
||||||
|
COMPLETED: 'completed',
|
||||||
|
VERIFIED: 'verified',
|
||||||
|
REJECTED: 'rejected',
|
||||||
|
} as const;
|
||||||
6
resources/js/constants/employee-advance-status.ts
Normal file
6
resources/js/constants/employee-advance-status.ts
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
export const EmployeeAdvanceStatus = {
|
||||||
|
PENDING: 'pending',
|
||||||
|
APPROVED: 'approved',
|
||||||
|
REJECTED: 'rejected',
|
||||||
|
PAID: 'paid',
|
||||||
|
} as const;
|
||||||
19
resources/js/constants/index.ts
Normal file
19
resources/js/constants/index.ts
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
export * from './order-channel';
|
||||||
|
export * from './order-status';
|
||||||
|
export * from './cutting-status';
|
||||||
|
export * from './employee-advance-status';
|
||||||
|
export * from './leave-request-status';
|
||||||
|
export * from './payroll-status';
|
||||||
|
export * from './cash-transaction-type';
|
||||||
|
export * from './cash-reference-type';
|
||||||
|
export * from './order-payment-type';
|
||||||
|
export * from './stock-quality';
|
||||||
|
export * from './stock-status';
|
||||||
|
export * from './active-status';
|
||||||
|
export * from './marketplace-platform';
|
||||||
|
export * from './marketplace-fee-scope';
|
||||||
|
export * from './marketplace-fee-value-type';
|
||||||
|
export * from './setting-section';
|
||||||
|
export * from './appearance-mode';
|
||||||
|
export * from './catalog-sort-option';
|
||||||
|
export * from './paper-size';
|
||||||
5
resources/js/constants/leave-request-status.ts
Normal file
5
resources/js/constants/leave-request-status.ts
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
export const LeaveRequestStatus = {
|
||||||
|
PENDING: 'pending',
|
||||||
|
APPROVED: 'approved',
|
||||||
|
REJECTED: 'rejected',
|
||||||
|
} as const;
|
||||||
4
resources/js/constants/marketplace-fee-scope.ts
Normal file
4
resources/js/constants/marketplace-fee-scope.ts
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
export const MarketplaceFeeScope = {
|
||||||
|
PRODUCT: 'product',
|
||||||
|
TRANSACTION: 'transaction',
|
||||||
|
} as const;
|
||||||
4
resources/js/constants/marketplace-fee-value-type.ts
Normal file
4
resources/js/constants/marketplace-fee-value-type.ts
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
export const MarketplaceFeeValueType = {
|
||||||
|
FLAT: 'flat',
|
||||||
|
PERCENT: 'percent',
|
||||||
|
} as const;
|
||||||
4
resources/js/constants/marketplace-platform.ts
Normal file
4
resources/js/constants/marketplace-platform.ts
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
export const MarketplacePlatform = {
|
||||||
|
TIKTOK_SHOP: 'tiktok_shop',
|
||||||
|
SHOPEE: 'shopee',
|
||||||
|
} as const;
|
||||||
4
resources/js/constants/order-payment-type.ts
Normal file
4
resources/js/constants/order-payment-type.ts
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
export const OrderPaymentType = {
|
||||||
|
CASH: 'cash',
|
||||||
|
MARKETPLACE: 'marketplace',
|
||||||
|
} as const;
|
||||||
5
resources/js/constants/order-status.ts
Normal file
5
resources/js/constants/order-status.ts
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
export const OrderStatus = {
|
||||||
|
PROCESSING: 'processing',
|
||||||
|
COMPLETED: 'completed',
|
||||||
|
CANCELLED: 'cancelled',
|
||||||
|
} as const;
|
||||||
4
resources/js/constants/paper-size.ts
Normal file
4
resources/js/constants/paper-size.ts
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
export const PaperSize = {
|
||||||
|
MM_58: '58mm',
|
||||||
|
MM_80: '80mm',
|
||||||
|
} as const;
|
||||||
4
resources/js/constants/payroll-status.ts
Normal file
4
resources/js/constants/payroll-status.ts
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
export const PayrollStatus = {
|
||||||
|
UNPAID: 'unpaid',
|
||||||
|
PAID: 'paid',
|
||||||
|
} as const;
|
||||||
7
resources/js/constants/setting-section.ts
Normal file
7
resources/js/constants/setting-section.ts
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
export const SettingSection = {
|
||||||
|
SYSTEM: 'system',
|
||||||
|
SOCIAL: 'social',
|
||||||
|
MARKETPLACE: 'marketplace',
|
||||||
|
HR: 'hr',
|
||||||
|
HOMEPAGE: 'homepage',
|
||||||
|
} as const;
|
||||||
4
resources/js/constants/stock-quality.ts
Normal file
4
resources/js/constants/stock-quality.ts
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
export const StockQuality = {
|
||||||
|
GOOD: 'good',
|
||||||
|
REJECT: 'reject',
|
||||||
|
} as const;
|
||||||
4
resources/js/constants/stock-status.ts
Normal file
4
resources/js/constants/stock-status.ts
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
export const StockStatus = {
|
||||||
|
OUT_OF_STOCK: 'out_of_stock',
|
||||||
|
LOW_STOCK: 'low_stock',
|
||||||
|
} as const;
|
||||||
@ -1,5 +1,6 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { Home, Settings2, Share2, ShoppingBag, Users } from '@lucide/vue';
|
import { Home, Settings2, Share2, ShoppingBag, Users } from '@lucide/vue';
|
||||||
|
import { SettingSection as SettingSectionConst } from '@/constants/setting-section';
|
||||||
import AdminLayout from '@/layouts/AdminLayout.vue';
|
import AdminLayout from '@/layouts/AdminLayout.vue';
|
||||||
import { cn } from '@/lib/utils';
|
import { cn } from '@/lib/utils';
|
||||||
import type { SettingSection } from '@/types/setting';
|
import type { SettingSection } from '@/types/setting';
|
||||||
@ -11,11 +12,11 @@ const navItems: Array<{
|
|||||||
label: string;
|
label: string;
|
||||||
icon: typeof Settings2;
|
icon: typeof Settings2;
|
||||||
}> = [
|
}> = [
|
||||||
{ key: 'system', label: 'Sistem', icon: Settings2 },
|
{ key: SettingSectionConst.SYSTEM, label: 'Sistem', icon: Settings2 },
|
||||||
{ key: 'homepage', label: 'Homepage', icon: Home },
|
{ key: SettingSectionConst.HOMEPAGE, label: 'Homepage', icon: Home },
|
||||||
{ key: 'social', label: 'Media Sosial', icon: Share2 },
|
{ key: SettingSectionConst.SOCIAL, label: 'Media Sosial', icon: Share2 },
|
||||||
{ key: 'marketplace', label: 'Marketplace', icon: ShoppingBag },
|
{ key: SettingSectionConst.MARKETPLACE, label: 'Marketplace', icon: ShoppingBag },
|
||||||
{ key: 'hr', label: 'HR / Pegawai', icon: Users },
|
{ key: SettingSectionConst.HR, label: 'HR / Pegawai', icon: Users },
|
||||||
];
|
];
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|||||||
@ -18,6 +18,7 @@ import {
|
|||||||
Filter
|
Filter
|
||||||
} from '@lucide/vue';
|
} from '@lucide/vue';
|
||||||
import { ref, computed } from 'vue';
|
import { ref, computed } from 'vue';
|
||||||
|
import { CatalogSortOption } from '@/constants/catalog-sort-option';
|
||||||
import RupiahInput from '@/components/form/rupiah-input/RupiahInput.vue';
|
import RupiahInput from '@/components/form/rupiah-input/RupiahInput.vue';
|
||||||
import Select from '@/components/ui/select/Select.vue';
|
import Select from '@/components/ui/select/Select.vue';
|
||||||
import SelectContent from '@/components/ui/select/SelectContent.vue';
|
import SelectContent from '@/components/ui/select/SelectContent.vue';
|
||||||
@ -97,7 +98,7 @@ const searchInput = ref('');
|
|||||||
const selectedCategory = ref<string | null>(null);
|
const selectedCategory = ref<string | null>(null);
|
||||||
const minPrice = ref<number | null>(null);
|
const minPrice = ref<number | null>(null);
|
||||||
const maxPrice = ref<number | null>(null);
|
const maxPrice = ref<number | null>(null);
|
||||||
const sortBy = ref<string>('default'); // 'default', 'price_asc', 'price_desc', 'name_asc'
|
const sortBy = ref<string>(CatalogSortOption.DEFAULT);
|
||||||
const showFilters = ref(false);
|
const showFilters = ref(false);
|
||||||
|
|
||||||
// Hardcoded order steps
|
// Hardcoded order steps
|
||||||
@ -155,11 +156,11 @@ const filteredProducts = computed(() => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Sorting
|
// Sorting
|
||||||
if (sortBy.value === 'price_asc') {
|
if (sortBy.value === CatalogSortOption.PRICE_ASC) {
|
||||||
result.sort((a, b) => getPrimaryEcerPrice(a) - getPrimaryEcerPrice(b));
|
result.sort((a, b) => getPrimaryEcerPrice(a) - getPrimaryEcerPrice(b));
|
||||||
} else if (sortBy.value === 'price_desc') {
|
} else if (sortBy.value === CatalogSortOption.PRICE_DESC) {
|
||||||
result.sort((a, b) => getPrimaryEcerPrice(b) - getPrimaryEcerPrice(a));
|
result.sort((a, b) => getPrimaryEcerPrice(b) - getPrimaryEcerPrice(a));
|
||||||
} else if (sortBy.value === 'name_asc') {
|
} else if (sortBy.value === CatalogSortOption.NAME_ASC) {
|
||||||
result.sort((a, b) => a.name.localeCompare(b.name));
|
result.sort((a, b) => a.name.localeCompare(b.name));
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -359,7 +360,7 @@ return;
|
|||||||
const resetPriceFilters = () => {
|
const resetPriceFilters = () => {
|
||||||
minPrice.value = null;
|
minPrice.value = null;
|
||||||
maxPrice.value = null;
|
maxPrice.value = null;
|
||||||
sortBy.value = 'default';
|
sortBy.value = CatalogSortOption.DEFAULT;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
@ -645,10 +646,10 @@ const resetPriceFilters = () => {
|
|||||||
<SelectValue placeholder="Pilih urutan" />
|
<SelectValue placeholder="Pilih urutan" />
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
<SelectItem value="default">Default (Urutan Utama)</SelectItem>
|
<SelectItem :value="CatalogSortOption.DEFAULT">Default (Urutan Utama)</SelectItem>
|
||||||
<SelectItem value="price_asc">Harga Terendah</SelectItem>
|
<SelectItem :value="CatalogSortOption.PRICE_ASC">Harga Terendah</SelectItem>
|
||||||
<SelectItem value="price_desc">Harga Tertinggi</SelectItem>
|
<SelectItem :value="CatalogSortOption.PRICE_DESC">Harga Tertinggi</SelectItem>
|
||||||
<SelectItem value="name_asc">Nama Produk (A-Z)</SelectItem>
|
<SelectItem :value="CatalogSortOption.NAME_ASC">Nama Produk (A-Z)</SelectItem>
|
||||||
</SelectContent>
|
</SelectContent>
|
||||||
</Select>
|
</Select>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -13,6 +13,7 @@ import {
|
|||||||
import { Label } from '@/components/ui/label';
|
import { Label } from '@/components/ui/label';
|
||||||
import { RadioGroup, RadioGroupItem } from '@/components/ui/radio-group';
|
import { RadioGroup, RadioGroupItem } from '@/components/ui/radio-group';
|
||||||
import { applyAppearance } from '@/composables/useAppearance';
|
import { applyAppearance } from '@/composables/useAppearance';
|
||||||
|
import { AppearanceMode as AppearanceModeConst } from '@/constants/appearance-mode';
|
||||||
import AccountLayout from '@/layouts/AccountLayout.vue';
|
import AccountLayout from '@/layouts/AccountLayout.vue';
|
||||||
import { formErrors } from '@/lib/form';
|
import { formErrors } from '@/lib/form';
|
||||||
import { cn } from '@/lib/utils';
|
import { cn } from '@/lib/utils';
|
||||||
@ -33,19 +34,19 @@ const options: Array<{
|
|||||||
icon: typeof Sun;
|
icon: typeof Sun;
|
||||||
}> = [
|
}> = [
|
||||||
{
|
{
|
||||||
value: 'light',
|
value: AppearanceModeConst.LIGHT,
|
||||||
label: 'Terang',
|
label: 'Terang',
|
||||||
description: 'Tampilan terang untuk lingkungan yang cukup pencahayaan.',
|
description: 'Tampilan terang untuk lingkungan yang cukup pencahayaan.',
|
||||||
icon: Sun,
|
icon: Sun,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
value: 'dark',
|
value: AppearanceModeConst.DARK,
|
||||||
label: 'Gelap',
|
label: 'Gelap',
|
||||||
description: 'Tampilan gelap yang nyaman di malam hari.',
|
description: 'Tampilan gelap yang nyaman di malam hari.',
|
||||||
icon: Moon,
|
icon: Moon,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
value: 'system',
|
value: AppearanceModeConst.SYSTEM,
|
||||||
label: 'Sistem',
|
label: 'Sistem',
|
||||||
description: 'Mengikuti pengaturan tampilan perangkat Anda.',
|
description: 'Mengikuti pengaturan tampilan perangkat Anda.',
|
||||||
icon: Monitor,
|
icon: Monitor,
|
||||||
|
|||||||
@ -2,16 +2,18 @@
|
|||||||
import { Head } from '@inertiajs/vue3';
|
import { Head } from '@inertiajs/vue3';
|
||||||
import { ArrowDownCircle, ArrowUpCircle, Wallet } from '@lucide/vue';
|
import { ArrowDownCircle, ArrowUpCircle, Wallet } from '@lucide/vue';
|
||||||
import { computed, ref, watch } from 'vue';
|
import { computed, ref, watch } from 'vue';
|
||||||
import CashTransactionFormModal from './form/CashTransactionFormModal.vue';
|
|
||||||
import { createColumns } from './table/columns';
|
|
||||||
import { DataTable } from '@/components/data-table';
|
import { DataTable } from '@/components/data-table';
|
||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
||||||
import { useCan } from '@/composables/useCan';
|
import { useCan } from '@/composables/useCan';
|
||||||
|
import { CashReferenceType } from '@/constants/cash-reference-type';
|
||||||
|
import { CashTransactionType } from '@/constants/cash-transaction-type';
|
||||||
import { useDataTableQuery, useDataTableQuerySync } from '@/composables/useDataTableQuery';
|
import { useDataTableQuery, useDataTableQuerySync } from '@/composables/useDataTableQuery';
|
||||||
import AdminLayout from '@/layouts/AdminLayout.vue';
|
import AdminLayout from '@/layouts/AdminLayout.vue';
|
||||||
import type { CashAccount, CashTransactionListItem, PaginatedCashTransactions } from '@/types/cash';
|
import type { CashAccount, CashTransactionListItem, PaginatedCashTransactions } from '@/types/cash';
|
||||||
import type { DataTableFilterDef, DataTableSort } from '@/types/data-table';
|
import type { DataTableFilterDef, DataTableSort } from '@/types/data-table';
|
||||||
|
import CashTransactionFormModal from './form/CashTransactionFormModal.vue';
|
||||||
|
import { createColumns } from './table/columns';
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
cashAccount: CashAccount;
|
cashAccount: CashAccount;
|
||||||
@ -27,7 +29,7 @@ const props = defineProps<{
|
|||||||
const { can } = useCan();
|
const { can } = useCan();
|
||||||
const search = ref(props.filters.search ?? '');
|
const search = ref(props.filters.search ?? '');
|
||||||
const formModalOpen = ref(false);
|
const formModalOpen = ref(false);
|
||||||
const formModalMode = ref<'deposit' | 'withdrawal'>('deposit');
|
const formModalMode = ref<'deposit' | 'withdrawal'>(CashTransactionType.DEPOSIT);
|
||||||
const editingTransaction = ref<CashTransactionListItem | null>(null);
|
const editingTransaction = ref<CashTransactionListItem | null>(null);
|
||||||
|
|
||||||
const { query, setSearch, setSort, setFilter, resetFilters, syncFromServer } = useDataTableQuery({
|
const { query, setSearch, setSort, setFilter, resetFilters, syncFromServer } = useDataTableQuery({
|
||||||
@ -57,11 +59,11 @@ const filterDefs = computed<DataTableFilterDef[]>(() => [
|
|||||||
label: 'Sumber',
|
label: 'Sumber',
|
||||||
type: 'select',
|
type: 'select',
|
||||||
options: [
|
options: [
|
||||||
{ value: 'manual', label: 'Setor/Tarik Kas' },
|
{ value: CashReferenceType.MANUAL, label: 'Setor/Tarik Kas' },
|
||||||
{ value: 'App\\Models\\Expense', label: 'Pengeluaran' },
|
{ value: CashReferenceType.EXPENSE, label: 'Pengeluaran' },
|
||||||
{ value: 'App\\Models\\EmployeeAdvance', label: 'Kasbon Pegawai' },
|
{ value: CashReferenceType.EMPLOYEE_ADVANCE, label: 'Kasbon Pegawai' },
|
||||||
{ value: 'App\\Models\\Payroll', label: 'Gaji Pegawai' },
|
{ value: CashReferenceType.PAYROLL, label: 'Gaji Pegawai' },
|
||||||
{ value: 'App\\Models\\Order', label: 'Pesanan' },
|
{ value: CashReferenceType.ORDER, label: 'Pesanan' },
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
]);
|
]);
|
||||||
@ -77,7 +79,7 @@ const pagination = computed(() => ({
|
|||||||
total: props.transactions.total,
|
total: props.transactions.total,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
function openCreateModal(mode: 'deposit' | 'withdrawal') {
|
function openCreateModal(mode: typeof CashTransactionType.DEPOSIT | typeof CashTransactionType.WITHDRAWAL) {
|
||||||
editingTransaction.value = null;
|
editingTransaction.value = null;
|
||||||
formModalMode.value = mode;
|
formModalMode.value = mode;
|
||||||
formModalOpen.value = true;
|
formModalOpen.value = true;
|
||||||
@ -113,11 +115,11 @@ watch(
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex shrink-0 items-center gap-2 self-start sm:self-center">
|
<div class="flex shrink-0 items-center gap-2 self-start sm:self-center">
|
||||||
<Button v-if="can('cash.deposit')" variant="outline" @click="openCreateModal('deposit')">
|
<Button v-if="can('cash.deposit')" variant="outline" @click="openCreateModal(CashTransactionType.DEPOSIT)">
|
||||||
<ArrowDownCircle class="size-4" />
|
<ArrowDownCircle class="size-4" />
|
||||||
Setor Kas
|
Setor Kas
|
||||||
</Button>
|
</Button>
|
||||||
<Button v-if="can('cash.withdraw')" variant="outline" @click="openCreateModal('withdrawal')">
|
<Button v-if="can('cash.withdraw')" variant="outline" @click="openCreateModal(CashTransactionType.WITHDRAWAL)">
|
||||||
<ArrowUpCircle class="size-4" />
|
<ArrowUpCircle class="size-4" />
|
||||||
Tarik Kas
|
Tarik Kas
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
@ -22,6 +22,7 @@ import {
|
|||||||
} from '@/components/ui/field';
|
} from '@/components/ui/field';
|
||||||
import { Textarea } from '@/components/ui/textarea';
|
import { Textarea } from '@/components/ui/textarea';
|
||||||
import { useFormDialog } from '@/composables/useFormDialog';
|
import { useFormDialog } from '@/composables/useFormDialog';
|
||||||
|
import { CashTransactionType } from '@/constants/cash-transaction-type';
|
||||||
import { FIELD_LIMITS } from '@/lib/field-limits';
|
import { FIELD_LIMITS } from '@/lib/field-limits';
|
||||||
import { formErrors } from '@/lib/form';
|
import { formErrors } from '@/lib/form';
|
||||||
import { parseRupiah } from '@/lib/rupiah';
|
import { parseRupiah } from '@/lib/rupiah';
|
||||||
@ -41,9 +42,9 @@ const currentMode = computed(() => {
|
|||||||
return props.transaction.type;
|
return props.transaction.type;
|
||||||
}
|
}
|
||||||
|
|
||||||
return props.mode ?? 'deposit';
|
return props.mode ?? CashTransactionType.DEPOSIT;
|
||||||
});
|
});
|
||||||
const isWithdrawal = computed(() => currentMode.value === 'withdrawal');
|
const isWithdrawal = computed(() => currentMode.value === CashTransactionType.WITHDRAWAL);
|
||||||
|
|
||||||
const existingPhotoId = ref<number | null>(null);
|
const existingPhotoId = ref<number | null>(null);
|
||||||
|
|
||||||
|
|||||||
@ -9,6 +9,7 @@ import { DataTable } from '@/components/data-table';
|
|||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
||||||
import { useCan } from '@/composables/useCan';
|
import { useCan } from '@/composables/useCan';
|
||||||
|
import { EmployeeAdvanceStatus } from '@/constants/employee-advance-status';
|
||||||
import { useDataTableQuery, useDataTableQuerySync } from '@/composables/useDataTableQuery';
|
import { useDataTableQuery, useDataTableQuerySync } from '@/composables/useDataTableQuery';
|
||||||
import AdminLayout from '@/layouts/AdminLayout.vue';
|
import AdminLayout from '@/layouts/AdminLayout.vue';
|
||||||
import type { DataTableFilterDef, DataTableSort } from '@/types/data-table';
|
import type { DataTableFilterDef, DataTableSort } from '@/types/data-table';
|
||||||
@ -54,10 +55,10 @@ const filterDefs = computed<DataTableFilterDef[]>(() => [
|
|||||||
label: 'Status',
|
label: 'Status',
|
||||||
type: 'select',
|
type: 'select',
|
||||||
options: [
|
options: [
|
||||||
{ value: 'pending', label: 'Menunggu' },
|
{ value: EmployeeAdvanceStatus.PENDING, label: 'Menunggu' },
|
||||||
{ value: 'approved', label: 'Disetujui' },
|
{ value: EmployeeAdvanceStatus.APPROVED, label: 'Disetujui' },
|
||||||
{ value: 'rejected', label: 'Ditolak' },
|
{ value: EmployeeAdvanceStatus.REJECTED, label: 'Ditolak' },
|
||||||
{ value: 'paid', label: 'Lunas' },
|
{ value: EmployeeAdvanceStatus.PAID, label: 'Lunas' },
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
]);
|
]);
|
||||||
|
|||||||
@ -2,16 +2,17 @@ import type { ColumnDef } from '@tanstack/vue-table';
|
|||||||
import { h } from 'vue';
|
import { h } from 'vue';
|
||||||
import { DataTableColumnHeader } from '@/components/data-table';
|
import { DataTableColumnHeader } from '@/components/data-table';
|
||||||
import { Badge } from '@/components/ui/badge';
|
import { Badge } from '@/components/ui/badge';
|
||||||
|
import { EmployeeAdvanceStatus } from '@/constants/employee-advance-status';
|
||||||
import type { EmployeeAdvanceListItem } from '@/types/employee-advance';
|
import type { EmployeeAdvanceListItem } from '@/types/employee-advance';
|
||||||
import DataTableActions from './data-table-actions.vue';
|
import DataTableActions from './data-table-actions.vue';
|
||||||
|
|
||||||
function statusVariant(status: string): 'default' | 'secondary' | 'destructive' | 'outline' {
|
function statusVariant(status: string): 'default' | 'secondary' | 'destructive' | 'outline' {
|
||||||
switch (status) {
|
switch (status) {
|
||||||
case 'approved':
|
case EmployeeAdvanceStatus.APPROVED:
|
||||||
return 'default';
|
return 'default';
|
||||||
case 'paid':
|
case EmployeeAdvanceStatus.PAID:
|
||||||
return 'secondary';
|
return 'secondary';
|
||||||
case 'rejected':
|
case EmployeeAdvanceStatus.REJECTED:
|
||||||
return 'destructive';
|
return 'destructive';
|
||||||
default:
|
default:
|
||||||
return 'outline';
|
return 'outline';
|
||||||
|
|||||||
@ -2,11 +2,12 @@ import type { ColumnDef } from '@tanstack/vue-table';
|
|||||||
import { h } from 'vue';
|
import { h } from 'vue';
|
||||||
import { DataTableColumnHeader } from '@/components/data-table';
|
import { DataTableColumnHeader } from '@/components/data-table';
|
||||||
import { Badge } from '@/components/ui/badge';
|
import { Badge } from '@/components/ui/badge';
|
||||||
|
import { PayrollStatus } from '@/constants/payroll-status';
|
||||||
import type { PayrollListItem } from '@/types/payroll';
|
import type { PayrollListItem } from '@/types/payroll';
|
||||||
import DataTableActions from './data-table-actions.vue';
|
import DataTableActions from './data-table-actions.vue';
|
||||||
|
|
||||||
function statusVariant(status: string): 'default' | 'secondary' | 'destructive' | 'outline' {
|
function statusVariant(status: string): 'default' | 'secondary' | 'destructive' | 'outline' {
|
||||||
return status === 'paid' ? 'secondary' : 'outline';
|
return status === PayrollStatus.PAID ? 'secondary' : 'outline';
|
||||||
}
|
}
|
||||||
|
|
||||||
export function createColumns(
|
export function createColumns(
|
||||||
|
|||||||
@ -7,6 +7,7 @@ import { DataTable } from '@/components/data-table';
|
|||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
import { Card, CardContent } from '@/components/ui/card';
|
import { Card, CardContent } from '@/components/ui/card';
|
||||||
import { useCan } from '@/composables/useCan';
|
import { useCan } from '@/composables/useCan';
|
||||||
|
import { ActiveStatus } from '@/constants/active-status';
|
||||||
import {
|
import {
|
||||||
useDataTableQuery,
|
useDataTableQuery,
|
||||||
useDataTableQuerySync,
|
useDataTableQuerySync,
|
||||||
@ -66,8 +67,8 @@ const filterDefs = computed<DataTableFilterDef[]>(() => [
|
|||||||
label: 'Status Akun',
|
label: 'Status Akun',
|
||||||
type: 'select',
|
type: 'select',
|
||||||
options: [
|
options: [
|
||||||
{ value: '1', label: 'Aktif' },
|
{ value: ActiveStatus.ACTIVE, label: 'Aktif' },
|
||||||
{ value: '0', label: 'Nonaktif' },
|
{ value: ActiveStatus.INACTIVE, label: 'Nonaktif' },
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
]);
|
]);
|
||||||
|
|||||||
@ -2,14 +2,15 @@ import type { ColumnDef } from '@tanstack/vue-table';
|
|||||||
import { h } from 'vue';
|
import { h } from 'vue';
|
||||||
import { DataTableColumnHeader } from '@/components/data-table';
|
import { DataTableColumnHeader } from '@/components/data-table';
|
||||||
import { Badge } from '@/components/ui/badge';
|
import { Badge } from '@/components/ui/badge';
|
||||||
|
import { LeaveRequestStatus } from '@/constants/leave-request-status';
|
||||||
import type { LeaveRequestListItem } from '@/types/leave-request';
|
import type { LeaveRequestListItem } from '@/types/leave-request';
|
||||||
import DataTableActions from './data-table-actions.vue';
|
import DataTableActions from './data-table-actions.vue';
|
||||||
|
|
||||||
function statusVariant(status: string): 'default' | 'secondary' | 'destructive' | 'outline' {
|
function statusVariant(status: string): 'default' | 'secondary' | 'destructive' | 'outline' {
|
||||||
switch (status) {
|
switch (status) {
|
||||||
case 'approved':
|
case LeaveRequestStatus.APPROVED:
|
||||||
return 'default';
|
return 'default';
|
||||||
case 'rejected':
|
case LeaveRequestStatus.REJECTED:
|
||||||
return 'destructive';
|
return 'destructive';
|
||||||
default:
|
default:
|
||||||
return 'outline';
|
return 'outline';
|
||||||
|
|||||||
@ -2,6 +2,7 @@
|
|||||||
import { Link } from '@inertiajs/vue3';
|
import { Link } from '@inertiajs/vue3';
|
||||||
import { computed } from 'vue';
|
import { computed } from 'vue';
|
||||||
import DataTableActions from './data-table-actions.vue';
|
import DataTableActions from './data-table-actions.vue';
|
||||||
|
import { CuttingStatus } from '@/constants/cutting-status';
|
||||||
import DataTableToolbar from '@/components/data-table/DataTableToolbar.vue';
|
import DataTableToolbar from '@/components/data-table/DataTableToolbar.vue';
|
||||||
import { Badge } from '@/components/ui/badge';
|
import { Badge } from '@/components/ui/badge';
|
||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
@ -59,15 +60,15 @@ function rowNumber(index: number): number {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function statusVariant(status: string): 'default' | 'secondary' | 'destructive' | 'outline' {
|
function statusVariant(status: string): 'default' | 'secondary' | 'destructive' | 'outline' {
|
||||||
if (status === 'verified') {
|
if (status === CuttingStatus.VERIFIED) {
|
||||||
return 'default';
|
return 'default';
|
||||||
}
|
}
|
||||||
|
|
||||||
if (status === 'rejected') {
|
if (status === CuttingStatus.REJECTED) {
|
||||||
return 'destructive';
|
return 'destructive';
|
||||||
}
|
}
|
||||||
|
|
||||||
if (status === 'completed') {
|
if (status === CuttingStatus.COMPLETED) {
|
||||||
return 'secondary';
|
return 'secondary';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -30,6 +30,7 @@ import {
|
|||||||
TooltipTrigger,
|
TooltipTrigger,
|
||||||
} from '@/components/ui/tooltip';
|
} from '@/components/ui/tooltip';
|
||||||
import { useCan } from '@/composables/useCan';
|
import { useCan } from '@/composables/useCan';
|
||||||
|
import { CuttingStatus } from '@/constants/cutting-status';
|
||||||
import { RupiahInput } from '@/components/form/rupiah-input';
|
import { RupiahInput } from '@/components/form/rupiah-input';
|
||||||
import { parseRupiah } from '@/lib/rupiah';
|
import { parseRupiah } from '@/lib/rupiah';
|
||||||
import {
|
import {
|
||||||
@ -54,7 +55,7 @@ const allMatches = ref(true);
|
|||||||
const pendingAction = ref<CuttingStatusAction | null>(null);
|
const pendingAction = ref<CuttingStatusAction | null>(null);
|
||||||
|
|
||||||
const rejectForm = useForm({
|
const rejectForm = useForm({
|
||||||
status: 'rejected',
|
status: CuttingStatus.REJECTED,
|
||||||
reason: '',
|
reason: '',
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -63,7 +64,7 @@ function buildEmptyPrices(): Record<string, string> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const verifyForm = useForm({
|
const verifyForm = useForm({
|
||||||
status: 'verified',
|
status: CuttingStatus.VERIFIED,
|
||||||
verification_note: '',
|
verification_note: '',
|
||||||
results: props.cutting.results.map(res => ({
|
results: props.cutting.results.map(res => ({
|
||||||
product_variant_id: res.product_variant?.id || 0,
|
product_variant_id: res.product_variant?.id || 0,
|
||||||
@ -151,7 +152,7 @@ const canEdit = computed(
|
|||||||
() => props.cutting.is_editable && can('cuttings.update'),
|
() => props.cutting.is_editable && can('cuttings.update'),
|
||||||
);
|
);
|
||||||
const canDelete = computed(
|
const canDelete = computed(
|
||||||
() => can('cuttings.delete') && ['in_progress', 'rejected'].includes(props.cutting.status),
|
() => can('cuttings.delete') && [CuttingStatus.IN_PROGRESS, CuttingStatus.REJECTED].includes(props.cutting.status),
|
||||||
);
|
);
|
||||||
|
|
||||||
function canPerformAction(action: CuttingStatusAction): boolean {
|
function canPerformAction(action: CuttingStatusAction): boolean {
|
||||||
@ -159,15 +160,15 @@ function canPerformAction(action: CuttingStatusAction): boolean {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function statusConfirmDescription(action: CuttingStatusAction): string {
|
function statusConfirmDescription(action: CuttingStatusAction): string {
|
||||||
if (action.status === 'completed') {
|
if (action.status === CuttingStatus.COMPLETED) {
|
||||||
return 'Cutting akan ditandai selesai. Stok bahan baku akan dipotong dan sisa dikembalikan. Menunggu verifikasi admin toko.';
|
return 'Cutting akan ditandai selesai. Stok bahan baku akan dipotong dan sisa dikembalikan. Menunggu verifikasi admin toko.';
|
||||||
}
|
}
|
||||||
|
|
||||||
if (action.status === 'verified') {
|
if (action.status === CuttingStatus.VERIFIED) {
|
||||||
return 'Hasil cutting akan diverifikasi. Stok bagus dan reject akan ditambahkan ke produk.';
|
return 'Hasil cutting akan diverifikasi. Stok bagus dan reject akan ditambahkan ke produk.';
|
||||||
}
|
}
|
||||||
|
|
||||||
if (action.status === 'in_progress') {
|
if (action.status === CuttingStatus.IN_PROGRESS) {
|
||||||
return 'Cutting dikembalikan ke proses untuk diperbaiki.';
|
return 'Cutting dikembalikan ke proses untuk diperbaiki.';
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -175,13 +176,13 @@ function statusConfirmDescription(action: CuttingStatusAction): string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function openStatusConfirm(action: CuttingStatusAction) {
|
function openStatusConfirm(action: CuttingStatusAction) {
|
||||||
if (action.status === 'verified') {
|
if (action.status === CuttingStatus.VERIFIED) {
|
||||||
verifyDialogOpen.value = true;
|
verifyDialogOpen.value = true;
|
||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (action.status === 'rejected') {
|
if (action.status === CuttingStatus.REJECTED) {
|
||||||
rejectForm.reset();
|
rejectForm.reset();
|
||||||
rejectForm.clearErrors();
|
rejectForm.clearErrors();
|
||||||
rejectDialogOpen.value = true;
|
rejectDialogOpen.value = true;
|
||||||
@ -299,15 +300,15 @@ function destroyCutting() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function actionIcon(status: string) {
|
function actionIcon(status: string) {
|
||||||
if (status === 'completed') {
|
if (status === CuttingStatus.COMPLETED) {
|
||||||
return Scissors;
|
return Scissors;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (status === 'verified') {
|
if (status === CuttingStatus.VERIFIED) {
|
||||||
return Check;
|
return Check;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (status === 'in_progress') {
|
if (status === CuttingStatus.IN_PROGRESS) {
|
||||||
return RotateCcw;
|
return RotateCcw;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -32,6 +32,10 @@ import {
|
|||||||
TableRow,
|
TableRow,
|
||||||
} from '@/components/ui/table';
|
} from '@/components/ui/table';
|
||||||
import { useCan } from '@/composables/useCan';
|
import { useCan } from '@/composables/useCan';
|
||||||
|
import { MarketplaceFeeScope } from '@/constants/marketplace-fee-scope';
|
||||||
|
import { MarketplaceFeeValueType } from '@/constants/marketplace-fee-value-type';
|
||||||
|
import { OrderStatus } from '@/constants/order-status';
|
||||||
|
import { StockQuality } from '@/constants/stock-quality';
|
||||||
import AdminLayout from '@/layouts/AdminLayout.vue';
|
import AdminLayout from '@/layouts/AdminLayout.vue';
|
||||||
import type { OrderDetail, OrderStatusAction } from '@/types/order';
|
import type { OrderDetail, OrderStatusAction } from '@/types/order';
|
||||||
|
|
||||||
@ -59,15 +63,15 @@ const marketingName = computed(() => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
function statusVariant(status: string): 'default' | 'secondary' | 'destructive' | 'outline' {
|
function statusVariant(status: string): 'default' | 'secondary' | 'destructive' | 'outline' {
|
||||||
if (status === 'completed') return 'default';
|
if (status === OrderStatus.COMPLETED) return 'default';
|
||||||
if (status === 'cancelled') return 'destructive';
|
if (status === OrderStatus.CANCELLED) return 'destructive';
|
||||||
if (status === 'processing') return 'secondary';
|
if (status === OrderStatus.PROCESSING) return 'secondary';
|
||||||
return 'outline';
|
return 'outline';
|
||||||
}
|
}
|
||||||
|
|
||||||
function actionIcon(status: string) {
|
function actionIcon(status: string) {
|
||||||
if (status === 'processing') return Send;
|
if (status === OrderStatus.PROCESSING) return Send;
|
||||||
if (status === 'completed') return Check;
|
if (status === OrderStatus.COMPLETED) return Check;
|
||||||
return X;
|
return X;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -76,10 +80,10 @@ function canPerformAction(action: OrderStatusAction): boolean {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function statusConfirmDescription(action: OrderStatusAction): string {
|
function statusConfirmDescription(action: OrderStatusAction): string {
|
||||||
if (action.status === 'processing') {
|
if (action.status === OrderStatus.PROCESSING) {
|
||||||
return `Pesanan ${props.order.order_number} akan dikirim dan diproses.`;
|
return `Pesanan ${props.order.order_number} akan dikirim dan diproses.`;
|
||||||
}
|
}
|
||||||
if (action.status === 'completed') {
|
if (action.status === OrderStatus.COMPLETED) {
|
||||||
return `Pesanan ${props.order.order_number} akan ditandai selesai.`;
|
return `Pesanan ${props.order.order_number} akan ditandai selesai.`;
|
||||||
}
|
}
|
||||||
return `Pesanan ${props.order.order_number} akan dibatalkan. Stok produk akan dikembalikan.`;
|
return `Pesanan ${props.order.order_number} akan dibatalkan. Stok produk akan dikembalikan.`;
|
||||||
@ -182,9 +186,9 @@ function formatRp(amount: number): string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function formatFeeRule(fee: { scope: string; value_type: string; value: number }): string {
|
function formatFeeRule(fee: { scope: string; value_type: string; value: number }): string {
|
||||||
const scope = fee.scope === 'product' ? '/produk' : '/transaksi';
|
const scope = fee.scope === MarketplaceFeeScope.PRODUCT ? '/produk' : '/transaksi';
|
||||||
|
|
||||||
if (fee.value_type === 'percent') {
|
if (fee.value_type === MarketplaceFeeValueType.PERCENT) {
|
||||||
return `${fee.value}%${scope}`;
|
return `${fee.value}%${scope}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -374,7 +378,7 @@ const feeEntries = computed(() => {
|
|||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell>
|
<TableCell>
|
||||||
<Badge variant="outline" class="text-xs">
|
<Badge variant="outline" class="text-xs">
|
||||||
{{ item.stock_quality === 'reject' ? 'Reject' : 'Bagus' }}
|
{{ item.stock_quality === StockQuality.REJECT ? 'Reject' : 'Bagus' }}
|
||||||
</Badge>
|
</Badge>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell class="text-right tabular-nums">
|
<TableCell class="text-right tabular-nums">
|
||||||
|
|||||||
@ -41,6 +41,9 @@ import { Switch } from '@/components/ui/switch';
|
|||||||
import { Textarea } from '@/components/ui/textarea';
|
import { Textarea } from '@/components/ui/textarea';
|
||||||
import { useCan } from '@/composables/useCan';
|
import { useCan } from '@/composables/useCan';
|
||||||
import { OrderChannel } from '@/constants/order-channel';
|
import { OrderChannel } from '@/constants/order-channel';
|
||||||
|
import { OrderPaymentType } from '@/constants/order-payment-type';
|
||||||
|
import { PaperSize } from '@/constants/paper-size';
|
||||||
|
import { StockQuality } from '@/constants/stock-quality';
|
||||||
import { apiFetch } from '@/lib/api';
|
import { apiFetch } from '@/lib/api';
|
||||||
import { getFirstCoverImage } from '@/lib/catalog-cover';
|
import { getFirstCoverImage } from '@/lib/catalog-cover';
|
||||||
import { formErrors } from '@/lib/form';
|
import { formErrors } from '@/lib/form';
|
||||||
@ -95,12 +98,12 @@ const isMarketingUser = computed(() =>
|
|||||||
);
|
);
|
||||||
|
|
||||||
const search = ref('');
|
const search = ref('');
|
||||||
const selectedStockQuality = ref<'good' | 'reject'>('good');
|
const selectedStockQuality = ref<'good' | 'reject'>(StockQuality.GOOD);
|
||||||
const cart = ref<OrderCartItem[]>([]);
|
const cart = ref<OrderCartItem[]>([]);
|
||||||
const customerFormOpen = ref(false);
|
const customerFormOpen = ref(false);
|
||||||
const cartDetailOpen = ref(false);
|
const cartDetailOpen = ref(false);
|
||||||
const printAfterSave = ref(false);
|
const printAfterSave = ref(false);
|
||||||
const selectedPaperSize = ref<'58mm' | '80mm'>('80mm');
|
const selectedPaperSize = ref<'58mm' | '80mm'>(PaperSize.MM_80);
|
||||||
|
|
||||||
function onCustomerCreated(customer: { id: number; name: string }) {
|
function onCustomerCreated(customer: { id: number; name: string }) {
|
||||||
emit('customer-created', customer);
|
emit('customer-created', customer);
|
||||||
@ -117,7 +120,7 @@ const form = useForm({
|
|||||||
marketing_id: defaultMarketingId.value || 'none',
|
marketing_id: defaultMarketingId.value || 'none',
|
||||||
channel: 'store',
|
channel: 'store',
|
||||||
price_type: 'ecer',
|
price_type: 'ecer',
|
||||||
payment_type: 'cash',
|
payment_type: OrderPaymentType.CASH,
|
||||||
is_affiliate: false,
|
is_affiliate: false,
|
||||||
tiktok_order_id: '',
|
tiktok_order_id: '',
|
||||||
shopee_order_id: '',
|
shopee_order_id: '',
|
||||||
@ -127,7 +130,7 @@ const form = useForm({
|
|||||||
});
|
});
|
||||||
|
|
||||||
const isStoreChannel = computed(() => form.channel === 'store');
|
const isStoreChannel = computed(() => form.channel === 'store');
|
||||||
const isMarketplaceChannel = computed(() => form.channel === 'shopee' || form.channel === 'tiktok');
|
const isMarketplaceChannel = computed(() => form.channel === OrderChannel.SHOPEE || form.channel === OrderChannel.TIKTOK);
|
||||||
|
|
||||||
function populateForm() {
|
function populateForm() {
|
||||||
if (!props.initialData) {
|
if (!props.initialData) {
|
||||||
@ -147,7 +150,7 @@ function populateForm() {
|
|||||||
form.notes = props.initialData.notes;
|
form.notes = props.initialData.notes;
|
||||||
cart.value = props.initialData.items.map((item) => ({
|
cart.value = props.initialData.items.map((item) => ({
|
||||||
...item,
|
...item,
|
||||||
stock_quality: item.stock_quality ?? 'good',
|
stock_quality: item.stock_quality ?? StockQuality.GOOD,
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -172,12 +175,12 @@ populateDraftItems();
|
|||||||
watch(
|
watch(
|
||||||
() => form.channel,
|
() => form.channel,
|
||||||
(channel) => {
|
(channel) => {
|
||||||
if (channel === 'shopee') {
|
if (channel === OrderChannel.SHOPEE) {
|
||||||
form.price_type = 'shopee';
|
form.price_type = 'shopee';
|
||||||
form.payment_type = 'marketplace';
|
form.payment_type = OrderPaymentType.MARKETPLACE;
|
||||||
} else if (channel === 'tiktok') {
|
} else if (channel === OrderChannel.TIKTOK) {
|
||||||
form.price_type = 'tiktok';
|
form.price_type = 'tiktok';
|
||||||
form.payment_type = 'marketplace';
|
form.payment_type = OrderPaymentType.MARKETPLACE;
|
||||||
} else if (!props.storePriceTypes.some((type) => type.value === form.price_type)) {
|
} else if (!props.storePriceTypes.some((type) => type.value === form.price_type)) {
|
||||||
form.price_type = 'ecer';
|
form.price_type = 'ecer';
|
||||||
}
|
}
|
||||||
@ -228,7 +231,7 @@ function upsertCartItem(item: OrderCartItem) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function availableStockForVariant(variant: ProductVariantItem, stockQuality: string): number {
|
function availableStockForVariant(variant: ProductVariantItem, stockQuality: string): number {
|
||||||
return stockQuality === 'reject' ? variant.reject_stock : variant.stock;
|
return stockQuality === StockQuality.REJECT ? variant.reject_stock : variant.stock;
|
||||||
}
|
}
|
||||||
|
|
||||||
const filteredCatalog = computed(() => {
|
const filteredCatalog = computed(() => {
|
||||||
@ -309,7 +312,7 @@ async function addToCart(product: OrderCatalogItem, variant: ProductVariantItem)
|
|||||||
const availableStock = availableStockForVariant(variant, stockQuality);
|
const availableStock = availableStockForVariant(variant, stockQuality);
|
||||||
|
|
||||||
if (availableStock < 1) {
|
if (availableStock < 1) {
|
||||||
toast.error(`Stok ${stockQuality === 'reject' ? 'reject' : 'bagus'} tidak tersedia.`);
|
toast.error(`Stok ${stockQuality === StockQuality.REJECT ? 'reject' : 'bagus'} tidak tersedia.`);
|
||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -322,7 +325,7 @@ async function addToCart(product: OrderCatalogItem, variant: ProductVariantItem)
|
|||||||
const nextQty = existing ? (Number(existing.quantity) || 0) + 1 : 1;
|
const nextQty = existing ? (Number(existing.quantity) || 0) + 1 : 1;
|
||||||
|
|
||||||
if (nextQty > availableStock) {
|
if (nextQty > availableStock) {
|
||||||
toast.error(`Stok ${stockQuality === 'reject' ? 'reject' : 'bagus'} tidak mencukupi.`);
|
toast.error(`Stok ${stockQuality === StockQuality.REJECT ? 'reject' : 'bagus'} tidak mencukupi.`);
|
||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -441,15 +444,15 @@ function buildFormData(): FormData {
|
|||||||
formData.append('price_type', form.price_type);
|
formData.append('price_type', form.price_type);
|
||||||
formData.append('payment_type', form.payment_type);
|
formData.append('payment_type', form.payment_type);
|
||||||
|
|
||||||
if (form.channel !== 'store') {
|
if (!isStoreChannel.value) {
|
||||||
formData.append('is_affiliate', form.is_affiliate ? '1' : '0');
|
formData.append('is_affiliate', form.is_affiliate ? '1' : '0');
|
||||||
}
|
}
|
||||||
|
|
||||||
if (form.channel === 'tiktok' && form.tiktok_order_id) {
|
if (form.channel === OrderChannel.TIKTOK && form.tiktok_order_id) {
|
||||||
formData.append('tiktok_order_id', form.tiktok_order_id);
|
formData.append('tiktok_order_id', form.tiktok_order_id);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (form.channel === 'shopee' && form.shopee_order_id) {
|
if (form.channel === OrderChannel.SHOPEE && form.shopee_order_id) {
|
||||||
formData.append('shopee_order_id', form.shopee_order_id);
|
formData.append('shopee_order_id', form.shopee_order_id);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -645,7 +648,7 @@ function submit() {
|
|||||||
</div>
|
</div>
|
||||||
</Field>
|
</Field>
|
||||||
|
|
||||||
<Field v-if="form.channel === 'tiktok'">
|
<Field v-if="form.channel === OrderChannel.TIKTOK">
|
||||||
<FieldLabel for="tiktok_order_id" :required="form.channel === OrderChannel.TIKTOK">ID
|
<FieldLabel for="tiktok_order_id" :required="form.channel === OrderChannel.TIKTOK">ID
|
||||||
Pesanan
|
Pesanan
|
||||||
TikTok Shop</FieldLabel>
|
TikTok Shop</FieldLabel>
|
||||||
@ -654,7 +657,7 @@ function submit() {
|
|||||||
<FieldError :errors="formErrors(form, 'tiktok_order_id')" />
|
<FieldError :errors="formErrors(form, 'tiktok_order_id')" />
|
||||||
</Field>
|
</Field>
|
||||||
|
|
||||||
<Field v-if="form.channel === 'shopee'">
|
<Field v-if="form.channel === OrderChannel.SHOPEE">
|
||||||
<FieldLabel for="shopee_order_id" :required="form.channel === OrderChannel.SHOPEE">ID
|
<FieldLabel for="shopee_order_id" :required="form.channel === OrderChannel.SHOPEE">ID
|
||||||
Pesanan
|
Pesanan
|
||||||
Shopee</FieldLabel>
|
Shopee</FieldLabel>
|
||||||
@ -763,7 +766,7 @@ function submit() {
|
|||||||
<p class="truncate text-xs text-muted-foreground">
|
<p class="truncate text-xs text-muted-foreground">
|
||||||
{{ item.variant_name }}
|
{{ item.variant_name }}
|
||||||
·
|
·
|
||||||
{{ item.stock_quality_label ?? (item.stock_quality === 'reject'
|
{{ item.stock_quality_label ?? (item.stock_quality === StockQuality.REJECT
|
||||||
? 'Reject' : 'Bagus') }}
|
? 'Reject' : 'Bagus') }}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
@ -843,7 +846,7 @@ function submit() {
|
|||||||
<div v-if="printAfterSave" class="mt-2 flex items-center gap-2 pl-1">
|
<div v-if="printAfterSave" class="mt-2 flex items-center gap-2 pl-1">
|
||||||
<span class="text-xs text-muted-foreground">Ukuran kertas:</span>
|
<span class="text-xs text-muted-foreground">Ukuran kertas:</span>
|
||||||
<div class="flex gap-1.5">
|
<div class="flex gap-1.5">
|
||||||
<button v-for="size in (['58mm', '80mm'] as const)" :key="size" type="button"
|
<button v-for="size in ([PaperSize.MM_58, PaperSize.MM_80] as const)" :key="size" type="button"
|
||||||
class="inline-flex h-7 items-center rounded-md border px-2.5 text-xs font-medium transition-colors cursor-pointer"
|
class="inline-flex h-7 items-center rounded-md border px-2.5 text-xs font-medium transition-colors cursor-pointer"
|
||||||
:class="selectedPaperSize === size
|
:class="selectedPaperSize === size
|
||||||
? 'border-primary bg-primary/5 text-primary'
|
? 'border-primary bg-primary/5 text-primary'
|
||||||
@ -881,7 +884,7 @@ function submit() {
|
|||||||
<p class="truncate text-sm font-medium">{{ item.product_name }}</p>
|
<p class="truncate text-sm font-medium">{{ item.product_name }}</p>
|
||||||
<p class="truncate text-xs text-muted-foreground">
|
<p class="truncate text-xs text-muted-foreground">
|
||||||
{{ item.variant_name }}
|
{{ item.variant_name }}
|
||||||
· {{ item.stock_quality_label ?? (item.stock_quality === 'reject' ? 'Reject' : 'Bagus')
|
· {{ item.stock_quality_label ?? (item.stock_quality === StockQuality.REJECT ? 'Reject' : 'Bagus')
|
||||||
}}
|
}}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -3,6 +3,7 @@ import { Link } from '@inertiajs/vue3';
|
|||||||
import { computed } from 'vue';
|
import { computed } from 'vue';
|
||||||
import DataTableActions from './data-table-actions.vue';
|
import DataTableActions from './data-table-actions.vue';
|
||||||
import { formatRupiah } from '@/lib/rupiah';
|
import { formatRupiah } from '@/lib/rupiah';
|
||||||
|
import { OrderStatus } from '@/constants/order-status';
|
||||||
import DataTableToolbar from '@/components/data-table/DataTableToolbar.vue';
|
import DataTableToolbar from '@/components/data-table/DataTableToolbar.vue';
|
||||||
import { Badge } from '@/components/ui/badge';
|
import { Badge } from '@/components/ui/badge';
|
||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
@ -60,15 +61,15 @@ function rowNumber(index: number): number {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function statusVariant(status: string): 'default' | 'secondary' | 'destructive' | 'outline' {
|
function statusVariant(status: string): 'default' | 'secondary' | 'destructive' | 'outline' {
|
||||||
if (status === 'completed') {
|
if (status === OrderStatus.COMPLETED) {
|
||||||
return 'default';
|
return 'default';
|
||||||
}
|
}
|
||||||
|
|
||||||
if (status === 'cancelled') {
|
if (status === OrderStatus.CANCELLED) {
|
||||||
return 'destructive';
|
return 'destructive';
|
||||||
}
|
}
|
||||||
|
|
||||||
if (status === 'processing') {
|
if (status === OrderStatus.PROCESSING) {
|
||||||
return 'secondary';
|
return 'secondary';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -8,6 +8,7 @@ import ConfirmDialog from '@/components/ConfirmDialog.vue';
|
|||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip';
|
import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip';
|
||||||
import { useCan } from '@/composables/useCan';
|
import { useCan } from '@/composables/useCan';
|
||||||
|
import { OrderStatus } from '@/constants/order-status';
|
||||||
import type { OrderListItem, OrderStatusAction } from '@/types/order';
|
import type { OrderListItem, OrderStatusAction } from '@/types/order';
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
@ -32,11 +33,11 @@ function canPerformAction(action: OrderStatusAction): boolean {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function statusConfirmDescription(action: OrderStatusAction): string {
|
function statusConfirmDescription(action: OrderStatusAction): string {
|
||||||
if (action.status === 'processing') {
|
if (action.status === OrderStatus.PROCESSING) {
|
||||||
return `Pesanan ${props.order.order_number} akan dikirim dan diproses.`;
|
return `Pesanan ${props.order.order_number} akan dikirim dan diproses.`;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (action.status === 'completed') {
|
if (action.status === OrderStatus.COMPLETED) {
|
||||||
return `Pesanan ${props.order.order_number} akan ditandai selesai.`;
|
return `Pesanan ${props.order.order_number} akan ditandai selesai.`;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -92,11 +93,11 @@ function destroyOrder() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function actionIcon(status: string) {
|
function actionIcon(status: string) {
|
||||||
if (status === 'processing') {
|
if (status === OrderStatus.PROCESSING) {
|
||||||
return Send;
|
return Send;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (status === 'completed') {
|
if (status === OrderStatus.COMPLETED) {
|
||||||
return Check;
|
return Check;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -5,6 +5,8 @@ import { computed, ref, watch } from 'vue';
|
|||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
import { Card, CardContent } from '@/components/ui/card';
|
import { Card, CardContent } from '@/components/ui/card';
|
||||||
import { useCan } from '@/composables/useCan';
|
import { useCan } from '@/composables/useCan';
|
||||||
|
import { ActiveStatus } from '@/constants/active-status';
|
||||||
|
import { StockStatus } from '@/constants/stock-status';
|
||||||
import {
|
import {
|
||||||
useDataTableQuery,
|
useDataTableQuery,
|
||||||
useDataTableQuerySync,
|
useDataTableQuerySync,
|
||||||
@ -54,8 +56,8 @@ const filterDefs = computed<DataTableFilterDef[]>(() => [
|
|||||||
label: 'Status',
|
label: 'Status',
|
||||||
type: 'select',
|
type: 'select',
|
||||||
options: [
|
options: [
|
||||||
{ value: '1', label: 'Aktif' },
|
{ value: ActiveStatus.ACTIVE, label: 'Aktif' },
|
||||||
{ value: '0', label: 'Nonaktif' },
|
{ value: ActiveStatus.INACTIVE, label: 'Nonaktif' },
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -63,8 +65,8 @@ const filterDefs = computed<DataTableFilterDef[]>(() => [
|
|||||||
label: 'Stok',
|
label: 'Stok',
|
||||||
type: 'select',
|
type: 'select',
|
||||||
options: [
|
options: [
|
||||||
{ value: 'out_of_stock', label: 'Stok Habis' },
|
{ value: StockStatus.OUT_OF_STOCK, label: 'Stok Habis' },
|
||||||
{ value: 'low_stock', label: 'Stok Menipis' },
|
{ value: StockStatus.LOW_STOCK, label: 'Stok Menipis' },
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
]);
|
]);
|
||||||
|
|||||||
@ -5,6 +5,8 @@ import { computed, ref, watch } from 'vue';
|
|||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
import { Card, CardContent } from '@/components/ui/card';
|
import { Card, CardContent } from '@/components/ui/card';
|
||||||
import { useCan } from '@/composables/useCan';
|
import { useCan } from '@/composables/useCan';
|
||||||
|
import { ActiveStatus } from '@/constants/active-status';
|
||||||
|
import { StockStatus } from '@/constants/stock-status';
|
||||||
import {
|
import {
|
||||||
useDataTableQuery,
|
useDataTableQuery,
|
||||||
useDataTableQuerySync,
|
useDataTableQuerySync,
|
||||||
@ -43,8 +45,8 @@ const filterDefs = computed<DataTableFilterDef[]>(() => [
|
|||||||
label: 'Status',
|
label: 'Status',
|
||||||
type: 'select',
|
type: 'select',
|
||||||
options: [
|
options: [
|
||||||
{ value: '1', label: 'Aktif' },
|
{ value: ActiveStatus.ACTIVE, label: 'Aktif' },
|
||||||
{ value: '0', label: 'Nonaktif' },
|
{ value: ActiveStatus.INACTIVE, label: 'Nonaktif' },
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -52,8 +54,8 @@ const filterDefs = computed<DataTableFilterDef[]>(() => [
|
|||||||
label: 'Stok',
|
label: 'Stok',
|
||||||
type: 'select',
|
type: 'select',
|
||||||
options: [
|
options: [
|
||||||
{ value: 'out_of_stock', label: 'Stok Habis' },
|
{ value: StockStatus.OUT_OF_STOCK, label: 'Stok Habis' },
|
||||||
{ value: 'low_stock', label: 'Stok Menipis' },
|
{ value: StockStatus.LOW_STOCK, label: 'Stok Menipis' },
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
]);
|
]);
|
||||||
|
|||||||
@ -6,6 +6,7 @@ import HrSection from './form/HrSection.vue';
|
|||||||
import MarketplaceSection from './form/MarketplaceSection.vue';
|
import MarketplaceSection from './form/MarketplaceSection.vue';
|
||||||
import SocialMediaSection from './form/SocialMediaSection.vue';
|
import SocialMediaSection from './form/SocialMediaSection.vue';
|
||||||
import SystemSection from './form/SystemSection.vue';
|
import SystemSection from './form/SystemSection.vue';
|
||||||
|
import { SettingSection as SettingSectionConst } from '@/constants/setting-section';
|
||||||
import SettingLayout from '@/layouts/SettingLayout.vue';
|
import SettingLayout from '@/layouts/SettingLayout.vue';
|
||||||
import type {
|
import type {
|
||||||
HomepageSettingsData,
|
HomepageSettingsData,
|
||||||
@ -24,7 +25,7 @@ defineProps<{
|
|||||||
homepage: HomepageSettingsData;
|
homepage: HomepageSettingsData;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
const activeSection = ref<SettingSection>('system');
|
const activeSection = ref<SettingSection>(SettingSectionConst.SYSTEM);
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@ -32,10 +33,10 @@ const activeSection = ref<SettingSection>('system');
|
|||||||
<Head title="Pengaturan" />
|
<Head title="Pengaturan" />
|
||||||
|
|
||||||
<SettingLayout v-model:section="activeSection">
|
<SettingLayout v-model:section="activeSection">
|
||||||
<SystemSection v-if="activeSection === 'system'" :data="system" />
|
<SystemSection v-if="activeSection === SettingSectionConst.SYSTEM" :data="system" />
|
||||||
<HomepageSection v-else-if="activeSection === 'homepage'" :data="homepage" />
|
<HomepageSection v-else-if="activeSection === SettingSectionConst.HOMEPAGE" :data="homepage" />
|
||||||
<SocialMediaSection v-else-if="activeSection === 'social'" :data="socialMedia" />
|
<SocialMediaSection v-else-if="activeSection === SettingSectionConst.SOCIAL" :data="socialMedia" />
|
||||||
<MarketplaceSection v-else-if="activeSection === 'marketplace'" :data="marketplace" />
|
<MarketplaceSection v-else-if="activeSection === SettingSectionConst.MARKETPLACE" :data="marketplace" />
|
||||||
<HrSection v-else-if="activeSection === 'hr'" :data="hr" />
|
<HrSection v-else-if="activeSection === SettingSectionConst.HR" :data="hr" />
|
||||||
</SettingLayout>
|
</SettingLayout>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@ -14,6 +14,8 @@ import {
|
|||||||
SelectTrigger,
|
SelectTrigger,
|
||||||
SelectValue,
|
SelectValue,
|
||||||
} from '@/components/ui/select';
|
} from '@/components/ui/select';
|
||||||
|
import { MarketplaceFeeScope } from '@/constants/marketplace-fee-scope';
|
||||||
|
import { MarketplaceFeeValueType } from '@/constants/marketplace-fee-value-type';
|
||||||
import type { MarketplaceFeeRule } from '@/types/setting';
|
import type { MarketplaceFeeRule } from '@/types/setting';
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
@ -27,7 +29,7 @@ const emit = defineEmits<{
|
|||||||
(event: 'update:modelValue', value: MarketplaceFeeRule): void;
|
(event: 'update:modelValue', value: MarketplaceFeeRule): void;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
const isPercent = computed(() => props.modelValue.value_type === 'percent');
|
const isPercent = computed(() => props.modelValue.value_type === MarketplaceFeeValueType.PERCENT);
|
||||||
|
|
||||||
function update(partial: Partial<MarketplaceFeeRule>) {
|
function update(partial: Partial<MarketplaceFeeRule>) {
|
||||||
emit('update:modelValue', {
|
emit('update:modelValue', {
|
||||||
@ -38,7 +40,7 @@ function update(partial: Partial<MarketplaceFeeRule>) {
|
|||||||
|
|
||||||
function updateValue(value: string | number) {
|
function updateValue(value: string | number) {
|
||||||
update({
|
update({
|
||||||
value: props.modelValue.value_type === 'flat'
|
value: props.modelValue.value_type === MarketplaceFeeValueType.FLAT
|
||||||
? Number(value) || 0
|
? Number(value) || 0
|
||||||
: Number(value),
|
: Number(value),
|
||||||
});
|
});
|
||||||
@ -62,10 +64,10 @@ function updateValue(value: string | number) {
|
|||||||
<SelectValue placeholder="Pilih dasar" />
|
<SelectValue placeholder="Pilih dasar" />
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
<SelectItem value="transaction">
|
<SelectItem :value="MarketplaceFeeScope.TRANSACTION">
|
||||||
Per Transaksi
|
Per Transaksi
|
||||||
</SelectItem>
|
</SelectItem>
|
||||||
<SelectItem value="product">
|
<SelectItem :value="MarketplaceFeeScope.PRODUCT">
|
||||||
Per Produk
|
Per Produk
|
||||||
</SelectItem>
|
</SelectItem>
|
||||||
</SelectContent>
|
</SelectContent>
|
||||||
@ -84,10 +86,10 @@ function updateValue(value: string | number) {
|
|||||||
<SelectValue placeholder="Pilih tipe" />
|
<SelectValue placeholder="Pilih tipe" />
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
<SelectItem value="percent">
|
<SelectItem :value="MarketplaceFeeValueType.PERCENT">
|
||||||
Persentase (%)
|
Persentase (%)
|
||||||
</SelectItem>
|
</SelectItem>
|
||||||
<SelectItem value="flat">
|
<SelectItem :value="MarketplaceFeeValueType.FLAT">
|
||||||
Flat (Rp)
|
Flat (Rp)
|
||||||
</SelectItem>
|
</SelectItem>
|
||||||
</SelectContent>
|
</SelectContent>
|
||||||
|
|||||||
@ -13,6 +13,7 @@ import {
|
|||||||
FieldGroup,
|
FieldGroup,
|
||||||
FieldSet
|
FieldSet
|
||||||
} from '@/components/ui/field';
|
} from '@/components/ui/field';
|
||||||
|
import { MarketplacePlatform as MarketplacePlatformConst } from '@/constants/marketplace-platform';
|
||||||
import { formErrors } from '@/lib/form';
|
import { formErrors } from '@/lib/form';
|
||||||
import { cn } from '@/lib/utils';
|
import { cn } from '@/lib/utils';
|
||||||
import type {
|
import type {
|
||||||
@ -24,15 +25,15 @@ const props = defineProps<{
|
|||||||
data: MarketplaceSettingsData;
|
data: MarketplaceSettingsData;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
const activePlatform = ref<MarketplacePlatform>('tiktok_shop');
|
const activePlatform = ref<MarketplacePlatform>(MarketplacePlatformConst.TIKTOK_SHOP);
|
||||||
|
|
||||||
const platformItems: Array<{
|
const platformItems: Array<{
|
||||||
key: MarketplacePlatform;
|
key: MarketplacePlatform;
|
||||||
label: string;
|
label: string;
|
||||||
icon: typeof Store;
|
icon: typeof Store;
|
||||||
}> = [
|
}> = [
|
||||||
{ key: 'tiktok_shop', label: 'TikTok Shop', icon: Store },
|
{ key: MarketplacePlatformConst.TIKTOK_SHOP, label: 'TikTok Shop', icon: Store },
|
||||||
{ key: 'shopee', label: 'Shopee', icon: ShoppingBag },
|
{ key: MarketplacePlatformConst.SHOPEE, label: 'Shopee', icon: ShoppingBag },
|
||||||
];
|
];
|
||||||
|
|
||||||
const form = useForm({
|
const form = useForm({
|
||||||
@ -88,7 +89,7 @@ function submit() {
|
|||||||
</nav>
|
</nav>
|
||||||
|
|
||||||
<div class="min-w-0 flex-1 space-y-6">
|
<div class="min-w-0 flex-1 space-y-6">
|
||||||
<Card v-show="activePlatform === 'tiktok_shop'">
|
<Card v-show="activePlatform === MarketplacePlatformConst.TIKTOK_SHOP">
|
||||||
<CardContent>
|
<CardContent>
|
||||||
<FieldSet>
|
<FieldSet>
|
||||||
<FieldGroup class="grid gap-4 sm:grid-cols-2">
|
<FieldGroup class="grid gap-4 sm:grid-cols-2">
|
||||||
@ -138,7 +139,7 @@ function submit() {
|
|||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
<Card v-show="activePlatform === 'shopee'">
|
<Card v-show="activePlatform === MarketplacePlatformConst.SHOPEE">
|
||||||
<CardContent>
|
<CardContent>
|
||||||
<FieldSet>
|
<FieldSet>
|
||||||
<FieldGroup class="grid gap-4 sm:grid-cols-2">
|
<FieldGroup class="grid gap-4 sm:grid-cols-2">
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user