Compare commits
22 Commits
6546e1e3eb
...
1f39565754
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1f39565754 | ||
|
|
3ef948c47f | ||
|
|
659104fa19 | ||
|
|
71128002d7 | ||
|
|
aaafbb92a2 | ||
|
|
b45052dc55 | ||
|
|
577329365a | ||
|
|
62341ac50a | ||
|
|
bcf5842c4a | ||
|
|
b4cb3f4d48 | ||
|
|
f49cebb35a | ||
|
|
fb70cca1dc | ||
|
|
3b49f5ff3a | ||
|
|
4ac83c8e78 | ||
|
|
fc1c84f420 | ||
|
|
c887eddd88 | ||
|
|
bd20162c13 | ||
|
|
705f0d3efa | ||
|
|
d853e742ed | ||
|
|
87439677fc | ||
|
|
3f77a9436d | ||
|
|
b70a40af4d |
@ -16,7 +16,7 @@
|
||||
class AdminSettingsController extends Controller
|
||||
{
|
||||
public function __construct(
|
||||
private readonly AdminSettingsService $service
|
||||
private AdminSettingsService $service
|
||||
) {}
|
||||
|
||||
public function index(): Response
|
||||
|
||||
@ -15,7 +15,7 @@
|
||||
class CashAccountController extends Controller
|
||||
{
|
||||
public function __construct(
|
||||
private readonly CashAccountService $service
|
||||
private CashAccountService $service
|
||||
) {}
|
||||
|
||||
public function index(PaginatedRequest $request): Response
|
||||
|
||||
@ -14,7 +14,7 @@
|
||||
class EmployeeAdvanceController extends Controller
|
||||
{
|
||||
public function __construct(
|
||||
private readonly EmployeeAdvanceService $service
|
||||
private EmployeeAdvanceService $service
|
||||
) {}
|
||||
|
||||
public function index(PaginatedRequest $request): Response
|
||||
|
||||
@ -14,7 +14,7 @@
|
||||
class ExpenseController extends Controller
|
||||
{
|
||||
public function __construct(
|
||||
private readonly ExpenseService $service
|
||||
private ExpenseService $service
|
||||
) {}
|
||||
|
||||
public function index(PaginatedRequest $request): Response
|
||||
|
||||
@ -12,7 +12,7 @@
|
||||
class PayrollAdjustmentController extends Controller
|
||||
{
|
||||
public function __construct(
|
||||
private readonly PayrollAdjustmentService $service
|
||||
private PayrollAdjustmentService $service
|
||||
) {}
|
||||
|
||||
public function store(PayrollAdjustmentRequest $request, Payroll $payroll): RedirectResponse
|
||||
@ -21,7 +21,7 @@ public function store(PayrollAdjustmentRequest $request, Payroll $payroll): Redi
|
||||
fn () => $this->service->create($payroll, $request->validated()),
|
||||
'Adjustment gaji berhasil ditambahkan.',
|
||||
'admin.finance.payroll-periods.show',
|
||||
parameters : ['payroll_period' => $payroll->payroll_period_id]
|
||||
parameters: ['payroll_period' => $payroll->payroll_period_id]
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@ -10,7 +10,7 @@
|
||||
class PayrollController extends Controller
|
||||
{
|
||||
public function __construct(
|
||||
private readonly PayrollPeriodService $service
|
||||
private PayrollPeriodService $service
|
||||
) {}
|
||||
|
||||
public function pay(Payroll $payroll): RedirectResponse
|
||||
|
||||
@ -14,7 +14,7 @@
|
||||
class PayrollPeriodController extends Controller
|
||||
{
|
||||
public function __construct(
|
||||
private readonly PayrollPeriodService $service
|
||||
private PayrollPeriodService $service
|
||||
) {}
|
||||
|
||||
public function index(PaginatedRequest $request): Response
|
||||
|
||||
@ -15,7 +15,7 @@
|
||||
class AttendanceController extends Controller
|
||||
{
|
||||
public function __construct(
|
||||
private readonly AttendanceService $service
|
||||
private AttendanceService $service
|
||||
) {}
|
||||
|
||||
public function index(Request $request): Response
|
||||
|
||||
@ -15,7 +15,7 @@
|
||||
class EmployeeController extends Controller
|
||||
{
|
||||
public function __construct(
|
||||
private readonly EmployeeService $service
|
||||
private EmployeeService $service
|
||||
) {}
|
||||
|
||||
public function index(PaginatedRequest $request): Response
|
||||
@ -39,7 +39,7 @@ public function create(): Response
|
||||
public function store(EmployeeRequest $request): RedirectResponse
|
||||
{
|
||||
return $this->handleAction(
|
||||
fn() => $this->service->create($request->validated()),
|
||||
fn () => $this->service->create($request->validated()),
|
||||
'Pegawai berhasil ditambahkan.',
|
||||
'admin.hr.employees.index'
|
||||
);
|
||||
@ -58,7 +58,7 @@ public function edit(User $user): Response
|
||||
public function update(EmployeeRequest $request, User $user): RedirectResponse
|
||||
{
|
||||
return $this->handleAction(
|
||||
fn() => $this->service->update($user, $request->validated()),
|
||||
fn () => $this->service->update($user, $request->validated()),
|
||||
'Pegawai berhasil diperbarui.',
|
||||
'admin.hr.employees.index'
|
||||
);
|
||||
@ -67,7 +67,7 @@ public function update(EmployeeRequest $request, User $user): RedirectResponse
|
||||
public function destroy(User $user): RedirectResponse
|
||||
{
|
||||
return $this->handleAction(
|
||||
fn() => $this->service->delete($user),
|
||||
fn () => $this->service->delete($user),
|
||||
'Pegawai berhasil dihapus.',
|
||||
'admin.hr.employees.index'
|
||||
);
|
||||
@ -86,7 +86,7 @@ public function toggleActive(User $user): RedirectResponse
|
||||
public function resetPassword(User $user): RedirectResponse
|
||||
{
|
||||
return $this->handleAction(
|
||||
fn() => $this->service->resetPassword($user),
|
||||
fn () => $this->service->resetPassword($user),
|
||||
'Kata sandi pegawai berhasil direset ke kata sandi default.',
|
||||
'admin.hr.employees.index'
|
||||
);
|
||||
|
||||
@ -15,7 +15,7 @@
|
||||
class LeaveRequestController extends Controller
|
||||
{
|
||||
public function __construct(
|
||||
private readonly LeaveRequestService $service
|
||||
private LeaveRequestService $service
|
||||
) {}
|
||||
|
||||
public function index(PaginatedRequest $request): Response
|
||||
|
||||
@ -14,7 +14,7 @@
|
||||
class CuttingController extends Controller
|
||||
{
|
||||
public function __construct(
|
||||
private readonly CuttingService $service,
|
||||
private CuttingService $service,
|
||||
) {}
|
||||
|
||||
public function index(PaginatedRequest $request): Response
|
||||
|
||||
@ -7,6 +7,7 @@
|
||||
use App\Http\Requests\PaginatedRequest;
|
||||
use App\Models\Purchase;
|
||||
use App\Services\Admin\Manage\PurchaseService;
|
||||
use App\Services\Admin\Master\SupplierService;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Inertia\Inertia;
|
||||
use Inertia\Response;
|
||||
@ -15,6 +16,7 @@ class PurchaseController extends Controller
|
||||
{
|
||||
public function __construct(
|
||||
private readonly PurchaseService $service,
|
||||
private readonly SupplierService $supplierService,
|
||||
) {}
|
||||
|
||||
public function index(PaginatedRequest $request): Response
|
||||
@ -22,7 +24,10 @@ public function index(PaginatedRequest $request): Response
|
||||
return Inertia::render('admin/manage/purchase/index', [
|
||||
'purchases' => $this->service->paginated(
|
||||
...$request->validatedWithDefaults(),
|
||||
filters: $request->only(['supplier_id']),
|
||||
),
|
||||
'suppliers' => $this->supplierService->getAll(),
|
||||
'filters' => $request->only(['supplier_id']),
|
||||
]);
|
||||
}
|
||||
|
||||
|
||||
@ -14,7 +14,7 @@
|
||||
class RestockController extends Controller
|
||||
{
|
||||
public function __construct(
|
||||
private readonly RestockService $service,
|
||||
private RestockService $service,
|
||||
) {}
|
||||
|
||||
public function index(PaginatedRequest $request): Response
|
||||
|
||||
@ -14,7 +14,7 @@
|
||||
class TransactionController extends Controller
|
||||
{
|
||||
public function __construct(
|
||||
private readonly TransactionService $service,
|
||||
private TransactionService $service,
|
||||
) {}
|
||||
|
||||
public function index(PaginatedRequest $request): Response
|
||||
|
||||
@ -14,7 +14,7 @@
|
||||
class CategoryController extends Controller
|
||||
{
|
||||
public function __construct(
|
||||
private readonly CategoryService $service
|
||||
private CategoryService $service
|
||||
) {}
|
||||
|
||||
public function index(PaginatedRequest $request): Response
|
||||
|
||||
@ -14,7 +14,7 @@
|
||||
class CustomerController extends Controller
|
||||
{
|
||||
public function __construct(
|
||||
private readonly CustomerService $service
|
||||
private CustomerService $service
|
||||
) {}
|
||||
|
||||
public function index(PaginatedRequest $request): Response
|
||||
|
||||
@ -15,8 +15,8 @@
|
||||
class ProductController extends Controller
|
||||
{
|
||||
public function __construct(
|
||||
private readonly ProductService $service,
|
||||
private readonly CategoryService $categoryService,
|
||||
private ProductService $service,
|
||||
private CategoryService $categoryService,
|
||||
) {}
|
||||
|
||||
public function index(PaginatedRequest $request): Response
|
||||
|
||||
@ -15,7 +15,7 @@
|
||||
class ProductVariantController extends Controller
|
||||
{
|
||||
public function __construct(
|
||||
private readonly ProductVariantService $variantService,
|
||||
private ProductVariantService $variantService,
|
||||
) {}
|
||||
|
||||
public function edit(Product $product, ProductVariant $variant): Response
|
||||
|
||||
@ -13,7 +13,7 @@
|
||||
class StockMutationController extends Controller
|
||||
{
|
||||
public function __construct(
|
||||
private readonly StockMutationService $service,
|
||||
private StockMutationService $service,
|
||||
) {}
|
||||
|
||||
public function index(StockMutationRequest $request, Product $product, ProductVariant $variant): Response
|
||||
|
||||
@ -14,7 +14,7 @@
|
||||
class RawMaterialController extends Controller
|
||||
{
|
||||
public function __construct(
|
||||
private readonly RawMaterialService $service,
|
||||
private RawMaterialService $service,
|
||||
) {}
|
||||
|
||||
public function index(PaginatedRequest $request): Response
|
||||
|
||||
@ -14,7 +14,7 @@
|
||||
class RawMaterialVariantController extends Controller
|
||||
{
|
||||
public function __construct(
|
||||
private readonly RawMaterialVariantService $variantService,
|
||||
private RawMaterialVariantService $variantService,
|
||||
) {}
|
||||
|
||||
public function edit(RawMaterial $rawMaterial, RawMaterialPrice $variant): Response
|
||||
|
||||
@ -14,7 +14,7 @@
|
||||
class SupplierController extends Controller
|
||||
{
|
||||
public function __construct(
|
||||
private readonly SupplierService $service
|
||||
private SupplierService $service
|
||||
) {}
|
||||
|
||||
public function index(PaginatedRequest $request): Response
|
||||
|
||||
@ -14,7 +14,7 @@
|
||||
class RoleController extends Controller
|
||||
{
|
||||
public function __construct(
|
||||
private readonly RoleService $service
|
||||
private RoleService $service
|
||||
) {}
|
||||
|
||||
public function index(PaginatedRequest $request): Response
|
||||
|
||||
@ -11,7 +11,7 @@
|
||||
class PresignedUrlController extends Controller
|
||||
{
|
||||
public function __construct(
|
||||
private readonly S3PresignedService $service
|
||||
private S3PresignedService $service
|
||||
) {}
|
||||
|
||||
public function store(PresignedUrlRequest $request): JsonResponse
|
||||
|
||||
@ -11,7 +11,7 @@
|
||||
class PushSubscriptionController extends Controller
|
||||
{
|
||||
public function __construct(
|
||||
private readonly PushSubscriptionService $service,
|
||||
private PushSubscriptionService $service,
|
||||
) {}
|
||||
|
||||
public function store(StorePushSubscriptionRequest $request): JsonResponse
|
||||
|
||||
@ -16,16 +16,16 @@ public function rules(): array
|
||||
return [
|
||||
'description' => ['nullable', 'string', 'max:100'],
|
||||
'product_name' => ['required', 'string', 'max:255'],
|
||||
'sample' => ['required', 'integer', 'min:0'],
|
||||
'original_outside_sample' => ['required', 'integer', 'min:0'],
|
||||
'cutting_result' => ['required', 'integer', 'min:0'],
|
||||
'sample' => ['required', 'integer'],
|
||||
'original_outside_sample' => ['required', 'integer'],
|
||||
'cutting_result' => ['required', 'integer', 'min:1'],
|
||||
'materials' => ['required', 'array', 'min:1'],
|
||||
'materials.*.raw_material_price_id' => ['required', 'integer', 'exists:raw_material_prices,id'],
|
||||
'materials.*.material_usage' => ['required', 'integer', 'min:0'],
|
||||
'materials.*.material_result' => ['required', 'integer', 'min:0'],
|
||||
'materials.*.combination_index' => ['nullable', 'integer', 'min:0'],
|
||||
'materials.*.material_usage' => ['required', 'integer', 'min:1'],
|
||||
'materials.*.material_result' => ['required', 'integer'],
|
||||
'materials.*.combination_index' => ['nullable', 'integer'],
|
||||
'combinations' => ['nullable', 'array'],
|
||||
'combinations.*.material_result' => ['nullable', 'integer', 'min:0'],
|
||||
'combinations.*.material_result' => ['nullable', 'integer'],
|
||||
'photo_key' => ['nullable', 'string', 'max:500'],
|
||||
];
|
||||
}
|
||||
|
||||
@ -27,7 +27,7 @@ protected function casts(): array
|
||||
protected function formattedBalance(): Attribute
|
||||
{
|
||||
return Attribute::make(
|
||||
get: fn () => 'Rp ' . number_format($this->balance, 0, ',', '.'),
|
||||
get: fn () => 'Rp '.number_format($this->balance, 0, ',', '.'),
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@ -36,14 +36,14 @@ protected function casts(): array
|
||||
protected function formattedAmount(): Attribute
|
||||
{
|
||||
return Attribute::make(
|
||||
get: fn () => 'Rp ' . number_format($this->amount, 0, ',', '.'),
|
||||
get: fn () => 'Rp '.number_format($this->amount, 0, ',', '.'),
|
||||
);
|
||||
}
|
||||
|
||||
protected function formattedBalanceAfter(): Attribute
|
||||
{
|
||||
return Attribute::make(
|
||||
get: fn () => 'Rp ' . number_format($this->balance_after, 0, ',', '.'),
|
||||
get: fn () => 'Rp '.number_format($this->balance_after, 0, ',', '.'),
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@ -27,7 +27,7 @@ protected function formattedPhoneNumber(): Attribute
|
||||
{
|
||||
return Attribute::make(
|
||||
get: fn () => $this->phone_number
|
||||
? substr($this->phone_number, 0, 4) . ' ' . substr($this->phone_number, 4, 4) . ' ' . substr($this->phone_number, 8)
|
||||
? substr($this->phone_number, 0, 4).' '.substr($this->phone_number, 4, 4).' '.substr($this->phone_number, 8)
|
||||
: null,
|
||||
set: fn (?string $value) => $value ? preg_replace('/\s/', '', $value) : null,
|
||||
);
|
||||
|
||||
@ -36,21 +36,21 @@ protected function casts(): array
|
||||
protected function formattedCostPerUnit(): Attribute
|
||||
{
|
||||
return Attribute::make(
|
||||
get: fn () => 'Rp ' . number_format($this->cost_per_unit, 0, ',', '.'),
|
||||
get: fn () => 'Rp '.number_format($this->cost_per_unit, 0, ',', '.'),
|
||||
);
|
||||
}
|
||||
|
||||
protected function formattedOtherCost(): Attribute
|
||||
{
|
||||
return Attribute::make(
|
||||
get: fn () => 'Rp ' . number_format($this->other_cost, 0, ',', '.'),
|
||||
get: fn () => 'Rp '.number_format($this->other_cost, 0, ',', '.'),
|
||||
);
|
||||
}
|
||||
|
||||
protected function formattedSewingCost(): Attribute
|
||||
{
|
||||
return Attribute::make(
|
||||
get: fn () => 'Rp ' . number_format($this->sewing_cost, 0, ',', '.'),
|
||||
get: fn () => 'Rp '.number_format($this->sewing_cost, 0, ',', '.'),
|
||||
);
|
||||
}
|
||||
|
||||
@ -64,7 +64,7 @@ protected function statusLabel(): Attribute
|
||||
protected function formattedTotalMaterialCost(): Attribute
|
||||
{
|
||||
return Attribute::make(
|
||||
get: fn () => 'Rp ' . number_format($this->total_material_cost, 0, ',', '.'),
|
||||
get: fn () => 'Rp '.number_format($this->total_material_cost, 0, ',', '.'),
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@ -33,7 +33,7 @@ protected function casts(): array
|
||||
protected function formattedBaseSalary(): Attribute
|
||||
{
|
||||
return Attribute::make(
|
||||
get: fn () => 'Rp ' . number_format($this->base_salary, 0, ',', '.'),
|
||||
get: fn () => 'Rp '.number_format($this->base_salary, 0, ',', '.'),
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@ -34,7 +34,7 @@ protected function casts(): array
|
||||
protected function formattedAmount(): Attribute
|
||||
{
|
||||
return Attribute::make(
|
||||
get: fn () => 'Rp ' . number_format($this->amount, 0, ',', '.'),
|
||||
get: fn () => 'Rp '.number_format($this->amount, 0, ',', '.'),
|
||||
);
|
||||
}
|
||||
|
||||
@ -55,7 +55,7 @@ protected function formattedDueDate(): Attribute
|
||||
protected function formattedPaidAmount(): Attribute
|
||||
{
|
||||
return Attribute::make(
|
||||
get: fn () => 'Rp ' . number_format($this->paid_amount, 0, ',', '.'),
|
||||
get: fn () => 'Rp '.number_format($this->paid_amount, 0, ',', '.'),
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@ -29,7 +29,7 @@ protected function casts(): array
|
||||
protected function formattedAmount(): Attribute
|
||||
{
|
||||
return Attribute::make(
|
||||
get: fn () => 'Rp ' . number_format($this->amount, 0, ',', '.'),
|
||||
get: fn () => 'Rp '.number_format($this->amount, 0, ',', '.'),
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@ -52,21 +52,21 @@ protected function channelLabel(): Attribute
|
||||
protected function formattedCogs(): Attribute
|
||||
{
|
||||
return Attribute::make(
|
||||
get: fn () => 'Rp ' . number_format($this->cogs, 0, ',', '.'),
|
||||
get: fn () => 'Rp '.number_format($this->cogs, 0, ',', '.'),
|
||||
);
|
||||
}
|
||||
|
||||
protected function formattedDiscount(): Attribute
|
||||
{
|
||||
return Attribute::make(
|
||||
get: fn () => 'Rp ' . number_format($this->discount, 0, ',', '.'),
|
||||
get: fn () => 'Rp '.number_format($this->discount, 0, ',', '.'),
|
||||
);
|
||||
}
|
||||
|
||||
protected function formattedNegoPrice(): Attribute
|
||||
{
|
||||
return Attribute::make(
|
||||
get: fn () => 'Rp ' . number_format($this->nego_price, 0, ',', '.'),
|
||||
get: fn () => 'Rp '.number_format($this->nego_price, 0, ',', '.'),
|
||||
);
|
||||
}
|
||||
|
||||
@ -87,14 +87,14 @@ protected function statusLabel(): Attribute
|
||||
protected function formattedSubtotal(): Attribute
|
||||
{
|
||||
return Attribute::make(
|
||||
get: fn () => 'Rp ' . number_format($this->subtotal, 0, ',', '.'),
|
||||
get: fn () => 'Rp '.number_format($this->subtotal, 0, ',', '.'),
|
||||
);
|
||||
}
|
||||
|
||||
protected function formattedTotalAmount(): Attribute
|
||||
{
|
||||
return Attribute::make(
|
||||
get: fn () => 'Rp ' . number_format($this->total_amount, 0, ',', '.'),
|
||||
get: fn () => 'Rp '.number_format($this->total_amount, 0, ',', '.'),
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@ -39,14 +39,14 @@ protected function stockQualityLabel(): Attribute
|
||||
protected function formattedSubtotal(): Attribute
|
||||
{
|
||||
return Attribute::make(
|
||||
get: fn () => 'Rp ' . number_format($this->subtotal, 0, ',', '.'),
|
||||
get: fn () => 'Rp '.number_format($this->subtotal, 0, ',', '.'),
|
||||
);
|
||||
}
|
||||
|
||||
protected function formattedUnitPrice(): Attribute
|
||||
{
|
||||
return Attribute::make(
|
||||
get: fn () => 'Rp ' . number_format($this->unit_price, 0, ',', '.'),
|
||||
get: fn () => 'Rp '.number_format($this->unit_price, 0, ',', '.'),
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@ -35,28 +35,28 @@ protected function casts(): array
|
||||
protected function formattedBaseSalary(): Attribute
|
||||
{
|
||||
return Attribute::make(
|
||||
get: fn () => 'Rp ' . number_format($this->base_salary, 0, ',', '.'),
|
||||
get: fn () => 'Rp '.number_format($this->base_salary, 0, ',', '.'),
|
||||
);
|
||||
}
|
||||
|
||||
protected function formattedBonusAmount(): Attribute
|
||||
{
|
||||
return Attribute::make(
|
||||
get: fn () => 'Rp ' . number_format($this->bonus_amount, 0, ',', '.'),
|
||||
get: fn () => 'Rp '.number_format($this->bonus_amount, 0, ',', '.'),
|
||||
);
|
||||
}
|
||||
|
||||
protected function formattedDeductionAmount(): Attribute
|
||||
{
|
||||
return Attribute::make(
|
||||
get: fn () => 'Rp ' . number_format($this->deduction_amount, 0, ',', '.'),
|
||||
get: fn () => 'Rp '.number_format($this->deduction_amount, 0, ',', '.'),
|
||||
);
|
||||
}
|
||||
|
||||
protected function formattedAmount(): Attribute
|
||||
{
|
||||
return Attribute::make(
|
||||
get: fn () => 'Rp ' . number_format($this->attributes['total_amount'] ?? 0, 0, ',', '.'),
|
||||
get: fn () => 'Rp '.number_format($this->attributes['total_amount'] ?? 0, 0, ',', '.'),
|
||||
);
|
||||
}
|
||||
|
||||
@ -70,7 +70,7 @@ protected function statusLabel(): Attribute
|
||||
protected function formattedTotalAmount(): Attribute
|
||||
{
|
||||
return Attribute::make(
|
||||
get: fn () => 'Rp ' . number_format($this->total_amount, 0, ',', '.'),
|
||||
get: fn () => 'Rp '.number_format($this->total_amount, 0, ',', '.'),
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@ -30,7 +30,7 @@ protected function casts(): array
|
||||
protected function formattedAmount(): Attribute
|
||||
{
|
||||
return Attribute::make(
|
||||
get: fn () => 'Rp ' . number_format($this->amount, 0, ',', '.'),
|
||||
get: fn () => 'Rp '.number_format($this->amount, 0, ',', '.'),
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@ -39,7 +39,7 @@ protected function formattedName(): Attribute
|
||||
protected function statusLabel(): Attribute
|
||||
{
|
||||
return Attribute::make(
|
||||
get: fn () => $this->status->label(),
|
||||
get: fn () => $this->status?->label(),
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@ -29,7 +29,7 @@ protected function casts(): array
|
||||
protected function formattedPrice(): Attribute
|
||||
{
|
||||
return Attribute::make(
|
||||
get: fn () => 'Rp ' . number_format($this->price, 0, ',', '.'),
|
||||
get: fn () => 'Rp '.number_format($this->price, 0, ',', '.'),
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@ -2,8 +2,10 @@
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use App\Models\Scopes\ProductVariantScope;
|
||||
use Illuminate\Database\Eloquent\Attributes\Appends;
|
||||
use Illuminate\Database\Eloquent\Attributes\Guarded;
|
||||
use Illuminate\Database\Eloquent\Attributes\ScopedBy;
|
||||
use Illuminate\Database\Eloquent\Casts\Attribute;
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
@ -15,6 +17,7 @@
|
||||
|
||||
#[Appends(['formatted_name'])]
|
||||
#[Guarded(['id'])]
|
||||
#[ScopedBy([ProductVariantScope::class])]
|
||||
class ProductVariant extends Model implements HasMedia
|
||||
{
|
||||
use HasFactory, InteractsWithMedia, SoftDeletes;
|
||||
|
||||
@ -32,28 +32,28 @@ protected function casts(): array
|
||||
protected function formattedDiscount(): Attribute
|
||||
{
|
||||
return Attribute::make(
|
||||
get: fn () => 'Rp ' . number_format($this->discount, 0, ',', '.'),
|
||||
get: fn () => 'Rp '.number_format($this->discount, 0, ',', '.'),
|
||||
);
|
||||
}
|
||||
|
||||
protected function formattedShippingCost(): Attribute
|
||||
{
|
||||
return Attribute::make(
|
||||
get: fn () => 'Rp ' . number_format($this->shipping_cost, 0, ',', '.'),
|
||||
get: fn () => 'Rp '.number_format($this->shipping_cost, 0, ',', '.'),
|
||||
);
|
||||
}
|
||||
|
||||
protected function formattedSubtotal(): Attribute
|
||||
{
|
||||
return Attribute::make(
|
||||
get: fn () => 'Rp ' . number_format($this->subtotal, 0, ',', '.'),
|
||||
get: fn () => 'Rp '.number_format($this->subtotal, 0, ',', '.'),
|
||||
);
|
||||
}
|
||||
|
||||
protected function formattedTotal(): Attribute
|
||||
{
|
||||
return Attribute::make(
|
||||
get: fn () => 'Rp ' . number_format($this->total, 0, ',', '.'),
|
||||
get: fn () => 'Rp '.number_format($this->total, 0, ',', '.'),
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@ -28,14 +28,14 @@ protected function casts(): array
|
||||
protected function formattedSubtotal(): Attribute
|
||||
{
|
||||
return Attribute::make(
|
||||
get: fn () => 'Rp ' . number_format($this->subtotal, 0, ',', '.'),
|
||||
get: fn () => 'Rp '.number_format($this->subtotal, 0, ',', '.'),
|
||||
);
|
||||
}
|
||||
|
||||
protected function formattedUnitPrice(): Attribute
|
||||
{
|
||||
return Attribute::make(
|
||||
get: fn () => 'Rp ' . number_format($this->unit_price, 0, ',', '.'),
|
||||
get: fn () => 'Rp '.number_format($this->unit_price, 0, ',', '.'),
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@ -5,15 +5,15 @@
|
||||
use Illuminate\Database\Eloquent\Attributes\Guarded;
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
use Illuminate\Database\Eloquent\Relations\MorphTo;
|
||||
|
||||
#[Guarded(['id'])]
|
||||
class PushSubscription extends Model
|
||||
{
|
||||
use HasFactory;
|
||||
|
||||
public function user(): BelongsTo
|
||||
public function user(): MorphTo
|
||||
{
|
||||
return $this->belongsTo(User::class);
|
||||
return $this->morphTo();
|
||||
}
|
||||
}
|
||||
|
||||
@ -41,6 +41,18 @@ protected function unitLabel(): Attribute
|
||||
);
|
||||
}
|
||||
|
||||
#[Scope]
|
||||
protected function active(Builder $query): void
|
||||
{
|
||||
$query->where('is_active', true);
|
||||
}
|
||||
|
||||
#[Scope]
|
||||
protected function nonactive(Builder $query): void
|
||||
{
|
||||
$query->where('is_active', false);
|
||||
}
|
||||
|
||||
#[Scope]
|
||||
protected function kg(Builder $query): void
|
||||
{
|
||||
|
||||
@ -2,9 +2,11 @@
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use App\Models\Scopes\RawMaterialPriceScope;
|
||||
use App\Services\S3PresignedService;
|
||||
use Illuminate\Database\Eloquent\Attributes\Appends;
|
||||
use Illuminate\Database\Eloquent\Attributes\Guarded;
|
||||
use Illuminate\Database\Eloquent\Attributes\ScopedBy;
|
||||
use Illuminate\Database\Eloquent\Casts\Attribute;
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
@ -16,6 +18,7 @@
|
||||
|
||||
#[Appends(['formatted_price'])]
|
||||
#[Guarded(['id'])]
|
||||
#[ScopedBy([RawMaterialPriceScope::class])]
|
||||
class RawMaterialPrice extends Model implements HasMedia
|
||||
{
|
||||
use HasFactory, InteractsWithMedia, SoftDeletes;
|
||||
@ -31,7 +34,7 @@ protected function casts(): array
|
||||
protected function formattedPrice(): Attribute
|
||||
{
|
||||
return Attribute::make(
|
||||
get: fn () => 'Rp ' . number_format($this->price, 0, ',', '.'),
|
||||
get: fn () => 'Rp '.number_format($this->price, 0, ',', '.'),
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@ -41,14 +41,14 @@ protected function stockTypeLabel(): Attribute
|
||||
protected function formattedSubtotal(): Attribute
|
||||
{
|
||||
return Attribute::make(
|
||||
get: fn () => 'Rp ' . number_format($this->subtotal, 0, ',', '.'),
|
||||
get: fn () => 'Rp '.number_format($this->subtotal, 0, ',', '.'),
|
||||
);
|
||||
}
|
||||
|
||||
protected function formattedTotal(): Attribute
|
||||
{
|
||||
return Attribute::make(
|
||||
get: fn () => 'Rp ' . number_format($this->total, 0, ',', '.'),
|
||||
get: fn () => 'Rp '.number_format($this->total, 0, ',', '.'),
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@ -28,14 +28,14 @@ protected function casts(): array
|
||||
protected function formattedSubtotal(): Attribute
|
||||
{
|
||||
return Attribute::make(
|
||||
get: fn () => 'Rp ' . number_format($this->subtotal, 0, ',', '.'),
|
||||
get: fn () => 'Rp '.number_format($this->subtotal, 0, ',', '.'),
|
||||
);
|
||||
}
|
||||
|
||||
protected function formattedUnitPrice(): Attribute
|
||||
{
|
||||
return Attribute::make(
|
||||
get: fn () => 'Rp ' . number_format($this->unit_price, 0, ',', '.'),
|
||||
get: fn () => 'Rp '.number_format($this->unit_price, 0, ',', '.'),
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
15
app/Models/Scopes/ProductVariantScope.php
Normal file
15
app/Models/Scopes/ProductVariantScope.php
Normal file
@ -0,0 +1,15 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models\Scopes;
|
||||
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Scope;
|
||||
|
||||
class ProductVariantScope implements Scope
|
||||
{
|
||||
public function apply(Builder $builder, Model $model): void
|
||||
{
|
||||
$builder->orderBy('name');
|
||||
}
|
||||
}
|
||||
15
app/Models/Scopes/RawMaterialPriceScope.php
Normal file
15
app/Models/Scopes/RawMaterialPriceScope.php
Normal file
@ -0,0 +1,15 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models\Scopes;
|
||||
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Scope;
|
||||
|
||||
class RawMaterialPriceScope implements Scope
|
||||
{
|
||||
public function apply(Builder $builder, Model $model): void
|
||||
{
|
||||
$builder->orderBy('variant');
|
||||
}
|
||||
}
|
||||
@ -27,7 +27,7 @@ protected function formattedPhoneNumber(): Attribute
|
||||
{
|
||||
return Attribute::make(
|
||||
get: fn () => $this->phone_number
|
||||
? substr($this->phone_number, 0, 4) . ' ' . substr($this->phone_number, 4, 4) . ' ' . substr($this->phone_number, 8)
|
||||
? substr($this->phone_number, 0, 4).' '.substr($this->phone_number, 4, 4).' '.substr($this->phone_number, 8)
|
||||
: null,
|
||||
set: fn (?string $value) => $value ? preg_replace('/\s/', '', $value) : null,
|
||||
);
|
||||
|
||||
@ -181,11 +181,6 @@ public function purchaseItems(): HasMany
|
||||
return $this->hasMany(PurchaseItem::class);
|
||||
}
|
||||
|
||||
public function pushSubscriptions(): HasMany
|
||||
{
|
||||
return $this->hasMany(PushSubscription::class);
|
||||
}
|
||||
|
||||
public function rejections(): HasMany
|
||||
{
|
||||
return $this->hasMany(Rejection::class, 'rejected_by_id');
|
||||
|
||||
@ -45,7 +45,7 @@ protected function formattedPhoneNumber(): Attribute
|
||||
{
|
||||
return Attribute::make(
|
||||
get: fn () => $this->phone_number
|
||||
? substr($this->phone_number, 0, 4) . ' ' . substr($this->phone_number, 4, 4) . ' ' . substr($this->phone_number, 8)
|
||||
? substr($this->phone_number, 0, 4).' '.substr($this->phone_number, 4, 4).' '.substr($this->phone_number, 8)
|
||||
: null,
|
||||
set: fn (?string $value) => $value ? preg_replace('/\s/', '', $value) : null,
|
||||
);
|
||||
|
||||
@ -13,7 +13,7 @@
|
||||
class AdminSettingsService
|
||||
{
|
||||
public function __construct(
|
||||
private readonly S3PresignedService $s3Service,
|
||||
private S3PresignedService $s3Service,
|
||||
) {}
|
||||
|
||||
public function getSystemData(): array
|
||||
|
||||
@ -20,12 +20,12 @@ class CashAccountService
|
||||
use HandlesCashTransactions, RegistersMedia;
|
||||
|
||||
public function __construct(
|
||||
private readonly S3PresignedService $s3Service,
|
||||
private S3PresignedService $s3Service,
|
||||
) {}
|
||||
|
||||
public function get(): ?CashAccount
|
||||
{
|
||||
return CashAccount::select('id', 'name', 'balance')->first();
|
||||
return CashAccount::select(['id', 'name', 'balance'])->first();
|
||||
}
|
||||
|
||||
public function getAllTransactions(array $filters = []): Collection
|
||||
@ -37,7 +37,7 @@ public function getAllTransactions(array $filters = []): Collection
|
||||
}
|
||||
|
||||
return $cashAccount->cashTransactions()
|
||||
->select('id', 'created_by_id', 'reference_type', 'reference_id', 'amount', 'balance_after', 'type', 'description', 'created_at')
|
||||
->select(['id', 'created_by_id', 'reference_type', 'reference_id', 'amount', 'balance_after', 'type', 'description', 'created_at'])
|
||||
->with('createdBy.userProfile', 'media')
|
||||
->when($filters['type'] ?? null, function ($query, $type) {
|
||||
$query->where('type', $type);
|
||||
@ -56,7 +56,7 @@ public function paginatedTransactions(int $perPage = 25, string $search = '', st
|
||||
}
|
||||
|
||||
$paginator = $cashAccount->cashTransactions()
|
||||
->select('id', 'created_by_id', 'reference_type', 'reference_id', 'amount', 'balance_after', 'type', 'description', 'created_at')
|
||||
->select(['id', 'created_by_id', 'reference_type', 'reference_id', 'amount', 'balance_after', 'type', 'description', 'created_at'])
|
||||
->with('createdBy.userProfile', 'media')
|
||||
->when($search, fn ($q) => $q->where('description', 'like', "%{$search}%"))
|
||||
->when($filters['type'] ?? null, function ($query, $type) {
|
||||
|
||||
@ -2,10 +2,8 @@
|
||||
|
||||
namespace App\Services\Admin\Finance;
|
||||
|
||||
use App\Enums\CashTransactionType;
|
||||
use App\Enums\EmployeeAdvanceStatus;
|
||||
use App\Models\CashAccount;
|
||||
use App\Models\CashTransaction;
|
||||
use App\Models\EmployeeAdvance;
|
||||
use App\Services\Concerns\HandlesCashTransactions;
|
||||
use App\Services\NotificationService;
|
||||
@ -17,9 +15,10 @@
|
||||
class EmployeeAdvanceService
|
||||
{
|
||||
use HandlesCashTransactions;
|
||||
|
||||
public function getAll(array $filters = []): Collection
|
||||
{
|
||||
return EmployeeAdvance::select('id', 'employee_id', 'amount', 'paid_amount', 'description', 'due_date', 'status', 'created_at')
|
||||
return EmployeeAdvance::select(['id', 'employee_id', 'amount', 'paid_amount', 'description', 'due_date', 'status', 'created_at'])
|
||||
->with(['employee.user.userProfile'])
|
||||
->latest()
|
||||
->get();
|
||||
@ -28,7 +27,7 @@ public function getAll(array $filters = []): Collection
|
||||
public function paginated(int $perPage = 25, string $search = '', string $sort = 'created_at', string $direction = 'desc', array $filters = []): LengthAwarePaginator
|
||||
{
|
||||
return EmployeeAdvance::query()
|
||||
->select('id', 'employee_id', 'amount', 'paid_amount', 'description', 'due_date', 'status', 'created_at')
|
||||
->select(['id', 'employee_id', 'amount', 'paid_amount', 'description', 'due_date', 'status', 'created_at'])
|
||||
->with(['employee.user.userProfile'])
|
||||
->when($search, fn ($q) => $q->where('description', 'like', "%{$search}%"))
|
||||
->orderBy($sort, $direction)
|
||||
|
||||
@ -2,9 +2,7 @@
|
||||
|
||||
namespace App\Services\Admin\Finance;
|
||||
|
||||
use App\Enums\CashTransactionType;
|
||||
use App\Models\CashAccount;
|
||||
use App\Models\CashTransaction;
|
||||
use App\Models\Expense;
|
||||
use App\Services\Concerns\HandlesCashTransactions;
|
||||
use App\Services\Concerns\RegistersMedia;
|
||||
@ -21,12 +19,12 @@ class ExpenseService
|
||||
use HandlesCashTransactions, RegistersMedia;
|
||||
|
||||
public function __construct(
|
||||
private readonly S3PresignedService $s3Service,
|
||||
private S3PresignedService $s3Service,
|
||||
) {}
|
||||
|
||||
public function getAll(array $filters = []): Collection
|
||||
{
|
||||
return Expense::select('id', 'created_by_id', 'amount', 'description', 'created_at')
|
||||
return Expense::select(['id', 'created_by_id', 'amount', 'description', 'created_at'])
|
||||
->with('createdBy.userProfile', 'media')
|
||||
->latest()
|
||||
->get()
|
||||
@ -36,7 +34,7 @@ public function getAll(array $filters = []): Collection
|
||||
public function paginated(int $perPage = 25, string $search = '', string $sort = 'created_at', string $direction = 'desc', array $filters = []): LengthAwarePaginator
|
||||
{
|
||||
$paginator = Expense::query()
|
||||
->select('id', 'created_by_id', 'amount', 'description', 'created_at')
|
||||
->select(['id', 'created_by_id', 'amount', 'description', 'created_at'])
|
||||
->with('createdBy.userProfile', 'media')
|
||||
->when($search, fn ($q) => $q->where('description', 'like', "%{$search}%"))
|
||||
->orderBy($sort, $direction)
|
||||
|
||||
@ -2,11 +2,8 @@
|
||||
|
||||
namespace App\Services\Admin\Finance;
|
||||
|
||||
use App\Enums\CashTransactionType;
|
||||
use App\Enums\PayrollPeriodStatus;
|
||||
use App\Enums\PayrollStatus;
|
||||
use App\Models\CashAccount;
|
||||
use App\Models\CashTransaction;
|
||||
use App\Models\Payroll;
|
||||
use App\Models\PayrollPeriod;
|
||||
use App\Services\Concerns\HandlesCashTransactions;
|
||||
@ -19,9 +16,10 @@
|
||||
class PayrollPeriodService
|
||||
{
|
||||
use HandlesCashTransactions;
|
||||
|
||||
public function getAll(array $filters = []): Collection
|
||||
{
|
||||
return PayrollPeriod::select('id', 'year', 'month', 'status', 'closed_at', 'created_at')
|
||||
return PayrollPeriod::select(['id', 'year', 'month', 'status', 'closed_at', 'created_at'])
|
||||
->withCount('payrolls')
|
||||
->withSum('payrolls', 'total_amount')
|
||||
->withSum('payrolls', 'bonus_amount')
|
||||
@ -40,7 +38,7 @@ public function getAll(array $filters = []): Collection
|
||||
public function paginated(int $perPage = 25, string $search = '', string $sort = 'created_at', string $direction = 'desc', array $filters = []): LengthAwarePaginator
|
||||
{
|
||||
return PayrollPeriod::query()
|
||||
->select('id', 'year', 'month', 'status', 'closed_at', 'created_at')
|
||||
->select(['id', 'year', 'month', 'status', 'closed_at', 'created_at'])
|
||||
->withCount('payrolls')
|
||||
->withSum('payrolls', 'total_amount')
|
||||
->withSum('payrolls', 'bonus_amount')
|
||||
|
||||
@ -16,7 +16,7 @@ class AttendanceService
|
||||
use RegistersMedia;
|
||||
|
||||
public function __construct(
|
||||
private readonly S3PresignedService $s3Service,
|
||||
private S3PresignedService $s3Service,
|
||||
) {}
|
||||
|
||||
public function getAll(): Collection
|
||||
|
||||
@ -14,13 +14,13 @@ public function getAll(array $filters = []): Collection
|
||||
return User::select(['id', 'email', 'username', 'is_active'])
|
||||
->where(fn ($q) => $q->whereHas('employee')->orWhereHas('roles', fn ($rq) => $rq->where('name', 'Owner')))
|
||||
->with([
|
||||
'userProfile' => fn($q) => $q->select('id', 'user_id', 'full_name', 'phone_number', 'gender'),
|
||||
'employee' => fn($q) => $q->select('id', 'user_id', 'join_date', 'employment_status', 'base_salary'),
|
||||
'roles' => fn($q) => $q->select('id', 'name'),
|
||||
'userProfile' => fn ($q) => $q->select(['id', 'user_id', 'full_name', 'phone_number', 'gender']),
|
||||
'employee' => fn ($q) => $q->select(['id', 'user_id', 'join_date', 'employment_status', 'base_salary']),
|
||||
'roles' => fn ($q) => $q->select(['id', 'name']),
|
||||
])
|
||||
->when($filters['employment_status'] ?? null, fn($q, $status) => $q->whereHas('employee', fn($eq) => $eq->where('employment_status', $status)))
|
||||
->when(isset($filters['is_active']) && $filters['is_active'] !== '', fn($q) => $q->where('is_active', filter_var($filters['is_active'], FILTER_VALIDATE_BOOLEAN)))
|
||||
->when($filters['gender'] ?? null, fn($q, $gender) => $q->whereHas('userProfile', fn($uq) => $uq->where('gender', $gender)))
|
||||
->when($filters['employment_status'] ?? null, fn ($q, $status) => $q->whereHas('employee', fn ($eq) => $eq->where('employment_status', $status)))
|
||||
->when(isset($filters['is_active']) && $filters['is_active'] !== '', fn ($q) => $q->where('is_active', filter_var($filters['is_active'], FILTER_VALIDATE_BOOLEAN)))
|
||||
->when($filters['gender'] ?? null, fn ($q, $gender) => $q->whereHas('userProfile', fn ($uq) => $uq->where('gender', $gender)))
|
||||
->latest()
|
||||
->get();
|
||||
}
|
||||
@ -31,14 +31,14 @@ public function paginated(int $perPage = 25, string $search = '', string $sort =
|
||||
->select(['id', 'email', 'username', 'is_active'])
|
||||
->where(fn ($q) => $q->whereHas('employee')->orWhereHas('roles', fn ($rq) => $rq->where('name', 'Owner')))
|
||||
->with([
|
||||
'userProfile' => fn($q) => $q->select('id', 'user_id', 'full_name', 'phone_number', 'gender'),
|
||||
'employee' => fn($q) => $q->select('id', 'user_id', 'join_date', 'employment_status', 'base_salary'),
|
||||
'roles' => fn($q) => $q->select('id', 'name'),
|
||||
'userProfile' => fn ($q) => $q->select(['id', 'user_id', 'full_name', 'phone_number', 'gender']),
|
||||
'employee' => fn ($q) => $q->select(['id', 'user_id', 'join_date', 'employment_status', 'base_salary']),
|
||||
'roles' => fn ($q) => $q->select(['id', 'name']),
|
||||
])
|
||||
->when($search, fn($q) => $q->whereHas('userProfile', fn($uq) => $uq->where('full_name', 'like', "%{$search}%")))
|
||||
->when($filters['employment_status'] ?? null, fn($q, $status) => $q->whereHas('employee', fn($eq) => $eq->where('employment_status', $status)))
|
||||
->when(isset($filters['is_active']) && $filters['is_active'] !== '', fn($q) => $q->where('is_active', filter_var($filters['is_active'], FILTER_VALIDATE_BOOLEAN)))
|
||||
->when($filters['gender'] ?? null, fn($q, $gender) => $q->whereHas('userProfile', fn($uq) => $uq->where('gender', $gender)))
|
||||
->when($search, fn ($q) => $q->whereHas('userProfile', fn ($uq) => $uq->where('full_name', 'like', "%{$search}%")))
|
||||
->when($filters['employment_status'] ?? null, fn ($q, $status) => $q->whereHas('employee', fn ($eq) => $eq->where('employment_status', $status)))
|
||||
->when(isset($filters['is_active']) && $filters['is_active'] !== '', fn ($q) => $q->where('is_active', filter_var($filters['is_active'], FILTER_VALIDATE_BOOLEAN)))
|
||||
->when($filters['gender'] ?? null, fn ($q, $gender) => $q->whereHas('userProfile', fn ($uq) => $uq->where('gender', $gender)))
|
||||
->orderBy($sort, $direction)
|
||||
->paginate($perPage);
|
||||
}
|
||||
|
||||
@ -14,7 +14,7 @@ class LeaveRequestService
|
||||
{
|
||||
public function getAll(array $filters = []): Collection
|
||||
{
|
||||
return LeaveRequest::select('id', 'employee_id', 'start_date', 'end_date', 'total_days', 'status', 'created_at')
|
||||
return LeaveRequest::select(['id', 'employee_id', 'start_date', 'end_date', 'total_days', 'status', 'created_at'])
|
||||
->with(['employee.user.userProfile'])
|
||||
->when($filters['status'] ?? null, function ($query, $status) {
|
||||
$query->where('status', $status);
|
||||
@ -26,7 +26,7 @@ public function getAll(array $filters = []): Collection
|
||||
public function paginated(int $perPage = 25, string $search = '', string $sort = 'created_at', string $direction = 'desc', array $filters = []): LengthAwarePaginator
|
||||
{
|
||||
return LeaveRequest::query()
|
||||
->select('id', 'employee_id', 'start_date', 'end_date', 'total_days', 'status', 'created_at')
|
||||
->select(['id', 'employee_id', 'start_date', 'end_date', 'total_days', 'status', 'created_at'])
|
||||
->with(['employee.user.userProfile'])
|
||||
->when($search, fn ($q) => $q->whereHas('employee.user.userProfile', fn ($uq) => $uq->where('full_name', 'like', "%{$search}%")))
|
||||
->when($filters['status'] ?? null, function ($query, $status) {
|
||||
|
||||
@ -19,13 +19,13 @@ class CuttingService
|
||||
use RegistersMedia;
|
||||
|
||||
public function __construct(
|
||||
private readonly S3PresignedService $s3Service,
|
||||
private S3PresignedService $s3Service,
|
||||
) {}
|
||||
|
||||
public function paginated(int $perPage = 25, string $search = '', string $sort = 'created_at', string $direction = 'desc'): LengthAwarePaginator
|
||||
{
|
||||
$paginator = Cutting::query()
|
||||
->select('id', 'created_by_id', 'status', 'description', 'total_material_cost', 'cost_per_unit', 'created_at')
|
||||
->select(['id', 'created_by_id', 'status', 'description', 'total_material_cost', 'cost_per_unit', 'created_at'])
|
||||
->with([
|
||||
'createdBy:id',
|
||||
'createdBy.userProfile:id,user_id,full_name',
|
||||
@ -65,10 +65,11 @@ public function getForCreate(): array
|
||||
{
|
||||
return [
|
||||
'rawMaterials' => RawMaterial::query()
|
||||
->select('id', 'name', 'unit', 'is_active')
|
||||
->select(['id', 'name', 'unit', 'is_active'])
|
||||
->with([
|
||||
'rawMaterialPrices:id,raw_material_id,variant,price,stock',
|
||||
])
|
||||
->active()
|
||||
->orderBy('name')
|
||||
->get()
|
||||
->each(function (RawMaterial $rawMaterial) {
|
||||
@ -144,23 +145,23 @@ public function getForEdit(Cutting $cutting): array
|
||||
|
||||
public function create(array $data): Cutting
|
||||
{
|
||||
foreach ($data['materials'] as $materialData) {
|
||||
$usage = (int) ($materialData['material_usage'] ?? 0);
|
||||
if ($usage <= 0) {
|
||||
continue;
|
||||
}
|
||||
$price = RawMaterialPrice::find($materialData['raw_material_price_id']);
|
||||
if (! $price) {
|
||||
continue;
|
||||
}
|
||||
if ($usage > $price->stock) {
|
||||
throw ValidationException::withMessages([
|
||||
'materials' => "Stok {$price->variant} tidak mencukupi. Tersedia: {$price->stock}, dibutuhkan: {$usage}.",
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
return DB::transaction(function () use ($data) {
|
||||
foreach ($data['materials'] as $materialData) {
|
||||
$usage = (int) ($materialData['material_usage'] ?? 0);
|
||||
if ($usage <= 0) {
|
||||
continue;
|
||||
}
|
||||
$price = RawMaterialPrice::find($materialData['raw_material_price_id']);
|
||||
if (! $price) {
|
||||
continue;
|
||||
}
|
||||
if ($usage > $price->stock) {
|
||||
throw ValidationException::withMessages([
|
||||
'materials' => "Stok {$price->variant} tidak mencukupi. Tersedia: {$price->stock}, dibutuhkan: {$usage}.",
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
$cutting = Cutting::create([
|
||||
'created_by_id' => auth()->id(),
|
||||
'status' => 'in_progress',
|
||||
@ -246,31 +247,31 @@ public function create(array $data): Cutting
|
||||
|
||||
public function update(Cutting $cutting, array $data): Cutting
|
||||
{
|
||||
$cutting->load(['cuttingMaterials.rawMaterialPrice']);
|
||||
|
||||
foreach ($cutting->cuttingMaterials as $oldMaterial) {
|
||||
if ($oldMaterial->material_usage > 0 && $oldMaterial->rawMaterialPrice) {
|
||||
$oldMaterial->rawMaterialPrice->increment('stock', (int) $oldMaterial->material_usage);
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($data['materials'] as $materialData) {
|
||||
$usage = (int) ($materialData['material_usage'] ?? 0);
|
||||
if ($usage <= 0) {
|
||||
continue;
|
||||
}
|
||||
$price = RawMaterialPrice::find($materialData['raw_material_price_id']);
|
||||
if (! $price) {
|
||||
continue;
|
||||
}
|
||||
if ($usage > $price->stock) {
|
||||
throw ValidationException::withMessages([
|
||||
'materials' => "Stok {$price->variant} tidak mencukupi. Tersedia: {$price->stock}, dibutuhkan: {$usage}.",
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
return DB::transaction(function () use ($cutting, $data) {
|
||||
$cutting->load(['cuttingMaterials.rawMaterialPrice']);
|
||||
|
||||
foreach ($cutting->cuttingMaterials as $oldMaterial) {
|
||||
if ($oldMaterial->material_usage > 0 && $oldMaterial->rawMaterialPrice) {
|
||||
$oldMaterial->rawMaterialPrice->increment('stock', (int) $oldMaterial->material_usage);
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($data['materials'] as $materialData) {
|
||||
$usage = (int) ($materialData['material_usage'] ?? 0);
|
||||
if ($usage <= 0) {
|
||||
continue;
|
||||
}
|
||||
$price = RawMaterialPrice::find($materialData['raw_material_price_id']);
|
||||
if (! $price) {
|
||||
continue;
|
||||
}
|
||||
if ($usage > $price->stock) {
|
||||
throw ValidationException::withMessages([
|
||||
'materials' => "Stok {$price->variant} tidak mencukupi. Tersedia: {$price->stock}, dibutuhkan: {$usage}.",
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
$cutting->load(['cuttingMaterials', 'cuttingMaterialCombinations', 'cuttingResults']);
|
||||
|
||||
$cutting->cuttingResults()->delete();
|
||||
|
||||
@ -18,18 +18,20 @@ class PurchaseService
|
||||
use RegistersMedia;
|
||||
|
||||
public function __construct(
|
||||
private readonly S3PresignedService $s3Service,
|
||||
private S3PresignedService $s3Service,
|
||||
) {}
|
||||
|
||||
public function paginated(int $perPage = 25, string $search = '', string $sort = 'created_at', string $direction = 'desc'): LengthAwarePaginator
|
||||
public function paginated(int $perPage = 25, string $search = '', string $sort = 'created_at', string $direction = 'desc', array $filters = []): LengthAwarePaginator
|
||||
{
|
||||
$paginator = Purchase::query()
|
||||
->select('id', 'supplier_id', 'created_by_id', 'subtotal', 'discount', 'shipping_cost', 'total', 'notes', 'created_at')
|
||||
->select(['id', 'supplier_id', 'created_by_id', 'subtotal', 'discount', 'shipping_cost', 'total', 'notes', 'created_at'])
|
||||
->with([
|
||||
'supplier:id,name',
|
||||
'createdBy:id',
|
||||
'createdBy.userProfile:id,user_id,full_name',
|
||||
'purchaseItems:id,purchase_id,raw_material_price_id,quantity,unit_price,subtotal',
|
||||
'purchaseItems' => fn ($q) => $q
|
||||
->select(['id', 'purchase_id', 'raw_material_price_id', 'quantity', 'unit_price', 'subtotal'])
|
||||
->orderByRaw('(SELECT variant FROM raw_material_prices WHERE raw_material_prices.id = purchase_items.raw_material_price_id)'),
|
||||
'purchaseItems.rawMaterialPrice:id,raw_material_id,variant,price,stock',
|
||||
'purchaseItems.rawMaterialPrice.rawMaterial:id,name,unit',
|
||||
])
|
||||
@ -37,6 +39,7 @@ public function paginated(int $perPage = 25, string $search = '', string $sort =
|
||||
$q->whereHas('supplier', fn ($sq) => $sq->where('name', 'like', "%{$search}%"))
|
||||
->orWhere('notes', 'like', "%{$search}%");
|
||||
})
|
||||
->when($filters['supplier_id'] ?? null, fn ($q, $supplierId) => $q->where('supplier_id', $supplierId))
|
||||
->orderBy($sort, $direction)
|
||||
->paginate($perPage);
|
||||
|
||||
@ -64,9 +67,9 @@ public function paginated(int $perPage = 25, string $search = '', string $sort =
|
||||
public function getForCreate(): array
|
||||
{
|
||||
return [
|
||||
'suppliers' => Supplier::select('id', 'name')->latest()->get(),
|
||||
'suppliers' => Supplier::select(['id', 'name'])->latest()->get(),
|
||||
'rawMaterials' => RawMaterial::query()
|
||||
->select('id', 'name', 'unit', 'is_active')
|
||||
->select(['id', 'name', 'unit', 'is_active'])
|
||||
->with([
|
||||
'rawMaterialPrices:id,raw_material_id,variant,price,stock',
|
||||
])
|
||||
@ -86,6 +89,8 @@ public function getForCreate(): array
|
||||
public function getForEdit(Purchase $purchase): array
|
||||
{
|
||||
$purchase->load([
|
||||
'purchaseItems' => fn ($q) => $q
|
||||
->orderByRaw('(SELECT variant FROM raw_material_prices WHERE raw_material_prices.id = purchase_items.raw_material_price_id)'),
|
||||
'purchaseItems.rawMaterialPrice.rawMaterial',
|
||||
'supplier',
|
||||
]);
|
||||
|
||||
@ -20,17 +20,19 @@ class RestockService
|
||||
use HasStockAdjustment, RegistersMedia;
|
||||
|
||||
public function __construct(
|
||||
private readonly S3PresignedService $s3Service,
|
||||
private S3PresignedService $s3Service,
|
||||
) {}
|
||||
|
||||
public function paginated(int $perPage = 25, string $search = '', string $sort = 'created_at', string $direction = 'desc'): LengthAwarePaginator
|
||||
{
|
||||
$paginator = Restock::query()
|
||||
->select('id', 'created_by_id', 'subtotal', 'total', 'notes', 'stock_type', 'created_at')
|
||||
->select(['id', 'created_by_id', 'subtotal', 'total', 'notes', 'stock_type', 'created_at'])
|
||||
->with([
|
||||
'createdBy:id',
|
||||
'createdBy.userProfile:id,user_id,full_name',
|
||||
'restockItems:id,restock_id,product_variant_id,quantity,unit_price,subtotal',
|
||||
'restockItems' => fn ($q) => $q
|
||||
->select(['id', 'restock_id', 'product_variant_id', 'quantity', 'unit_price', 'subtotal'])
|
||||
->orderByRaw('(SELECT name FROM product_variants WHERE product_variants.id = restock_items.product_variant_id)'),
|
||||
'restockItems.productVariant:id,product_id,name,stock,reject_stock,retail_stock',
|
||||
'restockItems.productVariant.product:id,name',
|
||||
])
|
||||
@ -61,7 +63,7 @@ public function getForCreate(): array
|
||||
{
|
||||
return [
|
||||
'products' => Product::query()
|
||||
->select('id', 'name', 'status')
|
||||
->select(['id', 'name', 'status'])
|
||||
->with([
|
||||
'productVariants:id,product_id,name,stock,reject_stock',
|
||||
'productVariants.productPrices:id,variant_id,type,price',
|
||||
@ -89,7 +91,11 @@ public function getForCreate(): array
|
||||
|
||||
public function getForEdit(Restock $restock): array
|
||||
{
|
||||
$restock->load('restockItems.productVariant.product');
|
||||
$restock->load([
|
||||
'restockItems' => fn ($q) => $q
|
||||
->orderByRaw('(SELECT name FROM product_variants WHERE product_variants.id = restock_items.product_variant_id)'),
|
||||
'restockItems.productVariant.product',
|
||||
]);
|
||||
|
||||
$media = $restock->getFirstMedia('photos');
|
||||
|
||||
@ -235,5 +241,4 @@ private function buildItemRows(array $items, string $stockType, $now, int &$subt
|
||||
];
|
||||
})->toArray();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@ -35,13 +35,13 @@ class TransactionService
|
||||
];
|
||||
|
||||
public function __construct(
|
||||
private readonly S3PresignedService $s3Service,
|
||||
private S3PresignedService $s3Service,
|
||||
) {}
|
||||
|
||||
public function paginated(int $perPage = 25, string $search = '', string $sort = 'created_at', string $direction = 'desc', array $filters = []): LengthAwarePaginator
|
||||
{
|
||||
$paginator = Order::query()
|
||||
->select('id', 'created_by_id', 'customer_id', 'marketing_id', 'order_number', 'channel', 'price_type', 'status', 'payment_type', 'subtotal', 'discount', 'nego_price', 'total_amount', 'cogs', 'notes', 'created_at')
|
||||
->select(['id', 'created_by_id', 'customer_id', 'marketing_id', 'order_number', 'channel', 'price_type', 'status', 'payment_type', 'subtotal', 'discount', 'nego_price', 'total_amount', 'cogs', 'notes', 'created_at'])
|
||||
->with([
|
||||
'createdBy:id',
|
||||
'createdBy.userProfile:id,user_id,full_name',
|
||||
@ -96,7 +96,7 @@ public function getFilterOptions(): array
|
||||
'channelOptions' => OrderChannel::toSelect(),
|
||||
'paymentTypeOptions' => PaymentType::toSelect(),
|
||||
'customers' => Customer::query()
|
||||
->select('id', 'name')
|
||||
->select(['id', 'name'])
|
||||
->orderBy('name')
|
||||
->get(),
|
||||
'employees' => User::query()
|
||||
@ -118,7 +118,7 @@ public function getForCreate(): array
|
||||
{
|
||||
return [
|
||||
'products' => Product::query()
|
||||
->select('id', 'name', 'status')
|
||||
->select(['id', 'name', 'status'])
|
||||
->with([
|
||||
'productVariants:id,product_id,name,stock,reject_stock',
|
||||
'productVariants.productPrices:id,variant_id,type,price',
|
||||
@ -137,7 +137,7 @@ public function getForCreate(): array
|
||||
});
|
||||
}),
|
||||
'customers' => Customer::query()
|
||||
->select('id', 'name')
|
||||
->select(['id', 'name'])
|
||||
->orderBy('name')
|
||||
->get(),
|
||||
'employees' => User::query()
|
||||
|
||||
@ -10,13 +10,13 @@ class CategoryService
|
||||
{
|
||||
public function getAll(array $filters = []): Collection
|
||||
{
|
||||
return Category::select('id', 'name')->latest()->get();
|
||||
return Category::select(['id', 'name'])->latest()->get();
|
||||
}
|
||||
|
||||
public function paginated(int $perPage = 25, string $search = '', string $sort = 'created_at', string $direction = 'desc', array $filters = []): LengthAwarePaginator
|
||||
{
|
||||
return Category::query()
|
||||
->select('id', 'name')
|
||||
->select(['id', 'name'])
|
||||
->when($search, fn ($q) => $q->where('name', 'like', "%{$search}%"))
|
||||
->orderBy($sort, $direction)
|
||||
->paginate($perPage);
|
||||
|
||||
@ -10,13 +10,13 @@ class CustomerService
|
||||
{
|
||||
public function getAll(array $filters = []): Collection
|
||||
{
|
||||
return Customer::select('id', 'name', 'phone_number', 'address')->latest()->get();
|
||||
return Customer::select(['id', 'name', 'phone_number', 'address'])->latest()->get();
|
||||
}
|
||||
|
||||
public function paginated(int $perPage = 25, string $search = '', string $sort = 'created_at', string $direction = 'desc', array $filters = []): LengthAwarePaginator
|
||||
{
|
||||
return Customer::query()
|
||||
->select('id', 'name', 'phone_number', 'address')
|
||||
->select(['id', 'name', 'phone_number', 'address'])
|
||||
->when($search, fn ($q) => $q->where('name', 'like', "%{$search}%"))
|
||||
->orderBy($sort, $direction)
|
||||
->paginate($perPage);
|
||||
|
||||
@ -14,14 +14,14 @@
|
||||
class ProductService
|
||||
{
|
||||
public function __construct(
|
||||
private readonly ProductVariantService $variantService,
|
||||
private readonly S3PresignedService $s3Service,
|
||||
private readonly StockMutationService $stockMutationService,
|
||||
private ProductVariantService $variantService,
|
||||
private S3PresignedService $s3Service,
|
||||
private StockMutationService $stockMutationService,
|
||||
) {}
|
||||
|
||||
public function getAll(array $filters = []): Collection
|
||||
{
|
||||
$products = Product::select('id', 'name', 'slug', 'description', 'status')
|
||||
$products = Product::select(['id', 'name', 'slug', 'description', 'status'])
|
||||
->with([
|
||||
'categories:id,name',
|
||||
'productVariants:id,product_id,name,stock,reject_stock,retail_stock',
|
||||
@ -46,7 +46,7 @@ public function getAll(array $filters = []): Collection
|
||||
public function paginated(int $perPage = 25, string $search = '', string $sort = 'created_at', string $direction = 'desc', array $filters = []): LengthAwarePaginator
|
||||
{
|
||||
$paginator = Product::query()
|
||||
->select('id', 'name', 'slug', 'description', 'status')
|
||||
->select(['id', 'name', 'slug', 'description', 'status'])
|
||||
->with([
|
||||
'categories:id,name',
|
||||
'productVariants:id,product_id,name,stock,reject_stock,retail_stock',
|
||||
|
||||
@ -16,8 +16,8 @@ class ProductVariantService
|
||||
use RegistersMedia;
|
||||
|
||||
public function __construct(
|
||||
private readonly S3PresignedService $s3Service,
|
||||
private readonly StockMutationService $stockMutationService,
|
||||
private S3PresignedService $s3Service,
|
||||
private StockMutationService $stockMutationService,
|
||||
) {}
|
||||
|
||||
public function getForEdit(ProductVariant $variant): array
|
||||
|
||||
@ -15,12 +15,12 @@ class RawMaterialService
|
||||
use RegistersMedia;
|
||||
|
||||
public function __construct(
|
||||
private readonly S3PresignedService $s3Service,
|
||||
private S3PresignedService $s3Service,
|
||||
) {}
|
||||
|
||||
public function getAll(array $filters = []): Collection
|
||||
{
|
||||
return RawMaterial::select('id', 'name', 'unit', 'is_active')
|
||||
return RawMaterial::select(['id', 'name', 'unit', 'is_active'])
|
||||
->with([
|
||||
'rawMaterialPrices:id,raw_material_id,variant,price,stock',
|
||||
'rawMaterialPrices.media',
|
||||
@ -42,7 +42,7 @@ public function getAll(array $filters = []): Collection
|
||||
public function paginated(int $perPage = 25, string $search = '', string $sort = 'created_at', string $direction = 'desc', array $filters = []): LengthAwarePaginator
|
||||
{
|
||||
$paginator = RawMaterial::query()
|
||||
->select('id', 'name', 'unit', 'is_active')
|
||||
->select(['id', 'name', 'unit', 'is_active'])
|
||||
->with([
|
||||
'rawMaterialPrices:id,raw_material_id,variant,price,stock',
|
||||
])
|
||||
|
||||
@ -14,7 +14,7 @@ class RawMaterialVariantService
|
||||
use RegistersMedia;
|
||||
|
||||
public function __construct(
|
||||
private readonly S3PresignedService $s3Service,
|
||||
private S3PresignedService $s3Service,
|
||||
) {}
|
||||
|
||||
public function getForEdit(RawMaterialPrice $variant): array
|
||||
|
||||
@ -10,13 +10,13 @@ class SupplierService
|
||||
{
|
||||
public function getAll(array $filters = []): Collection
|
||||
{
|
||||
return Supplier::select('id', 'name', 'phone_number', 'address')->latest()->get();
|
||||
return Supplier::select(['id', 'name', 'phone_number', 'address'])->latest()->get();
|
||||
}
|
||||
|
||||
public function paginated(int $perPage = 25, string $search = '', string $sort = 'created_at', string $direction = 'desc', array $filters = []): LengthAwarePaginator
|
||||
{
|
||||
return Supplier::query()
|
||||
->select('id', 'name', 'phone_number', 'address')
|
||||
->select(['id', 'name', 'phone_number', 'address'])
|
||||
->when($search, fn ($q) => $q->where('name', 'like', "%{$search}%"))
|
||||
->orderBy($sort, $direction)
|
||||
->paginate($perPage);
|
||||
|
||||
@ -5,7 +5,6 @@
|
||||
use App\Enums\CashTransactionType;
|
||||
use App\Models\CashAccount;
|
||||
use App\Models\CashTransaction;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Validation\ValidationException;
|
||||
|
||||
trait HandlesCashTransactions
|
||||
|
||||
@ -3,6 +3,7 @@
|
||||
namespace App\Services\Concerns;
|
||||
|
||||
use App\Enums\ProductStockQuality;
|
||||
use App\Models\ProductVariant;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
trait HasStockAdjustment
|
||||
@ -26,7 +27,7 @@ private function adjustVariantStock(int $variantId, int $quantity, int $sign, st
|
||||
$field = self::QUALITY_STOCK_MAP[$stockType] ?? 'stock';
|
||||
|
||||
$this->adjustStock(
|
||||
model: app(\App\Models\ProductVariant::class)->newQuery()->findOrFail($variantId),
|
||||
model: app(ProductVariant::class)->newQuery()->findOrFail($variantId),
|
||||
field: $field,
|
||||
quantity: $quantity,
|
||||
sign: $sign,
|
||||
|
||||
@ -60,7 +60,7 @@ public function run(): void
|
||||
|
||||
$developerOwnerPerms = array_values(array_filter(
|
||||
$allPermissions,
|
||||
fn($p) => ! in_array($p, $excludedFromDeveloperOwner, true)
|
||||
fn ($p) => ! in_array($p, $excludedFromDeveloperOwner, true)
|
||||
));
|
||||
|
||||
$rolePermissions = [
|
||||
@ -241,11 +241,6 @@ public function run(): void
|
||||
'leave_requests.update',
|
||||
'leave_requests.delete',
|
||||
|
||||
'categories.view',
|
||||
'categories.create',
|
||||
'categories.update',
|
||||
'categories.delete',
|
||||
|
||||
'suppliers.view',
|
||||
'suppliers.create',
|
||||
'suppliers.update',
|
||||
@ -259,12 +254,6 @@ public function run(): void
|
||||
|
||||
'owner_verifications.view',
|
||||
|
||||
'products.view',
|
||||
'products.create',
|
||||
'products.update',
|
||||
'products.delete',
|
||||
'products.toggle_status',
|
||||
|
||||
'cuttings.view',
|
||||
'cuttings.create',
|
||||
'cuttings.update',
|
||||
|
||||
@ -140,6 +140,29 @@ @layer base {
|
||||
@apply border-border outline-ring/50;
|
||||
}
|
||||
|
||||
*::-webkit-scrollbar {
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
}
|
||||
|
||||
*::-webkit-scrollbar-track {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
*::-webkit-scrollbar-thumb {
|
||||
background-color: oklch(0.708 0 0 / 30%);
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
*::-webkit-scrollbar-thumb:hover {
|
||||
background-color: oklch(0.708 0 0 / 50%);
|
||||
}
|
||||
|
||||
* {
|
||||
scrollbar-width: thin;
|
||||
scrollbar-color: oklch(0.708 0 0 / 30%) transparent;
|
||||
}
|
||||
|
||||
body {
|
||||
@apply bg-background text-foreground;
|
||||
}
|
||||
|
||||
@ -1,3 +1,29 @@
|
||||
import { Link, router } from '@inertiajs/react';
|
||||
import type { LucideIcon } from 'lucide-react';
|
||||
import {
|
||||
Activity,
|
||||
ArrowUpFromLine,
|
||||
BarChart3,
|
||||
Boxes,
|
||||
CalendarCheck,
|
||||
CalendarDays,
|
||||
ClipboardCheck,
|
||||
DollarSign,
|
||||
HandCoins,
|
||||
LayoutGrid,
|
||||
Package,
|
||||
RefreshCw,
|
||||
Scissors,
|
||||
Settings,
|
||||
Shield,
|
||||
ShoppingCart,
|
||||
Tags,
|
||||
Truck,
|
||||
UserCircle,
|
||||
Users,
|
||||
Wallet,
|
||||
} from 'lucide-react';
|
||||
import React from 'react';
|
||||
import AppLogo from '@/components/app-logo';
|
||||
import {
|
||||
Sidebar,
|
||||
@ -30,32 +56,6 @@ import { index as productsIndex } from '@/routes/admin/master/products';
|
||||
import { index as rawMaterialsIndex } from '@/routes/admin/master/raw-materials';
|
||||
import { index as suppliersIndex } from '@/routes/admin/master/suppliers';
|
||||
import { index as rolesIndex } from '@/routes/admin/settings/roles';
|
||||
import { Link, router } from '@inertiajs/react';
|
||||
import type { LucideIcon } from 'lucide-react';
|
||||
import {
|
||||
Activity,
|
||||
ArrowUpFromLine,
|
||||
BarChart3,
|
||||
Boxes,
|
||||
CalendarCheck,
|
||||
CalendarDays,
|
||||
ClipboardCheck,
|
||||
DollarSign,
|
||||
HandCoins,
|
||||
LayoutGrid,
|
||||
Package,
|
||||
RefreshCw,
|
||||
Scissors,
|
||||
Settings,
|
||||
Shield,
|
||||
ShoppingCart,
|
||||
Tags,
|
||||
Truck,
|
||||
UserCircle,
|
||||
Users,
|
||||
Wallet,
|
||||
} from 'lucide-react';
|
||||
import React from 'react';
|
||||
|
||||
type NavMenuItem = { title: string; href: string; icon: LucideIcon; permission?: string | string[] };
|
||||
|
||||
@ -111,12 +111,20 @@ function MenuGroup({ label, items }: { label: string; items: NavMenuItem[] }) {
|
||||
const { can, canAny } = useCan();
|
||||
|
||||
const filtered = items.filter((item) => {
|
||||
if (!item.permission) return true;
|
||||
if (Array.isArray(item.permission)) return canAny(...item.permission);
|
||||
if (!item.permission) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (Array.isArray(item.permission)) {
|
||||
return canAny(...item.permission);
|
||||
}
|
||||
|
||||
return can(item.permission);
|
||||
});
|
||||
|
||||
if (filtered.length === 0) return null;
|
||||
if (filtered.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<SidebarGroup>
|
||||
|
||||
@ -53,6 +53,7 @@ interface CardTableProps<TData> {
|
||||
onPerPageChange?: (perPage: number) => void;
|
||||
|
||||
emptyText?: string;
|
||||
rowClassName?: (item: TData) => string;
|
||||
}
|
||||
|
||||
function useDebounce(callback: (value: string) => void, delay: number) {
|
||||
@ -87,6 +88,7 @@ export function CardTable<TData>({
|
||||
onPageChange,
|
||||
onPerPageChange,
|
||||
emptyText = 'Tidak ada data.',
|
||||
rowClassName,
|
||||
}: CardTableProps<TData>) {
|
||||
const [localSearch, setLocalSearch] = React.useState(searchValue ?? '');
|
||||
|
||||
@ -176,7 +178,7 @@ return true;
|
||||
const isExpanded = isItemExpanded(key);
|
||||
|
||||
return (
|
||||
<div key={key} className="flex flex-col">
|
||||
<div key={key} className={`flex flex-col ${rowClassName?.(item) ?? ''}`}>
|
||||
{renderCard({
|
||||
item,
|
||||
index,
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
import { Clock, LogIn, LogOut } from 'lucide-react';
|
||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
||||
|
||||
type TodayAttendance = {
|
||||
id: number;
|
||||
@ -29,8 +29,12 @@ export function AttendanceCard({ todayAttendance, isOnLeave, canCheckIn, onCheck
|
||||
const hasCheckedOut = !!todayAttendance?.check_out_at;
|
||||
|
||||
function formatTime(dateStr: string | null): string {
|
||||
if (!dateStr) return '-';
|
||||
if (!dateStr) {
|
||||
return '-';
|
||||
}
|
||||
|
||||
const d = new Date(dateStr);
|
||||
|
||||
return d.toLocaleTimeString('id-ID', { hour: '2-digit', minute: '2-digit', hour12: false });
|
||||
}
|
||||
|
||||
|
||||
@ -6,8 +6,14 @@ export function useCardTableExpand(
|
||||
defaultExpanded: boolean | (number | string)[] = false,
|
||||
) {
|
||||
const [expandedKeys, setExpandedKeys] = useState<ExpandState>(() => {
|
||||
if (defaultExpanded === true) return 'all';
|
||||
if (Array.isArray(defaultExpanded)) return new Set(defaultExpanded);
|
||||
if (defaultExpanded === true) {
|
||||
return 'all';
|
||||
}
|
||||
|
||||
if (Array.isArray(defaultExpanded)) {
|
||||
return new Set(defaultExpanded);
|
||||
}
|
||||
|
||||
return new Set();
|
||||
});
|
||||
|
||||
@ -16,12 +22,15 @@ export function useCardTableExpand(
|
||||
if (prev === 'all') {
|
||||
return new Set([key]);
|
||||
}
|
||||
|
||||
const next = new Set(prev);
|
||||
|
||||
if (next.has(key)) {
|
||||
next.delete(key);
|
||||
} else {
|
||||
next.add(key);
|
||||
}
|
||||
|
||||
return next;
|
||||
});
|
||||
}, []);
|
||||
@ -36,7 +45,10 @@ export function useCardTableExpand(
|
||||
|
||||
const isExpanded = useCallback(
|
||||
(key: number | string): boolean => {
|
||||
if (expandedKeys === 'all') return true;
|
||||
if (expandedKeys === 'all') {
|
||||
return true;
|
||||
}
|
||||
|
||||
return expandedKeys.has(key);
|
||||
},
|
||||
[expandedKeys],
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
import { router } from '@inertiajs/react';
|
||||
import { Bell, Check, Trash2 } from 'lucide-react';
|
||||
import { useCallback, useEffect, useRef, useState } from 'react';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
@ -189,15 +190,11 @@ export function NotificationBell() {
|
||||
: 'opacity-60'
|
||||
}`}
|
||||
>
|
||||
<a
|
||||
href={notification.url || '#'}
|
||||
<div
|
||||
className="min-w-0 flex-1 cursor-pointer"
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
|
||||
onClick={() => {
|
||||
if (notification.url) {
|
||||
window.location.href =
|
||||
notification.url;
|
||||
router.visit(notification.url);
|
||||
}
|
||||
}}
|
||||
>
|
||||
@ -225,7 +222,7 @@ export function NotificationBell() {
|
||||
minute: '2-digit',
|
||||
})}
|
||||
</span>
|
||||
</a>
|
||||
</div>
|
||||
<div className="flex shrink-0 flex-col gap-1 pt-0.5">
|
||||
{!notification.is_read && (
|
||||
<Button
|
||||
|
||||
@ -17,7 +17,10 @@ type PageProps = {
|
||||
};
|
||||
|
||||
function extractNames(items?: RoleOrPermission[]): string[] {
|
||||
if (!items) return [];
|
||||
if (!items) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return items.map((item) => (typeof item === 'string' ? item : item.name));
|
||||
}
|
||||
|
||||
@ -29,24 +32,42 @@ export function useCan() {
|
||||
const permissionNames = extractNames(user?.permissions);
|
||||
|
||||
function can(permission: string): boolean {
|
||||
if (!user) return false;
|
||||
if (roleNames.includes('developer') || roleNames.includes('owner')) return true;
|
||||
if (!user) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (roleNames.includes('developer') || roleNames.includes('owner')) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return permissionNames.includes(permission);
|
||||
}
|
||||
|
||||
function canAny(...permissions: string[]): boolean {
|
||||
if (!user) return false;
|
||||
if (roleNames.includes('developer') || roleNames.includes('owner')) return true;
|
||||
if (!user) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (roleNames.includes('developer') || roleNames.includes('owner')) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return permissions.some((p) => permissionNames.includes(p));
|
||||
}
|
||||
|
||||
function hasRole(role: string): boolean {
|
||||
if (!user) return false;
|
||||
if (!user) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return roleNames.includes(role);
|
||||
}
|
||||
|
||||
function hasAnyRole(roles: string[]): boolean {
|
||||
if (!user) return false;
|
||||
if (!user) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return roles.some((role) => roleNames.includes(role));
|
||||
}
|
||||
|
||||
|
||||
@ -6,12 +6,15 @@ export function formatRupiahShort(value: number): string {
|
||||
if (value >= 1_000_000_000) {
|
||||
return (value / 1_000_000_000).toFixed(1).replace('.0', '') + 'jt';
|
||||
}
|
||||
|
||||
if (value >= 1_000_000) {
|
||||
return (value / 1_000_000).toFixed(1).replace('.0', '') + 'jt';
|
||||
}
|
||||
|
||||
if (value >= 1_000) {
|
||||
return (value / 1_000).toFixed(0) + 'rb';
|
||||
}
|
||||
|
||||
return value.toString();
|
||||
}
|
||||
|
||||
|
||||
@ -12,12 +12,13 @@ export type CashAccount = {
|
||||
type CreateColumnsParams = {
|
||||
handleEdit: (cashAccount: CashAccount) => void;
|
||||
handleDeleteClick: (cashAccount: CashAccount) => void;
|
||||
can: (permission: string) => boolean;
|
||||
};
|
||||
|
||||
export function createCashAccountColumns(
|
||||
params: CreateColumnsParams,
|
||||
): ColumnDef<CashAccount>[] {
|
||||
const { handleEdit, handleDeleteClick } = params;
|
||||
const { handleEdit, handleDeleteClick, can } = params;
|
||||
|
||||
return [
|
||||
{
|
||||
@ -54,6 +55,7 @@ export function createCashAccountColumns(
|
||||
{
|
||||
label: 'Edit',
|
||||
icon: <Pencil className="h-4 w-4" />,
|
||||
show: can('cash.update'),
|
||||
onClick: () => handleEdit(cashAccount),
|
||||
},
|
||||
{
|
||||
@ -61,6 +63,7 @@ export function createCashAccountColumns(
|
||||
icon: (
|
||||
<Trash2 className="h-4 w-4 text-destructive" />
|
||||
),
|
||||
show: can('cash.delete'),
|
||||
onClick: () => handleDeleteClick(cashAccount),
|
||||
},
|
||||
]}
|
||||
|
||||
@ -21,6 +21,7 @@ import {
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from '@/components/ui/select';
|
||||
import { useCan } from '@/hooks/use-can';
|
||||
import { useServerTable } from '@/hooks/use-server-table';
|
||||
import { formatCurrency } from '@/lib/utils';
|
||||
import {
|
||||
@ -69,6 +70,7 @@ export default function CashAccountIndex({
|
||||
filters,
|
||||
filterOptions,
|
||||
}: Props) {
|
||||
const { can } = useCan();
|
||||
const [depositOpen, setDepositOpen] = useState(false);
|
||||
const [withdrawalOpen, setWithdrawalOpen] = useState(false);
|
||||
const [editing, setEditing] = useState<CashTransaction | null>(null);
|
||||
@ -137,6 +139,7 @@ export default function CashAccountIndex({
|
||||
setEditReceiptKey(transaction.receipt_key ?? null);
|
||||
},
|
||||
handleDeleteClick: (transaction) => setDeleting(transaction),
|
||||
can,
|
||||
});
|
||||
|
||||
const filterToolbar = (
|
||||
@ -180,20 +183,24 @@ export default function CashAccountIndex({
|
||||
title="Kas Toko"
|
||||
actions={
|
||||
<div className="flex items-center gap-2">
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={() => setDepositOpen(true)}
|
||||
>
|
||||
<ArrowDownToLine className="h-4 w-4" />
|
||||
Deposit
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={() => setWithdrawalOpen(true)}
|
||||
>
|
||||
<ArrowUpFromLine className="h-4 w-4" />
|
||||
Withdrawal
|
||||
</Button>
|
||||
{can('cash.deposit') && (
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={() => setDepositOpen(true)}
|
||||
>
|
||||
<ArrowDownToLine className="h-4 w-4" />
|
||||
Deposit
|
||||
</Button>
|
||||
)}
|
||||
{can('cash.withdraw') && (
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={() => setWithdrawalOpen(true)}
|
||||
>
|
||||
<ArrowUpFromLine className="h-4 w-4" />
|
||||
Withdrawal
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
|
||||
@ -50,12 +50,13 @@ function getReferenceLabel(type: string): string {
|
||||
type CreateColumnsParams = {
|
||||
handleEdit: (transaction: CashTransaction) => void;
|
||||
handleDeleteClick: (transaction: CashTransaction) => void;
|
||||
can: (permission: string) => boolean;
|
||||
};
|
||||
|
||||
export function createTransactionColumns(
|
||||
params: CreateColumnsParams,
|
||||
): ColumnDef<CashTransaction>[] {
|
||||
const { handleEdit, handleDeleteClick } = params;
|
||||
const { handleEdit, handleDeleteClick, can } = params;
|
||||
|
||||
return [
|
||||
{
|
||||
@ -174,6 +175,7 @@ export function createTransactionColumns(
|
||||
{
|
||||
label: 'Edit',
|
||||
icon: <Pencil className="h-4 w-4" />,
|
||||
show: can('cash.update'),
|
||||
onClick: () => handleEdit(transaction),
|
||||
},
|
||||
{
|
||||
@ -181,6 +183,7 @@ export function createTransactionColumns(
|
||||
icon: (
|
||||
<Trash2 className="h-4 w-4 text-destructive" />
|
||||
),
|
||||
show: can('cash.delete'),
|
||||
onClick: () => handleDeleteClick(transaction),
|
||||
},
|
||||
]}
|
||||
|
||||
@ -62,12 +62,14 @@ type CreateColumnsParams = {
|
||||
handleDeleteClick: (employeeAdvance: EmployeeAdvance) => void;
|
||||
handleApprove: (employeeAdvance: EmployeeAdvance) => void;
|
||||
handlePay: (employeeAdvance: EmployeeAdvance) => void;
|
||||
can: (permission: string) => boolean;
|
||||
};
|
||||
|
||||
export function createEmployeeAdvanceColumns(
|
||||
params: CreateColumnsParams,
|
||||
): ColumnDef<EmployeeAdvance>[] {
|
||||
const { handleEdit, handleDeleteClick, handleApprove, handlePay } = params;
|
||||
const { handleEdit, handleDeleteClick, handleApprove, handlePay, can } =
|
||||
params;
|
||||
|
||||
return [
|
||||
{
|
||||
@ -142,7 +144,9 @@ export function createEmployeeAdvanceColumns(
|
||||
icon: (
|
||||
<CheckCircle className="h-4 w-4 text-green-600" />
|
||||
),
|
||||
show: employeeAdvance.status === 'pending',
|
||||
show:
|
||||
can('employee_advances.verify') &&
|
||||
employeeAdvance.status === 'pending',
|
||||
onClick: () => handleApprove(employeeAdvance),
|
||||
},
|
||||
{
|
||||
@ -150,12 +154,15 @@ export function createEmployeeAdvanceColumns(
|
||||
icon: (
|
||||
<CircleDollarSign className="h-4 w-4 text-blue-600" />
|
||||
),
|
||||
show: employeeAdvance.status === 'approved',
|
||||
show:
|
||||
can('employee_advances.pay') &&
|
||||
employeeAdvance.status === 'approved',
|
||||
onClick: () => handlePay(employeeAdvance),
|
||||
},
|
||||
{
|
||||
label: 'Edit',
|
||||
icon: <Pencil className="h-4 w-4" />,
|
||||
show: can('employee_advances.update'),
|
||||
onClick: () => handleEdit(employeeAdvance),
|
||||
},
|
||||
{
|
||||
@ -163,6 +170,7 @@ export function createEmployeeAdvanceColumns(
|
||||
icon: (
|
||||
<Trash2 className="h-4 w-4 text-destructive" />
|
||||
),
|
||||
show: can('employee_advances.delete'),
|
||||
onClick: () =>
|
||||
handleDeleteClick(employeeAdvance),
|
||||
},
|
||||
|
||||
@ -12,6 +12,7 @@ import { RupiahInput } from '@/components/rupiah-input';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { Label } from '@/components/ui/label';
|
||||
import { useCan } from '@/hooks/use-can';
|
||||
import { useServerTable } from '@/hooks/use-server-table';
|
||||
import {
|
||||
destroy,
|
||||
@ -35,6 +36,7 @@ type Props = {
|
||||
};
|
||||
|
||||
export default function EmployeeAdvanceIndex({ employeeAdvances }: Props) {
|
||||
const { can } = useCan();
|
||||
const [createOpen, setCreateOpen] = useState(false);
|
||||
const [editing, setEditing] = useState<EmployeeAdvance | null>(null);
|
||||
const [deleting, setDeleting] = useState<EmployeeAdvance | null>(null);
|
||||
@ -113,6 +115,7 @@ export default function EmployeeAdvanceIndex({ employeeAdvances }: Props) {
|
||||
handleDeleteClick: (employeeAdvance) => setDeleting(employeeAdvance),
|
||||
handleApprove: (employeeAdvance) => setApproving(employeeAdvance),
|
||||
handlePay: (employeeAdvance) => setPaying(employeeAdvance),
|
||||
can,
|
||||
});
|
||||
|
||||
return (
|
||||
@ -123,15 +126,17 @@ export default function EmployeeAdvanceIndex({ employeeAdvances }: Props) {
|
||||
<PageHeader
|
||||
title="Kasbon"
|
||||
actions={
|
||||
<Button asChild>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setCreateOpen(true)}
|
||||
>
|
||||
<Plus className="h-4 w-4" />
|
||||
Tambah
|
||||
</button>
|
||||
</Button>
|
||||
can('employee_advances.create') ? (
|
||||
<Button asChild>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setCreateOpen(true)}
|
||||
>
|
||||
<Plus className="h-4 w-4" />
|
||||
Tambah
|
||||
</button>
|
||||
</Button>
|
||||
) : undefined
|
||||
}
|
||||
/>
|
||||
|
||||
|
||||
@ -22,12 +22,13 @@ export type Expense = {
|
||||
type CreateColumnsParams = {
|
||||
handleEdit: (expense: Expense) => void;
|
||||
handleDeleteClick: (expense: Expense) => void;
|
||||
can: (permission: string) => boolean;
|
||||
};
|
||||
|
||||
export function createExpenseColumns(
|
||||
params: CreateColumnsParams,
|
||||
): ColumnDef<Expense>[] {
|
||||
const { handleEdit, handleDeleteClick } = params;
|
||||
const { handleEdit, handleDeleteClick, can } = params;
|
||||
|
||||
return [
|
||||
{
|
||||
@ -98,6 +99,7 @@ export function createExpenseColumns(
|
||||
{
|
||||
label: 'Edit',
|
||||
icon: <Pencil className="h-4 w-4" />,
|
||||
show: can('expenses.update'),
|
||||
onClick: () => handleEdit(expense),
|
||||
},
|
||||
{
|
||||
@ -105,6 +107,7 @@ export function createExpenseColumns(
|
||||
icon: (
|
||||
<Trash2 className="h-4 w-4 text-destructive" />
|
||||
),
|
||||
show: can('expenses.delete'),
|
||||
onClick: () => handleDeleteClick(expense),
|
||||
},
|
||||
]}
|
||||
|
||||
@ -12,6 +12,7 @@ import { RupiahInput } from '@/components/rupiah-input';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { Label } from '@/components/ui/label';
|
||||
import { useCan } from '@/hooks/use-can';
|
||||
import { useServerTable } from '@/hooks/use-server-table';
|
||||
import {
|
||||
destroy,
|
||||
@ -33,6 +34,7 @@ type Props = {
|
||||
};
|
||||
|
||||
export default function ExpenseIndex({ expenses }: Props) {
|
||||
const { can } = useCan();
|
||||
const [createOpen, setCreateOpen] = useState(false);
|
||||
const [editing, setEditing] = useState<Expense | null>(null);
|
||||
const [deleting, setDeleting] = useState<Expense | null>(null);
|
||||
@ -84,6 +86,7 @@ export default function ExpenseIndex({ expenses }: Props) {
|
||||
setEditReceiptKey(expense.receipt_key ?? null);
|
||||
},
|
||||
handleDeleteClick: (expense) => setDeleting(expense),
|
||||
can,
|
||||
});
|
||||
|
||||
return (
|
||||
@ -94,15 +97,17 @@ export default function ExpenseIndex({ expenses }: Props) {
|
||||
<PageHeader
|
||||
title="Pengeluaran"
|
||||
actions={
|
||||
<Button asChild>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setCreateOpen(true)}
|
||||
>
|
||||
<Plus className="h-4 w-4" />
|
||||
Tambah
|
||||
</button>
|
||||
</Button>
|
||||
can('expenses.create') ? (
|
||||
<Button asChild>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setCreateOpen(true)}
|
||||
>
|
||||
<Plus className="h-4 w-4" />
|
||||
Tambah
|
||||
</button>
|
||||
</Button>
|
||||
) : undefined
|
||||
}
|
||||
/>
|
||||
|
||||
|
||||
@ -49,12 +49,13 @@ type CreateColumnsParams = {
|
||||
showUrl: (id: number) => string;
|
||||
handleClose: (period: PayrollPeriod) => void;
|
||||
handleReopen: (period: PayrollPeriod) => void;
|
||||
can: (permission: string) => boolean;
|
||||
};
|
||||
|
||||
export function createPayrollPeriodColumns(
|
||||
params: CreateColumnsParams,
|
||||
): ColumnDef<PayrollPeriod>[] {
|
||||
const { showUrl, handleClose, handleReopen } = params;
|
||||
const { showUrl, handleClose, handleReopen, can } = params;
|
||||
|
||||
return [
|
||||
{
|
||||
@ -191,7 +192,9 @@ export function createPayrollPeriodColumns(
|
||||
icon: (
|
||||
<Lock className="h-4 w-4 text-orange-600" />
|
||||
),
|
||||
show: period.status === 'open',
|
||||
show:
|
||||
can('payroll.adjust') &&
|
||||
period.status === 'open',
|
||||
onClick: () => handleClose(period),
|
||||
},
|
||||
{
|
||||
@ -199,7 +202,9 @@ export function createPayrollPeriodColumns(
|
||||
icon: (
|
||||
<Unlock className="h-4 w-4 text-blue-600" />
|
||||
),
|
||||
show: period.status === 'closed',
|
||||
show:
|
||||
can('payroll.adjust') &&
|
||||
period.status === 'closed',
|
||||
onClick: () => handleReopen(period),
|
||||
},
|
||||
]}
|
||||
|
||||
@ -4,6 +4,7 @@ import type { PaginationState } from '@/components/data-table';
|
||||
import { DataTable } from '@/components/data-table';
|
||||
import { DeleteConfirmDialog } from '@/components/delete-confirm-dialog';
|
||||
import { PageHeader } from '@/components/page-header';
|
||||
import { useCan } from '@/hooks/use-can';
|
||||
import { useServerTable } from '@/hooks/use-server-table';
|
||||
import { MONTH_NAMES } from '@/lib/constants';
|
||||
import {
|
||||
@ -26,6 +27,7 @@ type Props = {
|
||||
};
|
||||
|
||||
export default function PayrollPeriodIndex({ payrollPeriods }: Props) {
|
||||
const { can } = useCan();
|
||||
const [closing, setClosing] = useState<PayrollPeriod | null>(null);
|
||||
const [reopening, setReopening] = useState<PayrollPeriod | null>(null);
|
||||
|
||||
@ -78,6 +80,7 @@ export default function PayrollPeriodIndex({ payrollPeriods }: Props) {
|
||||
showUrl: (id) => payrollPeriodShow(id).url,
|
||||
handleClose: (period) => setClosing(period),
|
||||
handleReopen: (period) => setReopening(period),
|
||||
can,
|
||||
});
|
||||
|
||||
return (
|
||||
|
||||
@ -67,6 +67,7 @@ type CreateColumnsParams = {
|
||||
adjustment: PayrollAdjustment,
|
||||
payrollId: number,
|
||||
) => void;
|
||||
can: (permission: string) => boolean;
|
||||
};
|
||||
|
||||
export function createPayrollColumns(
|
||||
@ -77,6 +78,7 @@ export function createPayrollColumns(
|
||||
handleCancel,
|
||||
handleAddAdjustment,
|
||||
handleDeleteAdjustment,
|
||||
can,
|
||||
} = params;
|
||||
|
||||
return [
|
||||
@ -218,7 +220,9 @@ export function createPayrollColumns(
|
||||
{
|
||||
label: 'Tambah Adjustment',
|
||||
icon: <Pencil className="h-4 w-4" />,
|
||||
show: payroll.status === 'unpaid',
|
||||
show:
|
||||
can('payroll.adjust') &&
|
||||
payroll.status === 'unpaid',
|
||||
onClick: () => handleAddAdjustment(payroll),
|
||||
},
|
||||
{
|
||||
@ -226,7 +230,9 @@ export function createPayrollColumns(
|
||||
icon: (
|
||||
<CircleDollarSign className="h-4 w-4 text-green-600" />
|
||||
),
|
||||
show: payroll.status === 'unpaid',
|
||||
show:
|
||||
can('payroll.pay') &&
|
||||
payroll.status === 'unpaid',
|
||||
onClick: () => handlePay(payroll),
|
||||
},
|
||||
{
|
||||
@ -234,7 +240,9 @@ export function createPayrollColumns(
|
||||
icon: (
|
||||
<XCircle className="h-4 w-4 text-destructive" />
|
||||
),
|
||||
show: payroll.status === 'unpaid',
|
||||
show:
|
||||
can('payroll.cancel') &&
|
||||
payroll.status === 'unpaid',
|
||||
onClick: () => handleCancel(payroll),
|
||||
},
|
||||
]}
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import { Form, Head, router } from '@inertiajs/react';
|
||||
import { Form, Head, Link, router } from '@inertiajs/react';
|
||||
import { ArrowLeft } from 'lucide-react';
|
||||
import { useState } from 'react';
|
||||
import { ConfirmDialog } from '@/components/confirm-dialog';
|
||||
@ -16,6 +16,7 @@ import {
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { Label } from '@/components/ui/label';
|
||||
import { RadioGroup, RadioGroupItem } from '@/components/ui/radio-group';
|
||||
import { useCan } from '@/hooks/use-can';
|
||||
import { MONTH_NAMES } from '@/lib/constants';
|
||||
import { formatCurrency } from '@/lib/utils';
|
||||
import { destroy as adjustmentDestroy } from '@/routes/admin/finance/payroll-adjustments';
|
||||
@ -39,6 +40,7 @@ type Props = {
|
||||
};
|
||||
|
||||
export default function PayrollPeriodShow({ payrollPeriod }: Props) {
|
||||
const { can } = useCan();
|
||||
const [paying, setPaying] = useState<Payroll | null>(null);
|
||||
const [cancelling, setCancelling] = useState<Payroll | null>(null);
|
||||
const [addingAdjustment, setAddingAdjustment] = useState<Payroll | null>(
|
||||
@ -98,6 +100,7 @@ export default function PayrollPeriodShow({ payrollPeriod }: Props) {
|
||||
handleDeleteAdjustment: (adjustment, payrollId) => {
|
||||
setDeletingAdjustment({ adjustment, payrollId });
|
||||
},
|
||||
can,
|
||||
});
|
||||
|
||||
const totalBaseSalary = payrollPeriod.payrolls.reduce(
|
||||
@ -139,10 +142,10 @@ export default function PayrollPeriodShow({ payrollPeriod }: Props) {
|
||||
</p>
|
||||
</div>
|
||||
<Button asChild variant="outline">
|
||||
<a href={payrollPeriodsIndex.url()}>
|
||||
<Link href={payrollPeriodsIndex.url()}>
|
||||
<ArrowLeft className="h-4 w-4" />
|
||||
Kembali
|
||||
</a>
|
||||
</Link>
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
|
||||
@ -38,6 +38,7 @@ type CreateColumnsParams = {
|
||||
handleDeleteClick: (employee: Employee) => void;
|
||||
handleResetPassword: (employee: Employee) => void;
|
||||
toggleActiveUrl: (id: number) => string;
|
||||
can: (permission: string) => boolean;
|
||||
};
|
||||
|
||||
export function createEmployeeColumns(
|
||||
@ -48,6 +49,7 @@ export function createEmployeeColumns(
|
||||
handleDeleteClick,
|
||||
handleResetPassword,
|
||||
toggleActiveUrl,
|
||||
can,
|
||||
} = params;
|
||||
|
||||
return [
|
||||
@ -161,6 +163,7 @@ export function createEmployeeColumns(
|
||||
{
|
||||
label: 'Edit',
|
||||
icon: <Pencil className="h-4 w-4" />,
|
||||
show: can('employees.update'),
|
||||
onClick: () => handleEdit(employee),
|
||||
},
|
||||
{
|
||||
@ -168,6 +171,7 @@ export function createEmployeeColumns(
|
||||
icon: (
|
||||
<KeyRound className="h-4 w-4 text-muted-foreground" />
|
||||
),
|
||||
show: can('employees.reset_password'),
|
||||
onClick: () => handleResetPassword(employee),
|
||||
},
|
||||
{
|
||||
@ -175,6 +179,7 @@ export function createEmployeeColumns(
|
||||
icon: (
|
||||
<Trash2 className="h-4 w-4 text-destructive" />
|
||||
),
|
||||
show: can('employees.delete'),
|
||||
onClick: () => handleDeleteClick(employee),
|
||||
},
|
||||
]}
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import { Form, Head } from '@inertiajs/react';
|
||||
import { Form, Head, Link } from '@inertiajs/react';
|
||||
import { AlertCircle, ArrowLeft } from 'lucide-react';
|
||||
import { useState } from 'react';
|
||||
import { DatePicker } from '@/components/date-picker';
|
||||
@ -55,10 +55,10 @@ export default function EmployeeCreate({ roles }: Props) {
|
||||
</h2>
|
||||
</div>
|
||||
<Button asChild variant="outline">
|
||||
<a href={employeeIndex.url()}>
|
||||
<Link href={employeeIndex.url()}>
|
||||
<ArrowLeft className="h-4 w-4" />
|
||||
Kembali
|
||||
</a>
|
||||
</Link>
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import { Form, Head } from '@inertiajs/react';
|
||||
import { Form, Head, Link } from '@inertiajs/react';
|
||||
import { ArrowLeft } from 'lucide-react';
|
||||
import { useMemo, useState } from 'react';
|
||||
import { DatePicker } from '@/components/date-picker';
|
||||
@ -94,10 +94,10 @@ export default function EmployeeEdit({ employee, roles }: Props) {
|
||||
</h2>
|
||||
</div>
|
||||
<Button asChild variant="outline">
|
||||
<a href={employeeIndex.url()}>
|
||||
<Link href={employeeIndex.url()}>
|
||||
<ArrowLeft className="h-4 w-4" />
|
||||
Kembali
|
||||
</a>
|
||||
</Link>
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import { Head, router } from '@inertiajs/react';
|
||||
import { Head, Link, router } from '@inertiajs/react';
|
||||
import { Plus } from 'lucide-react';
|
||||
import { useState } from 'react';
|
||||
import type { PaginationState } from '@/components/data-table';
|
||||
@ -14,6 +14,7 @@ import {
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from '@/components/ui/select';
|
||||
import { useCan } from '@/hooks/use-can';
|
||||
import { useServerTable } from '@/hooks/use-server-table';
|
||||
import {
|
||||
destroy,
|
||||
@ -42,6 +43,7 @@ type Props = {
|
||||
};
|
||||
|
||||
export default function EmployeeIndex({ employees, filters }: Props) {
|
||||
const { can } = useCan();
|
||||
const [deleting, setDeleting] = useState<Employee | null>(null);
|
||||
const [resetPasswordTarget, setResetPasswordTarget] =
|
||||
useState<Employee | null>(null);
|
||||
@ -94,11 +96,12 @@ export default function EmployeeIndex({ employees, filters }: Props) {
|
||||
|
||||
const columns = createEmployeeColumns({
|
||||
handleEdit: (employee) => {
|
||||
window.location.href = employeeEdit.url(employee.id);
|
||||
router.visit(employeeEdit.url(employee.id));
|
||||
},
|
||||
handleDeleteClick: (employee) => setDeleting(employee),
|
||||
handleResetPassword: (employee) => setResetPasswordTarget(employee),
|
||||
toggleActiveUrl: (id) => toggleActive.url(id),
|
||||
can,
|
||||
});
|
||||
|
||||
const filterToolbar = (
|
||||
@ -183,12 +186,14 @@ export default function EmployeeIndex({ employees, filters }: Props) {
|
||||
<PageHeader
|
||||
title="Pegawai"
|
||||
actions={
|
||||
<Button asChild>
|
||||
<a href={employeeCreate.url()}>
|
||||
<Plus className="h-4 w-4" />
|
||||
Tambah
|
||||
</a>
|
||||
</Button>
|
||||
can('employees.create') ? (
|
||||
<Button asChild>
|
||||
<Link href={employeeCreate.url()}>
|
||||
<Plus className="h-4 w-4" />
|
||||
Tambah
|
||||
</Link>
|
||||
</Button>
|
||||
) : undefined
|
||||
}
|
||||
/>
|
||||
|
||||
|
||||
@ -55,12 +55,13 @@ type CreateColumnsParams = {
|
||||
handleDeleteClick: (leaveRequest: LeaveRequest) => void;
|
||||
handleApprove: (leaveRequest: LeaveRequest) => void;
|
||||
handleReject: (leaveRequest: LeaveRequest) => void;
|
||||
can: (permission: string) => boolean;
|
||||
};
|
||||
|
||||
export function createLeaveRequestColumns(
|
||||
params: CreateColumnsParams,
|
||||
): ColumnDef<LeaveRequest>[] {
|
||||
const { handleEdit, handleDeleteClick, handleApprove, handleReject } =
|
||||
const { handleEdit, handleDeleteClick, handleApprove, handleReject, can } =
|
||||
params;
|
||||
|
||||
return [
|
||||
@ -129,7 +130,9 @@ export function createLeaveRequestColumns(
|
||||
icon: (
|
||||
<CheckCircle className="h-4 w-4 text-green-600" />
|
||||
),
|
||||
show: leaveRequest.status === 'pending',
|
||||
show:
|
||||
can('leave_requests.verify') &&
|
||||
leaveRequest.status === 'pending',
|
||||
onClick: () => handleApprove(leaveRequest),
|
||||
},
|
||||
{
|
||||
@ -137,12 +140,15 @@ export function createLeaveRequestColumns(
|
||||
icon: (
|
||||
<XCircle className="h-4 w-4 text-red-600" />
|
||||
),
|
||||
show: leaveRequest.status === 'pending',
|
||||
show:
|
||||
can('leave_requests.verify') &&
|
||||
leaveRequest.status === 'pending',
|
||||
onClick: () => handleReject(leaveRequest),
|
||||
},
|
||||
{
|
||||
label: 'Edit',
|
||||
icon: <Pencil className="h-4 w-4" />,
|
||||
show: can('leave_requests.update'),
|
||||
onClick: () => handleEdit(leaveRequest),
|
||||
},
|
||||
{
|
||||
@ -150,6 +156,7 @@ export function createLeaveRequestColumns(
|
||||
icon: (
|
||||
<Trash2 className="h-4 w-4 text-destructive" />
|
||||
),
|
||||
show: can('leave_requests.delete'),
|
||||
onClick: () => handleDeleteClick(leaveRequest),
|
||||
},
|
||||
]}
|
||||
|
||||
@ -18,6 +18,7 @@ import {
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from '@/components/ui/select';
|
||||
import { useCan } from '@/hooks/use-can';
|
||||
import { useServerTable } from '@/hooks/use-server-table';
|
||||
import {
|
||||
approve,
|
||||
@ -52,6 +53,7 @@ type Props = {
|
||||
};
|
||||
|
||||
export default function LeaveRequestIndex({ leaveRequests, filters, filterOptions }: Props) {
|
||||
const { can } = useCan();
|
||||
const [createOpen, setCreateOpen] = useState(false);
|
||||
const [editing, setEditing] = useState<LeaveRequest | null>(null);
|
||||
const [deleting, setDeleting] = useState<LeaveRequest | null>(null);
|
||||
@ -141,6 +143,7 @@ export default function LeaveRequestIndex({ leaveRequests, filters, filterOption
|
||||
handleDeleteClick: (leaveRequest) => setDeleting(leaveRequest),
|
||||
handleApprove: (leaveRequest) => setApproving(leaveRequest),
|
||||
handleReject: (leaveRequest) => setRejecting(leaveRequest),
|
||||
can,
|
||||
});
|
||||
|
||||
const filterToolbar = (
|
||||
@ -181,15 +184,17 @@ export default function LeaveRequestIndex({ leaveRequests, filters, filterOption
|
||||
<PageHeader
|
||||
title="Cuti"
|
||||
actions={
|
||||
<Button asChild>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setCreateOpen(true)}
|
||||
>
|
||||
<Plus className="h-4 w-4" />
|
||||
Tambah
|
||||
</button>
|
||||
</Button>
|
||||
can('leave_requests.create') ? (
|
||||
<Button asChild>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setCreateOpen(true)}
|
||||
>
|
||||
<Plus className="h-4 w-4" />
|
||||
Tambah
|
||||
</button>
|
||||
</Button>
|
||||
) : undefined
|
||||
}
|
||||
/>
|
||||
|
||||
|
||||
@ -19,7 +19,7 @@ import { formatNumber } from '@/lib/format';
|
||||
import { getTemporaryUrl } from '@/lib/upload';
|
||||
import { formatCurrency } from '@/lib/utils';
|
||||
import { index as cuttingIndex, store } from '@/routes/admin/manage/cuttings';
|
||||
import { Form, Head, usePage } from '@inertiajs/react';
|
||||
import { Form, Head, Link, usePage } from '@inertiajs/react';
|
||||
import { ArrowLeft, Check, Layers, Plus, ShoppingCart, Trash2 } from 'lucide-react';
|
||||
import { useCallback, useMemo, useRef, useState } from 'react';
|
||||
import type { CuttingCreateData } from './columns';
|
||||
@ -45,7 +45,7 @@ type Props = {
|
||||
|
||||
export default function CuttingCreate({ data }: Props) {
|
||||
const { rawMaterials } = data;
|
||||
const { auth } = usePage().props as { auth: { user?: { id?: number } } };
|
||||
const { auth, errors } = usePage().props as { auth: { user?: { id?: number } }; errors: Record<string, string> };
|
||||
const userId = auth.user?.id;
|
||||
|
||||
const draft = loadCuttingDraft('create', userId);
|
||||
@ -63,6 +63,7 @@ export default function CuttingCreate({ data }: Props) {
|
||||
photo_url: m.photo_url,
|
||||
}));
|
||||
}
|
||||
|
||||
return [];
|
||||
});
|
||||
const [combinations, setCombinations] = useState<CombinationState[]>(() => {
|
||||
@ -71,6 +72,7 @@ export default function CuttingCreate({ data }: Props) {
|
||||
material_result: c.material_result ?? 0,
|
||||
}));
|
||||
}
|
||||
|
||||
return [];
|
||||
});
|
||||
const [selectedMaterialName, setSelectedMaterialName] = useState(draft?.selectedMaterialName ?? '');
|
||||
@ -149,12 +151,21 @@ export default function CuttingCreate({ data }: Props) {
|
||||
|
||||
const addVariant = useCallback(
|
||||
(priceId: number) => {
|
||||
if (!selectedMaterial) return;
|
||||
if (!selectedMaterial) {
|
||||
return;
|
||||
}
|
||||
|
||||
const price = selectedMaterial.raw_material_prices.find((p) => p.id === priceId);
|
||||
if (!price) return;
|
||||
|
||||
if (!price) {
|
||||
return;
|
||||
}
|
||||
|
||||
setMaterials((prev) => {
|
||||
if (prev.some((m) => m.raw_material_price_id === priceId && m.combination_id === null)) return prev;
|
||||
if (prev.some((m) => m.raw_material_price_id === priceId && m.combination_id === null)) {
|
||||
return prev;
|
||||
}
|
||||
|
||||
return [
|
||||
...prev,
|
||||
{
|
||||
@ -187,21 +198,26 @@ export default function CuttingCreate({ data }: Props) {
|
||||
}, []);
|
||||
|
||||
const confirmCombo = useCallback(() => {
|
||||
if (comboSelectedPriceIds.length < 2) return;
|
||||
if (comboSelectedPriceIds.length < 2) {
|
||||
return;
|
||||
}
|
||||
|
||||
const comboIndex = combinations.length;
|
||||
|
||||
const newMaterials: MaterialState[] = comboSelectedPriceIds.map((priceId) => {
|
||||
let foundMaterial: typeof rawMaterials[number] | undefined;
|
||||
let foundPrice: typeof rawMaterials[number]['raw_material_prices'][number] | undefined;
|
||||
|
||||
for (const rm of rawMaterials) {
|
||||
const p = rm.raw_material_prices.find((pp) => pp.id === priceId);
|
||||
|
||||
if (p) {
|
||||
foundMaterial = rm;
|
||||
foundPrice = p;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
raw_material_price_id: priceId,
|
||||
material_usage: 0,
|
||||
@ -238,6 +254,7 @@ export default function CuttingCreate({ data }: Props) {
|
||||
setMaterials((prev) => {
|
||||
const updated = [...prev];
|
||||
(updated[index] as Record<string, unknown>)[field] = value;
|
||||
|
||||
return updated;
|
||||
});
|
||||
},
|
||||
@ -251,6 +268,7 @@ export default function CuttingCreate({ data }: Props) {
|
||||
const totalMaterialCost = useMemo(() => {
|
||||
return materials.reduce((sum, m) => {
|
||||
const price = priceMap.get(m.raw_material_price_id);
|
||||
|
||||
return sum + (price ? price.price * m.material_usage : 0);
|
||||
}, 0);
|
||||
}, [materials, priceMap]);
|
||||
@ -266,9 +284,9 @@ export default function CuttingCreate({ data }: Props) {
|
||||
return {
|
||||
description: notes || null,
|
||||
product_name: productName || null,
|
||||
sample: sample || null,
|
||||
original_outside_sample: originalOutsideSample || null,
|
||||
cutting_result: cuttingResult || null,
|
||||
sample: sample,
|
||||
original_outside_sample: originalOutsideSample,
|
||||
cutting_result: cuttingResult,
|
||||
materials: materialsRef.current.map((m) => ({
|
||||
raw_material_price_id: m.raw_material_price_id,
|
||||
material_usage: m.material_usage,
|
||||
@ -290,14 +308,16 @@ export default function CuttingCreate({ data }: Props) {
|
||||
<div className="flex items-center justify-between">
|
||||
<h2 className="text-2xl font-semibold tracking-tight">Tambah Cutting</h2>
|
||||
<Button asChild variant="outline">
|
||||
<a href={cuttingIndex.url()}>
|
||||
<Link href={cuttingIndex.url()}>
|
||||
<ArrowLeft className="h-4 w-4" />
|
||||
Kembali
|
||||
</a>
|
||||
</Link>
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<Form action={store()} transform={(data) => ({ ...data, ...getPayload() })} onSubmit={() => { submittingRef.current = true; }}>
|
||||
<Form action={store()} transform={(data) => ({ ...data, ...getPayload() })} onSubmit={() => {
|
||||
submittingRef.current = true;
|
||||
}}>
|
||||
{({ errors, processing }) => (
|
||||
<div className="grid gap-6 md:grid-cols-3">
|
||||
<div className="space-y-6 md:col-span-2">
|
||||
@ -337,6 +357,7 @@ export default function CuttingCreate({ data }: Props) {
|
||||
{selectedMaterial.raw_material_prices.map((price) => {
|
||||
const isAdded = materials.some((m) => m.raw_material_price_id === price.id);
|
||||
const addedCount = materials.filter((m) => m.raw_material_price_id === price.id).length;
|
||||
|
||||
return (
|
||||
<div key={price.id} className={isAdded ? 'flex items-center justify-between gap-3 rounded-lg border border-primary p-3' : 'flex items-center justify-between gap-3 rounded-lg border p-3'}>
|
||||
<div className="flex min-w-0 items-center gap-3">
|
||||
@ -369,8 +390,6 @@ export default function CuttingCreate({ data }: Props) {
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
@ -421,7 +440,7 @@ export default function CuttingCreate({ data }: Props) {
|
||||
</div>
|
||||
{cuttingResult > 0 && (
|
||||
<div className="flex items-center justify-between text-sm">
|
||||
<span className="text-muted-foreground">Biaya Per Unit</span>
|
||||
<span className="text-muted-foreground">Biaya Per Produk</span>
|
||||
<span className="font-medium">{formatCurrency(costPerUnit)}</span>
|
||||
</div>
|
||||
)}
|
||||
@ -435,11 +454,13 @@ export default function CuttingCreate({ data }: Props) {
|
||||
|
||||
<div className="grid gap-2">
|
||||
<Label>Foto</Label>
|
||||
<FileUpload value={photo} onChange={(key) => { setPhoto(key); setPhotoUrl(key ? getTemporaryUrl(key) : null); }} folder="cutting" existingUrl={photoUrl} onUploadingChange={setUploading} />
|
||||
<FileUpload value={photo} onChange={(key) => {
|
||||
setPhoto(key); setPhotoUrl(key ? getTemporaryUrl(key) : null);
|
||||
}} folder="cutting" existingUrl={photoUrl} onUploadingChange={setUploading} />
|
||||
<InputError message={errors.photo_key} />
|
||||
</div>
|
||||
|
||||
<Button type="submit" className="w-full" disabled={processing || submittingRef.current || uploading || materials.length === 0 || !productName || !sample}>
|
||||
<Button type="submit" className="w-full" disabled={processing}>
|
||||
{processing ? 'Menyimpan...' : 'Simpan'}
|
||||
</Button>
|
||||
</CardContent>
|
||||
@ -474,7 +495,10 @@ export default function CuttingCreate({ data }: Props) {
|
||||
|
||||
materials.forEach((m, i) => {
|
||||
if (m.combination_id !== null) {
|
||||
if (!comboMap.has(m.combination_id)) comboMap.set(m.combination_id, []);
|
||||
if (!comboMap.has(m.combination_id)) {
|
||||
comboMap.set(m.combination_id, []);
|
||||
}
|
||||
|
||||
comboMap.get(m.combination_id)!.push({ m, index: i });
|
||||
} else {
|
||||
singleItems.push({ m, index: i });
|
||||
@ -487,28 +511,32 @@ export default function CuttingCreate({ data }: Props) {
|
||||
return groups.map((group, gi) => (
|
||||
<div key={gi} className="space-y-2 rounded-lg border p-3">
|
||||
{group.comboIndex !== null && (
|
||||
<div className="flex items-center justify-between border-b pb-2">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-xs font-semibold text-muted-foreground">Kombinasi {group.comboIndex + 1}</span>
|
||||
<span className="text-xs text-muted-foreground">·</span>
|
||||
<Label className="text-xs whitespace-nowrap">Hasil <span className="text-destructive">*</span></Label>
|
||||
<NumberInput
|
||||
className="w-20"
|
||||
value={combinations[group.comboIndex]?.material_result ?? 0}
|
||||
onValueChange={(val) => updateCombinationResult(group.comboIndex!, val)}
|
||||
/>
|
||||
<>
|
||||
<div className="flex items-center justify-between border-b pb-2">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-xs font-semibold text-muted-foreground">Kombinasi {group.comboIndex + 1}</span>
|
||||
<span className="text-xs text-muted-foreground">·</span>
|
||||
<Label className="text-xs whitespace-nowrap">Hasil <span className="text-destructive">*</span></Label>
|
||||
<NumberInput
|
||||
className="w-20"
|
||||
value={combinations[group.comboIndex]?.material_result ?? 0}
|
||||
onValueChange={(val) => updateCombinationResult(group.comboIndex!, val)}
|
||||
/>
|
||||
</div>
|
||||
<Button type="button" variant="ghost" size="icon-sm" onClick={() => {
|
||||
setComboDeleteIndex(group.comboIndex);
|
||||
setComboDeleteConfirmOpen(true);
|
||||
}}>
|
||||
<Trash2 className="h-4 w-4 text-destructive" />
|
||||
</Button>
|
||||
</div>
|
||||
<Button type="button" variant="ghost" size="icon-sm" onClick={() => {
|
||||
setComboDeleteIndex(group.comboIndex);
|
||||
setComboDeleteConfirmOpen(true);
|
||||
}}>
|
||||
<Trash2 className="h-4 w-4 text-destructive" />
|
||||
</Button>
|
||||
</div>
|
||||
<InputError message={errors[`combinations.${group.comboIndex}.material_result` as keyof typeof errors]} />
|
||||
</>
|
||||
)}
|
||||
{group.items.map(({ m, index }) => {
|
||||
const price = priceMap.get(m.raw_material_price_id);
|
||||
const cartKey = `material-${index}`;
|
||||
|
||||
return (
|
||||
<div key={cartKey} className="space-y-2">
|
||||
<div className="flex items-start justify-between gap-2">
|
||||
@ -542,6 +570,7 @@ export default function CuttingCreate({ data }: Props) {
|
||||
onValueChange={(val) => updateMaterial(index, 'material_usage', val)}
|
||||
/>
|
||||
</div>
|
||||
<InputError message={errors[`materials.${index}.material_usage` as keyof typeof errors]} />
|
||||
{group.comboIndex === null && (
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-xs text-muted-foreground whitespace-nowrap">Hasil <span className="text-destructive">*</span></span>
|
||||
@ -552,6 +581,7 @@ export default function CuttingCreate({ data }: Props) {
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
{group.comboIndex === null && <InputError message={errors[`materials.${index}.material_result` as keyof typeof errors]} />}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
@ -571,7 +601,11 @@ export default function CuttingCreate({ data }: Props) {
|
||||
|
||||
<ImagePreviewModal
|
||||
open={previewKey !== null}
|
||||
onOpenChange={(open) => { if (!open) setPreviewKey(null); }}
|
||||
onOpenChange={(open) => {
|
||||
if (!open) {
|
||||
setPreviewKey(null);
|
||||
}
|
||||
}}
|
||||
src={previewKey && previewKey.startsWith('material-') ? (materials[Number(previewKey.replace('material-', ''))]?.photo_url ?? null) : null}
|
||||
title={previewKey && previewKey.startsWith('material-') ? `${materials[Number(previewKey.replace('material-', ''))]?.material_name} — ${materials[Number(previewKey.replace('material-', ''))]?.variant}` : undefined}
|
||||
sources={materials.filter((m) => m.photo_url).map((m) => m.photo_url!)}
|
||||
@ -619,8 +653,10 @@ export default function CuttingCreate({ data }: Props) {
|
||||
let variantName = '';
|
||||
let materialName = '';
|
||||
let photoUrl: string | null = null;
|
||||
|
||||
for (const rm of rawMaterials) {
|
||||
const p = rm.raw_material_prices.find((pp) => pp.id === priceId);
|
||||
|
||||
if (p) {
|
||||
variantName = p.variant;
|
||||
materialName = rm.name;
|
||||
@ -628,6 +664,7 @@ export default function CuttingCreate({ data }: Props) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div key={priceId} className="flex items-center justify-between gap-2 rounded-md border border-primary bg-primary/5 px-3 py-2">
|
||||
<div className="flex min-w-0 items-center gap-3">
|
||||
@ -657,6 +694,7 @@ export default function CuttingCreate({ data }: Props) {
|
||||
<div className="space-y-2">
|
||||
{comboMaterial.raw_material_prices.map((price) => {
|
||||
const isSelected = comboSelectedPriceIds.includes(price.id);
|
||||
|
||||
return (
|
||||
<div key={price.id} className={`flex items-center justify-between gap-3 rounded-lg border p-3 ${isSelected ? 'border-primary' : ''}`}>
|
||||
<div className="flex min-w-0 items-center gap-3">
|
||||
@ -689,11 +727,41 @@ export default function CuttingCreate({ data }: Props) {
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
<ConfirmDialog open={deleteConfirmOpen} onOpenChange={(open) => { if (!open) { setDeleteConfirmOpen(false); setDeleteMaterialIndex(null); } }} title="Hapus Bahan Baku" description="Apakah Anda yakin ingin menghapus bahan baku ini?" confirmLabel="Hapus" onConfirm={() => { if (deleteMaterialIndex !== null) setMaterials((prev) => prev.filter((_, i) => i !== deleteMaterialIndex)); setDeleteConfirmOpen(false); setDeleteMaterialIndex(null); }} />
|
||||
<ConfirmDialog open={deleteConfirmOpen} onOpenChange={(open) => {
|
||||
if (!open) {
|
||||
setDeleteConfirmOpen(false); setDeleteMaterialIndex(null);
|
||||
}
|
||||
}} title="Hapus Bahan Baku" description="Apakah Anda yakin ingin menghapus bahan baku ini?" confirmLabel="Hapus" onConfirm={() => {
|
||||
if (deleteMaterialIndex !== null) {
|
||||
setMaterials((prev) => prev.filter((_, i) => i !== deleteMaterialIndex));
|
||||
}
|
||||
|
||||
<ConfirmDialog open={cartDeleteConfirmOpen} onOpenChange={(open) => { if (!open) { setCartDeleteConfirmOpen(false); setCartDeleteIndex(null); } }} title="Hapus dari Keranjang" description="Apakah Anda yakin ingin menghapus item ini dari keranjang?" confirmLabel="Hapus" variant="destructive" onConfirm={() => { if (cartDeleteIndex !== null) setMaterials((prev) => prev.filter((_, i) => i !== cartDeleteIndex)); setCartDeleteConfirmOpen(false); setCartDeleteIndex(null); }} />
|
||||
setDeleteConfirmOpen(false); setDeleteMaterialIndex(null);
|
||||
}} />
|
||||
|
||||
<ConfirmDialog open={comboDeleteConfirmOpen} onOpenChange={(open) => { if (!open) { setComboDeleteConfirmOpen(false); setComboDeleteIndex(null); } }} title="Hapus Kombinasi" description="Apakah Anda yakin ingin menghapus kombinasi ini beserta semua bahannya?" confirmLabel="Hapus" variant="destructive" onConfirm={() => { if (comboDeleteIndex !== null) { setCombinations((prev) => prev.filter((_, i) => i !== comboDeleteIndex)); setMaterials((prev) => prev.filter((m) => m.combination_id !== comboDeleteIndex)); } setComboDeleteConfirmOpen(false); setComboDeleteIndex(null); }} />
|
||||
<ConfirmDialog open={cartDeleteConfirmOpen} onOpenChange={(open) => {
|
||||
if (!open) {
|
||||
setCartDeleteConfirmOpen(false); setCartDeleteIndex(null);
|
||||
}
|
||||
}} title="Hapus dari Keranjang" description="Apakah Anda yakin ingin menghapus item ini dari keranjang?" confirmLabel="Hapus" variant="destructive" onConfirm={() => {
|
||||
if (cartDeleteIndex !== null) {
|
||||
setMaterials((prev) => prev.filter((_, i) => i !== cartDeleteIndex));
|
||||
}
|
||||
|
||||
setCartDeleteConfirmOpen(false); setCartDeleteIndex(null);
|
||||
}} />
|
||||
|
||||
<ConfirmDialog open={comboDeleteConfirmOpen} onOpenChange={(open) => {
|
||||
if (!open) {
|
||||
setComboDeleteConfirmOpen(false); setComboDeleteIndex(null);
|
||||
}
|
||||
}} title="Hapus Kombinasi" description="Apakah Anda yakin ingin menghapus kombinasi ini beserta semua bahannya?" confirmLabel="Hapus" variant="destructive" onConfirm={() => {
|
||||
if (comboDeleteIndex !== null) {
|
||||
setCombinations((prev) => prev.filter((_, i) => i !== comboDeleteIndex)); setMaterials((prev) => prev.filter((m) => m.combination_id !== comboDeleteIndex));
|
||||
}
|
||||
|
||||
setComboDeleteConfirmOpen(false); setComboDeleteIndex(null);
|
||||
}} />
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user