feat: implement Employee Advance management functionality
- Add EmployeeAdvanceService for handling employee advance operations including create, update, delete, approve, and pay. - Create new components for date picking and calendar UI. - Develop Employee Advance columns for data table representation. - Implement Employee Advance index page with CRUD operations and dialogs for creating, editing, approving, and paying advances. - Update routes to include employee advances resource and specific actions for approval and payment. - Add necessary dependencies for date handling and UI components.
This commit is contained in:
parent
db58fbe049
commit
912f576c74
@ -0,0 +1,97 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Controllers\Admin\Finance;
|
||||||
|
|
||||||
|
use App\Http\Controllers\Controller;
|
||||||
|
use App\Http\Requests\Admin\Finance\EmployeeAdvanceRequest;
|
||||||
|
use App\Models\EmployeeAdvance;
|
||||||
|
use App\Services\Admin\Finance\EmployeeAdvanceService;
|
||||||
|
use Illuminate\Http\RedirectResponse;
|
||||||
|
use Illuminate\Validation\ValidationException;
|
||||||
|
use Inertia\Inertia;
|
||||||
|
use Inertia\Response;
|
||||||
|
|
||||||
|
class EmployeeAdvanceController extends Controller
|
||||||
|
{
|
||||||
|
public function __construct(
|
||||||
|
private EmployeeAdvanceService $service
|
||||||
|
) {}
|
||||||
|
|
||||||
|
public function index(): Response
|
||||||
|
{
|
||||||
|
return Inertia::render('admin/finance/employee-advance/index', [
|
||||||
|
'employeeAdvances' => $this->service->getAll(),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function store(EmployeeAdvanceRequest $request): RedirectResponse
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
$this->service->create($request->validated());
|
||||||
|
|
||||||
|
Inertia::flash('toast', ['type' => 'success', 'message' => 'Kasbon berhasil ditambahkan.']);
|
||||||
|
} catch (ValidationException $e) {
|
||||||
|
$firstError = collect($e->errors())->flatten()->first();
|
||||||
|
Inertia::flash('toast', ['type' => 'error', 'message' => $firstError ?? 'Terjadi kesalahan.']);
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
Inertia::flash('toast', ['type' => 'error', 'message' => $e->getMessage()]);
|
||||||
|
}
|
||||||
|
|
||||||
|
return to_route('admin.finance.employee-advances.index');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function update(EmployeeAdvanceRequest $request, EmployeeAdvance $employeeAdvance): RedirectResponse
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
$this->service->update($employeeAdvance, $request->validated());
|
||||||
|
|
||||||
|
Inertia::flash('toast', ['type' => 'success', 'message' => 'Kasbon berhasil diperbarui.']);
|
||||||
|
} catch (ValidationException $e) {
|
||||||
|
$firstError = collect($e->errors())->flatten()->first();
|
||||||
|
Inertia::flash('toast', ['type' => 'error', 'message' => $firstError ?? 'Terjadi kesalahan.']);
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
Inertia::flash('toast', ['type' => 'error', 'message' => $e->getMessage()]);
|
||||||
|
}
|
||||||
|
|
||||||
|
return to_route('admin.finance.employee-advances.index');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function destroy(EmployeeAdvance $employeeAdvance): RedirectResponse
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
$this->service->delete($employeeAdvance);
|
||||||
|
|
||||||
|
Inertia::flash('toast', ['type' => 'success', 'message' => 'Kasbon berhasil dihapus.']);
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
Inertia::flash('toast', ['type' => 'error', 'message' => $e->getMessage()]);
|
||||||
|
}
|
||||||
|
|
||||||
|
return to_route('admin.finance.employee-advances.index');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function approve(EmployeeAdvance $employeeAdvance): RedirectResponse
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
$this->service->approve($employeeAdvance);
|
||||||
|
|
||||||
|
Inertia::flash('toast', ['type' => 'success', 'message' => 'Kasbon berhasil disetujui.']);
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
Inertia::flash('toast', ['type' => 'error', 'message' => $e->getMessage()]);
|
||||||
|
}
|
||||||
|
|
||||||
|
return to_route('admin.finance.employee-advances.index');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function pay(EmployeeAdvance $employeeAdvance): RedirectResponse
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
$this->service->pay($employeeAdvance);
|
||||||
|
|
||||||
|
Inertia::flash('toast', ['type' => 'success', 'message' => 'Kasbon berhasil dibayar.']);
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
Inertia::flash('toast', ['type' => 'error', 'message' => $e->getMessage()]);
|
||||||
|
}
|
||||||
|
|
||||||
|
return to_route('admin.finance.employee-advances.index');
|
||||||
|
}
|
||||||
|
}
|
||||||
42
app/Http/Requests/Admin/Finance/EmployeeAdvanceRequest.php
Normal file
42
app/Http/Requests/Admin/Finance/EmployeeAdvanceRequest.php
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Requests\Admin\Finance;
|
||||||
|
|
||||||
|
use Illuminate\Foundation\Http\FormRequest;
|
||||||
|
use Override;
|
||||||
|
|
||||||
|
class EmployeeAdvanceRequest extends FormRequest
|
||||||
|
{
|
||||||
|
public function authorize(): bool
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
#[Override]
|
||||||
|
public function prepareForValidation()
|
||||||
|
{
|
||||||
|
if ($this->has('amount')) {
|
||||||
|
$this->merge([
|
||||||
|
'amount' => str_replace('.', '', $this->amount),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function rules(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'amount' => ['required', 'integer', 'min:1'],
|
||||||
|
'description' => ['required', 'string', 'max:100'],
|
||||||
|
'due_date' => ['required', 'date', 'after_or_equal:today'],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function attributes(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'amount' => 'jumlah',
|
||||||
|
'description' => 'keterangan',
|
||||||
|
'due_date' => 'jatuh tempo',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
150
app/Services/Admin/Finance/EmployeeAdvanceService.php
Normal file
150
app/Services/Admin/Finance/EmployeeAdvanceService.php
Normal file
@ -0,0 +1,150 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Services\Admin\Finance;
|
||||||
|
|
||||||
|
use App\Enums\CashTransactionType;
|
||||||
|
use App\Enums\EmployeeAdvanceStatus;
|
||||||
|
use App\Models\CashAccount;
|
||||||
|
use App\Models\CashTransaction;
|
||||||
|
use App\Models\EmployeeAdvance;
|
||||||
|
use Illuminate\Support\Collection;
|
||||||
|
use Illuminate\Support\Facades\DB;
|
||||||
|
use Illuminate\Validation\ValidationException;
|
||||||
|
|
||||||
|
class EmployeeAdvanceService
|
||||||
|
{
|
||||||
|
public function getAll(): Collection
|
||||||
|
{
|
||||||
|
return EmployeeAdvance::with(['employee.user.userProfile'])
|
||||||
|
->latest()
|
||||||
|
->get();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function create(array $data): EmployeeAdvance
|
||||||
|
{
|
||||||
|
return DB::transaction(function () use ($data) {
|
||||||
|
$cashAccount = CashAccount::firstOrFail();
|
||||||
|
$employee = auth()->user()->employee;
|
||||||
|
|
||||||
|
if (!$employee) {
|
||||||
|
throw new \Exception('Anda tidak terdaftar sebagai karyawan.');
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($cashAccount->balance < $data['amount']) {
|
||||||
|
throw ValidationException::withMessages([
|
||||||
|
'amount' => 'Saldo tidak mencukupi.',
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
$newBalance = $cashAccount->balance - $data['amount'];
|
||||||
|
$cashAccount->update(['balance' => $newBalance]);
|
||||||
|
|
||||||
|
$cashTransaction = CashTransaction::create([
|
||||||
|
'cash_account_id' => $cashAccount->id,
|
||||||
|
'created_by_id' => auth()->id(),
|
||||||
|
'amount' => $data['amount'],
|
||||||
|
'balance_after' => $newBalance,
|
||||||
|
'type' => CashTransactionType::EXPENSE,
|
||||||
|
'description' => 'Kasbon: ' . $data['description'],
|
||||||
|
]);
|
||||||
|
|
||||||
|
return EmployeeAdvance::create([
|
||||||
|
'cash_transaction_id' => $cashTransaction->id,
|
||||||
|
'employee_id' => $employee->id,
|
||||||
|
'amount' => $data['amount'],
|
||||||
|
'description' => $data['description'],
|
||||||
|
'due_date' => $data['due_date'],
|
||||||
|
'status' => EmployeeAdvanceStatus::PENDING,
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
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([
|
||||||
|
'amount' => $newAmount,
|
||||||
|
'description' => $data['description'],
|
||||||
|
'due_date' => $data['due_date'],
|
||||||
|
]);
|
||||||
|
|
||||||
|
return $employeeAdvance;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public function delete(EmployeeAdvance $employeeAdvance): bool
|
||||||
|
{
|
||||||
|
return DB::transaction(function () use ($employeeAdvance) {
|
||||||
|
$cashAccount = CashAccount::firstOrFail();
|
||||||
|
|
||||||
|
if ($employeeAdvance->status === EmployeeAdvanceStatus::PAID) {
|
||||||
|
$newBalance = $cashAccount->balance + $employeeAdvance->amount;
|
||||||
|
$cashAccount->update(['balance' => $newBalance]);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $employeeAdvance->delete();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public function approve(EmployeeAdvance $employeeAdvance): EmployeeAdvance
|
||||||
|
{
|
||||||
|
$employeeAdvance->update([
|
||||||
|
'status' => EmployeeAdvanceStatus::APPROVED,
|
||||||
|
'verified_by_id' => auth()->id(),
|
||||||
|
'verified_at' => now(),
|
||||||
|
]);
|
||||||
|
|
||||||
|
return $employeeAdvance;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function pay(EmployeeAdvance $employeeAdvance): EmployeeAdvance
|
||||||
|
{
|
||||||
|
return DB::transaction(function () use ($employeeAdvance) {
|
||||||
|
$cashAccount = CashAccount::firstOrFail();
|
||||||
|
|
||||||
|
$newBalance = $cashAccount->balance + $employeeAdvance->amount;
|
||||||
|
$cashAccount->update(['balance' => $newBalance]);
|
||||||
|
|
||||||
|
$cashTransaction = CashTransaction::create([
|
||||||
|
'cash_account_id' => $cashAccount->id,
|
||||||
|
'created_by_id' => auth()->id(),
|
||||||
|
'amount' => $employeeAdvance->amount,
|
||||||
|
'balance_after' => $newBalance,
|
||||||
|
'type' => CashTransactionType::DEPOSIT,
|
||||||
|
'description' => 'Pembayaran kasbon: ' . $employeeAdvance->description,
|
||||||
|
]);
|
||||||
|
|
||||||
|
$employeeAdvance->update([
|
||||||
|
'status' => EmployeeAdvanceStatus::PAID,
|
||||||
|
'paid_by_id' => auth()->id(),
|
||||||
|
'paid_amount' => $employeeAdvance->amount,
|
||||||
|
'paid_at' => now(),
|
||||||
|
'repayment_cash_transaction_id' => $cashTransaction->id,
|
||||||
|
]);
|
||||||
|
|
||||||
|
return $employeeAdvance;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -53,6 +53,7 @@
|
|||||||
"class-variance-authority": "^0.7.1",
|
"class-variance-authority": "^0.7.1",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
"concurrently": "^9.0.1",
|
"concurrently": "^9.0.1",
|
||||||
|
"date-fns": "^4.4.0",
|
||||||
"globals": "^15.14.0",
|
"globals": "^15.14.0",
|
||||||
"input-otp": "^1.4.2",
|
"input-otp": "^1.4.2",
|
||||||
"laravel-vite-plugin": "^3.0.0",
|
"laravel-vite-plugin": "^3.0.0",
|
||||||
@ -60,6 +61,7 @@
|
|||||||
"next-themes": "^0.4.6",
|
"next-themes": "^0.4.6",
|
||||||
"radix-ui": "^1.6.7",
|
"radix-ui": "^1.6.7",
|
||||||
"react": "^19.2.0",
|
"react": "^19.2.0",
|
||||||
|
"react-day-picker": "^10.0.1",
|
||||||
"react-dom": "^19.2.0",
|
"react-dom": "^19.2.0",
|
||||||
"shadcn": "^4.16.0",
|
"shadcn": "^4.16.0",
|
||||||
"sonner": "^2.0.0",
|
"sonner": "^2.0.0",
|
||||||
|
|||||||
34
pnpm-lock.yaml
generated
34
pnpm-lock.yaml
generated
@ -80,6 +80,9 @@ importers:
|
|||||||
concurrently:
|
concurrently:
|
||||||
specifier: ^9.0.1
|
specifier: ^9.0.1
|
||||||
version: 9.2.4
|
version: 9.2.4
|
||||||
|
date-fns:
|
||||||
|
specifier: ^4.4.0
|
||||||
|
version: 4.4.0
|
||||||
globals:
|
globals:
|
||||||
specifier: ^15.14.0
|
specifier: ^15.14.0
|
||||||
version: 15.15.0
|
version: 15.15.0
|
||||||
@ -101,6 +104,9 @@ importers:
|
|||||||
react:
|
react:
|
||||||
specifier: ^19.2.0
|
specifier: ^19.2.0
|
||||||
version: 19.2.8
|
version: 19.2.8
|
||||||
|
react-day-picker:
|
||||||
|
specifier: ^10.0.1
|
||||||
|
version: 10.0.1(@types/react@19.2.17)(react@19.2.8)
|
||||||
react-dom:
|
react-dom:
|
||||||
specifier: ^19.2.0
|
specifier: ^19.2.0
|
||||||
version: 19.2.8(react@19.2.8)
|
version: 19.2.8(react@19.2.8)
|
||||||
@ -331,6 +337,9 @@ packages:
|
|||||||
resolution: {integrity: sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA==}
|
resolution: {integrity: sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA==}
|
||||||
engines: {node: '>=6.9.0'}
|
engines: {node: '>=6.9.0'}
|
||||||
|
|
||||||
|
'@date-fns/tz@1.5.0':
|
||||||
|
resolution: {integrity: sha512-lwYN/vDPeNRULcepoE/LO2Pgx+7/RV+S9ARfbc9lr2DtGkOD7pAiruHvbR1RX3Qyf6ja47EWJDMsNK5vK08DJg==}
|
||||||
|
|
||||||
'@dotenvx/dotenvx@1.75.1':
|
'@dotenvx/dotenvx@1.75.1':
|
||||||
resolution: {integrity: sha512-/BITOC9dmS/edY2zQwZNicQ059O6RKabtQfyEafV0nGtfYRNHYy1DIPiYVcov40+tob9hfmBnbR963dS+EQ1DQ==}
|
resolution: {integrity: sha512-/BITOC9dmS/edY2zQwZNicQ059O6RKabtQfyEafV0nGtfYRNHYy1DIPiYVcov40+tob9hfmBnbR963dS+EQ1DQ==}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
@ -1950,6 +1959,9 @@ packages:
|
|||||||
resolution: {integrity: sha512-BS8PfmtDGnrgYdOonGZQdLZslWIeCGFP9tpan0hi1Co2Zr2NKADsvGYA8XxuG/4UWgJ6Cjtv+YJnB6MM69QGlQ==}
|
resolution: {integrity: sha512-BS8PfmtDGnrgYdOonGZQdLZslWIeCGFP9tpan0hi1Co2Zr2NKADsvGYA8XxuG/4UWgJ6Cjtv+YJnB6MM69QGlQ==}
|
||||||
engines: {node: '>= 0.4'}
|
engines: {node: '>= 0.4'}
|
||||||
|
|
||||||
|
date-fns@4.4.0:
|
||||||
|
resolution: {integrity: sha512-+1UMbeh68lH1SegH83CGWwpb6OHHbpSgr3+s5Eww5M4CAgswBpoWS0AjTOfEJ33HiYKz1hdj/KTFprzXHmq/6w==}
|
||||||
|
|
||||||
debounce-fn@4.0.0:
|
debounce-fn@4.0.0:
|
||||||
resolution: {integrity: sha512-8pYCQiL9Xdcg0UPSD3d+0KMlOjp+KGU5EPwYddgzQ7DATsg4fuUDjQtsYLmWjnk2obnNHgV3vE2Y4jejSOJVBQ==}
|
resolution: {integrity: sha512-8pYCQiL9Xdcg0UPSD3d+0KMlOjp+KGU5EPwYddgzQ7DATsg4fuUDjQtsYLmWjnk2obnNHgV3vE2Y4jejSOJVBQ==}
|
||||||
engines: {node: '>=10'}
|
engines: {node: '>=10'}
|
||||||
@ -3378,6 +3390,16 @@ packages:
|
|||||||
resolution: {integrity: sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA==}
|
resolution: {integrity: sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA==}
|
||||||
engines: {node: '>= 0.10'}
|
engines: {node: '>= 0.10'}
|
||||||
|
|
||||||
|
react-day-picker@10.0.1:
|
||||||
|
resolution: {integrity: sha512-eNh6BlwcYInWaJtRv18mXQ06Ys/H6rdTZAnTaSdOYJuTpwP1JMCHNd1FDRadA+gbeinq+psdULN5Xnowy9mV8w==}
|
||||||
|
engines: {node: '>=18'}
|
||||||
|
peerDependencies:
|
||||||
|
'@types/react': '>=16.8.0'
|
||||||
|
react: '>=16.8.0'
|
||||||
|
peerDependenciesMeta:
|
||||||
|
'@types/react':
|
||||||
|
optional: true
|
||||||
|
|
||||||
react-dom@19.2.8:
|
react-dom@19.2.8:
|
||||||
resolution: {integrity: sha512-rVprimfGBG3DR+Tq0IQG2DT5PxKth1WIGDmj5yPmlzr4YBe7uyE+Du4oVqTDXZSHGGGXRtTJEGSSePyQCMBglQ==}
|
resolution: {integrity: sha512-rVprimfGBG3DR+Tq0IQG2DT5PxKth1WIGDmj5yPmlzr4YBe7uyE+Du4oVqTDXZSHGGGXRtTJEGSSePyQCMBglQ==}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
@ -4161,6 +4183,8 @@ snapshots:
|
|||||||
'@babel/helper-string-parser': 7.29.7
|
'@babel/helper-string-parser': 7.29.7
|
||||||
'@babel/helper-validator-identifier': 7.29.7
|
'@babel/helper-validator-identifier': 7.29.7
|
||||||
|
|
||||||
|
'@date-fns/tz@1.5.0': {}
|
||||||
|
|
||||||
'@dotenvx/dotenvx@1.75.1':
|
'@dotenvx/dotenvx@1.75.1':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@dotenvx/primitives': 0.8.0
|
'@dotenvx/primitives': 0.8.0
|
||||||
@ -5832,6 +5856,8 @@ snapshots:
|
|||||||
es-errors: 1.3.0
|
es-errors: 1.3.0
|
||||||
is-data-view: 1.0.2
|
is-data-view: 1.0.2
|
||||||
|
|
||||||
|
date-fns@4.4.0: {}
|
||||||
|
|
||||||
debounce-fn@4.0.0:
|
debounce-fn@4.0.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
mimic-fn: 3.1.0
|
mimic-fn: 3.1.0
|
||||||
@ -7267,6 +7293,14 @@ snapshots:
|
|||||||
iconv-lite: 0.7.3
|
iconv-lite: 0.7.3
|
||||||
unpipe: 1.0.0
|
unpipe: 1.0.0
|
||||||
|
|
||||||
|
react-day-picker@10.0.1(@types/react@19.2.17)(react@19.2.8):
|
||||||
|
dependencies:
|
||||||
|
'@date-fns/tz': 1.5.0
|
||||||
|
date-fns: 4.4.0
|
||||||
|
react: 19.2.8
|
||||||
|
optionalDependencies:
|
||||||
|
'@types/react': 19.2.17
|
||||||
|
|
||||||
react-dom@19.2.8(react@19.2.8):
|
react-dom@19.2.8(react@19.2.8):
|
||||||
dependencies:
|
dependencies:
|
||||||
react: 19.2.8
|
react: 19.2.8
|
||||||
|
|||||||
@ -40,6 +40,7 @@ import { index as customersIndex } from '@/routes/admin/master/customers';
|
|||||||
import { index as suppliersIndex } from '@/routes/admin/master/suppliers';
|
import { index as suppliersIndex } from '@/routes/admin/master/suppliers';
|
||||||
import { index as cashAccountsIndex } from '@/routes/admin/finance/cash-accounts';
|
import { index as cashAccountsIndex } from '@/routes/admin/finance/cash-accounts';
|
||||||
import { index as expensesIndex } from '@/routes/admin/finance/expenses';
|
import { index as expensesIndex } from '@/routes/admin/finance/expenses';
|
||||||
|
import { index as employeeAdvancesIndex } from '@/routes/admin/finance/employee-advances';
|
||||||
|
|
||||||
type NavMenuItem = { title: string; href: string; icon: LucideIcon };
|
type NavMenuItem = { title: string; href: string; icon: LucideIcon };
|
||||||
|
|
||||||
@ -73,7 +74,7 @@ const kelolaItems: NavMenuItem[] = [
|
|||||||
const keuanganItems: NavMenuItem[] = [
|
const keuanganItems: NavMenuItem[] = [
|
||||||
{ title: 'Kas Toko', href: cashAccountsIndex.url(), icon: Wallet },
|
{ title: 'Kas Toko', href: cashAccountsIndex.url(), icon: Wallet },
|
||||||
{ title: 'Pengeluaran', href: expensesIndex.url(), icon: ArrowUpFromLine },
|
{ title: 'Pengeluaran', href: expensesIndex.url(), icon: ArrowUpFromLine },
|
||||||
{ title: 'Kasbon', href: '#', icon: HandCoins },
|
{ title: 'Kasbon', href: employeeAdvancesIndex.url(), icon: HandCoins },
|
||||||
{ title: 'Gaji', href: '#', icon: DollarSign },
|
{ title: 'Gaji', href: '#', icon: DollarSign },
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|||||||
91
resources/js/components/date-picker.tsx
Normal file
91
resources/js/components/date-picker.tsx
Normal file
@ -0,0 +1,91 @@
|
|||||||
|
import * as React from "react"
|
||||||
|
import { format } from "date-fns"
|
||||||
|
import { id } from "date-fns/locale"
|
||||||
|
import { CalendarIcon } from "lucide-react"
|
||||||
|
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
import { Button } from "@/components/ui/button"
|
||||||
|
import { Calendar } from "@/components/ui/calendar"
|
||||||
|
import {
|
||||||
|
Popover,
|
||||||
|
PopoverContent,
|
||||||
|
PopoverTrigger,
|
||||||
|
} from "@/components/ui/popover"
|
||||||
|
|
||||||
|
interface DatePickerProps {
|
||||||
|
value?: Date | string | null
|
||||||
|
onChange?: (date: Date | undefined) => void
|
||||||
|
placeholder?: string
|
||||||
|
disabled?: boolean
|
||||||
|
className?: string
|
||||||
|
name?: string
|
||||||
|
id?: string
|
||||||
|
min?: Date
|
||||||
|
max?: Date
|
||||||
|
}
|
||||||
|
|
||||||
|
function DatePicker({
|
||||||
|
value,
|
||||||
|
onChange,
|
||||||
|
placeholder = "Pilih tanggal",
|
||||||
|
disabled = false,
|
||||||
|
className,
|
||||||
|
name,
|
||||||
|
id,
|
||||||
|
min,
|
||||||
|
max,
|
||||||
|
}: DatePickerProps) {
|
||||||
|
const [open, setOpen] = React.useState(false)
|
||||||
|
|
||||||
|
const date = React.useMemo(() => {
|
||||||
|
if (!value) return undefined
|
||||||
|
if (value instanceof Date) return value
|
||||||
|
return new Date(value)
|
||||||
|
}, [value])
|
||||||
|
|
||||||
|
const formattedDate = React.useMemo(() => {
|
||||||
|
if (!date) return ""
|
||||||
|
return format(date, "dd MMM yyyy", { locale: id })
|
||||||
|
}, [date])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Popover open={open} onOpenChange={setOpen}>
|
||||||
|
<PopoverTrigger asChild>
|
||||||
|
<Button
|
||||||
|
id={id}
|
||||||
|
variant="outline"
|
||||||
|
disabled={disabled}
|
||||||
|
className={cn(
|
||||||
|
"w-full justify-start text-left font-normal",
|
||||||
|
!date && "text-muted-foreground",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<CalendarIcon className="mr-2 h-4 w-4" />
|
||||||
|
{date ? formattedDate : placeholder}
|
||||||
|
</Button>
|
||||||
|
</PopoverTrigger>
|
||||||
|
<PopoverContent className="w-auto p-0" align="start">
|
||||||
|
<Calendar
|
||||||
|
mode="single"
|
||||||
|
selected={date}
|
||||||
|
onSelect={(selectedDate) => {
|
||||||
|
onChange?.(selectedDate)
|
||||||
|
setOpen(false)
|
||||||
|
}}
|
||||||
|
disabled={(date) => {
|
||||||
|
if (min && date < min) return true
|
||||||
|
if (max && date > max) return true
|
||||||
|
return false
|
||||||
|
}}
|
||||||
|
initialFocus
|
||||||
|
/>
|
||||||
|
</PopoverContent>
|
||||||
|
{name && (
|
||||||
|
<input type="hidden" name={name} value={date ? format(date, "yyyy-MM-dd") : ""} />
|
||||||
|
)}
|
||||||
|
</Popover>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export { DatePicker }
|
||||||
218
resources/js/components/ui/calendar.tsx
Normal file
218
resources/js/components/ui/calendar.tsx
Normal file
@ -0,0 +1,218 @@
|
|||||||
|
import * as React from "react"
|
||||||
|
import {
|
||||||
|
ChevronDownIcon,
|
||||||
|
ChevronLeftIcon,
|
||||||
|
ChevronRightIcon,
|
||||||
|
} from "lucide-react"
|
||||||
|
import {
|
||||||
|
DayPicker,
|
||||||
|
getDefaultClassNames,
|
||||||
|
type DayButton,
|
||||||
|
} from "react-day-picker"
|
||||||
|
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
import { Button, buttonVariants } from "@/components/ui/button"
|
||||||
|
|
||||||
|
function Calendar({
|
||||||
|
className,
|
||||||
|
classNames,
|
||||||
|
showOutsideDays = true,
|
||||||
|
captionLayout = "label",
|
||||||
|
buttonVariant = "ghost",
|
||||||
|
formatters,
|
||||||
|
components,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof DayPicker> & {
|
||||||
|
buttonVariant?: React.ComponentProps<typeof Button>["variant"]
|
||||||
|
}) {
|
||||||
|
const defaultClassNames = getDefaultClassNames()
|
||||||
|
|
||||||
|
return (
|
||||||
|
<DayPicker
|
||||||
|
showOutsideDays={showOutsideDays}
|
||||||
|
className={cn(
|
||||||
|
"group/calendar bg-background p-3 [--cell-size:--spacing(8)] [[data-slot=card-content]_&]:bg-transparent [[data-slot=popover-content]_&]:bg-transparent",
|
||||||
|
String.raw`rtl:**:[.rdp-button\_next>svg]:rotate-180`,
|
||||||
|
String.raw`rtl:**:[.rdp-button\_previous>svg]:rotate-180`,
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
captionLayout={captionLayout}
|
||||||
|
formatters={{
|
||||||
|
formatMonthDropdown: (date) =>
|
||||||
|
date.toLocaleString("default", { month: "short" }),
|
||||||
|
...formatters,
|
||||||
|
}}
|
||||||
|
classNames={{
|
||||||
|
root: cn("w-fit", defaultClassNames.root),
|
||||||
|
months: cn(
|
||||||
|
"relative flex flex-col gap-4 md:flex-row",
|
||||||
|
defaultClassNames.months
|
||||||
|
),
|
||||||
|
month: cn("flex w-full flex-col gap-4", defaultClassNames.month),
|
||||||
|
nav: cn(
|
||||||
|
"absolute inset-x-0 top-0 flex w-full items-center justify-between gap-1",
|
||||||
|
defaultClassNames.nav
|
||||||
|
),
|
||||||
|
button_previous: cn(
|
||||||
|
buttonVariants({ variant: buttonVariant }),
|
||||||
|
"size-(--cell-size) p-0 select-none aria-disabled:opacity-50",
|
||||||
|
defaultClassNames.button_previous
|
||||||
|
),
|
||||||
|
button_next: cn(
|
||||||
|
buttonVariants({ variant: buttonVariant }),
|
||||||
|
"size-(--cell-size) p-0 select-none aria-disabled:opacity-50",
|
||||||
|
defaultClassNames.button_next
|
||||||
|
),
|
||||||
|
month_caption: cn(
|
||||||
|
"flex h-(--cell-size) w-full items-center justify-center px-(--cell-size)",
|
||||||
|
defaultClassNames.month_caption
|
||||||
|
),
|
||||||
|
dropdowns: cn(
|
||||||
|
"flex h-(--cell-size) w-full items-center justify-center gap-1.5 text-sm font-medium",
|
||||||
|
defaultClassNames.dropdowns
|
||||||
|
),
|
||||||
|
dropdown_root: cn(
|
||||||
|
"relative rounded-md border border-input shadow-xs has-focus:border-ring has-focus:ring-[3px] has-focus:ring-ring/50",
|
||||||
|
defaultClassNames.dropdown_root
|
||||||
|
),
|
||||||
|
dropdown: cn(
|
||||||
|
"absolute inset-0 bg-popover opacity-0",
|
||||||
|
defaultClassNames.dropdown
|
||||||
|
),
|
||||||
|
caption_label: cn(
|
||||||
|
"font-medium select-none",
|
||||||
|
captionLayout === "label"
|
||||||
|
? "text-sm"
|
||||||
|
: "flex h-8 items-center gap-1 rounded-md pr-1 pl-2 text-sm [&>svg]:size-3.5 [&>svg]:text-muted-foreground",
|
||||||
|
defaultClassNames.caption_label
|
||||||
|
),
|
||||||
|
month_grid: cn("w-full border-collapse", defaultClassNames.month_grid),
|
||||||
|
weekdays: cn("flex", defaultClassNames.weekdays),
|
||||||
|
weekday: cn(
|
||||||
|
"flex-1 rounded-md text-[0.8rem] font-normal text-muted-foreground select-none",
|
||||||
|
defaultClassNames.weekday
|
||||||
|
),
|
||||||
|
week: cn("mt-2 flex w-full", defaultClassNames.week),
|
||||||
|
week_number_header: cn(
|
||||||
|
"w-(--cell-size) select-none",
|
||||||
|
defaultClassNames.week_number_header
|
||||||
|
),
|
||||||
|
week_number: cn(
|
||||||
|
"text-[0.8rem] text-muted-foreground select-none",
|
||||||
|
defaultClassNames.week_number
|
||||||
|
),
|
||||||
|
day: cn(
|
||||||
|
"group/day relative aspect-square h-full w-full p-0 text-center select-none [&:last-child[data-selected=true]_button]:rounded-r-md",
|
||||||
|
props.showWeekNumber
|
||||||
|
? "[&:nth-child(2)[data-selected=true]_button]:rounded-l-md"
|
||||||
|
: "[&:first-child[data-selected=true]_button]:rounded-l-md",
|
||||||
|
defaultClassNames.day
|
||||||
|
),
|
||||||
|
range_start: cn(
|
||||||
|
"rounded-l-md bg-accent",
|
||||||
|
defaultClassNames.range_start
|
||||||
|
),
|
||||||
|
range_middle: cn("rounded-none", defaultClassNames.range_middle),
|
||||||
|
range_end: cn("rounded-r-md bg-accent", defaultClassNames.range_end),
|
||||||
|
today: cn(
|
||||||
|
"rounded-md bg-accent text-accent-foreground data-[selected=true]:rounded-none",
|
||||||
|
defaultClassNames.today
|
||||||
|
),
|
||||||
|
outside: cn(
|
||||||
|
"text-muted-foreground aria-selected:text-muted-foreground",
|
||||||
|
defaultClassNames.outside
|
||||||
|
),
|
||||||
|
disabled: cn(
|
||||||
|
"text-muted-foreground opacity-50",
|
||||||
|
defaultClassNames.disabled
|
||||||
|
),
|
||||||
|
hidden: cn("invisible", defaultClassNames.hidden),
|
||||||
|
...classNames,
|
||||||
|
}}
|
||||||
|
components={{
|
||||||
|
Root: ({ className, rootRef, ...props }) => {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
data-slot="calendar"
|
||||||
|
ref={rootRef}
|
||||||
|
className={cn(className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
},
|
||||||
|
Chevron: ({ className, orientation, ...props }) => {
|
||||||
|
if (orientation === "left") {
|
||||||
|
return (
|
||||||
|
<ChevronLeftIcon className={cn("size-4", className)} {...props} />
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (orientation === "right") {
|
||||||
|
return (
|
||||||
|
<ChevronRightIcon
|
||||||
|
className={cn("size-4", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ChevronDownIcon className={cn("size-4", className)} {...props} />
|
||||||
|
)
|
||||||
|
},
|
||||||
|
DayButton: CalendarDayButton,
|
||||||
|
WeekNumber: ({ children, ...props }) => {
|
||||||
|
return (
|
||||||
|
<td {...props}>
|
||||||
|
<div className="flex size-(--cell-size) items-center justify-center text-center">
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
)
|
||||||
|
},
|
||||||
|
...components,
|
||||||
|
}}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function CalendarDayButton({
|
||||||
|
className,
|
||||||
|
day,
|
||||||
|
modifiers,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof DayButton>) {
|
||||||
|
const defaultClassNames = getDefaultClassNames()
|
||||||
|
|
||||||
|
const ref = React.useRef<HTMLButtonElement>(null)
|
||||||
|
React.useEffect(() => {
|
||||||
|
if (modifiers.focused) ref.current?.focus()
|
||||||
|
}, [modifiers.focused])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Button
|
||||||
|
ref={ref}
|
||||||
|
variant="ghost"
|
||||||
|
size="icon"
|
||||||
|
data-day={day.date.toLocaleDateString()}
|
||||||
|
data-selected-single={
|
||||||
|
modifiers.selected &&
|
||||||
|
!modifiers.range_start &&
|
||||||
|
!modifiers.range_end &&
|
||||||
|
!modifiers.range_middle
|
||||||
|
}
|
||||||
|
data-range-start={modifiers.range_start}
|
||||||
|
data-range-end={modifiers.range_end}
|
||||||
|
data-range-middle={modifiers.range_middle}
|
||||||
|
className={cn(
|
||||||
|
"flex aspect-square size-auto w-full min-w-(--cell-size) flex-col gap-1 leading-none font-normal group-data-[focused=true]/day:relative group-data-[focused=true]/day:z-10 group-data-[focused=true]/day:border-ring group-data-[focused=true]/day:ring-[3px] group-data-[focused=true]/day:ring-ring/50 data-[range-end=true]:rounded-md data-[range-end=true]:rounded-r-md data-[range-end=true]:bg-primary data-[range-end=true]:text-primary-foreground data-[range-middle=true]:rounded-none data-[range-middle=true]:bg-accent data-[range-middle=true]:text-accent-foreground data-[range-start=true]:rounded-md data-[range-start=true]:rounded-l-md data-[range-start=true]:bg-primary data-[range-start=true]:text-primary-foreground data-[selected-single=true]:bg-primary data-[selected-single=true]:text-primary-foreground dark:hover:text-accent-foreground [&>span]:text-xs [&>span]:opacity-70",
|
||||||
|
defaultClassNames.day,
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export { Calendar, CalendarDayButton }
|
||||||
87
resources/js/components/ui/popover.tsx
Normal file
87
resources/js/components/ui/popover.tsx
Normal file
@ -0,0 +1,87 @@
|
|||||||
|
import * as React from "react"
|
||||||
|
import { Popover as PopoverPrimitive } from "radix-ui"
|
||||||
|
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
|
function Popover({
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof PopoverPrimitive.Root>) {
|
||||||
|
return <PopoverPrimitive.Root data-slot="popover" {...props} />
|
||||||
|
}
|
||||||
|
|
||||||
|
function PopoverTrigger({
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof PopoverPrimitive.Trigger>) {
|
||||||
|
return <PopoverPrimitive.Trigger data-slot="popover-trigger" {...props} />
|
||||||
|
}
|
||||||
|
|
||||||
|
function PopoverContent({
|
||||||
|
className,
|
||||||
|
align = "center",
|
||||||
|
sideOffset = 4,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof PopoverPrimitive.Content>) {
|
||||||
|
return (
|
||||||
|
<PopoverPrimitive.Portal>
|
||||||
|
<PopoverPrimitive.Content
|
||||||
|
data-slot="popover-content"
|
||||||
|
align={align}
|
||||||
|
sideOffset={sideOffset}
|
||||||
|
className={cn(
|
||||||
|
"z-50 w-72 origin-(--radix-popover-content-transform-origin) rounded-md border bg-popover p-4 text-popover-foreground shadow-md outline-hidden data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[state=open]:animate-in data-[state=open]:fade-in-0 data-[state=open]:zoom-in-95",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
</PopoverPrimitive.Portal>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function PopoverAnchor({
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof PopoverPrimitive.Anchor>) {
|
||||||
|
return <PopoverPrimitive.Anchor data-slot="popover-anchor" {...props} />
|
||||||
|
}
|
||||||
|
|
||||||
|
function PopoverHeader({ className, ...props }: React.ComponentProps<"div">) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
data-slot="popover-header"
|
||||||
|
className={cn("flex flex-col gap-1 text-sm", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function PopoverTitle({ className, ...props }: React.ComponentProps<"h2">) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
data-slot="popover-title"
|
||||||
|
className={cn("font-medium", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function PopoverDescription({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<"p">) {
|
||||||
|
return (
|
||||||
|
<p
|
||||||
|
data-slot="popover-description"
|
||||||
|
className={cn("text-muted-foreground", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export {
|
||||||
|
Popover,
|
||||||
|
PopoverTrigger,
|
||||||
|
PopoverContent,
|
||||||
|
PopoverAnchor,
|
||||||
|
PopoverHeader,
|
||||||
|
PopoverTitle,
|
||||||
|
PopoverDescription,
|
||||||
|
}
|
||||||
287
resources/js/pages/admin/finance/employee-advance/columns.tsx
Normal file
287
resources/js/pages/admin/finance/employee-advance/columns.tsx
Normal file
@ -0,0 +1,287 @@
|
|||||||
|
import type { ColumnDef } from '@tanstack/react-table';
|
||||||
|
import { ArrowUpDown, CheckCircle, CircleDollarSign, Pencil, Trash2 } from 'lucide-react';
|
||||||
|
import { Button } from '@/components/ui/button';
|
||||||
|
import { Badge } from '@/components/ui/badge';
|
||||||
|
import {
|
||||||
|
Tooltip,
|
||||||
|
TooltipContent,
|
||||||
|
TooltipProvider,
|
||||||
|
TooltipTrigger,
|
||||||
|
} from '@/components/ui/tooltip';
|
||||||
|
import { formatCurrency } from '@/lib/utils';
|
||||||
|
|
||||||
|
export type EmployeeAdvance = {
|
||||||
|
id: number;
|
||||||
|
amount: number;
|
||||||
|
paid_amount: number;
|
||||||
|
description: string;
|
||||||
|
due_date: string;
|
||||||
|
status: 'pending' | 'approved' | 'paid' | 'rejected' | 'cancelled';
|
||||||
|
created_at: string;
|
||||||
|
employee: {
|
||||||
|
user: {
|
||||||
|
user_profile: {
|
||||||
|
full_name: string;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
function formatDate(dateString: string): string {
|
||||||
|
const date = new Date(dateString);
|
||||||
|
|
||||||
|
return date.toLocaleDateString('id-ID', {
|
||||||
|
day: '2-digit',
|
||||||
|
month: 'short',
|
||||||
|
year: 'numeric',
|
||||||
|
}) + ' ' + date.toLocaleTimeString('id-ID', {
|
||||||
|
hour: '2-digit',
|
||||||
|
minute: '2-digit',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatShortDate(dateString: string): string {
|
||||||
|
const date = new Date(dateString);
|
||||||
|
|
||||||
|
return date.toLocaleDateString('id-ID', {
|
||||||
|
day: '2-digit',
|
||||||
|
month: 'short',
|
||||||
|
year: 'numeric',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function getStatusBadge(status: string) {
|
||||||
|
const statusConfig: Record<string, { label: string; className: string }> = {
|
||||||
|
pending: {
|
||||||
|
label: 'Menunggu',
|
||||||
|
className: 'bg-yellow-100 text-yellow-800 hover:bg-yellow-100',
|
||||||
|
},
|
||||||
|
approved: {
|
||||||
|
label: 'Disetujui',
|
||||||
|
className: 'bg-green-100 text-green-800 hover:bg-green-100',
|
||||||
|
},
|
||||||
|
paid: {
|
||||||
|
label: 'Dibayar',
|
||||||
|
className: 'bg-blue-100 text-blue-800 hover:bg-blue-100',
|
||||||
|
},
|
||||||
|
rejected: {
|
||||||
|
label: 'Ditolak',
|
||||||
|
className: 'bg-red-100 text-red-800 hover:bg-red-100',
|
||||||
|
},
|
||||||
|
cancelled: {
|
||||||
|
label: 'Dibatalkan',
|
||||||
|
className: 'bg-gray-100 text-gray-800 hover:bg-gray-100',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const config = statusConfig[status] ?? statusConfig.pending;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Badge variant="secondary" className={config.className}>
|
||||||
|
{config.label}
|
||||||
|
</Badge>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
type CreateColumnsParams = {
|
||||||
|
handleEdit: (employeeAdvance: EmployeeAdvance) => void;
|
||||||
|
handleDeleteClick: (employeeAdvance: EmployeeAdvance) => void;
|
||||||
|
handleApprove: (employeeAdvance: EmployeeAdvance) => void;
|
||||||
|
handlePay: (employeeAdvance: EmployeeAdvance) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function createEmployeeAdvanceColumns(
|
||||||
|
params: CreateColumnsParams,
|
||||||
|
): ColumnDef<EmployeeAdvance>[] {
|
||||||
|
const { handleEdit, handleDeleteClick, handleApprove, handlePay } = params;
|
||||||
|
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
id: 'no',
|
||||||
|
header: () => <span className="block text-center">No</span>,
|
||||||
|
cell: ({ row }) => (
|
||||||
|
<span className="block text-center">
|
||||||
|
{row.index + 1}
|
||||||
|
</span>
|
||||||
|
),
|
||||||
|
meta: {
|
||||||
|
className: 'w-[50px] text-center',
|
||||||
|
headerClassName: 'w-[50px] text-center',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
accessorKey: 'created_at',
|
||||||
|
header: ({ column }) => (
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
className="-ml-3 h-8"
|
||||||
|
onClick={() =>
|
||||||
|
column.toggleSorting(
|
||||||
|
column.getIsSorted() === 'asc',
|
||||||
|
)
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<span>Tanggal</span>
|
||||||
|
<ArrowUpDown className="ml-2 h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
),
|
||||||
|
cell: ({ row }) => (
|
||||||
|
<span>{formatDate(row.getValue('created_at') as string)}</span>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'employee_name',
|
||||||
|
header: () => <span>Oleh</span>,
|
||||||
|
cell: ({ row }) => {
|
||||||
|
const employee = row.original.employee;
|
||||||
|
|
||||||
|
return <span>{employee?.user?.user_profile?.full_name ?? '-'}</span>;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
accessorKey: 'amount',
|
||||||
|
header: ({ column }) => (
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
className="-ml-3 h-8"
|
||||||
|
onClick={() =>
|
||||||
|
column.toggleSorting(
|
||||||
|
column.getIsSorted() === 'asc',
|
||||||
|
)
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<span>Jumlah</span>
|
||||||
|
<ArrowUpDown className="ml-2 h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
),
|
||||||
|
cell: ({ row }) => (
|
||||||
|
<span className="text-red-600 font-medium">
|
||||||
|
- {formatCurrency(row.getValue('amount') as number)}
|
||||||
|
</span>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
accessorKey: 'description',
|
||||||
|
header: () => <span>Keterangan</span>,
|
||||||
|
cell: ({ row }) => (
|
||||||
|
<span className="max-w-[200px] truncate block">{row.getValue('description') as string}</span>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
accessorKey: 'due_date',
|
||||||
|
header: ({ column }) => (
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
className="-ml-3 h-8"
|
||||||
|
onClick={() =>
|
||||||
|
column.toggleSorting(
|
||||||
|
column.getIsSorted() === 'asc',
|
||||||
|
)
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<span>Jatuh Tempo</span>
|
||||||
|
<ArrowUpDown className="ml-2 h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
),
|
||||||
|
cell: ({ row }) => (
|
||||||
|
<span>{formatShortDate(row.getValue('due_date') as string)}</span>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
accessorKey: 'status',
|
||||||
|
header: () => <span>Status</span>,
|
||||||
|
cell: ({ row }) => (
|
||||||
|
<span>{getStatusBadge(row.getValue('status') as string)}</span>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'actions',
|
||||||
|
header: () => <span className="block text-center">Aksi</span>,
|
||||||
|
meta: {
|
||||||
|
className: 'w-[150px] text-center',
|
||||||
|
headerClassName: 'w-[150px] text-center',
|
||||||
|
},
|
||||||
|
cell: ({ row }) => {
|
||||||
|
const employeeAdvance = row.original;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<TooltipProvider>
|
||||||
|
<div className="flex items-center justify-center gap-1">
|
||||||
|
{employeeAdvance.status === 'pending' && (
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger asChild>
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="icon"
|
||||||
|
onClick={() =>
|
||||||
|
handleApprove(employeeAdvance)
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<CheckCircle className="h-4 w-4 text-green-600" />
|
||||||
|
</Button>
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent side="top">
|
||||||
|
Setujui
|
||||||
|
</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{employeeAdvance.status === 'approved' && (
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger asChild>
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="icon"
|
||||||
|
onClick={() =>
|
||||||
|
handlePay(employeeAdvance)
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<CircleDollarSign className="h-4 w-4 text-blue-600" />
|
||||||
|
</Button>
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent side="top">
|
||||||
|
Bayar
|
||||||
|
</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger asChild>
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="icon"
|
||||||
|
onClick={() =>
|
||||||
|
handleEdit(employeeAdvance)
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<Pencil className="h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent side="top">
|
||||||
|
Edit
|
||||||
|
</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger asChild>
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="icon"
|
||||||
|
onClick={() =>
|
||||||
|
handleDeleteClick(employeeAdvance)
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<Trash2 className="h-4 w-4 text-destructive" />
|
||||||
|
</Button>
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent side="top">
|
||||||
|
Hapus
|
||||||
|
</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
</div>
|
||||||
|
</TooltipProvider>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}
|
||||||
313
resources/js/pages/admin/finance/employee-advance/index.tsx
Normal file
313
resources/js/pages/admin/finance/employee-advance/index.tsx
Normal file
@ -0,0 +1,313 @@
|
|||||||
|
import { Form, Head, router } from '@inertiajs/react';
|
||||||
|
import { Plus } from 'lucide-react';
|
||||||
|
import { useEffect, useState } from 'react';
|
||||||
|
import InputError from '@/components/input-error';
|
||||||
|
import { RupiahInput } from '@/components/rupiah-input';
|
||||||
|
import { DatePicker } from '@/components/date-picker';
|
||||||
|
import { Button } from '@/components/ui/button';
|
||||||
|
import {
|
||||||
|
Dialog,
|
||||||
|
DialogContent,
|
||||||
|
DialogFooter,
|
||||||
|
DialogHeader,
|
||||||
|
DialogTitle,
|
||||||
|
} from '@/components/ui/dialog';
|
||||||
|
import { Input } from '@/components/ui/input';
|
||||||
|
import { Label } from '@/components/ui/label';
|
||||||
|
import { destroy, index as employeeAdvanceIndex, store, update, approve, pay } from '@/routes/admin/finance/employee-advances';
|
||||||
|
import { createEmployeeAdvanceColumns } from './columns';
|
||||||
|
import type { EmployeeAdvance } from './columns';
|
||||||
|
import { ConfirmDialog } from '@/components/confirm-dialog';
|
||||||
|
import { DataTable } from '@/components/data-table';
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
employeeAdvances: EmployeeAdvance[];
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function EmployeeAdvanceIndex({ employeeAdvances }: Props) {
|
||||||
|
const [createOpen, setCreateOpen] = useState(false);
|
||||||
|
const [editing, setEditing] = useState<EmployeeAdvance | null>(null);
|
||||||
|
const [deleting, setDeleting] = useState<EmployeeAdvance | null>(null);
|
||||||
|
const [approving, setApproving] = useState<EmployeeAdvance | null>(null);
|
||||||
|
const [paying, setPaying] = useState<EmployeeAdvance | null>(null);
|
||||||
|
const [dueDate, setDueDate] = useState<Date | undefined>(undefined);
|
||||||
|
const [editingDueDate, setEditingDueDate] = useState<Date | undefined>(undefined);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (editing) {
|
||||||
|
setEditingDueDate(new Date(editing.due_date));
|
||||||
|
} else {
|
||||||
|
setEditingDueDate(undefined);
|
||||||
|
}
|
||||||
|
}, [editing]);
|
||||||
|
|
||||||
|
function handleDelete() {
|
||||||
|
if (!deleting) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
router.delete(destroy(deleting.id), {
|
||||||
|
onSuccess: () => setDeleting(null),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleApprove() {
|
||||||
|
if (!approving) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
router.post(approve(approving.id), {}, {
|
||||||
|
onSuccess: () => setApproving(null),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function handlePay() {
|
||||||
|
if (!paying) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
router.post(pay(paying.id), {}, {
|
||||||
|
onSuccess: () => setPaying(null),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const columns = createEmployeeAdvanceColumns({
|
||||||
|
handleEdit: (employeeAdvance) => setEditing(employeeAdvance),
|
||||||
|
handleDeleteClick: (employeeAdvance) => setDeleting(employeeAdvance),
|
||||||
|
handleApprove: (employeeAdvance) => setApproving(employeeAdvance),
|
||||||
|
handlePay: (employeeAdvance) => setPaying(employeeAdvance),
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Head title="Kasbon" />
|
||||||
|
|
||||||
|
<div className="flex h-full flex-1 flex-col gap-6 overflow-x-auto p-4 md:p-6">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div>
|
||||||
|
<h2 className="text-2xl font-semibold tracking-tight">
|
||||||
|
Kasbon
|
||||||
|
</h2>
|
||||||
|
</div>
|
||||||
|
<Dialog open={createOpen} onOpenChange={(open) => {
|
||||||
|
setCreateOpen(open);
|
||||||
|
if (!open) {
|
||||||
|
setDueDate(undefined);
|
||||||
|
}
|
||||||
|
}}>
|
||||||
|
<Button asChild>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => setCreateOpen(true)}
|
||||||
|
>
|
||||||
|
<Plus className="h-4 w-4" />
|
||||||
|
Tambah
|
||||||
|
</button>
|
||||||
|
</Button>
|
||||||
|
<DialogContent>
|
||||||
|
<Form action={store()} resetOnSuccess onSuccess={() => setCreateOpen(false)}>
|
||||||
|
{({ errors, processing }) => {
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle>Tambah Kasbon</DialogTitle>
|
||||||
|
</DialogHeader>
|
||||||
|
<div className="grid gap-4 py-4">
|
||||||
|
<div className="grid gap-2">
|
||||||
|
<Label>
|
||||||
|
Jumlah{' '} <span className="text-destructive">*</span>
|
||||||
|
</Label>
|
||||||
|
<RupiahInput name="amount" min={1} />
|
||||||
|
<InputError message={errors.amount} />
|
||||||
|
</div>
|
||||||
|
<div className="grid gap-2">
|
||||||
|
<Label htmlFor="description">
|
||||||
|
Keterangan{' '} <span className="text-destructive">*</span>
|
||||||
|
</Label>
|
||||||
|
<Input
|
||||||
|
id="description"
|
||||||
|
name="description"
|
||||||
|
placeholder="Masukkan keterangan"
|
||||||
|
/>
|
||||||
|
<InputError message={errors.description} />
|
||||||
|
</div>
|
||||||
|
<div className="grid gap-2">
|
||||||
|
<Label htmlFor="due_date">
|
||||||
|
Jatuh Tempo{' '} <span className="text-destructive">*</span>
|
||||||
|
</Label>
|
||||||
|
<input type="hidden" name="due_date" value={dueDate ? dueDate.toISOString().split('T')[0] : ''} />
|
||||||
|
<DatePicker
|
||||||
|
value={dueDate}
|
||||||
|
onChange={setDueDate}
|
||||||
|
placeholder="Pilih jatuh tempo"
|
||||||
|
min={new Date()}
|
||||||
|
/>
|
||||||
|
<InputError message={errors.due_date} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<DialogFooter>
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant="outline"
|
||||||
|
onClick={() => setCreateOpen(false)}
|
||||||
|
>
|
||||||
|
Batal
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
type='submit'
|
||||||
|
disabled={processing}
|
||||||
|
>
|
||||||
|
{processing
|
||||||
|
? 'Menyimpan...'
|
||||||
|
: 'Simpan'}
|
||||||
|
</Button>
|
||||||
|
</DialogFooter>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
</Form>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<DataTable
|
||||||
|
columns={columns}
|
||||||
|
data={employeeAdvances}
|
||||||
|
searchKey="description"
|
||||||
|
searchPlaceholder="Cari kasbon..."
|
||||||
|
emptyText="Belum ada data kasbon."
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Dialog
|
||||||
|
open={editing !== null}
|
||||||
|
onOpenChange={(open) => {
|
||||||
|
if (!open) {
|
||||||
|
setEditing(null);
|
||||||
|
setEditingDueDate(undefined);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<DialogContent>
|
||||||
|
{editing && (
|
||||||
|
<Form action={update(editing.id)} resetOnSuccess onSuccess={() => {
|
||||||
|
setEditing(null);
|
||||||
|
setEditingDueDate(undefined);
|
||||||
|
}}>
|
||||||
|
{({ errors, processing }) => {
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle>Edit Kasbon</DialogTitle>
|
||||||
|
</DialogHeader>
|
||||||
|
<div className="grid gap-4 py-4">
|
||||||
|
<div className="grid gap-2">
|
||||||
|
<Label>Jumlah{' '} <span className="text-destructive">*</span></Label>
|
||||||
|
<RupiahInput name="amount" defaultValue={editing.amount} min={1} />
|
||||||
|
<InputError message={errors.amount} />
|
||||||
|
</div>
|
||||||
|
<div className="grid gap-2">
|
||||||
|
<Label htmlFor="edit-description">Keterangan{' '} <span className="text-destructive">*</span></Label>
|
||||||
|
<Input
|
||||||
|
id="edit-description"
|
||||||
|
name="description"
|
||||||
|
placeholder="Masukkan keterangan"
|
||||||
|
defaultValue={editing.description}
|
||||||
|
/>
|
||||||
|
<InputError message={errors.description} />
|
||||||
|
</div>
|
||||||
|
<div className="grid gap-2">
|
||||||
|
<Label htmlFor="edit-due_date">Jatuh Tempo{' '} <span className="text-destructive">*</span></Label>
|
||||||
|
<input type="hidden" name="due_date" value={editingDueDate ? editingDueDate.toISOString().split('T')[0] : ''} />
|
||||||
|
<DatePicker
|
||||||
|
value={editingDueDate}
|
||||||
|
onChange={setEditingDueDate}
|
||||||
|
placeholder="Pilih jatuh tempo"
|
||||||
|
min={new Date()}
|
||||||
|
/>
|
||||||
|
<InputError message={errors.due_date} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<DialogFooter>
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant="outline"
|
||||||
|
onClick={() => {
|
||||||
|
setEditing(null);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Batal
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
type="submit"
|
||||||
|
disabled={processing}
|
||||||
|
>
|
||||||
|
{processing
|
||||||
|
? 'Menyimpan...'
|
||||||
|
: 'Simpan'}
|
||||||
|
</Button>
|
||||||
|
</DialogFooter>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
</Form>
|
||||||
|
)}
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
|
||||||
|
<ConfirmDialog
|
||||||
|
open={deleting !== null}
|
||||||
|
onOpenChange={(open) => {
|
||||||
|
if (!open) {
|
||||||
|
setDeleting(null);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
title="Hapus Kasbon"
|
||||||
|
description={`Apakah Anda yakin ingin menghapus kasbon "${deleting?.description}"? Tindakan ini tidak dapat dibatalkan.`}
|
||||||
|
confirmLabel="Hapus"
|
||||||
|
onConfirm={handleDelete}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<ConfirmDialog
|
||||||
|
open={approving !== null}
|
||||||
|
onOpenChange={(open) => {
|
||||||
|
if (!open) {
|
||||||
|
setApproving(null);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
title="Setujui Kasbon"
|
||||||
|
description={`Apakah Anda yakin ingin menyetujui kasbon "${approving?.description}"?`}
|
||||||
|
confirmLabel="Setujui"
|
||||||
|
onConfirm={handleApprove}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<ConfirmDialog
|
||||||
|
open={paying !== null}
|
||||||
|
onOpenChange={(open) => {
|
||||||
|
if (!open) {
|
||||||
|
setPaying(null);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
title="Bayar Kasbon"
|
||||||
|
description={`Apakah Anda yakin ingin membayar kasbon "${paying?.description}" sebesar ${paying?.amount}? Saldo kas akan dikembalikan.`}
|
||||||
|
confirmLabel="Bayar"
|
||||||
|
onConfirm={handlePay}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
EmployeeAdvanceIndex.layout = {
|
||||||
|
breadcrumbs: [
|
||||||
|
{
|
||||||
|
title: 'Keuangan',
|
||||||
|
href: employeeAdvanceIndex(),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Kasbon',
|
||||||
|
href: employeeAdvanceIndex(),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
@ -1,6 +1,7 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
use App\Http\Controllers\Admin\Finance\CashAccountController;
|
use App\Http\Controllers\Admin\Finance\CashAccountController;
|
||||||
|
use App\Http\Controllers\Admin\Finance\EmployeeAdvanceController;
|
||||||
use App\Http\Controllers\Admin\Finance\ExpenseController;
|
use App\Http\Controllers\Admin\Finance\ExpenseController;
|
||||||
use App\Http\Controllers\Admin\Master\CategoryController;
|
use App\Http\Controllers\Admin\Master\CategoryController;
|
||||||
use App\Http\Controllers\Admin\Master\CustomerController;
|
use App\Http\Controllers\Admin\Master\CustomerController;
|
||||||
@ -26,6 +27,10 @@
|
|||||||
Route::delete('cash-accounts/transactions/{transaction}', [CashAccountController::class, 'destroy'])->name('cash-accounts.transactions.destroy');
|
Route::delete('cash-accounts/transactions/{transaction}', [CashAccountController::class, 'destroy'])->name('cash-accounts.transactions.destroy');
|
||||||
|
|
||||||
Route::resource('expenses', ExpenseController::class)->except(['show', 'create', 'edit']);
|
Route::resource('expenses', ExpenseController::class)->except(['show', 'create', 'edit']);
|
||||||
|
|
||||||
|
Route::resource('employee-advances', EmployeeAdvanceController::class)->except(['show', 'create', 'edit']);
|
||||||
|
Route::post('employee-advances/{employeeAdvance}/approve', [EmployeeAdvanceController::class, 'approve'])->name('employee-advances.approve');
|
||||||
|
Route::post('employee-advances/{employeeAdvance}/pay', [EmployeeAdvanceController::class, 'pay'])->name('employee-advances.pay');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user