refactor: streamline enum management and enhance role assignment logic in EmployeeController for improved maintainability

This commit is contained in:
Yoga Pangestu 2026-06-22 14:41:52 +07:00
parent 7508227768
commit 27c5b5fffa
65 changed files with 441 additions and 775 deletions

View File

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

View File

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

View File

@ -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 => [],
};
}
} }

View File

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

View File

@ -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 => [],
};
}
} }

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,4 @@
export const ActiveStatus = {
ACTIVE: '1',
INACTIVE: '0',
} as const;

View File

@ -0,0 +1,5 @@
export const AppearanceMode = {
LIGHT: 'light',
DARK: 'dark',
SYSTEM: 'system',
} as const;

View 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;

View File

@ -0,0 +1,4 @@
export const CashTransactionType = {
DEPOSIT: 'deposit',
WITHDRAWAL: 'withdrawal',
} as const;

View File

@ -0,0 +1,6 @@
export const CatalogSortOption = {
DEFAULT: 'default',
PRICE_ASC: 'price_asc',
PRICE_DESC: 'price_desc',
NAME_ASC: 'name_asc',
} as const;

View File

@ -0,0 +1,6 @@
export const CuttingStatus = {
IN_PROGRESS: 'in_progress',
COMPLETED: 'completed',
VERIFIED: 'verified',
REJECTED: 'rejected',
} as const;

View File

@ -0,0 +1,6 @@
export const EmployeeAdvanceStatus = {
PENDING: 'pending',
APPROVED: 'approved',
REJECTED: 'rejected',
PAID: 'paid',
} as const;

View 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';

View File

@ -0,0 +1,5 @@
export const LeaveRequestStatus = {
PENDING: 'pending',
APPROVED: 'approved',
REJECTED: 'rejected',
} as const;

View File

@ -0,0 +1,4 @@
export const MarketplaceFeeScope = {
PRODUCT: 'product',
TRANSACTION: 'transaction',
} as const;

View File

@ -0,0 +1,4 @@
export const MarketplaceFeeValueType = {
FLAT: 'flat',
PERCENT: 'percent',
} as const;

View File

@ -0,0 +1,4 @@
export const MarketplacePlatform = {
TIKTOK_SHOP: 'tiktok_shop',
SHOPEE: 'shopee',
} as const;

View File

@ -0,0 +1,4 @@
export const OrderPaymentType = {
CASH: 'cash',
MARKETPLACE: 'marketplace',
} as const;

View File

@ -0,0 +1,5 @@
export const OrderStatus = {
PROCESSING: 'processing',
COMPLETED: 'completed',
CANCELLED: 'cancelled',
} as const;

View File

@ -0,0 +1,4 @@
export const PaperSize = {
MM_58: '58mm',
MM_80: '80mm',
} as const;

View File

@ -0,0 +1,4 @@
export const PayrollStatus = {
UNPAID: 'unpaid',
PAID: 'paid',
} as const;

View File

@ -0,0 +1,7 @@
export const SettingSection = {
SYSTEM: 'system',
SOCIAL: 'social',
MARKETPLACE: 'marketplace',
HR: 'hr',
HOMEPAGE: 'homepage',
} as const;

View File

@ -0,0 +1,4 @@
export const StockQuality = {
GOOD: 'good',
REJECT: 'reject',
} as const;

View File

@ -0,0 +1,4 @@
export const StockStatus = {
OUT_OF_STOCK: 'out_of_stock',
LOW_STOCK: 'low_stock',
} as const;

View File

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

View File

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

View File

@ -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,

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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">

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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">