Compare commits
No commits in common. "6ebcbc24c0733c1602a9ac8b4dbb95d2bd32cd67" and "1f395657545d793479028af2c7b7af978895d219" have entirely different histories.
6ebcbc24c0
...
1f39565754
@ -2,9 +2,7 @@
|
|||||||
|
|
||||||
namespace App\Http\Controllers\Admin\Finance;
|
namespace App\Http\Controllers\Admin\Finance;
|
||||||
|
|
||||||
use App\Enums\EmployeeAdvanceStatus;
|
|
||||||
use App\Http\Controllers\Controller;
|
use App\Http\Controllers\Controller;
|
||||||
use App\Http\Requests\Admin\Finance\EmployeeAdvancePaymentRequest;
|
|
||||||
use App\Http\Requests\Admin\Finance\EmployeeAdvanceRequest;
|
use App\Http\Requests\Admin\Finance\EmployeeAdvanceRequest;
|
||||||
use App\Http\Requests\PaginatedRequest;
|
use App\Http\Requests\PaginatedRequest;
|
||||||
use App\Models\EmployeeAdvance;
|
use App\Models\EmployeeAdvance;
|
||||||
@ -22,14 +20,7 @@ public function __construct(
|
|||||||
public function index(PaginatedRequest $request): Response
|
public function index(PaginatedRequest $request): Response
|
||||||
{
|
{
|
||||||
return Inertia::render('admin/finance/employee-advance/index', [
|
return Inertia::render('admin/finance/employee-advance/index', [
|
||||||
'employeeAdvances' => $this->service->paginated(
|
'employeeAdvances' => $this->service->paginated(...$request->validatedWithDefaults()),
|
||||||
...$request->validatedWithDefaults(),
|
|
||||||
filters: $request->only(['status']),
|
|
||||||
),
|
|
||||||
'filters' => $request->only(['status']),
|
|
||||||
'filterOptions' => [
|
|
||||||
'statusOptions' => EmployeeAdvanceStatus::toSelect(),
|
|
||||||
],
|
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -69,11 +60,11 @@ public function approve(EmployeeAdvance $employeeAdvance): RedirectResponse
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function pay(EmployeeAdvancePaymentRequest $request, EmployeeAdvance $employeeAdvance): RedirectResponse
|
public function pay(EmployeeAdvance $employeeAdvance): RedirectResponse
|
||||||
{
|
{
|
||||||
return $this->handleAction(
|
return $this->handleAction(
|
||||||
fn () => $this->service->pay($employeeAdvance, $request->validated('amount')),
|
fn () => $this->service->pay($employeeAdvance),
|
||||||
'Pembayaran kasbon berhasil.',
|
'Kasbon berhasil dibayar.',
|
||||||
'admin.finance.employee-advances.index'
|
'admin.finance.employee-advances.index'
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -26,19 +26,13 @@ public function index(PaginatedRequest $request): Response
|
|||||||
filters: $request->only(['employment_status', 'is_active', 'gender']),
|
filters: $request->only(['employment_status', 'is_active', 'gender']),
|
||||||
),
|
),
|
||||||
'filters' => $request->only(['employment_status', 'is_active', 'gender']),
|
'filters' => $request->only(['employment_status', 'is_active', 'gender']),
|
||||||
'canViewAll' => $this->service->canViewAll(),
|
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function create(): Response
|
public function create(): Response
|
||||||
{
|
{
|
||||||
$user = auth()->user();
|
|
||||||
|
|
||||||
return Inertia::render('admin/hr/employee/create', [
|
return Inertia::render('admin/hr/employee/create', [
|
||||||
'roles' => $this->service->canViewAll()
|
'roles' => Role::where('name', '!=', 'Developer')->get(['id', 'name']),
|
||||||
? Role::where('name', '!=', 'Developer')->get(['id', 'name'])
|
|
||||||
: Role::where('name', '=', $user->roles->first()?->name)->get(['id', 'name']),
|
|
||||||
'canViewAll' => $this->service->canViewAll(),
|
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -19,14 +19,13 @@ public function __construct(
|
|||||||
|
|
||||||
public function index(PaginatedRequest $request): Response
|
public function index(PaginatedRequest $request): Response
|
||||||
{
|
{
|
||||||
$filters = $request->only(['status', 'channel', 'payment_type', 'customer_id', 'marketing_id', 'created_by_id', 'date_from', 'date_to']);
|
$filters = $request->only(['status', 'channel', 'payment_type', 'customer_id', 'marketing_id', 'created_by_id']);
|
||||||
|
|
||||||
return Inertia::render('admin/manage/transaction/index', [
|
return Inertia::render('admin/manage/transaction/index', [
|
||||||
'transactions' => $this->service->paginated(
|
'transactions' => $this->service->paginated(
|
||||||
...$request->validatedWithDefaults(),
|
...$request->validatedWithDefaults(),
|
||||||
filters: $filters,
|
filters: $filters,
|
||||||
),
|
),
|
||||||
'summary' => $this->service->getSummary($filters),
|
|
||||||
'filters' => $filters,
|
'filters' => $filters,
|
||||||
'filterOptions' => $this->service->getFilterOptions(),
|
'filterOptions' => $this->service->getFilterOptions(),
|
||||||
]);
|
]);
|
||||||
@ -76,13 +75,4 @@ public function destroy(Order $transaction): RedirectResponse
|
|||||||
'admin.manage.transactions.index'
|
'admin.manage.transactions.index'
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function updateStatus(Order $transaction): RedirectResponse
|
|
||||||
{
|
|
||||||
return $this->handleAction(
|
|
||||||
fn () => $this->service->updateStatus($transaction, request('status')),
|
|
||||||
'Status transaksi berhasil diperbarui.',
|
|
||||||
'admin.manage.transactions.index'
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -24,11 +24,10 @@ public function index(PaginatedRequest $request): Response
|
|||||||
return Inertia::render('admin/master/product/index', [
|
return Inertia::render('admin/master/product/index', [
|
||||||
'products' => $this->service->paginated(
|
'products' => $this->service->paginated(
|
||||||
...$request->validatedWithDefaults(),
|
...$request->validatedWithDefaults(),
|
||||||
filters: $request->only(['status', 'stock', 'category', 'name']),
|
filters: $request->only(['status', 'stock', 'category']),
|
||||||
),
|
),
|
||||||
'categories' => $this->categoryService->getAll(),
|
'categories' => $this->categoryService->getAll(),
|
||||||
'productNames' => $this->service->getNames(),
|
'filters' => $request->only(['status', 'stock', 'category']),
|
||||||
'filters' => $request->only(['status', 'stock', 'category', 'name']),
|
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -15,7 +15,7 @@ protected function handleAction(callable $action, string $successMessage, string
|
|||||||
$action();
|
$action();
|
||||||
Inertia::flash('toast', ['type' => 'success', 'message' => $successMessage]);
|
Inertia::flash('toast', ['type' => 'success', 'message' => $successMessage]);
|
||||||
|
|
||||||
return to_route($redirectRoute, $parameters);
|
return to_route($redirectRoute);
|
||||||
} catch (ValidationException $e) {
|
} catch (ValidationException $e) {
|
||||||
$firstError = collect($e->errors())->flatten()->first();
|
$firstError = collect($e->errors())->flatten()->first();
|
||||||
Inertia::flash('toast', ['type' => 'error', 'message' => $firstError ?? 'Terjadi kesalahan.']);
|
Inertia::flash('toast', ['type' => 'error', 'message' => $firstError ?? 'Terjadi kesalahan.']);
|
||||||
|
|||||||
@ -1,37 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
namespace App\Http\Requests\Admin\Finance;
|
|
||||||
|
|
||||||
use App\Concerns\CurrencyStripping;
|
|
||||||
use Illuminate\Foundation\Http\FormRequest;
|
|
||||||
use Override;
|
|
||||||
|
|
||||||
class EmployeeAdvancePaymentRequest extends FormRequest
|
|
||||||
{
|
|
||||||
use CurrencyStripping;
|
|
||||||
|
|
||||||
public function authorize(): bool
|
|
||||||
{
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
#[Override]
|
|
||||||
public function prepareForValidation(): void
|
|
||||||
{
|
|
||||||
$this->merge($this->stripCurrencyDot($this->all(), 'amount'));
|
|
||||||
}
|
|
||||||
|
|
||||||
public function rules(): array
|
|
||||||
{
|
|
||||||
return [
|
|
||||||
'amount' => ['required', 'integer', 'min:1'],
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
public function attributes(): array
|
|
||||||
{
|
|
||||||
return [
|
|
||||||
'amount' => 'jumlah bayar',
|
|
||||||
];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -11,10 +11,9 @@
|
|||||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||||
use Illuminate\Database\Eloquent\Model;
|
use Illuminate\Database\Eloquent\Model;
|
||||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||||
use Illuminate\Database\Eloquent\Relations\HasMany;
|
|
||||||
use Illuminate\Database\Eloquent\SoftDeletes;
|
use Illuminate\Database\Eloquent\SoftDeletes;
|
||||||
|
|
||||||
#[Appends(['formatted_amount', 'formatted_created_at', 'formatted_due_date', 'formatted_paid_amount', 'formatted_remaining_amount', 'status_label'])]
|
#[Appends(['formatted_amount', 'formatted_created_at', 'formatted_due_date', 'formatted_paid_amount', 'status_label'])]
|
||||||
#[Guarded(['id'])]
|
#[Guarded(['id'])]
|
||||||
class EmployeeAdvance extends Model
|
class EmployeeAdvance extends Model
|
||||||
{
|
{
|
||||||
@ -60,13 +59,6 @@ protected function formattedPaidAmount(): Attribute
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected function formattedRemainingAmount(): Attribute
|
|
||||||
{
|
|
||||||
return Attribute::make(
|
|
||||||
get: fn () => 'Rp '.number_format($this->amount - $this->paid_amount, 0, ',', '.'),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
protected function statusLabel(): Attribute
|
protected function statusLabel(): Attribute
|
||||||
{
|
{
|
||||||
return Attribute::make(
|
return Attribute::make(
|
||||||
@ -114,11 +106,6 @@ public function employee(): BelongsTo
|
|||||||
return $this->belongsTo(Employee::class);
|
return $this->belongsTo(Employee::class);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function payments(): HasMany
|
|
||||||
{
|
|
||||||
return $this->hasMany(EmployeeAdvancePayment::class);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function paidBy(): BelongsTo
|
public function paidBy(): BelongsTo
|
||||||
{
|
{
|
||||||
return $this->belongsTo(User::class, 'paid_by_id');
|
return $this->belongsTo(User::class, 'paid_by_id');
|
||||||
|
|||||||
@ -1,54 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
namespace App\Models;
|
|
||||||
|
|
||||||
use Illuminate\Database\Eloquent\Attributes\Appends;
|
|
||||||
use Illuminate\Database\Eloquent\Attributes\Guarded;
|
|
||||||
use Illuminate\Database\Eloquent\Casts\Attribute;
|
|
||||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
|
||||||
use Illuminate\Database\Eloquent\Model;
|
|
||||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
|
||||||
|
|
||||||
#[Appends(['formatted_amount', 'formatted_paid_at'])]
|
|
||||||
#[Guarded(['id'])]
|
|
||||||
class EmployeeAdvancePayment extends Model
|
|
||||||
{
|
|
||||||
use HasFactory;
|
|
||||||
|
|
||||||
protected function casts(): array
|
|
||||||
{
|
|
||||||
return [
|
|
||||||
'amount' => 'integer',
|
|
||||||
'paid_at' => 'datetime',
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
protected function formattedAmount(): Attribute
|
|
||||||
{
|
|
||||||
return Attribute::make(
|
|
||||||
get: fn() => 'Rp ' . number_format($this->amount, 0, ',', '.'),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
protected function formattedPaidAt(): Attribute
|
|
||||||
{
|
|
||||||
return Attribute::make(
|
|
||||||
get: fn() => $this->paid_at?->translatedFormat('l, d F Y'),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function employeeAdvance(): BelongsTo
|
|
||||||
{
|
|
||||||
return $this->belongsTo(EmployeeAdvance::class);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function paidBy(): BelongsTo
|
|
||||||
{
|
|
||||||
return $this->belongsTo(User::class, 'paid_by_id');
|
|
||||||
}
|
|
||||||
|
|
||||||
public function cashTransaction(): BelongsTo
|
|
||||||
{
|
|
||||||
return $this->belongsTo(CashTransaction::class);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -15,7 +15,7 @@
|
|||||||
use Spatie\MediaLibrary\HasMedia;
|
use Spatie\MediaLibrary\HasMedia;
|
||||||
use Spatie\MediaLibrary\InteractsWithMedia;
|
use Spatie\MediaLibrary\InteractsWithMedia;
|
||||||
|
|
||||||
#[Appends(['formatted_name', 'formatted_stock'])]
|
#[Appends(['formatted_name'])]
|
||||||
#[Guarded(['id'])]
|
#[Guarded(['id'])]
|
||||||
#[ScopedBy([ProductVariantScope::class])]
|
#[ScopedBy([ProductVariantScope::class])]
|
||||||
class ProductVariant extends Model implements HasMedia
|
class ProductVariant extends Model implements HasMedia
|
||||||
@ -38,13 +38,6 @@ protected function formattedName(): Attribute
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected function formattedStock(): Attribute
|
|
||||||
{
|
|
||||||
return Attribute::make(
|
|
||||||
get: fn() => number_format($this->stock, 0, ',', '.'),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function orderItems(): HasMany
|
public function orderItems(): HasMany
|
||||||
{
|
{
|
||||||
return $this->hasMany(OrderItem::class);
|
return $this->hasMany(OrderItem::class);
|
||||||
|
|||||||
@ -3,8 +3,8 @@
|
|||||||
namespace App\Services\Admin\Finance;
|
namespace App\Services\Admin\Finance;
|
||||||
|
|
||||||
use App\Enums\EmployeeAdvanceStatus;
|
use App\Enums\EmployeeAdvanceStatus;
|
||||||
|
use App\Models\CashAccount;
|
||||||
use App\Models\EmployeeAdvance;
|
use App\Models\EmployeeAdvance;
|
||||||
use App\Models\EmployeeAdvancePayment;
|
|
||||||
use App\Services\Concerns\HandlesCashTransactions;
|
use App\Services\Concerns\HandlesCashTransactions;
|
||||||
use App\Services\NotificationService;
|
use App\Services\NotificationService;
|
||||||
use Illuminate\Contracts\Pagination\LengthAwarePaginator;
|
use Illuminate\Contracts\Pagination\LengthAwarePaginator;
|
||||||
@ -16,16 +16,10 @@ class EmployeeAdvanceService
|
|||||||
{
|
{
|
||||||
use HandlesCashTransactions;
|
use HandlesCashTransactions;
|
||||||
|
|
||||||
private function canViewAll(): bool
|
|
||||||
{
|
|
||||||
return auth()->user()->hasAnyRole(['developer', 'owner', 'direktur', 'admin-toko']);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function getAll(array $filters = []): Collection
|
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', 'payments.paidBy.userProfile'])
|
->with(['employee.user.userProfile'])
|
||||||
->when(! $this->canViewAll(), fn ($q) => $q->whereHas('employee', fn ($eq) => $eq->where('user_id', auth()->id())))
|
|
||||||
->latest()
|
->latest()
|
||||||
->get();
|
->get();
|
||||||
}
|
}
|
||||||
@ -34,9 +28,7 @@ public function paginated(int $perPage = 25, string $search = '', string $sort =
|
|||||||
{
|
{
|
||||||
return EmployeeAdvance::query()
|
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', 'payments.paidBy.userProfile'])
|
->with(['employee.user.userProfile'])
|
||||||
->when(! $this->canViewAll(), fn ($q) => $q->whereHas('employee', fn ($eq) => $eq->where('user_id', auth()->id())))
|
|
||||||
->when($filters['status'] ?? null, fn ($q) => $q->where('status', $filters['status']))
|
|
||||||
->when($search, fn ($q) => $q->where('description', 'like', "%{$search}%"))
|
->when($search, fn ($q) => $q->where('description', 'like', "%{$search}%"))
|
||||||
->orderBy($sort, $direction)
|
->orderBy($sort, $direction)
|
||||||
->paginate($perPage);
|
->paginate($perPage);
|
||||||
@ -44,19 +36,27 @@ public function paginated(int $perPage = 25, string $search = '', string $sort =
|
|||||||
|
|
||||||
public function create(array $data): EmployeeAdvance
|
public function create(array $data): EmployeeAdvance
|
||||||
{
|
{
|
||||||
|
$employeeAdvance = DB::transaction(function () use ($data) {
|
||||||
$employee = auth()->user()->employee;
|
$employee = auth()->user()->employee;
|
||||||
|
|
||||||
if (! $employee) {
|
if (! $employee) {
|
||||||
throw new \Exception('Anda tidak terdaftar sebagai karyawan.');
|
throw new \Exception('Anda tidak terdaftar sebagai karyawan.');
|
||||||
}
|
}
|
||||||
|
|
||||||
$employeeAdvance = EmployeeAdvance::create([
|
$cashTransaction = $this->debitCash(
|
||||||
|
amount: $data['amount'],
|
||||||
|
description: 'Kasbon: '.$data['description'],
|
||||||
|
);
|
||||||
|
|
||||||
|
return EmployeeAdvance::create([
|
||||||
|
'cash_transaction_id' => $cashTransaction->id,
|
||||||
'employee_id' => $employee->id,
|
'employee_id' => $employee->id,
|
||||||
'amount' => $data['amount'],
|
'amount' => $data['amount'],
|
||||||
'description' => $data['description'],
|
'description' => $data['description'],
|
||||||
'due_date' => $data['due_date'],
|
'due_date' => $data['due_date'],
|
||||||
'status' => EmployeeAdvanceStatus::PENDING,
|
'status' => EmployeeAdvanceStatus::PENDING,
|
||||||
]);
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
$employeeAdvance->load('employee.user');
|
$employeeAdvance->load('employee.user');
|
||||||
|
|
||||||
@ -73,36 +73,56 @@ public function create(array $data): EmployeeAdvance
|
|||||||
|
|
||||||
public function update(EmployeeAdvance $employeeAdvance, array $data): EmployeeAdvance
|
public function update(EmployeeAdvance $employeeAdvance, array $data): EmployeeAdvance
|
||||||
{
|
{
|
||||||
|
return DB::transaction(function () use ($employeeAdvance, $data) {
|
||||||
|
$cashAccount = CashAccount::firstOrFail();
|
||||||
|
$cashTransaction = $employeeAdvance->cashTransaction;
|
||||||
|
|
||||||
|
$oldAmount = $employeeAdvance->amount;
|
||||||
|
$newAmount = $data['amount'];
|
||||||
|
$difference = $newAmount - $oldAmount;
|
||||||
|
|
||||||
|
if ($difference > 0 && $cashAccount->balance < $difference) {
|
||||||
|
throw ValidationException::withMessages([
|
||||||
|
'amount' => 'Saldo tidak mencukupi.',
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
$newBalance = $cashAccount->balance - $difference;
|
||||||
|
$cashAccount->update(['balance' => $newBalance]);
|
||||||
|
|
||||||
|
$cashTransaction->update([
|
||||||
|
'amount' => $newAmount,
|
||||||
|
'balance_after' => $newBalance,
|
||||||
|
'description' => 'Kasbon: '.$data['description'],
|
||||||
|
]);
|
||||||
|
|
||||||
$employeeAdvance->update([
|
$employeeAdvance->update([
|
||||||
'amount' => $data['amount'],
|
'amount' => $newAmount,
|
||||||
'description' => $data['description'],
|
'description' => $data['description'],
|
||||||
'due_date' => $data['due_date'],
|
'due_date' => $data['due_date'],
|
||||||
]);
|
]);
|
||||||
|
|
||||||
return $employeeAdvance;
|
return $employeeAdvance;
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public function delete(EmployeeAdvance $employeeAdvance): bool
|
public function delete(EmployeeAdvance $employeeAdvance): bool
|
||||||
{
|
{
|
||||||
if ($employeeAdvance->status === EmployeeAdvanceStatus::APPROVED && $employeeAdvance->cash_transaction_id) {
|
return DB::transaction(function () use ($employeeAdvance) {
|
||||||
$cashAccount = $this->getCashAccount();
|
$cashAccount = CashAccount::firstOrFail();
|
||||||
|
|
||||||
|
if ($employeeAdvance->status === EmployeeAdvanceStatus::PAID) {
|
||||||
$newBalance = $cashAccount->balance + $employeeAdvance->amount;
|
$newBalance = $cashAccount->balance + $employeeAdvance->amount;
|
||||||
$cashAccount->update(['balance' => $newBalance]);
|
$cashAccount->update(['balance' => $newBalance]);
|
||||||
$employeeAdvance->cashTransaction()->delete();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return $employeeAdvance->delete();
|
return $employeeAdvance->delete();
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public function approve(EmployeeAdvance $employeeAdvance): EmployeeAdvance
|
public function approve(EmployeeAdvance $employeeAdvance): EmployeeAdvance
|
||||||
{
|
{
|
||||||
$cashTransaction = $this->debitCash(
|
|
||||||
amount: $employeeAdvance->amount,
|
|
||||||
description: 'Kasbon: '.$employeeAdvance->description,
|
|
||||||
);
|
|
||||||
|
|
||||||
$employeeAdvance->update([
|
$employeeAdvance->update([
|
||||||
'cash_transaction_id' => $cashTransaction->id,
|
|
||||||
'status' => EmployeeAdvanceStatus::APPROVED,
|
'status' => EmployeeAdvanceStatus::APPROVED,
|
||||||
'verified_by_id' => auth()->id(),
|
'verified_by_id' => auth()->id(),
|
||||||
'verified_at' => now(),
|
'verified_at' => now(),
|
||||||
@ -119,51 +139,29 @@ public function approve(EmployeeAdvance $employeeAdvance): EmployeeAdvance
|
|||||||
return $employeeAdvance;
|
return $employeeAdvance;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function pay(EmployeeAdvance $employeeAdvance, int $amount): EmployeeAdvance
|
public function pay(EmployeeAdvance $employeeAdvance): EmployeeAdvance
|
||||||
{
|
{
|
||||||
$remaining = $employeeAdvance->amount - $employeeAdvance->paid_amount;
|
$employeeAdvance = DB::transaction(function () use ($employeeAdvance) {
|
||||||
|
|
||||||
if ($amount > $remaining) {
|
|
||||||
throw ValidationException::withMessages([
|
|
||||||
'amount' => 'Jumlah bayar melebihi sisa kasbon.',
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
$employeeAdvance = DB::transaction(function () use ($employeeAdvance, $amount) {
|
|
||||||
$cashTransaction = $this->creditCash(
|
$cashTransaction = $this->creditCash(
|
||||||
amount: $amount,
|
amount: $employeeAdvance->amount,
|
||||||
description: 'Pembayaran kasbon: '.$employeeAdvance->description,
|
description: 'Pembayaran kasbon: '.$employeeAdvance->description,
|
||||||
);
|
);
|
||||||
|
|
||||||
EmployeeAdvancePayment::create([
|
|
||||||
'employee_advance_id' => $employeeAdvance->id,
|
|
||||||
'paid_by_id' => auth()->id(),
|
|
||||||
'cash_transaction_id' => $cashTransaction->id,
|
|
||||||
'amount' => $amount,
|
|
||||||
'paid_at' => now(),
|
|
||||||
]);
|
|
||||||
|
|
||||||
$newPaidAmount = $employeeAdvance->paid_amount + $amount;
|
|
||||||
$isFullyPaid = $newPaidAmount >= $employeeAdvance->amount;
|
|
||||||
|
|
||||||
$employeeAdvance->update([
|
$employeeAdvance->update([
|
||||||
'paid_amount' => $newPaidAmount,
|
'status' => EmployeeAdvanceStatus::PAID,
|
||||||
'status' => $isFullyPaid ? EmployeeAdvanceStatus::PAID : $employeeAdvance->status,
|
'paid_by_id' => auth()->id(),
|
||||||
'paid_by_id' => $isFullyPaid ? auth()->id() : $employeeAdvance->paid_by_id,
|
'paid_amount' => $employeeAdvance->amount,
|
||||||
'paid_at' => $isFullyPaid ? now() : $employeeAdvance->paid_at,
|
'paid_at' => now(),
|
||||||
|
'repayment_cash_transaction_id' => $cashTransaction->id,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
return $employeeAdvance;
|
return $employeeAdvance;
|
||||||
});
|
});
|
||||||
|
|
||||||
$notificationBody = $employeeAdvance->status === EmployeeAdvanceStatus::PAID
|
|
||||||
? 'Kasbon sebesar Rp '.number_format($employeeAdvance->amount, 0, ',', '.').' telah dibayar lunas'.' oleh '.auth()->user()->full_name.'.'
|
|
||||||
: 'Pembayaran kasbon sebesar Rp '.number_format($amount, 0, ',', '.').' oleh '.auth()->user()->full_name.'. Sisa: Rp '.number_format($employeeAdvance->amount - $employeeAdvance->paid_amount, 0, ',', '.').'.';
|
|
||||||
|
|
||||||
NotificationService::notify(
|
NotificationService::notify(
|
||||||
roles: ['Owner', 'Developer', 'Direktur', 'Admin Toko'],
|
roles: ['Owner', 'Developer', 'Direktur', 'Admin Toko'],
|
||||||
title: $employeeAdvance->status === EmployeeAdvanceStatus::PAID ? 'Kasbon Dibayar' : 'Pembayaran Kasbon',
|
title: 'Kasbon Dibayar',
|
||||||
body: $notificationBody,
|
body: 'Kasbon sebesar Rp '.number_format($employeeAdvance->amount, 0, ',', '.').' telah dibayar lunas'.' oleh '.auth()->user()->full_name.'.',
|
||||||
url: route('admin.finance.employee-advances.index'),
|
url: route('admin.finance.employee-advances.index'),
|
||||||
additionalUser: $employeeAdvance->employee->user ?? null,
|
additionalUser: $employeeAdvance->employee->user ?? null,
|
||||||
);
|
);
|
||||||
|
|||||||
@ -17,30 +17,19 @@ class PayrollPeriodService
|
|||||||
{
|
{
|
||||||
use HandlesCashTransactions;
|
use HandlesCashTransactions;
|
||||||
|
|
||||||
private function canViewAll(): bool
|
|
||||||
{
|
|
||||||
return auth()->user()->hasAnyRole(['developer', 'owner', 'direktur', 'admin-toko']);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function getAll(array $filters = []): Collection
|
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'])
|
||||||
->when(! $this->canViewAll(), function ($query) {
|
->withCount('payrolls')
|
||||||
$query->withCount(['payrolls as payrolls_count' => fn ($q) => $q->whereHas('employee', fn ($eq) => $eq->where('user_id', auth()->id()))])
|
|
||||||
->withSum(['payrolls as payrolls_sum_total_amount' => fn ($q) => $q->whereHas('employee', fn ($eq) => $eq->where('user_id', auth()->id()))], 'total_amount')
|
|
||||||
->withSum(['payrolls as payrolls_sum_bonus_amount' => fn ($q) => $q->whereHas('employee', fn ($eq) => $eq->where('user_id', auth()->id()))], 'bonus_amount')
|
|
||||||
->withSum(['payrolls as payrolls_sum_deduction_amount' => fn ($q) => $q->whereHas('employee', fn ($eq) => $eq->where('user_id', auth()->id()))], 'deduction_amount')
|
|
||||||
->withCount(['payrolls as paid_count' => fn ($q) => $q->paid()->whereHas('employee', fn ($eq) => $eq->where('user_id', auth()->id()))])
|
|
||||||
->withCount(['payrolls as cancelled_count' => fn ($q) => $q->cancelled()->whereHas('employee', fn ($eq) => $eq->where('user_id', auth()->id()))]);
|
|
||||||
})
|
|
||||||
->when($this->canViewAll(), function ($query) {
|
|
||||||
$query->withCount('payrolls')
|
|
||||||
->withSum('payrolls', 'total_amount')
|
->withSum('payrolls', 'total_amount')
|
||||||
->withSum('payrolls', 'bonus_amount')
|
->withSum('payrolls', 'bonus_amount')
|
||||||
->withSum('payrolls', 'deduction_amount')
|
->withSum('payrolls', 'deduction_amount')
|
||||||
->withCount(['payrolls as paid_count' => fn ($q) => $q->paid()])
|
->withCount(['payrolls as paid_count' => function ($q) {
|
||||||
->withCount(['payrolls as cancelled_count' => fn ($q) => $q->cancelled()]);
|
$q->paid();
|
||||||
})
|
}])
|
||||||
|
->withCount(['payrolls as cancelled_count' => function ($q) {
|
||||||
|
$q->cancelled();
|
||||||
|
}])
|
||||||
->latest('year')
|
->latest('year')
|
||||||
->latest('month')
|
->latest('month')
|
||||||
->get();
|
->get();
|
||||||
@ -50,22 +39,16 @@ public function paginated(int $perPage = 25, string $search = '', string $sort =
|
|||||||
{
|
{
|
||||||
return PayrollPeriod::query()
|
return PayrollPeriod::query()
|
||||||
->select(['id', 'year', 'month', 'status', 'closed_at', 'created_at'])
|
->select(['id', 'year', 'month', 'status', 'closed_at', 'created_at'])
|
||||||
->when(! $this->canViewAll(), function ($query) {
|
->withCount('payrolls')
|
||||||
$query->withCount(['payrolls as payrolls_count' => fn ($q) => $q->whereHas('employee', fn ($eq) => $eq->where('user_id', auth()->id()))])
|
|
||||||
->withSum(['payrolls as payrolls_sum_total_amount' => fn ($q) => $q->whereHas('employee', fn ($eq) => $eq->where('user_id', auth()->id()))], 'total_amount')
|
|
||||||
->withSum(['payrolls as payrolls_sum_bonus_amount' => fn ($q) => $q->whereHas('employee', fn ($eq) => $eq->where('user_id', auth()->id()))], 'bonus_amount')
|
|
||||||
->withSum(['payrolls as payrolls_sum_deduction_amount' => fn ($q) => $q->whereHas('employee', fn ($eq) => $eq->where('user_id', auth()->id()))], 'deduction_amount')
|
|
||||||
->withCount(['payrolls as paid_count' => fn ($q) => $q->paid()->whereHas('employee', fn ($eq) => $eq->where('user_id', auth()->id()))])
|
|
||||||
->withCount(['payrolls as cancelled_count' => fn ($q) => $q->cancelled()->whereHas('employee', fn ($eq) => $eq->where('user_id', auth()->id()))]);
|
|
||||||
})
|
|
||||||
->when($this->canViewAll(), function ($query) {
|
|
||||||
$query->withCount('payrolls')
|
|
||||||
->withSum('payrolls', 'total_amount')
|
->withSum('payrolls', 'total_amount')
|
||||||
->withSum('payrolls', 'bonus_amount')
|
->withSum('payrolls', 'bonus_amount')
|
||||||
->withSum('payrolls', 'deduction_amount')
|
->withSum('payrolls', 'deduction_amount')
|
||||||
->withCount(['payrolls as paid_count' => fn ($q) => $q->paid()])
|
->withCount(['payrolls as paid_count' => function ($q) {
|
||||||
->withCount(['payrolls as cancelled_count' => fn ($q) => $q->cancelled()]);
|
$q->paid();
|
||||||
})
|
}])
|
||||||
|
->withCount(['payrolls as cancelled_count' => function ($q) {
|
||||||
|
$q->cancelled();
|
||||||
|
}])
|
||||||
->when($search, fn ($q) => $q->where('year', 'like', "%{$search}%"))
|
->when($search, fn ($q) => $q->where('year', 'like', "%{$search}%"))
|
||||||
->orderBy($sort, $direction)
|
->orderBy($sort, $direction)
|
||||||
->paginate($perPage);
|
->paginate($perPage);
|
||||||
@ -76,7 +59,6 @@ public function getDetail(PayrollPeriod $period): PayrollPeriod
|
|||||||
return $period->load([
|
return $period->load([
|
||||||
'payrolls' => function ($query) {
|
'payrolls' => function ($query) {
|
||||||
$query->with(['employee.user.userProfile', 'payrollAdjustments'])
|
$query->with(['employee.user.userProfile', 'payrollAdjustments'])
|
||||||
->when(! $this->canViewAll(), fn ($q) => $q->whereHas('employee', fn ($eq) => $eq->where('user_id', auth()->id())))
|
|
||||||
->orderBy('id');
|
->orderBy('id');
|
||||||
},
|
},
|
||||||
]);
|
]);
|
||||||
|
|||||||
@ -9,13 +9,6 @@
|
|||||||
|
|
||||||
class EmployeeService
|
class EmployeeService
|
||||||
{
|
{
|
||||||
private const ADMIN_ROLES = ['developer', 'owner', 'direktur', 'admin-toko'];
|
|
||||||
|
|
||||||
public function canViewAll(): bool
|
|
||||||
{
|
|
||||||
return auth()->user()->hasAnyRole(self::ADMIN_ROLES);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function getAll(array $filters = []): Collection
|
public function getAll(array $filters = []): Collection
|
||||||
{
|
{
|
||||||
return User::select(['id', 'email', 'username', 'is_active'])
|
return User::select(['id', 'email', 'username', 'is_active'])
|
||||||
@ -25,10 +18,6 @@ public function getAll(array $filters = []): Collection
|
|||||||
'employee' => fn ($q) => $q->select(['id', 'user_id', 'join_date', 'employment_status', 'base_salary']),
|
'employee' => fn ($q) => $q->select(['id', 'user_id', 'join_date', 'employment_status', 'base_salary']),
|
||||||
'roles' => fn ($q) => $q->select(['id', 'name']),
|
'roles' => fn ($q) => $q->select(['id', 'name']),
|
||||||
])
|
])
|
||||||
->when(! $this->canViewAll(), function ($q) {
|
|
||||||
$userRoles = auth()->user()->roles->pluck('name');
|
|
||||||
$q->whereHas('roles', fn ($rq) => $rq->whereIn('name', $userRoles));
|
|
||||||
})
|
|
||||||
->when($filters['employment_status'] ?? null, fn ($q, $status) => $q->whereHas('employee', fn ($eq) => $eq->where('employment_status', $status)))
|
->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(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['gender'] ?? null, fn ($q, $gender) => $q->whereHas('userProfile', fn ($uq) => $uq->where('gender', $gender)))
|
||||||
@ -46,10 +35,6 @@ public function paginated(int $perPage = 25, string $search = '', string $sort =
|
|||||||
'employee' => fn ($q) => $q->select(['id', 'user_id', 'join_date', 'employment_status', 'base_salary']),
|
'employee' => fn ($q) => $q->select(['id', 'user_id', 'join_date', 'employment_status', 'base_salary']),
|
||||||
'roles' => fn ($q) => $q->select(['id', 'name']),
|
'roles' => fn ($q) => $q->select(['id', 'name']),
|
||||||
])
|
])
|
||||||
->when(! $this->canViewAll(), function ($q) {
|
|
||||||
$userRoles = auth()->user()->roles->pluck('name');
|
|
||||||
$q->whereHas('roles', fn ($rq) => $rq->whereIn('name', $userRoles));
|
|
||||||
})
|
|
||||||
->when($search, fn ($q) => $q->whereHas('userProfile', fn ($uq) => $uq->where('full_name', 'like', "%{$search}%")))
|
->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($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(isset($filters['is_active']) && $filters['is_active'] !== '', fn ($q) => $q->where('is_active', filter_var($filters['is_active'], FILTER_VALIDATE_BOOLEAN)))
|
||||||
|
|||||||
@ -68,7 +68,6 @@ public function getForCreate(): array
|
|||||||
'productVariants:id,product_id,name,stock,reject_stock',
|
'productVariants:id,product_id,name,stock,reject_stock',
|
||||||
'productVariants.productPrices:id,variant_id,type,price',
|
'productVariants.productPrices:id,variant_id,type,price',
|
||||||
])
|
])
|
||||||
->active()
|
|
||||||
->orderBy('name')
|
->orderBy('name')
|
||||||
->get()
|
->get()
|
||||||
->each(function (Product $product) {
|
->each(function (Product $product) {
|
||||||
|
|||||||
@ -63,8 +63,6 @@ public function paginated(int $perPage = 25, string $search = '', string $sort =
|
|||||||
->when($filters['customer_id'] ?? null, fn ($q, $customerId) => $q->where('customer_id', $customerId))
|
->when($filters['customer_id'] ?? null, fn ($q, $customerId) => $q->where('customer_id', $customerId))
|
||||||
->when($filters['marketing_id'] ?? null, fn ($q, $marketingId) => $q->where('marketing_id', $marketingId))
|
->when($filters['marketing_id'] ?? null, fn ($q, $marketingId) => $q->where('marketing_id', $marketingId))
|
||||||
->when($filters['created_by_id'] ?? null, fn ($q, $createdById) => $q->where('created_by_id', $createdById))
|
->when($filters['created_by_id'] ?? null, fn ($q, $createdById) => $q->where('created_by_id', $createdById))
|
||||||
->when($filters['date_from'] ?? null, fn($q, $dateFrom) => $q->whereDate('created_at', '>=', $dateFrom))
|
|
||||||
->when($filters['date_to'] ?? null, fn($q, $dateTo) => $q->whereDate('created_at', '<=', $dateTo))
|
|
||||||
->orderBy($sort, $direction)
|
->orderBy($sort, $direction)
|
||||||
->paginate($perPage);
|
->paginate($perPage);
|
||||||
|
|
||||||
@ -91,33 +89,6 @@ public function paginated(int $perPage = 25, string $search = '', string $sort =
|
|||||||
return $paginator;
|
return $paginator;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function getSummary(array $filters = []): array
|
|
||||||
{
|
|
||||||
$query = Order::query()
|
|
||||||
->selectRaw('COUNT(*) as total_orders')
|
|
||||||
->selectRaw('COALESCE(SUM(subtotal), 0) as total_subtotal')
|
|
||||||
->selectRaw('COALESCE(SUM(subtotal) - SUM(COALESCE(nego_price, subtotal)), 0) as total_discount')
|
|
||||||
->selectRaw('COALESCE(SUM(total_amount), 0) as total_amount')
|
|
||||||
->selectRaw('COALESCE(SUM(cogs), 0) as total_cogs')
|
|
||||||
->when($filters['status'] ?? null, fn($q, $status) => $q->where('status', $status))
|
|
||||||
->when($filters['channel'] ?? null, fn($q, $channel) => $q->where('channel', $channel))
|
|
||||||
->when($filters['payment_type'] ?? null, fn($q, $paymentType) => $q->where('payment_type', $paymentType))
|
|
||||||
->when($filters['customer_id'] ?? null, fn($q, $customerId) => $q->where('customer_id', $customerId))
|
|
||||||
->when($filters['marketing_id'] ?? null, fn($q, $marketingId) => $q->where('marketing_id', $marketingId))
|
|
||||||
->when($filters['created_by_id'] ?? null, fn($q, $createdById) => $q->where('created_by_id', $createdById))
|
|
||||||
->when($filters['date_from'] ?? null, fn($q, $dateFrom) => $q->whereDate('created_at', '>=', $dateFrom))
|
|
||||||
->when($filters['date_to'] ?? null, fn($q, $dateTo) => $q->whereDate('created_at', '<=', $dateTo))
|
|
||||||
->first();
|
|
||||||
|
|
||||||
return [
|
|
||||||
'total_orders' => $query->total_orders,
|
|
||||||
'total_subtotal' => $query->total_subtotal,
|
|
||||||
'total_discount' => $query->total_discount,
|
|
||||||
'total_amount' => $query->total_amount,
|
|
||||||
'net_total' => $query->total_amount - $query->total_cogs,
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
public function getFilterOptions(): array
|
public function getFilterOptions(): array
|
||||||
{
|
{
|
||||||
return [
|
return [
|
||||||
@ -152,7 +123,6 @@ public function getForCreate(): array
|
|||||||
'productVariants:id,product_id,name,stock,reject_stock',
|
'productVariants:id,product_id,name,stock,reject_stock',
|
||||||
'productVariants.productPrices:id,variant_id,type,price',
|
'productVariants.productPrices:id,variant_id,type,price',
|
||||||
])
|
])
|
||||||
->active()
|
|
||||||
->orderBy('name')
|
->orderBy('name')
|
||||||
->get()
|
->get()
|
||||||
->each(function (Product $product) {
|
->each(function (Product $product) {
|
||||||
@ -358,13 +328,6 @@ public function delete(Order $order): bool
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public function updateStatus(Order $order, string $status): Order
|
|
||||||
{
|
|
||||||
$order->update(['status' => $status]);
|
|
||||||
|
|
||||||
return $order;
|
|
||||||
}
|
|
||||||
|
|
||||||
private function buildItemRows(array $items, string $stockType, string $priceType, $now, int &$subtotal, int &$totalCost): array
|
private function buildItemRows(array $items, string $stockType, string $priceType, $now, int &$subtotal, int &$totalCost): array
|
||||||
{
|
{
|
||||||
$resolvedPriceType = $stockType === ProductStockQuality::REJECT->value
|
$resolvedPriceType = $stockType === ProductStockQuality::REJECT->value
|
||||||
|
|||||||
@ -19,16 +19,6 @@ public function __construct(
|
|||||||
private StockMutationService $stockMutationService,
|
private StockMutationService $stockMutationService,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
public function getNames(): array
|
|
||||||
{
|
|
||||||
return Product::where('status', '!=', 'deleted')
|
|
||||||
->orderBy('name')
|
|
||||||
->pluck('name')
|
|
||||||
->unique()
|
|
||||||
->values()
|
|
||||||
->toArray();
|
|
||||||
}
|
|
||||||
|
|
||||||
public function getAll(array $filters = []): Collection
|
public function getAll(array $filters = []): Collection
|
||||||
{
|
{
|
||||||
$products = Product::select(['id', 'name', 'slug', 'description', 'status'])
|
$products = Product::select(['id', 'name', 'slug', 'description', 'status'])
|
||||||
@ -63,7 +53,6 @@ public function paginated(int $perPage = 25, string $search = '', string $sort =
|
|||||||
'productVariants.productPrices:id,variant_id,type,price',
|
'productVariants.productPrices:id,variant_id,type,price',
|
||||||
])
|
])
|
||||||
->when($search, fn ($q) => $q->where('name', 'like', "%{$search}%"))
|
->when($search, fn ($q) => $q->where('name', 'like', "%{$search}%"))
|
||||||
->when($filters['name'] ?? null, fn ($q, $name) => $q->where('name', 'like', "%{$name}%"))
|
|
||||||
->when($filters['status'] ?? null, fn ($q, $status) => $q->where('status', $status))
|
->when($filters['status'] ?? null, fn ($q, $status) => $q->where('status', $status))
|
||||||
->when($filters['category'] ?? null, fn ($q, $categoryId) => $q->whereHas('categories', function ($cq) use ($categoryId) {
|
->when($filters['category'] ?? null, fn ($q, $categoryId) => $q->whereHas('categories', function ($cq) use ($categoryId) {
|
||||||
$cq->where('categories.id', $categoryId);
|
$cq->where('categories.id', $categoryId);
|
||||||
|
|||||||
@ -5,7 +5,6 @@
|
|||||||
use App\Enums\ProductStockQuality;
|
use App\Enums\ProductStockQuality;
|
||||||
use App\Models\ProductVariant;
|
use App\Models\ProductVariant;
|
||||||
use Illuminate\Database\Eloquent\Model;
|
use Illuminate\Database\Eloquent\Model;
|
||||||
use Illuminate\Validation\ValidationException;
|
|
||||||
|
|
||||||
trait HasStockAdjustment
|
trait HasStockAdjustment
|
||||||
{
|
{
|
||||||
@ -19,17 +18,6 @@ private function adjustStock(Model $model, string $field, int $quantity, int $si
|
|||||||
if ($sign > 0) {
|
if ($sign > 0) {
|
||||||
$model->increment($field, $quantity);
|
$model->increment($field, $quantity);
|
||||||
} else {
|
} else {
|
||||||
if ($model->{$field} < $quantity) {
|
|
||||||
$label = match ($field) {
|
|
||||||
'stock' => 'stok bagus',
|
|
||||||
'reject_stock' => 'stok reject',
|
|
||||||
'retail_stock' => 'stok ecer',
|
|
||||||
default => $field,
|
|
||||||
};
|
|
||||||
throw ValidationException::withMessages([
|
|
||||||
'stock' => "Stok {$label} tidak mencukupi. Tersedia: {$model->{$field}}, dibutuhkan: {$quantity}.",
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
$model->decrement($field, $quantity);
|
$model->decrement($field, $quantity);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -268,6 +268,7 @@ public function run(): void
|
|||||||
'employee_advances.create',
|
'employee_advances.create',
|
||||||
'employee_advances.update',
|
'employee_advances.update',
|
||||||
'employee_advances.delete',
|
'employee_advances.delete',
|
||||||
|
'employee_advances.pay',
|
||||||
|
|
||||||
'activity_logs.view',
|
'activity_logs.view',
|
||||||
|
|
||||||
|
|||||||
@ -1,246 +0,0 @@
|
|||||||
import { useMemo } from "react"
|
|
||||||
import { cva, type VariantProps } from "class-variance-authority"
|
|
||||||
|
|
||||||
import { cn } from "@/lib/utils"
|
|
||||||
import { Label } from "@/components/ui/label"
|
|
||||||
import { Separator } from "@/components/ui/separator"
|
|
||||||
|
|
||||||
function FieldSet({ className, ...props }: React.ComponentProps<"fieldset">) {
|
|
||||||
return (
|
|
||||||
<fieldset
|
|
||||||
data-slot="field-set"
|
|
||||||
className={cn(
|
|
||||||
"flex flex-col gap-6",
|
|
||||||
"has-[>[data-slot=checkbox-group]]:gap-3 has-[>[data-slot=radio-group]]:gap-3",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function FieldLegend({
|
|
||||||
className,
|
|
||||||
variant = "legend",
|
|
||||||
...props
|
|
||||||
}: React.ComponentProps<"legend"> & { variant?: "legend" | "label" }) {
|
|
||||||
return (
|
|
||||||
<legend
|
|
||||||
data-slot="field-legend"
|
|
||||||
data-variant={variant}
|
|
||||||
className={cn(
|
|
||||||
"mb-3 font-medium",
|
|
||||||
"data-[variant=legend]:text-base",
|
|
||||||
"data-[variant=label]:text-sm",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function FieldGroup({ className, ...props }: React.ComponentProps<"div">) {
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
data-slot="field-group"
|
|
||||||
className={cn(
|
|
||||||
"group/field-group @container/field-group flex w-full flex-col gap-7 data-[slot=checkbox-group]:gap-3 [&>[data-slot=field-group]]:gap-4",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
const fieldVariants = cva(
|
|
||||||
"group/field flex w-full gap-3 data-[invalid=true]:text-destructive",
|
|
||||||
{
|
|
||||||
variants: {
|
|
||||||
orientation: {
|
|
||||||
vertical: ["flex-col [&>*]:w-full [&>.sr-only]:w-auto"],
|
|
||||||
horizontal: [
|
|
||||||
"flex-row items-center",
|
|
||||||
"[&>[data-slot=field-label]]:flex-auto",
|
|
||||||
"has-[>[data-slot=field-content]]:items-start has-[>[data-slot=field-content]]:[&>[role=checkbox],[role=radio]]:mt-px",
|
|
||||||
],
|
|
||||||
responsive: [
|
|
||||||
"flex-col @md/field-group:flex-row @md/field-group:items-center [&>*]:w-full @md/field-group:[&>*]:w-auto [&>.sr-only]:w-auto",
|
|
||||||
"@md/field-group:[&>[data-slot=field-label]]:flex-auto",
|
|
||||||
"@md/field-group:has-[>[data-slot=field-content]]:items-start @md/field-group:has-[>[data-slot=field-content]]:[&>[role=checkbox],[role=radio]]:mt-px",
|
|
||||||
],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
defaultVariants: {
|
|
||||||
orientation: "vertical",
|
|
||||||
},
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
function Field({
|
|
||||||
className,
|
|
||||||
orientation = "vertical",
|
|
||||||
...props
|
|
||||||
}: React.ComponentProps<"div"> & VariantProps<typeof fieldVariants>) {
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
role="group"
|
|
||||||
data-slot="field"
|
|
||||||
data-orientation={orientation}
|
|
||||||
className={cn(fieldVariants({ orientation }), className)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function FieldContent({ className, ...props }: React.ComponentProps<"div">) {
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
data-slot="field-content"
|
|
||||||
className={cn(
|
|
||||||
"group/field-content flex flex-1 flex-col gap-1.5 leading-snug",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function FieldLabel({
|
|
||||||
className,
|
|
||||||
...props
|
|
||||||
}: React.ComponentProps<typeof Label>) {
|
|
||||||
return (
|
|
||||||
<Label
|
|
||||||
data-slot="field-label"
|
|
||||||
className={cn(
|
|
||||||
"group/field-label peer/field-label flex w-fit gap-2 leading-snug group-data-[disabled=true]/field:opacity-50",
|
|
||||||
"has-[>[data-slot=field]]:w-full has-[>[data-slot=field]]:flex-col has-[>[data-slot=field]]:rounded-md has-[>[data-slot=field]]:border [&>*]:data-[slot=field]:p-4",
|
|
||||||
"has-data-[state=checked]:border-primary has-data-[state=checked]:bg-primary/5 dark:has-data-[state=checked]:bg-primary/10",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function FieldTitle({ className, ...props }: React.ComponentProps<"div">) {
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
data-slot="field-label"
|
|
||||||
className={cn(
|
|
||||||
"flex w-fit items-center gap-2 text-sm leading-snug font-medium group-data-[disabled=true]/field:opacity-50",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function FieldDescription({ className, ...props }: React.ComponentProps<"p">) {
|
|
||||||
return (
|
|
||||||
<p
|
|
||||||
data-slot="field-description"
|
|
||||||
className={cn(
|
|
||||||
"text-sm leading-normal font-normal text-muted-foreground group-has-[[data-orientation=horizontal]]/field:text-balance",
|
|
||||||
"last:mt-0 nth-last-2:-mt-1 [[data-variant=legend]+&]:-mt-1.5",
|
|
||||||
"[&>a]:underline [&>a]:underline-offset-4 [&>a:hover]:text-primary",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function FieldSeparator({
|
|
||||||
children,
|
|
||||||
className,
|
|
||||||
...props
|
|
||||||
}: React.ComponentProps<"div"> & {
|
|
||||||
children?: React.ReactNode
|
|
||||||
}) {
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
data-slot="field-separator"
|
|
||||||
data-content={!!children}
|
|
||||||
className={cn(
|
|
||||||
"relative -my-2 h-5 text-sm group-data-[variant=outline]/field-group:-mb-2",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
>
|
|
||||||
<Separator className="absolute inset-0 top-1/2" />
|
|
||||||
{children && (
|
|
||||||
<span
|
|
||||||
className="relative mx-auto block w-fit bg-background px-2 text-muted-foreground"
|
|
||||||
data-slot="field-separator-content"
|
|
||||||
>
|
|
||||||
{children}
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function FieldError({
|
|
||||||
className,
|
|
||||||
children,
|
|
||||||
errors,
|
|
||||||
...props
|
|
||||||
}: React.ComponentProps<"div"> & {
|
|
||||||
errors?: Array<{ message?: string } | undefined>
|
|
||||||
}) {
|
|
||||||
const content = useMemo(() => {
|
|
||||||
if (children) {
|
|
||||||
return children
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!errors?.length) {
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
const uniqueErrors = [
|
|
||||||
...new Map(errors.map((error) => [error?.message, error])).values(),
|
|
||||||
]
|
|
||||||
|
|
||||||
if (uniqueErrors?.length == 1) {
|
|
||||||
return uniqueErrors[0]?.message
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<ul className="ml-4 flex list-disc flex-col gap-1">
|
|
||||||
{uniqueErrors.map(
|
|
||||||
(error, index) =>
|
|
||||||
error?.message && <li key={index}>{error.message}</li>
|
|
||||||
)}
|
|
||||||
</ul>
|
|
||||||
)
|
|
||||||
}, [children, errors])
|
|
||||||
|
|
||||||
if (!content) {
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
role="alert"
|
|
||||||
data-slot="field-error"
|
|
||||||
className={cn("text-sm font-normal text-destructive", className)}
|
|
||||||
{...props}
|
|
||||||
>
|
|
||||||
{content}
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export {
|
|
||||||
Field,
|
|
||||||
FieldLabel,
|
|
||||||
FieldDescription,
|
|
||||||
FieldError,
|
|
||||||
FieldGroup,
|
|
||||||
FieldLegend,
|
|
||||||
FieldSeparator,
|
|
||||||
FieldSet,
|
|
||||||
FieldContent,
|
|
||||||
FieldTitle,
|
|
||||||
}
|
|
||||||
@ -1,3 +1,5 @@
|
|||||||
|
"use client"
|
||||||
|
|
||||||
import * as React from "react"
|
import * as React from "react"
|
||||||
import { Label as LabelPrimitive } from "radix-ui"
|
import { Label as LabelPrimitive } from "radix-ui"
|
||||||
|
|
||||||
@ -11,7 +13,7 @@ function Label({
|
|||||||
<LabelPrimitive.Root
|
<LabelPrimitive.Root
|
||||||
data-slot="label"
|
data-slot="label"
|
||||||
className={cn(
|
className={cn(
|
||||||
"flex items-center gap-2 text-sm leading-none font-medium select-none group-data-[disabled=true]:pointer-events-none group-data-[disabled=true]:opacity-50 peer-disabled:cursor-not-allowed peer-disabled:opacity-50",
|
"flex items-center gap-2 text-xs/relaxed leading-none font-medium select-none group-data-[disabled=true]:pointer-events-none group-data-[disabled=true]:opacity-50 peer-disabled:cursor-not-allowed peer-disabled:opacity-50",
|
||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
|
|||||||
@ -1,5 +1,3 @@
|
|||||||
"use client"
|
|
||||||
|
|
||||||
import * as React from "react"
|
import * as React from "react"
|
||||||
import { Separator as SeparatorPrimitive } from "radix-ui"
|
import { Separator as SeparatorPrimitive } from "radix-ui"
|
||||||
|
|
||||||
@ -17,7 +15,7 @@ function Separator({
|
|||||||
decorative={decorative}
|
decorative={decorative}
|
||||||
orientation={orientation}
|
orientation={orientation}
|
||||||
className={cn(
|
className={cn(
|
||||||
"shrink-0 bg-border data-[orientation=horizontal]:h-px data-[orientation=horizontal]:w-full data-[orientation=vertical]:h-full data-[orientation=vertical]:w-px",
|
"shrink-0 bg-border data-horizontal:h-px data-horizontal:w-full data-vertical:w-px data-vertical:self-stretch",
|
||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
|
|||||||
@ -120,7 +120,7 @@ export function createTransactionColumns(
|
|||||||
accessorKey: 'description',
|
accessorKey: 'description',
|
||||||
header: () => <span>Keterangan</span>,
|
header: () => <span>Keterangan</span>,
|
||||||
cell: ({ row }) => (
|
cell: ({ row }) => (
|
||||||
<span className="block max-w-[200px]">
|
<span className="block max-w-[200px] truncate">
|
||||||
{row.getValue('description') as string}
|
{row.getValue('description') as string}
|
||||||
</span>
|
</span>
|
||||||
),
|
),
|
||||||
|
|||||||
@ -1,21 +1,7 @@
|
|||||||
|
import type { ColumnDef } from '@tanstack/react-table';
|
||||||
|
import { CheckCircle, CircleDollarSign, Pencil, Trash2 } from 'lucide-react';
|
||||||
import { RowActions } from '@/components/row-actions';
|
import { RowActions } from '@/components/row-actions';
|
||||||
import { Badge } from '@/components/ui/badge';
|
import { Badge } from '@/components/ui/badge';
|
||||||
import type { ColumnDef } from '@tanstack/react-table';
|
|
||||||
import { CheckCircle, CircleDollarSign, History, Pencil, Trash2 } from 'lucide-react';
|
|
||||||
|
|
||||||
export type EmployeeAdvancePayment = {
|
|
||||||
id: number;
|
|
||||||
amount: number;
|
|
||||||
formatted_amount: string;
|
|
||||||
description: string | null;
|
|
||||||
paid_at: string;
|
|
||||||
formatted_paid_at: string;
|
|
||||||
paid_by: {
|
|
||||||
user_profile: {
|
|
||||||
full_name: string;
|
|
||||||
};
|
|
||||||
} | null;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type EmployeeAdvance = {
|
export type EmployeeAdvance = {
|
||||||
id: number;
|
id: number;
|
||||||
@ -23,8 +9,6 @@ export type EmployeeAdvance = {
|
|||||||
formatted_amount: string;
|
formatted_amount: string;
|
||||||
paid_amount: number;
|
paid_amount: number;
|
||||||
formatted_paid_amount: string;
|
formatted_paid_amount: string;
|
||||||
remaining_amount: number;
|
|
||||||
formatted_remaining_amount: string;
|
|
||||||
description: string;
|
description: string;
|
||||||
due_date: string;
|
due_date: string;
|
||||||
formatted_due_date: string;
|
formatted_due_date: string;
|
||||||
@ -33,13 +17,11 @@ export type EmployeeAdvance = {
|
|||||||
formatted_created_at: string;
|
formatted_created_at: string;
|
||||||
employee: {
|
employee: {
|
||||||
user: {
|
user: {
|
||||||
id: number;
|
|
||||||
user_profile: {
|
user_profile: {
|
||||||
full_name: string;
|
full_name: string;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
payments: EmployeeAdvancePayment[];
|
|
||||||
};
|
};
|
||||||
|
|
||||||
function getStatusBadge(status: string) {
|
function getStatusBadge(status: string) {
|
||||||
@ -80,15 +62,13 @@ type CreateColumnsParams = {
|
|||||||
handleDeleteClick: (employeeAdvance: EmployeeAdvance) => void;
|
handleDeleteClick: (employeeAdvance: EmployeeAdvance) => void;
|
||||||
handleApprove: (employeeAdvance: EmployeeAdvance) => void;
|
handleApprove: (employeeAdvance: EmployeeAdvance) => void;
|
||||||
handlePay: (employeeAdvance: EmployeeAdvance) => void;
|
handlePay: (employeeAdvance: EmployeeAdvance) => void;
|
||||||
handleShowPayments: (employeeAdvance: EmployeeAdvance) => void;
|
|
||||||
can: (permission: string) => boolean;
|
can: (permission: string) => boolean;
|
||||||
authUserId: number;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export function createEmployeeAdvanceColumns(
|
export function createEmployeeAdvanceColumns(
|
||||||
params: CreateColumnsParams,
|
params: CreateColumnsParams,
|
||||||
): ColumnDef<EmployeeAdvance>[] {
|
): ColumnDef<EmployeeAdvance>[] {
|
||||||
const { handleEdit, handleDeleteClick, handleApprove, handlePay, handleShowPayments, can, authUserId } =
|
const { handleEdit, handleDeleteClick, handleApprove, handlePay, can } =
|
||||||
params;
|
params;
|
||||||
|
|
||||||
return [
|
return [
|
||||||
@ -116,27 +96,11 @@ export function createEmployeeAdvanceColumns(
|
|||||||
accessorKey: 'formatted_amount',
|
accessorKey: 'formatted_amount',
|
||||||
header: () => <span>Jumlah</span>,
|
header: () => <span>Jumlah</span>,
|
||||||
cell: ({ row }) => (
|
cell: ({ row }) => (
|
||||||
<span className="font-medium">
|
<span className="font-medium text-red-600">
|
||||||
{row.getValue('formatted_amount') as string}
|
- {row.getValue('formatted_amount') as string}
|
||||||
</span>
|
</span>
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
{
|
|
||||||
accessorKey: 'formatted_remaining_amount',
|
|
||||||
header: () => <span>Sisa</span>,
|
|
||||||
cell: ({ row }) => {
|
|
||||||
const employeeAdvance = row.original;
|
|
||||||
if (employeeAdvance.status === 'paid') {
|
|
||||||
return <span className="text-green-600">Lunas</span>;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<span className="font-medium text-orange-600">
|
|
||||||
{row.getValue('formatted_remaining_amount') as string}
|
|
||||||
</span>
|
|
||||||
);
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
accessorKey: 'description',
|
accessorKey: 'description',
|
||||||
header: () => <span>Keterangan</span>,
|
header: () => <span>Keterangan</span>,
|
||||||
@ -195,19 +159,10 @@ export function createEmployeeAdvanceColumns(
|
|||||||
employeeAdvance.status === 'approved',
|
employeeAdvance.status === 'approved',
|
||||||
onClick: () => handlePay(employeeAdvance),
|
onClick: () => handlePay(employeeAdvance),
|
||||||
},
|
},
|
||||||
{
|
|
||||||
label: 'Riwayat',
|
|
||||||
icon: <History className="h-4 w-4" />,
|
|
||||||
show: employeeAdvance.payments.length > 0,
|
|
||||||
onClick: () => handleShowPayments(employeeAdvance),
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
label: 'Edit',
|
label: 'Edit',
|
||||||
icon: <Pencil className="h-4 w-4" />,
|
icon: <Pencil className="h-4 w-4" />,
|
||||||
show:
|
show: can('employee_advances.update'),
|
||||||
can('employee_advances.update') &&
|
|
||||||
employeeAdvance.status === 'pending' &&
|
|
||||||
employeeAdvance.employee?.user?.id === authUserId,
|
|
||||||
onClick: () => handleEdit(employeeAdvance),
|
onClick: () => handleEdit(employeeAdvance),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -215,10 +170,7 @@ export function createEmployeeAdvanceColumns(
|
|||||||
icon: (
|
icon: (
|
||||||
<Trash2 className="h-4 w-4 text-destructive" />
|
<Trash2 className="h-4 w-4 text-destructive" />
|
||||||
),
|
),
|
||||||
show:
|
show: can('employee_advances.delete'),
|
||||||
can('employee_advances.delete') &&
|
|
||||||
employeeAdvance.status === 'pending' &&
|
|
||||||
employeeAdvance.employee?.user?.id === authUserId,
|
|
||||||
onClick: () =>
|
onClick: () =>
|
||||||
handleDeleteClick(employeeAdvance),
|
handleDeleteClick(employeeAdvance),
|
||||||
},
|
},
|
||||||
|
|||||||
@ -1,50 +1,29 @@
|
|||||||
|
import { Head, router } from '@inertiajs/react';
|
||||||
|
import { Plus } from 'lucide-react';
|
||||||
|
import { useEffect, useState } from 'react';
|
||||||
import type { PaginationState } from '@/components/data-table';
|
import type { PaginationState } from '@/components/data-table';
|
||||||
import { DataTable } from '@/components/data-table';
|
import { DataTable } from '@/components/data-table';
|
||||||
import { DatePicker } from '@/components/date-picker';
|
import { DatePicker } from '@/components/date-picker';
|
||||||
import { DeleteConfirmDialog } from '@/components/delete-confirm-dialog';
|
import { DeleteConfirmDialog } from '@/components/delete-confirm-dialog';
|
||||||
import { FilterPopover } from '@/components/filter-popover';
|
|
||||||
import { FormDialog } from '@/components/form-dialog';
|
import { FormDialog } from '@/components/form-dialog';
|
||||||
import InputError from '@/components/input-error';
|
import InputError from '@/components/input-error';
|
||||||
import { PageHeader } from '@/components/page-header';
|
import { PageHeader } from '@/components/page-header';
|
||||||
import { RupiahInput } from '@/components/rupiah-input';
|
import { RupiahInput } from '@/components/rupiah-input';
|
||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
import {
|
|
||||||
Dialog,
|
|
||||||
DialogContent,
|
|
||||||
DialogDescription,
|
|
||||||
DialogHeader,
|
|
||||||
DialogTitle,
|
|
||||||
} from '@/components/ui/dialog';
|
|
||||||
import { Input } from '@/components/ui/input';
|
import { Input } from '@/components/ui/input';
|
||||||
import { Label } from '@/components/ui/label';
|
import { Label } from '@/components/ui/label';
|
||||||
import { Separator } from '@/components/ui/separator';
|
|
||||||
import {
|
|
||||||
Select,
|
|
||||||
SelectContent,
|
|
||||||
SelectItem,
|
|
||||||
SelectTrigger,
|
|
||||||
SelectValue,
|
|
||||||
} from '@/components/ui/select';
|
|
||||||
import { useCan } from '@/hooks/use-can';
|
import { useCan } from '@/hooks/use-can';
|
||||||
import { useServerTable } from '@/hooks/use-server-table';
|
import { useServerTable } from '@/hooks/use-server-table';
|
||||||
import {
|
import {
|
||||||
approve,
|
|
||||||
destroy,
|
destroy,
|
||||||
index as employeeAdvanceIndex,
|
index as employeeAdvanceIndex,
|
||||||
pay,
|
|
||||||
store,
|
store,
|
||||||
update,
|
update,
|
||||||
|
approve,
|
||||||
|
pay,
|
||||||
} from '@/routes/admin/finance/employee-advances';
|
} from '@/routes/admin/finance/employee-advances';
|
||||||
import { Head, router, usePage } from '@inertiajs/react';
|
|
||||||
import { Plus } from 'lucide-react';
|
|
||||||
import { useEffect, useState } from 'react';
|
|
||||||
import type { EmployeeAdvance } from './columns';
|
|
||||||
import { createEmployeeAdvanceColumns } from './columns';
|
import { createEmployeeAdvanceColumns } from './columns';
|
||||||
|
import type { EmployeeAdvance } from './columns';
|
||||||
type TypeOption = {
|
|
||||||
value: string;
|
|
||||||
label: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
employeeAdvances: {
|
employeeAdvances: {
|
||||||
@ -54,27 +33,15 @@ type Props = {
|
|||||||
per_page: number;
|
per_page: number;
|
||||||
total: number;
|
total: number;
|
||||||
};
|
};
|
||||||
filters: {
|
|
||||||
status?: string;
|
|
||||||
};
|
|
||||||
filterOptions: {
|
|
||||||
statusOptions: TypeOption[];
|
|
||||||
};
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function EmployeeAdvanceIndex({
|
export default function EmployeeAdvanceIndex({ employeeAdvances }: Props) {
|
||||||
employeeAdvances,
|
|
||||||
filters,
|
|
||||||
filterOptions,
|
|
||||||
}: Props) {
|
|
||||||
const { can } = useCan();
|
const { can } = useCan();
|
||||||
const { auth } = usePage().props as { auth: { user: { id: number } } };
|
|
||||||
const [createOpen, setCreateOpen] = useState(false);
|
const [createOpen, setCreateOpen] = useState(false);
|
||||||
const [editing, setEditing] = useState<EmployeeAdvance | null>(null);
|
const [editing, setEditing] = useState<EmployeeAdvance | null>(null);
|
||||||
const [deleting, setDeleting] = useState<EmployeeAdvance | null>(null);
|
const [deleting, setDeleting] = useState<EmployeeAdvance | null>(null);
|
||||||
const [approving, setApproving] = useState<EmployeeAdvance | null>(null);
|
const [approving, setApproving] = useState<EmployeeAdvance | null>(null);
|
||||||
const [paying, setPaying] = useState<EmployeeAdvance | null>(null);
|
const [paying, setPaying] = useState<EmployeeAdvance | null>(null);
|
||||||
const [viewingPayments, setViewingPayments] = useState<EmployeeAdvance | null>(null);
|
|
||||||
const [dueDate, setDueDate] = useState<Date | undefined>(undefined);
|
const [dueDate, setDueDate] = useState<Date | undefined>(undefined);
|
||||||
const [editingDueDate, setEditingDueDate] = useState<Date | undefined>(
|
const [editingDueDate, setEditingDueDate] = useState<Date | undefined>(
|
||||||
undefined,
|
undefined,
|
||||||
@ -89,17 +56,12 @@ export default function EmployeeAdvanceIndex({
|
|||||||
|
|
||||||
const {
|
const {
|
||||||
search,
|
search,
|
||||||
filterOpen,
|
|
||||||
setFilterOpen,
|
|
||||||
handlePageChange,
|
handlePageChange,
|
||||||
handlePerPageChange,
|
handlePerPageChange,
|
||||||
handleSearchChange,
|
handleSearchChange,
|
||||||
applyFilter,
|
|
||||||
clearFilters,
|
|
||||||
} = useServerTable({
|
} = useServerTable({
|
||||||
route: () => employeeAdvanceIndex.url(),
|
route: () => employeeAdvanceIndex.url(),
|
||||||
pagination,
|
pagination,
|
||||||
filters,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@ -134,46 +96,28 @@ export default function EmployeeAdvanceIndex({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function handlePay() {
|
||||||
|
if (!paying) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
router.post(
|
||||||
|
pay(paying.id),
|
||||||
|
{},
|
||||||
|
{
|
||||||
|
onSuccess: () => setPaying(null),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
const columns = createEmployeeAdvanceColumns({
|
const columns = createEmployeeAdvanceColumns({
|
||||||
handleEdit: (employeeAdvance) => setEditing(employeeAdvance),
|
handleEdit: (employeeAdvance) => setEditing(employeeAdvance),
|
||||||
handleDeleteClick: (employeeAdvance) => setDeleting(employeeAdvance),
|
handleDeleteClick: (employeeAdvance) => setDeleting(employeeAdvance),
|
||||||
handleApprove: (employeeAdvance) => setApproving(employeeAdvance),
|
handleApprove: (employeeAdvance) => setApproving(employeeAdvance),
|
||||||
handlePay: (employeeAdvance) => setPaying(employeeAdvance),
|
handlePay: (employeeAdvance) => setPaying(employeeAdvance),
|
||||||
handleShowPayments: (employeeAdvance) => setViewingPayments(employeeAdvance),
|
|
||||||
can,
|
can,
|
||||||
authUserId: auth.user.id,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const filterToolbar = (
|
|
||||||
<FilterPopover
|
|
||||||
open={filterOpen}
|
|
||||||
onOpenChange={setFilterOpen}
|
|
||||||
filters={filters}
|
|
||||||
hasActiveFilters={Boolean(filters.status)}
|
|
||||||
onClear={clearFilters}
|
|
||||||
>
|
|
||||||
<div className="flex flex-col gap-2">
|
|
||||||
<label className="text-xs text-muted-foreground">Status</label>
|
|
||||||
<Select
|
|
||||||
value={filters.status ?? 'all'}
|
|
||||||
onValueChange={(value) => applyFilter('status', value)}
|
|
||||||
>
|
|
||||||
<SelectTrigger className="w-full">
|
|
||||||
<SelectValue placeholder="Semua Status" />
|
|
||||||
</SelectTrigger>
|
|
||||||
<SelectContent>
|
|
||||||
<SelectItem value="all">Semua Status</SelectItem>
|
|
||||||
{filterOptions.statusOptions.map((opt) => (
|
|
||||||
<SelectItem key={opt.value} value={opt.value}>
|
|
||||||
{opt.label}
|
|
||||||
</SelectItem>
|
|
||||||
))}
|
|
||||||
</SelectContent>
|
|
||||||
</Select>
|
|
||||||
</div>
|
|
||||||
</FilterPopover>
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Head title="Kasbon" />
|
<Head title="Kasbon" />
|
||||||
@ -270,7 +214,6 @@ export default function EmployeeAdvanceIndex({
|
|||||||
onPerPageChange={handlePerPageChange}
|
onPerPageChange={handlePerPageChange}
|
||||||
onSearchChange={handleSearchChange}
|
onSearchChange={handleSearchChange}
|
||||||
searchValue={search}
|
searchValue={search}
|
||||||
toolbar={filterToolbar}
|
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<FormDialog
|
<FormDialog
|
||||||
@ -379,128 +322,20 @@ export default function EmployeeAdvanceIndex({
|
|||||||
onConfirm={handleApprove}
|
onConfirm={handleApprove}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<FormDialog
|
<DeleteConfirmDialog
|
||||||
open={paying !== null}
|
target={paying}
|
||||||
onOpenChange={(open) => {
|
onOpenChange={(open) => {
|
||||||
if (!open) {
|
if (!open) {
|
||||||
setPaying(null);
|
setPaying(null);
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
title="Bayar Kasbon"
|
title="Bayar Kasbon"
|
||||||
action={paying ? pay(paying.id) : ''}
|
description={(advance) =>
|
||||||
resetOnSuccess
|
`Apakah Anda yakin ingin membayar kasbon "${advance.description}" sebesar ${advance.amount}? Saldo kas akan dikembalikan.`
|
||||||
onSuccess={() => setPaying(null)}
|
}
|
||||||
>
|
confirmLabel="Bayar"
|
||||||
{({ errors }) =>
|
onConfirm={handlePay}
|
||||||
paying && (
|
|
||||||
<>
|
|
||||||
<div className="mb-4 rounded-md bg-muted p-3 text-sm">
|
|
||||||
<p>
|
|
||||||
Sisa:{' '}
|
|
||||||
<span className="font-medium">
|
|
||||||
{paying.formatted_remaining_amount}
|
|
||||||
</span>
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<div className="grid gap-2">
|
|
||||||
<Label>
|
|
||||||
Jumlah Bayar{' '}
|
|
||||||
<span className="text-destructive">
|
|
||||||
*
|
|
||||||
</span>
|
|
||||||
</Label>
|
|
||||||
<RupiahInput
|
|
||||||
name="amount"
|
|
||||||
defaultValue={paying.remaining_amount}
|
|
||||||
/>
|
/>
|
||||||
<InputError message={errors.amount} />
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
</FormDialog>
|
|
||||||
|
|
||||||
<Dialog
|
|
||||||
open={viewingPayments !== null}
|
|
||||||
onOpenChange={(open) => {
|
|
||||||
if (!open) {
|
|
||||||
setViewingPayments(null);
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<DialogContent className="sm:max-w-md">
|
|
||||||
<DialogHeader>
|
|
||||||
<DialogTitle>Riwayat Pembayaran</DialogTitle>
|
|
||||||
<DialogDescription>
|
|
||||||
{viewingPayments?.description}
|
|
||||||
</DialogDescription>
|
|
||||||
</DialogHeader>
|
|
||||||
{viewingPayments && (
|
|
||||||
<div className="space-y-4">
|
|
||||||
<div className="flex items-center justify-between text-sm">
|
|
||||||
<span className="text-muted-foreground">
|
|
||||||
Total:
|
|
||||||
</span>
|
|
||||||
<span className="font-medium">
|
|
||||||
{viewingPayments.formatted_amount}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<div className="flex items-center justify-between text-sm">
|
|
||||||
<span className="text-muted-foreground">
|
|
||||||
Terbayar:
|
|
||||||
</span>
|
|
||||||
<span className="font-medium text-green-600">
|
|
||||||
{viewingPayments.formatted_paid_amount}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<div className="flex items-center justify-between text-sm">
|
|
||||||
<span className="text-muted-foreground">
|
|
||||||
Sisa:
|
|
||||||
</span>
|
|
||||||
<span className="font-medium text-orange-600">
|
|
||||||
{viewingPayments.formatted_remaining_amount}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<Separator />
|
|
||||||
|
|
||||||
{viewingPayments.payments.length === 0 ? (
|
|
||||||
<p className="py-4 text-center text-sm text-muted-foreground">
|
|
||||||
Belum ada pembayaran.
|
|
||||||
</p>
|
|
||||||
) : (
|
|
||||||
<div className="space-y-3">
|
|
||||||
{viewingPayments.payments.map(
|
|
||||||
(payment) => (
|
|
||||||
<div
|
|
||||||
key={payment.id}
|
|
||||||
className="rounded-md border p-3"
|
|
||||||
>
|
|
||||||
<div className="flex items-center justify-between">
|
|
||||||
<span className="text-sm font-medium text-green-600">
|
|
||||||
{payment.formatted_amount}
|
|
||||||
</span>
|
|
||||||
<span className="text-xs text-muted-foreground">
|
|
||||||
{payment.formatted_paid_at}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
{payment.paid_by && (
|
|
||||||
<p className="mt-1 text-xs text-muted-foreground">
|
|
||||||
oleh{' '}
|
|
||||||
{payment.paid_by
|
|
||||||
.user_profile
|
|
||||||
.full_name ?? '-'}
|
|
||||||
</p>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
),
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</DialogContent>
|
|
||||||
</Dialog>
|
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -42,7 +42,7 @@ export function createExpenseColumns(
|
|||||||
accessorKey: 'description',
|
accessorKey: 'description',
|
||||||
header: () => <span>Keterangan</span>,
|
header: () => <span>Keterangan</span>,
|
||||||
cell: ({ row }) => (
|
cell: ({ row }) => (
|
||||||
<span className="block max-w-[200px]">
|
<span className="block max-w-[200px] truncate">
|
||||||
{row.getValue('description') as string}
|
{row.getValue('description') as string}
|
||||||
</span>
|
</span>
|
||||||
),
|
),
|
||||||
|
|||||||
@ -50,15 +50,14 @@ type CreateColumnsParams = {
|
|||||||
handleClose: (period: PayrollPeriod) => void;
|
handleClose: (period: PayrollPeriod) => void;
|
||||||
handleReopen: (period: PayrollPeriod) => void;
|
handleReopen: (period: PayrollPeriod) => void;
|
||||||
can: (permission: string) => boolean;
|
can: (permission: string) => boolean;
|
||||||
canViewAll: boolean;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export function createPayrollPeriodColumns(
|
export function createPayrollPeriodColumns(
|
||||||
params: CreateColumnsParams,
|
params: CreateColumnsParams,
|
||||||
): ColumnDef<PayrollPeriod>[] {
|
): ColumnDef<PayrollPeriod>[] {
|
||||||
const { showUrl, handleClose, handleReopen, can, canViewAll } = params;
|
const { showUrl, handleClose, handleReopen, can } = params;
|
||||||
|
|
||||||
const columns: ColumnDef<PayrollPeriod>[] = [
|
return [
|
||||||
{
|
{
|
||||||
accessorKey: 'year',
|
accessorKey: 'year',
|
||||||
header: () => <span>Periode</span>,
|
header: () => <span>Periode</span>,
|
||||||
@ -68,10 +67,7 @@ export function createPayrollPeriodColumns(
|
|||||||
</span>
|
</span>
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
];
|
{
|
||||||
|
|
||||||
if (canViewAll) {
|
|
||||||
columns.push({
|
|
||||||
accessorKey: 'payrolls_count',
|
accessorKey: 'payrolls_count',
|
||||||
header: () => <span>Jumlah Karyawan</span>,
|
header: () => <span>Jumlah Karyawan</span>,
|
||||||
cell: ({ row }) => (
|
cell: ({ row }) => (
|
||||||
@ -79,10 +75,7 @@ export function createPayrollPeriodColumns(
|
|||||||
{row.getValue('payrolls_count') as number}
|
{row.getValue('payrolls_count') as number}
|
||||||
</span>
|
</span>
|
||||||
),
|
),
|
||||||
});
|
},
|
||||||
}
|
|
||||||
|
|
||||||
columns.push(
|
|
||||||
{
|
{
|
||||||
accessorKey: 'payrolls_sum_total_amount',
|
accessorKey: 'payrolls_sum_total_amount',
|
||||||
header: () => <span>Total Gaji</span>,
|
header: () => <span>Total Gaji</span>,
|
||||||
@ -138,10 +131,7 @@ export function createPayrollPeriodColumns(
|
|||||||
);
|
);
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
);
|
{
|
||||||
|
|
||||||
if (canViewAll) {
|
|
||||||
columns.push({
|
|
||||||
id: 'payment_status',
|
id: 'payment_status',
|
||||||
header: () => <span>Status Bayar</span>,
|
header: () => <span>Status Bayar</span>,
|
||||||
cell: ({ row }) => {
|
cell: ({ row }) => {
|
||||||
@ -171,10 +161,7 @@ export function createPayrollPeriodColumns(
|
|||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
});
|
},
|
||||||
}
|
|
||||||
|
|
||||||
columns.push(
|
|
||||||
{
|
{
|
||||||
accessorKey: 'status',
|
accessorKey: 'status',
|
||||||
header: () => <span>Status</span>,
|
header: () => <span>Status</span>,
|
||||||
@ -225,7 +212,5 @@ export function createPayrollPeriodColumns(
|
|||||||
);
|
);
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
);
|
];
|
||||||
|
|
||||||
return columns;
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -27,8 +27,7 @@ type Props = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export default function PayrollPeriodIndex({ payrollPeriods }: Props) {
|
export default function PayrollPeriodIndex({ payrollPeriods }: Props) {
|
||||||
const { can, hasAnyRole } = useCan();
|
const { can } = useCan();
|
||||||
const canViewAll = hasAnyRole(['developer', 'owner', 'direktur', 'admin-toko']);
|
|
||||||
const [closing, setClosing] = useState<PayrollPeriod | null>(null);
|
const [closing, setClosing] = useState<PayrollPeriod | null>(null);
|
||||||
const [reopening, setReopening] = useState<PayrollPeriod | null>(null);
|
const [reopening, setReopening] = useState<PayrollPeriod | null>(null);
|
||||||
|
|
||||||
@ -82,7 +81,6 @@ export default function PayrollPeriodIndex({ payrollPeriods }: Props) {
|
|||||||
handleClose: (period) => setClosing(period),
|
handleClose: (period) => setClosing(period),
|
||||||
handleReopen: (period) => setReopening(period),
|
handleReopen: (period) => setReopening(period),
|
||||||
can,
|
can,
|
||||||
canViewAll,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@ -81,9 +81,7 @@ export function createPayrollColumns(
|
|||||||
can,
|
can,
|
||||||
} = params;
|
} = params;
|
||||||
|
|
||||||
const hasAnyPayrollAction = can('payroll.adjust') || can('payroll.pay') || can('payroll.cancel');
|
return [
|
||||||
|
|
||||||
const columns: ColumnDef<Payroll>[] = [
|
|
||||||
{
|
{
|
||||||
id: 'employee_name',
|
id: 'employee_name',
|
||||||
header: () => <span>Nama Karyawan</span>,
|
header: () => <span>Nama Karyawan</span>,
|
||||||
@ -184,10 +182,10 @@ export function createPayrollColumns(
|
|||||||
{adj.type === 'bonus' ? '+' : '-'}{' '}
|
{adj.type === 'bonus' ? '+' : '-'}{' '}
|
||||||
{formatCurrency(adj.amount)}
|
{formatCurrency(adj.amount)}
|
||||||
</span>
|
</span>
|
||||||
<span className="max-w-[100px] text-muted-foreground">
|
<span className="max-w-[100px] truncate text-muted-foreground">
|
||||||
{adj.description}
|
{adj.description}
|
||||||
</span>
|
</span>
|
||||||
{can('payroll.adjust') && payroll.status === 'unpaid' && (
|
{payroll.status === 'unpaid' && (
|
||||||
<button
|
<button
|
||||||
onClick={() =>
|
onClick={() =>
|
||||||
handleDeleteAdjustment(
|
handleDeleteAdjustment(
|
||||||
@ -206,10 +204,7 @@ export function createPayrollColumns(
|
|||||||
);
|
);
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
];
|
{
|
||||||
|
|
||||||
if (hasAnyPayrollAction) {
|
|
||||||
columns.push({
|
|
||||||
id: 'actions',
|
id: 'actions',
|
||||||
header: () => <span className="block text-center">Aksi</span>,
|
header: () => <span className="block text-center">Aksi</span>,
|
||||||
meta: {
|
meta: {
|
||||||
@ -254,8 +249,6 @@ export function createPayrollColumns(
|
|||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
});
|
},
|
||||||
}
|
];
|
||||||
|
|
||||||
return columns;
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -40,8 +40,7 @@ type Props = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export default function PayrollPeriodShow({ payrollPeriod }: Props) {
|
export default function PayrollPeriodShow({ payrollPeriod }: Props) {
|
||||||
const { can, hasAnyRole } = useCan();
|
const { can } = useCan();
|
||||||
const canViewAll = hasAnyRole(['developer', 'owner', 'direktur', 'admin-toko']);
|
|
||||||
const [paying, setPaying] = useState<Payroll | null>(null);
|
const [paying, setPaying] = useState<Payroll | null>(null);
|
||||||
const [cancelling, setCancelling] = useState<Payroll | null>(null);
|
const [cancelling, setCancelling] = useState<Payroll | null>(null);
|
||||||
const [addingAdjustment, setAddingAdjustment] = useState<Payroll | null>(
|
const [addingAdjustment, setAddingAdjustment] = useState<Payroll | null>(
|
||||||
@ -104,23 +103,19 @@ export default function PayrollPeriodShow({ payrollPeriod }: Props) {
|
|||||||
can,
|
can,
|
||||||
});
|
});
|
||||||
|
|
||||||
const activePayrolls = payrollPeriod.payrolls.filter(
|
const totalBaseSalary = payrollPeriod.payrolls.reduce(
|
||||||
(p) => p.status !== 'cancelled',
|
|
||||||
);
|
|
||||||
|
|
||||||
const totalBaseSalary = activePayrolls.reduce(
|
|
||||||
(sum, p) => sum + p.base_salary,
|
(sum, p) => sum + p.base_salary,
|
||||||
0,
|
0,
|
||||||
);
|
);
|
||||||
const totalBonus = activePayrolls.reduce(
|
const totalBonus = payrollPeriod.payrolls.reduce(
|
||||||
(sum, p) => sum + p.bonus_amount,
|
(sum, p) => sum + p.bonus_amount,
|
||||||
0,
|
0,
|
||||||
);
|
);
|
||||||
const totalDeduction = activePayrolls.reduce(
|
const totalDeduction = payrollPeriod.payrolls.reduce(
|
||||||
(sum, p) => sum + p.deduction_amount,
|
(sum, p) => sum + p.deduction_amount,
|
||||||
0,
|
0,
|
||||||
);
|
);
|
||||||
const totalAmount = activePayrolls.reduce(
|
const totalAmount = payrollPeriod.payrolls.reduce(
|
||||||
(sum, p) => sum + p.total_amount,
|
(sum, p) => sum + p.total_amount,
|
||||||
0,
|
0,
|
||||||
);
|
);
|
||||||
@ -138,7 +133,6 @@ export default function PayrollPeriodShow({ payrollPeriod }: Props) {
|
|||||||
Gaji {MONTH_NAMES[payrollPeriod.month]}{' '}
|
Gaji {MONTH_NAMES[payrollPeriod.month]}{' '}
|
||||||
{payrollPeriod.year}
|
{payrollPeriod.year}
|
||||||
</h2>
|
</h2>
|
||||||
{canViewAll && (
|
|
||||||
<p className="text-sm text-muted-foreground">
|
<p className="text-sm text-muted-foreground">
|
||||||
{payrollPeriod.payrolls.length} karyawan ·
|
{payrollPeriod.payrolls.length} karyawan ·
|
||||||
Status:{' '}
|
Status:{' '}
|
||||||
@ -146,7 +140,6 @@ export default function PayrollPeriodShow({ payrollPeriod }: Props) {
|
|||||||
? 'Terbuka'
|
? 'Terbuka'
|
||||||
: 'Ditutup'}
|
: 'Ditutup'}
|
||||||
</p>
|
</p>
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
<Button asChild variant="outline">
|
<Button asChild variant="outline">
|
||||||
<Link href={payrollPeriodsIndex.url()}>
|
<Link href={payrollPeriodsIndex.url()}>
|
||||||
|
|||||||
@ -39,7 +39,6 @@ type CreateColumnsParams = {
|
|||||||
handleResetPassword: (employee: Employee) => void;
|
handleResetPassword: (employee: Employee) => void;
|
||||||
toggleActiveUrl: (id: number) => string;
|
toggleActiveUrl: (id: number) => string;
|
||||||
can: (permission: string) => boolean;
|
can: (permission: string) => boolean;
|
||||||
canViewAll: boolean;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export function createEmployeeColumns(
|
export function createEmployeeColumns(
|
||||||
@ -51,7 +50,6 @@ export function createEmployeeColumns(
|
|||||||
handleResetPassword,
|
handleResetPassword,
|
||||||
toggleActiveUrl,
|
toggleActiveUrl,
|
||||||
can,
|
can,
|
||||||
canViewAll,
|
|
||||||
} = params;
|
} = params;
|
||||||
|
|
||||||
return [
|
return [
|
||||||
@ -95,15 +93,12 @@ export function createEmployeeColumns(
|
|||||||
);
|
);
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
...(canViewAll
|
|
||||||
? [
|
|
||||||
{
|
{
|
||||||
id: 'role',
|
id: 'role',
|
||||||
header: () => <span>Role</span>,
|
header: () => <span>Role</span>,
|
||||||
cell: ({ row }) => {
|
cell: ({ row }) => {
|
||||||
const employee = row.original;
|
const employee = row.original;
|
||||||
const roleName =
|
const roleName = employee.roles?.[0]?.name ?? '-';
|
||||||
employee.roles?.[0]?.name ?? '-';
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<span className="inline-flex items-center rounded-md bg-muted px-2 py-1 text-xs font-medium">
|
<span className="inline-flex items-center rounded-md bg-muted px-2 py-1 text-xs font-medium">
|
||||||
@ -112,8 +107,6 @@ export function createEmployeeColumns(
|
|||||||
);
|
);
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
]
|
|
||||||
: []),
|
|
||||||
{
|
{
|
||||||
accessorKey: 'employee.employment_status',
|
accessorKey: 'employee.employment_status',
|
||||||
id: 'employment_status',
|
id: 'employment_status',
|
||||||
|
|||||||
@ -36,15 +36,12 @@ type Role = {
|
|||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
roles: Role[];
|
roles: Role[];
|
||||||
canViewAll: boolean;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function EmployeeCreate({ roles, canViewAll }: Props) {
|
export default function EmployeeCreate({ roles }: Props) {
|
||||||
const [joinDate, setJoinDate] = useState<Date | undefined>(undefined);
|
const [joinDate, setJoinDate] = useState<Date | undefined>(undefined);
|
||||||
const [resignDate, setResignDate] = useState<Date | undefined>(undefined);
|
const [resignDate, setResignDate] = useState<Date | undefined>(undefined);
|
||||||
const [selectedRole, setSelectedRole] = useState<Role | null>(
|
const [selectedRole, setSelectedRole] = useState<Role | null>(null);
|
||||||
!canViewAll && roles.length === 1 ? roles[0] : null,
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@ -120,7 +117,6 @@ export default function EmployeeCreate({ roles, canViewAll }: Props) {
|
|||||||
message={errors.username}
|
message={errors.username}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
{canViewAll ? (
|
|
||||||
<div className="grid gap-2">
|
<div className="grid gap-2">
|
||||||
<Label>
|
<Label>
|
||||||
Role{' '}
|
Role{' '}
|
||||||
@ -130,9 +126,7 @@ export default function EmployeeCreate({ roles, canViewAll }: Props) {
|
|||||||
</Label>
|
</Label>
|
||||||
<Combobox
|
<Combobox
|
||||||
items={roles}
|
items={roles}
|
||||||
itemToStringLabel={(r) =>
|
itemToStringLabel={(r) => r.name}
|
||||||
r.name
|
|
||||||
}
|
|
||||||
value={selectedRole}
|
value={selectedRole}
|
||||||
onValueChange={(value) =>
|
onValueChange={(value) =>
|
||||||
setSelectedRole(value)
|
setSelectedRole(value)
|
||||||
@ -150,12 +144,8 @@ export default function EmployeeCreate({ roles, canViewAll }: Props) {
|
|||||||
<ComboboxList>
|
<ComboboxList>
|
||||||
{(role) => (
|
{(role) => (
|
||||||
<ComboboxItem
|
<ComboboxItem
|
||||||
key={
|
key={role.id}
|
||||||
role.id
|
value={role}
|
||||||
}
|
|
||||||
value={
|
|
||||||
role
|
|
||||||
}
|
|
||||||
>
|
>
|
||||||
{role.name}
|
{role.name}
|
||||||
</ComboboxItem>
|
</ComboboxItem>
|
||||||
@ -167,14 +157,6 @@ export default function EmployeeCreate({ roles, canViewAll }: Props) {
|
|||||||
message={errors.role}
|
message={errors.role}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
|
||||||
<div className="grid gap-2">
|
|
||||||
<Label>Role</Label>
|
|
||||||
<div className="flex h-10 w-full items-center rounded-md border border-input bg-muted px-3 py-2 text-sm">
|
|
||||||
{selectedRole?.name ?? '-'}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
|
|||||||
@ -56,10 +56,9 @@ type EmployeeData = {
|
|||||||
type Props = {
|
type Props = {
|
||||||
employee: EmployeeData;
|
employee: EmployeeData;
|
||||||
roles: Role[];
|
roles: Role[];
|
||||||
canViewAll: boolean;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function EmployeeEdit({ employee, roles, canViewAll }: Props) {
|
export default function EmployeeEdit({ employee, roles }: Props) {
|
||||||
const currentRole = useMemo(
|
const currentRole = useMemo(
|
||||||
() =>
|
() =>
|
||||||
roles.find((r) => r.name === employee.roles?.[0]?.name) ?? null,
|
roles.find((r) => r.name === employee.roles?.[0]?.name) ?? null,
|
||||||
|
|||||||
@ -40,10 +40,9 @@ type Props = {
|
|||||||
is_active?: string;
|
is_active?: string;
|
||||||
gender?: string;
|
gender?: string;
|
||||||
};
|
};
|
||||||
canViewAll: boolean;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function EmployeeIndex({ employees, filters, canViewAll }: Props) {
|
export default function EmployeeIndex({ employees, filters }: Props) {
|
||||||
const { can } = useCan();
|
const { can } = useCan();
|
||||||
const [deleting, setDeleting] = useState<Employee | null>(null);
|
const [deleting, setDeleting] = useState<Employee | null>(null);
|
||||||
const [resetPasswordTarget, setResetPasswordTarget] =
|
const [resetPasswordTarget, setResetPasswordTarget] =
|
||||||
@ -103,7 +102,6 @@ export default function EmployeeIndex({ employees, filters, canViewAll }: Props)
|
|||||||
handleResetPassword: (employee) => setResetPasswordTarget(employee),
|
handleResetPassword: (employee) => setResetPasswordTarget(employee),
|
||||||
toggleActiveUrl: (id) => toggleActive.url(id),
|
toggleActiveUrl: (id) => toggleActive.url(id),
|
||||||
can,
|
can,
|
||||||
canViewAll,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const filterToolbar = (
|
const filterToolbar = (
|
||||||
|
|||||||
@ -148,9 +148,7 @@ export function createLeaveRequestColumns(
|
|||||||
{
|
{
|
||||||
label: 'Edit',
|
label: 'Edit',
|
||||||
icon: <Pencil className="h-4 w-4" />,
|
icon: <Pencil className="h-4 w-4" />,
|
||||||
show:
|
show: can('leave_requests.update'),
|
||||||
can('leave_requests.update') &&
|
|
||||||
leaveRequest.status === 'pending',
|
|
||||||
onClick: () => handleEdit(leaveRequest),
|
onClick: () => handleEdit(leaveRequest),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -158,9 +156,7 @@ export function createLeaveRequestColumns(
|
|||||||
icon: (
|
icon: (
|
||||||
<Trash2 className="h-4 w-4 text-destructive" />
|
<Trash2 className="h-4 w-4 text-destructive" />
|
||||||
),
|
),
|
||||||
show:
|
show: can('leave_requests.delete'),
|
||||||
can('leave_requests.delete') &&
|
|
||||||
leaveRequest.status === 'pending',
|
|
||||||
onClick: () => handleDeleteClick(leaveRequest),
|
onClick: () => handleDeleteClick(leaveRequest),
|
||||||
},
|
},
|
||||||
]}
|
]}
|
||||||
|
|||||||
@ -7,7 +7,6 @@ import {
|
|||||||
TableHeader,
|
TableHeader,
|
||||||
TableRow,
|
TableRow,
|
||||||
} from '@/components/ui/table';
|
} from '@/components/ui/table';
|
||||||
import { Fragment } from 'react';
|
|
||||||
import { formatNumber } from '@/lib/format';
|
import { formatNumber } from '@/lib/format';
|
||||||
import { formatCurrency } from '@/lib/utils';
|
import { formatCurrency } from '@/lib/utils';
|
||||||
import type { Restock } from './columns';
|
import type { Restock } from './columns';
|
||||||
@ -15,20 +14,8 @@ import type { Restock } from './columns';
|
|||||||
export function RestockItemSubRow({ restock }: { restock: Restock }) {
|
export function RestockItemSubRow({ restock }: { restock: Restock }) {
|
||||||
const items = restock.restock_items ?? [];
|
const items = restock.restock_items ?? [];
|
||||||
|
|
||||||
const groupedByProduct = items.reduce(
|
|
||||||
(acc, item) => {
|
|
||||||
const name = item.product_variant?.product?.name ?? 'Tanpa Produk';
|
|
||||||
if (!acc[name]) acc[name] = [];
|
|
||||||
acc[name].push(item);
|
|
||||||
return acc;
|
|
||||||
},
|
|
||||||
{} as Record<string, typeof items>,
|
|
||||||
);
|
|
||||||
|
|
||||||
let counter = 0;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="overflow-x-auto">
|
<div className="space-y-4 overflow-x-auto">
|
||||||
<Table>
|
<Table>
|
||||||
<TableHeader>
|
<TableHeader>
|
||||||
<TableRow>
|
<TableRow>
|
||||||
@ -36,6 +23,7 @@ export function RestockItemSubRow({ restock }: { restock: Restock }) {
|
|||||||
No
|
No
|
||||||
</TableHead>
|
</TableHead>
|
||||||
<TableHead className="w-[60px]">Foto</TableHead>
|
<TableHead className="w-[60px]">Foto</TableHead>
|
||||||
|
<TableHead>Produk</TableHead>
|
||||||
<TableHead>Varian</TableHead>
|
<TableHead>Varian</TableHead>
|
||||||
<TableHead className="text-right">
|
<TableHead className="text-right">
|
||||||
Harga Modal
|
Harga Modal
|
||||||
@ -48,61 +36,25 @@ export function RestockItemSubRow({ restock }: { restock: Restock }) {
|
|||||||
{items.length === 0 ? (
|
{items.length === 0 ? (
|
||||||
<TableRow>
|
<TableRow>
|
||||||
<TableCell
|
<TableCell
|
||||||
colSpan={6}
|
colSpan={7}
|
||||||
className="text-center text-muted-foreground"
|
className="text-center text-muted-foreground"
|
||||||
>
|
>
|
||||||
Tidak ada item.
|
Tidak ada item.
|
||||||
</TableCell>
|
</TableCell>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
) : (
|
) : (
|
||||||
Object.entries(groupedByProduct).map(
|
items.map((item, index) => (
|
||||||
([productName, groupItems]) => {
|
|
||||||
const totalQty = groupItems.reduce(
|
|
||||||
(sum, item) => sum + (item.quantity ?? 0),
|
|
||||||
0,
|
|
||||||
);
|
|
||||||
const totalSubtotal = groupItems.reduce(
|
|
||||||
(sum, item) => sum + (item.subtotal ?? 0),
|
|
||||||
0,
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Fragment key={productName}>
|
|
||||||
<TableRow>
|
|
||||||
<TableCell
|
|
||||||
colSpan={4}
|
|
||||||
className="font-medium text-muted-foreground bg-muted/20"
|
|
||||||
>
|
|
||||||
{productName}
|
|
||||||
</TableCell>
|
|
||||||
<TableCell className="text-center font-medium text-muted-foreground bg-muted/20">
|
|
||||||
{formatNumber(totalQty)}
|
|
||||||
</TableCell>
|
|
||||||
<TableCell className="text-right font-medium text-muted-foreground bg-muted/20">
|
|
||||||
{formatCurrency(totalSubtotal)}
|
|
||||||
</TableCell>
|
|
||||||
</TableRow>
|
|
||||||
{groupItems.map((item) => {
|
|
||||||
counter++;
|
|
||||||
return (
|
|
||||||
<TableRow key={item.id}>
|
<TableRow key={item.id}>
|
||||||
<TableCell className="text-center">
|
<TableCell className="text-center">
|
||||||
{counter}
|
{index + 1}
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell>
|
<TableCell>
|
||||||
{item.product_variant
|
{item.product_variant?.photo_url ? (
|
||||||
?.photo_url ? (
|
|
||||||
<ImagePreviewButton
|
<ImagePreviewButton
|
||||||
srcs={[
|
srcs={[
|
||||||
item
|
item.product_variant.photo_url,
|
||||||
.product_variant
|
|
||||||
.photo_url,
|
|
||||||
]}
|
]}
|
||||||
title={
|
title={item.product_variant.name}
|
||||||
item
|
|
||||||
.product_variant
|
|
||||||
.name
|
|
||||||
}
|
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<div className="flex h-10 w-10 items-center justify-center rounded-md bg-muted text-xs text-muted-foreground">
|
<div className="flex h-10 w-10 items-center justify-center rounded-md bg-muted text-xs text-muted-foreground">
|
||||||
@ -111,31 +63,22 @@ export function RestockItemSubRow({ restock }: { restock: Restock }) {
|
|||||||
)}
|
)}
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell>
|
<TableCell>
|
||||||
{item.product_variant
|
{item.product_variant?.product?.name ?? '-'}
|
||||||
?.name ?? '-'}
|
</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
{item.product_variant?.name ?? '-'}
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell className="text-right">
|
<TableCell className="text-right">
|
||||||
{formatCurrency(
|
{formatCurrency(item.unit_price)}
|
||||||
item.unit_price,
|
|
||||||
)}
|
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell className="text-center">
|
<TableCell className="text-center">
|
||||||
{formatNumber(
|
{formatNumber(item.quantity)}
|
||||||
item.quantity,
|
|
||||||
)}
|
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell className="text-right font-medium">
|
<TableCell className="text-right font-medium">
|
||||||
{formatCurrency(
|
{formatCurrency(item.subtotal)}
|
||||||
item.subtotal,
|
|
||||||
)}
|
|
||||||
</TableCell>
|
</TableCell>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
);
|
))
|
||||||
})}
|
|
||||||
</Fragment>
|
|
||||||
);
|
|
||||||
},
|
|
||||||
)
|
|
||||||
)}
|
)}
|
||||||
</TableBody>
|
</TableBody>
|
||||||
</Table>
|
</Table>
|
||||||
|
|||||||
@ -1,5 +1,8 @@
|
|||||||
'use no memo';
|
'use no memo';
|
||||||
|
|
||||||
|
import { Form, Head, Link, usePage } from '@inertiajs/react';
|
||||||
|
import { ArrowLeft, Minus, Plus, ShoppingCart, Trash2 } from 'lucide-react';
|
||||||
|
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||||
import { ConfirmDialog } from '@/components/confirm-dialog';
|
import { ConfirmDialog } from '@/components/confirm-dialog';
|
||||||
import { FileUpload } from '@/components/file-upload';
|
import { FileUpload } from '@/components/file-upload';
|
||||||
import { ImagePreviewModal } from '@/components/image-preview-modal';
|
import { ImagePreviewModal } from '@/components/image-preview-modal';
|
||||||
@ -39,12 +42,8 @@ import { formatNumber } from '@/lib/format';
|
|||||||
import { loadTransactionDraft } from '@/lib/transaction-draft';
|
import { loadTransactionDraft } from '@/lib/transaction-draft';
|
||||||
import { getTemporaryUrl } from '@/lib/upload';
|
import { getTemporaryUrl } from '@/lib/upload';
|
||||||
import { formatCurrency } from '@/lib/utils';
|
import { formatCurrency } from '@/lib/utils';
|
||||||
import { store, index as transactionIndex } from '@/routes/admin/manage/transactions';
|
import { index as transactionIndex, store } from '@/routes/admin/manage/transactions';
|
||||||
import { Form, Head, Link, usePage } from '@inertiajs/react';
|
|
||||||
import { ArrowLeft, Minus, Plus, ShoppingCart, Trash2 } from 'lucide-react';
|
|
||||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
|
||||||
import type { ProductForTransaction, TransactionCreateData } from './columns';
|
import type { ProductForTransaction, TransactionCreateData } from './columns';
|
||||||
import { FieldDescription } from '@/components/ui/field';
|
|
||||||
|
|
||||||
type CartLine = {
|
type CartLine = {
|
||||||
key: string;
|
key: string;
|
||||||
@ -798,9 +797,6 @@ export default function TransactionCreate({ data }: Props) {
|
|||||||
}
|
}
|
||||||
placeholder="0"
|
placeholder="0"
|
||||||
/>
|
/>
|
||||||
<FieldDescription>
|
|
||||||
Jika diisi, harga nego menjadi total akhir.
|
|
||||||
</FieldDescription>
|
|
||||||
<InputError
|
<InputError
|
||||||
message={errors.nego_price}
|
message={errors.nego_price}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@ -1,5 +1,8 @@
|
|||||||
'use no memo';
|
'use no memo';
|
||||||
|
|
||||||
|
import { Form, Head, Link } from '@inertiajs/react';
|
||||||
|
import { ArrowLeft, Minus, Plus, ShoppingCart, Trash2 } from 'lucide-react';
|
||||||
|
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||||
import { ConfirmDialog } from '@/components/confirm-dialog';
|
import { ConfirmDialog } from '@/components/confirm-dialog';
|
||||||
import { FileUpload } from '@/components/file-upload';
|
import { FileUpload } from '@/components/file-upload';
|
||||||
import { ImagePreviewModal } from '@/components/image-preview-modal';
|
import { ImagePreviewModal } from '@/components/image-preview-modal';
|
||||||
@ -38,11 +41,7 @@ import { formatNumber } from '@/lib/format';
|
|||||||
import { getTemporaryUrl } from '@/lib/upload';
|
import { getTemporaryUrl } from '@/lib/upload';
|
||||||
import { formatCurrency } from '@/lib/utils';
|
import { formatCurrency } from '@/lib/utils';
|
||||||
import { index as transactionIndex, update } from '@/routes/admin/manage/transactions';
|
import { index as transactionIndex, update } from '@/routes/admin/manage/transactions';
|
||||||
import { FieldDescription } from '@base-ui/react';
|
import type { TransactionCreateData, TransactionForEdit, OptionItem } from './columns';
|
||||||
import { Form, Head, Link } from '@inertiajs/react';
|
|
||||||
import { ArrowLeft, Minus, Plus, ShoppingCart, Trash2 } from 'lucide-react';
|
|
||||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
|
||||||
import type { TransactionCreateData, TransactionForEdit } from './columns';
|
|
||||||
|
|
||||||
type CartLine = {
|
type CartLine = {
|
||||||
key: string;
|
key: string;
|
||||||
@ -734,9 +733,6 @@ export default function TransactionEdit({ transaction, data }: Props) {
|
|||||||
}
|
}
|
||||||
placeholder="0"
|
placeholder="0"
|
||||||
/>
|
/>
|
||||||
<FieldDescription>
|
|
||||||
Jika diisi, harga nego menjadi total akhir.
|
|
||||||
</FieldDescription>
|
|
||||||
<InputError
|
<InputError
|
||||||
message={errors.nego_price}
|
message={errors.nego_price}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@ -1,9 +1,7 @@
|
|||||||
import { Head, Link, router } from '@inertiajs/react';
|
import { Head, Link, router } from '@inertiajs/react';
|
||||||
import { format } from 'date-fns';
|
|
||||||
import { Plus } from 'lucide-react';
|
import { Plus } from 'lucide-react';
|
||||||
import { useMemo, useState } from 'react';
|
import { useMemo, useState } from 'react';
|
||||||
import { CardTable } from '@/components/card-table';
|
import { CardTable } from '@/components/card-table';
|
||||||
import { DatePicker } from '@/components/date-picker';
|
|
||||||
import { DeleteConfirmDialog } from '@/components/delete-confirm-dialog';
|
import { DeleteConfirmDialog } from '@/components/delete-confirm-dialog';
|
||||||
import { FilterPopover } from '@/components/filter-popover';
|
import { FilterPopover } from '@/components/filter-popover';
|
||||||
import { useCardTableExpand } from '@/components/hooks/use-card-table-expand';
|
import { useCardTableExpand } from '@/components/hooks/use-card-table-expand';
|
||||||
@ -31,12 +29,10 @@ import {
|
|||||||
create as transactionCreate,
|
create as transactionCreate,
|
||||||
index as transactionIndex,
|
index as transactionIndex,
|
||||||
edit as transactionEdit,
|
edit as transactionEdit,
|
||||||
updateStatus as transactionUpdateStatus,
|
|
||||||
} from '@/routes/admin/manage/transactions';
|
} from '@/routes/admin/manage/transactions';
|
||||||
import type { Transaction } from './columns';
|
import type { Transaction } from './columns';
|
||||||
import { TransactionCardRow } from './transaction-card';
|
import { TransactionCardRow } from './transaction-card';
|
||||||
import { TransactionItemSubRow } from './transaction-sub-row';
|
import { TransactionItemSubRow } from './transaction-sub-row';
|
||||||
import { TransactionSummaryCard } from './transaction-summary-card';
|
|
||||||
|
|
||||||
type FilterOption = {
|
type FilterOption = {
|
||||||
id: number;
|
id: number;
|
||||||
@ -56,13 +52,6 @@ type Props = {
|
|||||||
per_page: number;
|
per_page: number;
|
||||||
total: number;
|
total: number;
|
||||||
};
|
};
|
||||||
summary: {
|
|
||||||
total_orders: number;
|
|
||||||
total_subtotal: number;
|
|
||||||
total_discount: number;
|
|
||||||
total_amount: number;
|
|
||||||
net_total: number;
|
|
||||||
};
|
|
||||||
filters: {
|
filters: {
|
||||||
status?: string;
|
status?: string;
|
||||||
channel?: string;
|
channel?: string;
|
||||||
@ -70,8 +59,6 @@ type Props = {
|
|||||||
customer_id?: string;
|
customer_id?: string;
|
||||||
marketing_id?: string;
|
marketing_id?: string;
|
||||||
created_by_id?: string;
|
created_by_id?: string;
|
||||||
date_from?: string;
|
|
||||||
date_to?: string;
|
|
||||||
};
|
};
|
||||||
filterOptions: {
|
filterOptions: {
|
||||||
statusOptions: StatusOption[];
|
statusOptions: StatusOption[];
|
||||||
@ -84,7 +71,6 @@ type Props = {
|
|||||||
|
|
||||||
export default function TransactionIndex({
|
export default function TransactionIndex({
|
||||||
transactions,
|
transactions,
|
||||||
summary,
|
|
||||||
filters,
|
filters,
|
||||||
filterOptions,
|
filterOptions,
|
||||||
}: Props) {
|
}: Props) {
|
||||||
@ -148,10 +134,6 @@ export default function TransactionIndex({
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleUpdateStatus(transaction: Transaction, status: string) {
|
|
||||||
router.patch(transactionUpdateStatus.url(transaction.id), { status });
|
|
||||||
}
|
|
||||||
|
|
||||||
const filterToolbar = (
|
const filterToolbar = (
|
||||||
<FilterPopover
|
<FilterPopover
|
||||||
open={filterOpen}
|
open={filterOpen}
|
||||||
@ -163,9 +145,7 @@ export default function TransactionIndex({
|
|||||||
Boolean(filters.payment_type) ||
|
Boolean(filters.payment_type) ||
|
||||||
Boolean(filters.customer_id) ||
|
Boolean(filters.customer_id) ||
|
||||||
Boolean(filters.marketing_id) ||
|
Boolean(filters.marketing_id) ||
|
||||||
Boolean(filters.created_by_id) ||
|
Boolean(filters.created_by_id)
|
||||||
Boolean(filters.date_from) ||
|
|
||||||
Boolean(filters.date_to)
|
|
||||||
}
|
}
|
||||||
onClear={clearFilters}
|
onClear={clearFilters}
|
||||||
>
|
>
|
||||||
@ -331,22 +311,6 @@ export default function TransactionIndex({
|
|||||||
</ComboboxContent>
|
</ComboboxContent>
|
||||||
</Combobox>
|
</Combobox>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-col gap-2">
|
|
||||||
<label className="text-xs text-muted-foreground">Dari Tanggal</label>
|
|
||||||
<DatePicker
|
|
||||||
value={filters.date_from ?? null}
|
|
||||||
onChange={(date) => applyFilter('date_from', date ? format(date, 'yyyy-MM-dd') : '')}
|
|
||||||
placeholder="Pilih tanggal mulai"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className="flex flex-col gap-2">
|
|
||||||
<label className="text-xs text-muted-foreground">Sampai Tanggal</label>
|
|
||||||
<DatePicker
|
|
||||||
value={filters.date_to ?? null}
|
|
||||||
onChange={(date) => applyFilter('date_to', date ? format(date, 'yyyy-MM-dd') : '')}
|
|
||||||
placeholder="Pilih tanggal akhir"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</FilterPopover>
|
</FilterPopover>
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -369,8 +333,6 @@ export default function TransactionIndex({
|
|||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<TransactionSummaryCard summary={summary} />
|
|
||||||
|
|
||||||
<CardTable
|
<CardTable
|
||||||
data={transactions.data}
|
data={transactions.data}
|
||||||
getItemKey={(t) => t.id}
|
getItemKey={(t) => t.id}
|
||||||
@ -403,7 +365,6 @@ export default function TransactionIndex({
|
|||||||
router.visit(transactionEdit.url(t.id));
|
router.visit(transactionEdit.url(t.id));
|
||||||
}}
|
}}
|
||||||
onDelete={(t) => setDeleting(t)}
|
onDelete={(t) => setDeleting(t)}
|
||||||
onUpdateStatus={(t, status) => handleUpdateStatus(t, status)}
|
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
renderSubContent={(transaction) => (
|
renderSubContent={(transaction) => (
|
||||||
|
|||||||
@ -1,3 +1,4 @@
|
|||||||
|
import { ChevronDown, Pencil, Trash2 } from 'lucide-react';
|
||||||
import { RowActions } from '@/components/row-actions';
|
import { RowActions } from '@/components/row-actions';
|
||||||
import { Badge } from '@/components/ui/badge';
|
import { Badge } from '@/components/ui/badge';
|
||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
@ -5,7 +6,6 @@ import { Card, CardContent } from '@/components/ui/card';
|
|||||||
import { useCan } from '@/hooks/use-can';
|
import { useCan } from '@/hooks/use-can';
|
||||||
import { formatDateTime, formatNumber } from '@/lib/format';
|
import { formatDateTime, formatNumber } from '@/lib/format';
|
||||||
import { formatCurrency } from '@/lib/utils';
|
import { formatCurrency } from '@/lib/utils';
|
||||||
import { CheckCircle, ChevronDown, Pencil, Send, Trash2, XCircle } from 'lucide-react';
|
|
||||||
import type { Transaction } from './columns';
|
import type { Transaction } from './columns';
|
||||||
|
|
||||||
const STATUS_BADGE_CLASSES: Record<string, string> = {
|
const STATUS_BADGE_CLASSES: Record<string, string> = {
|
||||||
@ -23,7 +23,6 @@ export type TransactionCardRowParams = {
|
|||||||
onToggleExpand: () => void;
|
onToggleExpand: () => void;
|
||||||
onEdit: (transaction: Transaction) => void;
|
onEdit: (transaction: Transaction) => void;
|
||||||
onDelete: (transaction: Transaction) => void;
|
onDelete: (transaction: Transaction) => void;
|
||||||
onUpdateStatus: (transaction: Transaction, status: string) => void;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export function TransactionCardRow({
|
export function TransactionCardRow({
|
||||||
@ -33,7 +32,6 @@ export function TransactionCardRow({
|
|||||||
onToggleExpand,
|
onToggleExpand,
|
||||||
onEdit,
|
onEdit,
|
||||||
onDelete,
|
onDelete,
|
||||||
onUpdateStatus,
|
|
||||||
}: TransactionCardRowParams) {
|
}: TransactionCardRowParams) {
|
||||||
const { can } = useCan();
|
const { can } = useCan();
|
||||||
const items = transaction.order_items ?? [];
|
const items = transaction.order_items ?? [];
|
||||||
@ -72,7 +70,7 @@ export function TransactionCardRow({
|
|||||||
<span className="text-xs text-muted-foreground">
|
<span className="text-xs text-muted-foreground">
|
||||||
{index}.
|
{index}.
|
||||||
</span>
|
</span>
|
||||||
<h3 className="font-medium">
|
<h3 className="truncate font-medium">
|
||||||
{transaction.order_number}
|
{transaction.order_number}
|
||||||
</h3>
|
</h3>
|
||||||
<Badge
|
<Badge
|
||||||
@ -115,7 +113,7 @@ export function TransactionCardRow({
|
|||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
{transaction.notes && (
|
{transaction.notes && (
|
||||||
<span className="max-w-[200px]">
|
<span className="max-w-[200px] truncate">
|
||||||
{transaction.notes}
|
{transaction.notes}
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
@ -154,14 +152,6 @@ export function TransactionCardRow({
|
|||||||
{formatCurrency(transaction.discount)}
|
{formatCurrency(transaction.discount)}
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
{transaction.nego_price != null && transaction.nego_price > 0 && (
|
|
||||||
<span>
|
|
||||||
<span className="text-muted-foreground">
|
|
||||||
Nego:{' '}
|
|
||||||
</span>
|
|
||||||
{formatCurrency(transaction.nego_price)}
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
<span className="font-semibold">
|
<span className="font-semibold">
|
||||||
<span className="font-normal text-muted-foreground">
|
<span className="font-normal text-muted-foreground">
|
||||||
Total:{' '}
|
Total:{' '}
|
||||||
@ -185,38 +175,6 @@ export function TransactionCardRow({
|
|||||||
|
|
||||||
<RowActions
|
<RowActions
|
||||||
actions={[
|
actions={[
|
||||||
...(transaction.status === 'pending' && can('orders.update')
|
|
||||||
? [
|
|
||||||
{
|
|
||||||
label: 'Kirim',
|
|
||||||
icon: <Send className="h-4 w-4" />,
|
|
||||||
onClick: () => onUpdateStatus(transaction, 'processing'),
|
|
||||||
},
|
|
||||||
]
|
|
||||||
: []),
|
|
||||||
...((transaction.status === 'pending' || transaction.status === 'processing') && can('orders.update')
|
|
||||||
? [
|
|
||||||
{
|
|
||||||
label: 'Selesai',
|
|
||||||
icon: <CheckCircle className="h-4 w-4" />,
|
|
||||||
onClick: () => onUpdateStatus(transaction, 'completed'),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: 'Dibatalkan',
|
|
||||||
icon: <XCircle className="h-4 w-4 text-destructive" />,
|
|
||||||
onClick: () => onUpdateStatus(transaction, 'cancelled'),
|
|
||||||
},
|
|
||||||
]
|
|
||||||
: []),
|
|
||||||
...(transaction.status === 'completed' && can('orders.update')
|
|
||||||
? [
|
|
||||||
{
|
|
||||||
label: 'Refund',
|
|
||||||
icon: <XCircle className="h-4 w-4 text-destructive" />,
|
|
||||||
onClick: () => onUpdateStatus(transaction, 'refunded'),
|
|
||||||
},
|
|
||||||
]
|
|
||||||
: []),
|
|
||||||
{
|
{
|
||||||
label: 'Edit',
|
label: 'Edit',
|
||||||
icon: <Pencil className="h-4 w-4" />,
|
icon: <Pencil className="h-4 w-4" />,
|
||||||
|
|||||||
@ -1,90 +0,0 @@
|
|||||||
import { Card, CardContent } from '@/components/ui/card';
|
|
||||||
import { formatCurrency } from '@/lib/utils';
|
|
||||||
import {
|
|
||||||
Banknote,
|
|
||||||
CircleDollarSign,
|
|
||||||
FileText,
|
|
||||||
Percent,
|
|
||||||
TrendingUp,
|
|
||||||
} from 'lucide-react';
|
|
||||||
|
|
||||||
type Summary = {
|
|
||||||
total_orders: number;
|
|
||||||
total_subtotal: number;
|
|
||||||
total_discount: number;
|
|
||||||
total_amount: number;
|
|
||||||
net_total: number;
|
|
||||||
};
|
|
||||||
|
|
||||||
type TransactionSummaryCardProps = {
|
|
||||||
summary: Summary;
|
|
||||||
};
|
|
||||||
|
|
||||||
const summaryItems = [
|
|
||||||
{
|
|
||||||
key: 'total_orders',
|
|
||||||
label: 'Total Pesanan',
|
|
||||||
icon: FileText,
|
|
||||||
color: 'bg-blue-100',
|
|
||||||
iconColor: 'text-blue-600',
|
|
||||||
format: (value: number) => value.toLocaleString('id-ID'),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: 'total_subtotal',
|
|
||||||
label: 'Subtotal',
|
|
||||||
icon: Banknote,
|
|
||||||
color: 'bg-emerald-100',
|
|
||||||
iconColor: 'text-emerald-600',
|
|
||||||
format: formatCurrency,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: 'total_discount',
|
|
||||||
label: 'Diskon',
|
|
||||||
icon: Percent,
|
|
||||||
color: 'bg-amber-100',
|
|
||||||
iconColor: 'text-amber-600',
|
|
||||||
format: formatCurrency,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: 'total_amount',
|
|
||||||
label: 'Total Uang',
|
|
||||||
icon: CircleDollarSign,
|
|
||||||
color: 'bg-purple-100',
|
|
||||||
iconColor: 'text-purple-600',
|
|
||||||
format: formatCurrency,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: 'net_total',
|
|
||||||
label: 'Total Bersih',
|
|
||||||
icon: TrendingUp,
|
|
||||||
color: 'bg-sky-100',
|
|
||||||
iconColor: 'text-sky-600',
|
|
||||||
format: formatCurrency,
|
|
||||||
},
|
|
||||||
] as const;
|
|
||||||
|
|
||||||
export function TransactionSummaryCard({ summary }: TransactionSummaryCardProps) {
|
|
||||||
return (
|
|
||||||
<div className="grid grid-cols-1 gap-3 sm:grid-cols-3 lg:grid-cols-5">
|
|
||||||
{summaryItems.map((item) => (
|
|
||||||
<Card key={item.key}>
|
|
||||||
<CardContent className="flex items-center gap-3 py-3">
|
|
||||||
<div
|
|
||||||
className={`flex h-10 w-10 shrink-0 items-center justify-center rounded-lg ${item.color}`}
|
|
||||||
>
|
|
||||||
<item.icon className={`h-5 w-5 ${item.iconColor}`} />
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<p className="text-xs text-muted-foreground">
|
|
||||||
{item.label}
|
|
||||||
</p>
|
|
||||||
<p className="text-lg font-bold">
|
|
||||||
{item.format(summary[item.key] as number)}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@ -41,7 +41,7 @@ export function createCustomerColumns(
|
|||||||
accessorKey: 'address',
|
accessorKey: 'address',
|
||||||
header: () => <span>Alamat</span>,
|
header: () => <span>Alamat</span>,
|
||||||
cell: ({ row }) => (
|
cell: ({ row }) => (
|
||||||
<span className="block max-w-[200px]">
|
<span className="block max-w-[200px] truncate">
|
||||||
{(row.getValue('address') as string) ?? '-'}
|
{(row.getValue('address') as string) ?? '-'}
|
||||||
</span>
|
</span>
|
||||||
),
|
),
|
||||||
|
|||||||
@ -1,16 +1,16 @@
|
|||||||
|
import { router } from '@inertiajs/react';
|
||||||
|
import type { ColumnDef } from '@tanstack/react-table';
|
||||||
|
import { ChevronRight, Pencil, Trash2 } from 'lucide-react';
|
||||||
import { RowActions } from '@/components/row-actions';
|
import { RowActions } from '@/components/row-actions';
|
||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
import { Switch } from '@/components/ui/switch';
|
import { Switch } from '@/components/ui/switch';
|
||||||
import { formatNumber } from '@/lib/format';
|
import { formatNumber } from '@/lib/format';
|
||||||
import { router } from '@inertiajs/react';
|
import { formatCurrency } from '@/lib/utils';
|
||||||
import type { ColumnDef } from '@tanstack/react-table';
|
|
||||||
import { ChevronRight, Pencil, Trash2 } from 'lucide-react';
|
|
||||||
|
|
||||||
export type ProductVariant = {
|
export type ProductVariant = {
|
||||||
id: number;
|
id: number;
|
||||||
name: string;
|
name: string;
|
||||||
stock: number;
|
stock: number;
|
||||||
formatted_stock: string
|
|
||||||
reject_stock: number;
|
reject_stock: number;
|
||||||
retail_stock: number;
|
retail_stock: number;
|
||||||
photo_urls: string[];
|
photo_urls: string[];
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
import { Link, router } from '@inertiajs/react';
|
import { router } from '@inertiajs/react';
|
||||||
import { CardTable } from '@/components/card-table';
|
import { CardTable } from '@/components/card-table';
|
||||||
import { DeleteConfirmDialog } from '@/components/delete-confirm-dialog';
|
import { DeleteConfirmDialog } from '@/components/delete-confirm-dialog';
|
||||||
import { FilterPopover } from '@/components/filter-popover';
|
import { FilterPopover } from '@/components/filter-popover';
|
||||||
@ -34,7 +34,7 @@ import {
|
|||||||
edit as variantEdit,
|
edit as variantEdit,
|
||||||
} from '@/routes/admin/master/products/variants';
|
} from '@/routes/admin/master/products/variants';
|
||||||
import { Head } from '@inertiajs/react';
|
import { Head } from '@inertiajs/react';
|
||||||
import { Plus } from 'lucide-react';
|
import { Link, Plus } from 'lucide-react';
|
||||||
import { useMemo, useState } from 'react';
|
import { useMemo, useState } from 'react';
|
||||||
import type { Product, ProductVariant } from './columns';
|
import type { Product, ProductVariant } from './columns';
|
||||||
import { ProductCardRow } from './product-card';
|
import { ProductCardRow } from './product-card';
|
||||||
@ -52,7 +52,6 @@ type Props = {
|
|||||||
id: number;
|
id: number;
|
||||||
name: string;
|
name: string;
|
||||||
}[];
|
}[];
|
||||||
productNames: string[];
|
|
||||||
filters: {
|
filters: {
|
||||||
status?: string;
|
status?: string;
|
||||||
name?: string;
|
name?: string;
|
||||||
@ -61,7 +60,7 @@ type Props = {
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function ProductIndex({ products, categories, productNames, filters }: Props) {
|
export default function ProductIndex({ products, categories, filters }: Props) {
|
||||||
const { can } = useCan();
|
const { can } = useCan();
|
||||||
const [deleting, setDeleting] = useState<Product | null>(null);
|
const [deleting, setDeleting] = useState<Product | null>(null);
|
||||||
const [deletingVariant, setDeletingVariant] = useState<{
|
const [deletingVariant, setDeletingVariant] = useState<{
|
||||||
@ -93,10 +92,11 @@ export default function ProductIndex({ products, categories, productNames, filte
|
|||||||
filterWithParams: false,
|
filterWithParams: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
const sortedProductNames = useMemo(
|
const productNames = useMemo(() => {
|
||||||
() => [...productNames].sort(),
|
const names = products.data.map((p) => p.name);
|
||||||
[productNames],
|
|
||||||
);
|
return [...new Set(names)].sort();
|
||||||
|
}, [products.data]);
|
||||||
|
|
||||||
const selectedCategory = useMemo(
|
const selectedCategory = useMemo(
|
||||||
() => categories.find((c) => String(c.id) === filters.category) ?? null,
|
() => categories.find((c) => String(c.id) === filters.category) ?? null,
|
||||||
@ -147,7 +147,7 @@ export default function ProductIndex({ products, categories, productNames, filte
|
|||||||
Nama Produk
|
Nama Produk
|
||||||
</label>
|
</label>
|
||||||
<Combobox
|
<Combobox
|
||||||
items={sortedProductNames}
|
items={productNames}
|
||||||
value={filters.name ?? ''}
|
value={filters.name ?? ''}
|
||||||
onValueChange={(value) =>
|
onValueChange={(value) =>
|
||||||
applyFilter('name', (value as string) ?? '')
|
applyFilter('name', (value as string) ?? '')
|
||||||
|
|||||||
@ -67,7 +67,7 @@ export function TransferStockDialog({
|
|||||||
Stok Bagus Tersedia
|
Stok Bagus Tersedia
|
||||||
</Label>
|
</Label>
|
||||||
<p className="text-sm font-medium">
|
<p className="text-sm font-medium">
|
||||||
{variant.formatted_stock}
|
{variant.stock}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="grid gap-2">
|
<div className="grid gap-2">
|
||||||
|
|||||||
@ -41,7 +41,7 @@ export function createSupplierColumns(
|
|||||||
accessorKey: 'address',
|
accessorKey: 'address',
|
||||||
header: () => <span>Alamat</span>,
|
header: () => <span>Alamat</span>,
|
||||||
cell: ({ row }) => (
|
cell: ({ row }) => (
|
||||||
<span className="block max-w-[200px]">
|
<span className="block max-w-[200px] truncate">
|
||||||
{(row.getValue('address') as string) ?? '-'}
|
{(row.getValue('address') as string) ?? '-'}
|
||||||
</span>
|
</span>
|
||||||
),
|
),
|
||||||
|
|||||||
@ -65,7 +65,6 @@
|
|||||||
Route::resource('cuttings', CuttingController::class)->except(['show'])->middleware('permission:cuttings.view|cuttings.create|cuttings.update|cuttings.delete');
|
Route::resource('cuttings', CuttingController::class)->except(['show'])->middleware('permission:cuttings.view|cuttings.create|cuttings.update|cuttings.delete');
|
||||||
|
|
||||||
Route::resource('transactions', TransactionController::class)->except(['show'])->middleware('permission:orders.view|orders.create|orders.update|orders.delete');
|
Route::resource('transactions', TransactionController::class)->except(['show'])->middleware('permission:orders.view|orders.create|orders.update|orders.delete');
|
||||||
Route::patch('transactions/{transaction}/status', [TransactionController::class, 'updateStatus'])->name('transactions.updateStatus')->middleware('permission:orders.update');
|
|
||||||
|
|
||||||
Route::resource('restocks', RestockController::class)->except(['show'])->middleware('permission:restocks.view|restocks.create|restocks.update|restocks.delete');
|
Route::resource('restocks', RestockController::class)->except(['show'])->middleware('permission:restocks.view|restocks.create|restocks.update|restocks.delete');
|
||||||
});
|
});
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user