refactor: consolidate payroll management by merging PayrollPeriodController into PayrollController, enhancing data handling and UI components for improved functionality
This commit is contained in:
parent
50eaced126
commit
0803cbeb96
@ -2,33 +2,55 @@
|
|||||||
|
|
||||||
namespace App\Http\Controllers\Admin\Finance;
|
namespace App\Http\Controllers\Admin\Finance;
|
||||||
|
|
||||||
|
use App\Enums\PayrollAdjustmentType;
|
||||||
use App\Http\Controllers\Concerns\FlashesEntityMessage;
|
use App\Http\Controllers\Concerns\FlashesEntityMessage;
|
||||||
|
use App\Http\Controllers\Concerns\ParsesDataTableQuery;
|
||||||
use App\Http\Controllers\Controller;
|
use App\Http\Controllers\Controller;
|
||||||
use App\Http\Requests\Admin\Finance\PayrollAdjustmentRequest;
|
use App\Http\Requests\Admin\Finance\PayrollAdjustmentRequest;
|
||||||
use App\Models\Payroll;
|
use App\Models\Payroll;
|
||||||
use App\Models\PayrollAdjustment;
|
use App\Models\PayrollAdjustment;
|
||||||
use App\Services\Finance\PayrollService;
|
use App\Services\Finance\PayrollService;
|
||||||
use Illuminate\Http\RedirectResponse;
|
use Illuminate\Http\RedirectResponse;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
use Inertia\Inertia;
|
||||||
|
use Inertia\Response;
|
||||||
|
|
||||||
class PayrollController extends Controller
|
class PayrollController extends Controller
|
||||||
{
|
{
|
||||||
use FlashesEntityMessage;
|
use FlashesEntityMessage,ParsesDataTableQuery;
|
||||||
|
|
||||||
public function __construct(
|
public function __construct(
|
||||||
private readonly PayrollService $payrollService,
|
private readonly PayrollService $payrollService,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
public function storeAdjustment(PayrollAdjustmentRequest $request, Payroll $payroll): RedirectResponse
|
public function index(Request $request): Response
|
||||||
{
|
{
|
||||||
$user = $request->user();
|
$tableQuery = $this->parseDataTableQuery($request);
|
||||||
if ($user && ! $user->hasAnyRole(['owner', 'developer', 'direktur'])) {
|
$periodId = $request->integer('period_id') ?: null;
|
||||||
abort(403, 'Anda tidak memiliki akses untuk menambah penyesuaian gaji.');
|
$period = $this->payrollService->resolvePeriod($periodId);
|
||||||
|
|
||||||
|
return Inertia::render('admin/finance/payroll/Index', [
|
||||||
|
'periods' => $this->payrollService->listPeriods(),
|
||||||
|
'currentPeriod' => $period,
|
||||||
|
'payrolls' => $period
|
||||||
|
? $this->payrollService->paginateForPeriod($period, $tableQuery, auth()->user())
|
||||||
|
: null,
|
||||||
|
'summary' => $period
|
||||||
|
? $this->payrollService->periodSummary($period, auth()->user())
|
||||||
|
: null,
|
||||||
|
'adjustmentTypes' => PayrollAdjustmentType::selectOptions(),
|
||||||
|
'filters' => $this->dataTableFilters($tableQuery, [
|
||||||
|
'period_id' => $period?->id,
|
||||||
|
]),
|
||||||
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function storeAdjustment(PayrollAdjustmentRequest $request, Payroll $payroll): RedirectResponse
|
||||||
|
{
|
||||||
$this->payrollService->addAdjustment(
|
$this->payrollService->addAdjustment(
|
||||||
$payroll,
|
$payroll,
|
||||||
$request->validated(),
|
$request->validated(),
|
||||||
$user,
|
auth()->user(),
|
||||||
);
|
);
|
||||||
|
|
||||||
$this->flashSuccess('Penyesuaian gaji berhasil ditambahkan.');
|
$this->flashSuccess('Penyesuaian gaji berhasil ditambahkan.');
|
||||||
@ -40,15 +62,10 @@ public function storeAdjustment(PayrollAdjustmentRequest $request, Payroll $payr
|
|||||||
|
|
||||||
public function updateAdjustment(PayrollAdjustmentRequest $request, PayrollAdjustment $payrollAdjustment): RedirectResponse
|
public function updateAdjustment(PayrollAdjustmentRequest $request, PayrollAdjustment $payrollAdjustment): RedirectResponse
|
||||||
{
|
{
|
||||||
$user = $request->user();
|
|
||||||
if ($user && ! $user->hasAnyRole(['owner', 'developer', 'direktur'])) {
|
|
||||||
abort(403, 'Anda tidak memiliki akses untuk mengubah penyesuaian gaji.');
|
|
||||||
}
|
|
||||||
|
|
||||||
$this->payrollService->updateAdjustment(
|
$this->payrollService->updateAdjustment(
|
||||||
$payrollAdjustment,
|
$payrollAdjustment,
|
||||||
$request->validated(),
|
$request->validated(),
|
||||||
$user,
|
auth()->user(),
|
||||||
);
|
);
|
||||||
|
|
||||||
$this->flashSuccess('Penyesuaian gaji berhasil diperbarui.');
|
$this->flashSuccess('Penyesuaian gaji berhasil diperbarui.');
|
||||||
@ -60,11 +77,6 @@ public function updateAdjustment(PayrollAdjustmentRequest $request, PayrollAdjus
|
|||||||
|
|
||||||
public function destroyAdjustment(PayrollAdjustment $payrollAdjustment): RedirectResponse
|
public function destroyAdjustment(PayrollAdjustment $payrollAdjustment): RedirectResponse
|
||||||
{
|
{
|
||||||
$user = auth()->user();
|
|
||||||
if ($user && ! $user->hasAnyRole(['owner', 'developer', 'direktur'])) {
|
|
||||||
abort(403, 'Anda tidak memiliki akses untuk menghapus penyesuaian gaji.');
|
|
||||||
}
|
|
||||||
|
|
||||||
$payroll = $payrollAdjustment->payroll;
|
$payroll = $payrollAdjustment->payroll;
|
||||||
|
|
||||||
$this->payrollService->deleteAdjustment($payrollAdjustment);
|
$this->payrollService->deleteAdjustment($payrollAdjustment);
|
||||||
|
|||||||
@ -1,45 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
namespace App\Http\Controllers\Admin\Finance;
|
|
||||||
|
|
||||||
use App\Enums\PayrollAdjustmentType;
|
|
||||||
use App\Http\Controllers\Concerns\FlashesEntityMessage;
|
|
||||||
use App\Http\Controllers\Concerns\ParsesDataTableQuery;
|
|
||||||
use App\Http\Controllers\Controller;
|
|
||||||
use App\Services\Finance\PayrollService;
|
|
||||||
use Illuminate\Http\Request;
|
|
||||||
use Inertia\Inertia;
|
|
||||||
use Inertia\Response;
|
|
||||||
|
|
||||||
class PayrollPeriodController extends Controller
|
|
||||||
{
|
|
||||||
use FlashesEntityMessage, ParsesDataTableQuery;
|
|
||||||
|
|
||||||
public function __construct(
|
|
||||||
private readonly PayrollService $payrollService,
|
|
||||||
) {}
|
|
||||||
|
|
||||||
public function index(Request $request): Response
|
|
||||||
{
|
|
||||||
$tableQuery = $this->parseDataTableQuery($request);
|
|
||||||
$periodId = $request->integer('period_id') ?: null;
|
|
||||||
$period = $this->payrollService->resolvePeriod($periodId);
|
|
||||||
$user = $request->user();
|
|
||||||
|
|
||||||
return Inertia::render('admin/finance/payroll/Index', [
|
|
||||||
'periods' => $this->payrollService->listPeriods(),
|
|
||||||
'currentPeriod' => $period,
|
|
||||||
'payrolls' => $period
|
|
||||||
? $this->payrollService->paginateForPeriod($period, $tableQuery, $user)
|
|
||||||
: null,
|
|
||||||
'summary' => $period
|
|
||||||
? $this->payrollService->periodSummary($period, $user)
|
|
||||||
: null,
|
|
||||||
'adjustmentTypes' => PayrollAdjustmentType::selectOptions(),
|
|
||||||
'filters' => array_merge(
|
|
||||||
$this->dataTableFilters($tableQuery),
|
|
||||||
['period_id' => $period?->id],
|
|
||||||
),
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -19,7 +19,6 @@
|
|||||||
use Illuminate\Database\Eloquent\Builder;
|
use Illuminate\Database\Eloquent\Builder;
|
||||||
use Illuminate\Support\Collection;
|
use Illuminate\Support\Collection;
|
||||||
use Illuminate\Support\Facades\DB;
|
use Illuminate\Support\Facades\DB;
|
||||||
use Illuminate\Validation\ValidationException;
|
|
||||||
|
|
||||||
class PayrollService
|
class PayrollService
|
||||||
{
|
{
|
||||||
@ -90,7 +89,7 @@ public function paginateForPeriod(PayrollPeriod $period, array $tableQuery, User
|
|||||||
$query = Payroll::query()
|
$query = Payroll::query()
|
||||||
->with(['employee.user.profile', 'payrollPeriod', 'adjustments.createdBy.profile'])
|
->with(['employee.user.profile', 'payrollPeriod', 'adjustments.createdBy.profile'])
|
||||||
->where('payroll_period_id', $period->id)
|
->where('payroll_period_id', $period->id)
|
||||||
->when(! $user->hasAnyRole(['owner', 'developer', 'direktur']), function (Builder $query) use ($user): void {
|
->when(! $user->hasAnyRole([Role::OWNER->value, Role::DEVELOPER->value, Role::DIREKTUR->value]), function (Builder $query) use ($user): void {
|
||||||
$employeeId = $user->employee?->id ?? -1;
|
$employeeId = $user->employee?->id ?? -1;
|
||||||
$query->where('employee_id', $employeeId);
|
$query->where('employee_id', $employeeId);
|
||||||
})
|
})
|
||||||
@ -204,13 +203,6 @@ public function generatePayrollsForPeriod(PayrollPeriod $period): void
|
|||||||
*/
|
*/
|
||||||
public function addAdjustment(Payroll $payroll, array $validated, User $user): void
|
public function addAdjustment(Payroll $payroll, array $validated, User $user): void
|
||||||
{
|
{
|
||||||
$payroll->loadMissing(['payrollPeriod', 'employee.user']);
|
|
||||||
|
|
||||||
if (! $payroll->can_adjust) {
|
|
||||||
throw ValidationException::withMessages([
|
|
||||||
'payroll' => 'Penyesuaian hanya dapat ditambahkan pada gaji yang belum dibayar di periode terbuka.',
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
DB::transaction(function () use ($payroll, $validated, $user): void {
|
DB::transaction(function () use ($payroll, $validated, $user): void {
|
||||||
$payroll->adjustments()->create([
|
$payroll->adjustments()->create([
|
||||||
@ -242,18 +234,11 @@ public function updateAdjustment(PayrollAdjustment $adjustment, array $validated
|
|||||||
$payroll = $adjustment->payroll;
|
$payroll = $adjustment->payroll;
|
||||||
$payroll->loadMissing(['payrollPeriod', 'employee.user']);
|
$payroll->loadMissing(['payrollPeriod', 'employee.user']);
|
||||||
|
|
||||||
if (! $payroll->can_adjust) {
|
|
||||||
throw ValidationException::withMessages([
|
|
||||||
'payroll' => 'Penyesuaian hanya dapat diubah pada gaji yang belum dibayar di periode terbuka.',
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
DB::transaction(function () use ($payroll, $adjustment, $validated): void {
|
DB::transaction(function () use ($payroll, $adjustment, $validated): void {
|
||||||
$adjustment->update([
|
$adjustment->type = PayrollAdjustmentType::from($validated['type']);
|
||||||
'type' => PayrollAdjustmentType::from($validated['type']),
|
$adjustment->amount = (int) $validated['amount'];
|
||||||
'amount' => (int) $validated['amount'],
|
$adjustment->description = $validated['description'];
|
||||||
'description' => $validated['description'],
|
$adjustment->save();
|
||||||
]);
|
|
||||||
|
|
||||||
$payroll->load('adjustments');
|
$payroll->load('adjustments');
|
||||||
$payroll->recalculateAmounts();
|
$payroll->recalculateAmounts();
|
||||||
@ -277,12 +262,6 @@ public function deleteAdjustment(PayrollAdjustment $adjustment): void
|
|||||||
$payroll = $adjustment->payroll;
|
$payroll = $adjustment->payroll;
|
||||||
$payroll->loadMissing(['payrollPeriod', 'employee.user']);
|
$payroll->loadMissing(['payrollPeriod', 'employee.user']);
|
||||||
|
|
||||||
if (! $payroll->can_adjust) {
|
|
||||||
throw ValidationException::withMessages([
|
|
||||||
'payroll' => 'Penyesuaian hanya dapat dihapus pada gaji yang belum dibayar di periode terbuka.',
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
DB::transaction(function () use ($payroll, $adjustment): void {
|
DB::transaction(function () use ($payroll, $adjustment): void {
|
||||||
$adjustment->delete();
|
$adjustment->delete();
|
||||||
|
|
||||||
@ -296,18 +275,6 @@ public function pay(Payroll $payroll, User $user): void
|
|||||||
{
|
{
|
||||||
$payroll->loadMissing(['payrollPeriod', 'employee.user.profile']);
|
$payroll->loadMissing(['payrollPeriod', 'employee.user.profile']);
|
||||||
|
|
||||||
if ($payroll->status !== PayrollStatus::UNPAID) {
|
|
||||||
throw ValidationException::withMessages([
|
|
||||||
'payroll' => 'Gaji ini sudah dibayar.',
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (! $payroll->payrollPeriod?->isOpen()) {
|
|
||||||
throw ValidationException::withMessages([
|
|
||||||
'payroll' => 'Periode gaji sudah ditutup.',
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($payroll->total_amount <= 0) {
|
if ($payroll->total_amount <= 0) {
|
||||||
DB::transaction(function () use ($payroll, $user): void {
|
DB::transaction(function () use ($payroll, $user): void {
|
||||||
$payroll->status = PayrollStatus::PAID;
|
$payroll->status = PayrollStatus::PAID;
|
||||||
|
|||||||
@ -1,10 +1,9 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import ConfirmDialog from '@/components/ConfirmDialog.vue';
|
import { Head } from '@inertiajs/vue3';
|
||||||
import PayrollAdjustmentModal from '@/components/admin/finance/payroll/PayrollAdjustmentModal.vue';
|
import { Banknote } from '@lucide/vue';
|
||||||
import { createColumns } from '@/components/admin/finance/payroll/columns';
|
import { computed, ref, watch } from 'vue';
|
||||||
import { DataTable } from '@/components/data-table';
|
import { DataTable } from '@/components/data-table';
|
||||||
import { Badge } from '@/components/ui/badge';
|
import { Badge } from '@/components/ui/badge';
|
||||||
import { Button } from '@/components/ui/button';
|
|
||||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
||||||
import Empty from '@/components/ui/empty/Empty.vue';
|
import Empty from '@/components/ui/empty/Empty.vue';
|
||||||
import EmptyDescription from '@/components/ui/empty/EmptyDescription.vue';
|
import EmptyDescription from '@/components/ui/empty/EmptyDescription.vue';
|
||||||
@ -25,10 +24,8 @@ import {
|
|||||||
import AdminLayout from '@/layouts/AdminLayout.vue';
|
import AdminLayout from '@/layouts/AdminLayout.vue';
|
||||||
import type { DataTableSort } from '@/types/data-table';
|
import type { DataTableSort } from '@/types/data-table';
|
||||||
import type { PayrollListItem, PayrollPageProps } from '@/types/payroll';
|
import type { PayrollListItem, PayrollPageProps } from '@/types/payroll';
|
||||||
import { Head, router } from '@inertiajs/vue3';
|
import PayrollAdjustmentModal from './form/PayrollAdjustmentModal.vue';
|
||||||
import { Banknote } from '@lucide/vue';
|
import { createColumns } from './table/columns';
|
||||||
import { computed, ref, watch } from 'vue';
|
|
||||||
import { toast } from 'vue-sonner';
|
|
||||||
|
|
||||||
const props = defineProps<PayrollPageProps>();
|
const props = defineProps<PayrollPageProps>();
|
||||||
|
|
||||||
@ -41,9 +38,11 @@ const adjustmentModalOpen = ref(false);
|
|||||||
const adjustingPayroll = ref<PayrollListItem | null>(null);
|
const adjustingPayroll = ref<PayrollListItem | null>(null);
|
||||||
const currentAdjustingPayroll = computed(() => {
|
const currentAdjustingPayroll = computed(() => {
|
||||||
const current = adjustingPayroll.value;
|
const current = adjustingPayroll.value;
|
||||||
|
|
||||||
if (!current || !props.payrolls?.data) {
|
if (!current || !props.payrolls?.data) {
|
||||||
return current;
|
return current;
|
||||||
}
|
}
|
||||||
|
|
||||||
return props.payrolls.data.find((p) => p.id === current.id) ?? current;
|
return props.payrolls.data.find((p) => p.id === current.id) ?? current;
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -116,42 +115,31 @@ watch(
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
|
|
||||||
<Head title="Gaji" />
|
<Head title="Gaji" />
|
||||||
|
|
||||||
<AdminLayout>
|
<AdminLayout>
|
||||||
<div
|
<div class="flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between">
|
||||||
class="flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between"
|
|
||||||
>
|
|
||||||
<div class="space-y-1">
|
<div class="space-y-1">
|
||||||
<h2 class="text-2xl font-bold tracking-tight">Gaji</h2>
|
<h2 class="text-2xl font-bold tracking-tight">Gaji</h2>
|
||||||
<p v-if="currentPeriod" class="text-sm text-muted-foreground">
|
<p v-if="currentPeriod" class="text-sm text-muted-foreground">
|
||||||
Periode {{ currentPeriod.period_label }}
|
Periode {{ currentPeriod.period_label }}
|
||||||
<Badge
|
<Badge class="ml-2" :variant="currentPeriod.status === 'open'
|
||||||
class="ml-2"
|
|
||||||
:variant="
|
|
||||||
currentPeriod.status === 'open'
|
|
||||||
? 'default'
|
? 'default'
|
||||||
: 'secondary'
|
: 'secondary'
|
||||||
"
|
">
|
||||||
>
|
|
||||||
{{ currentPeriod.status_label }}
|
{{ currentPeriod.status_label }}
|
||||||
</Badge>
|
</Badge>
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div
|
<div class="flex shrink-0 flex-col gap-2 sm:flex-row sm:items-center">
|
||||||
class="flex shrink-0 flex-col gap-2 sm:flex-row sm:items-center"
|
|
||||||
>
|
|
||||||
<Select v-if="periods.length > 0" v-model="selectedPeriodId">
|
<Select v-if="periods.length > 0" v-model="selectedPeriodId">
|
||||||
<SelectTrigger class="w-full sm:w-[200px]">
|
<SelectTrigger class="w-full sm:w-[200px]">
|
||||||
<SelectValue placeholder="Pilih periode" />
|
<SelectValue placeholder="Pilih periode" />
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
<SelectItem
|
<SelectItem v-for="period in periods" :key="period.id" :value="String(period.id)">
|
||||||
v-for="period in periods"
|
|
||||||
:key="period.id"
|
|
||||||
:value="String(period.id)"
|
|
||||||
>
|
|
||||||
{{ period.period_label }}
|
{{ period.period_label }}
|
||||||
</SelectItem>
|
</SelectItem>
|
||||||
</SelectContent>
|
</SelectContent>
|
||||||
@ -159,14 +147,9 @@ watch(
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-if="summary" class="grid gap-4 md:grid-cols-2">
|
<Card v-if="summary">
|
||||||
<Card>
|
<CardHeader class="flex flex-row items-center justify-between space-y-0 pb-2">
|
||||||
<CardHeader
|
<CardTitle class="text-sm font-medium text-muted-foreground">
|
||||||
class="flex flex-row items-center justify-between space-y-0 pb-2"
|
|
||||||
>
|
|
||||||
<CardTitle
|
|
||||||
class="text-sm font-medium text-muted-foreground"
|
|
||||||
>
|
|
||||||
Total Gaji Periode
|
Total Gaji Periode
|
||||||
</CardTitle>
|
</CardTitle>
|
||||||
<Banknote class="size-4 text-muted-foreground" />
|
<Banknote class="size-4 text-muted-foreground" />
|
||||||
@ -181,48 +164,11 @@ watch(
|
|||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
<Card>
|
|
||||||
<CardHeader
|
|
||||||
class="flex flex-row items-center justify-between space-y-0 pb-2"
|
|
||||||
>
|
|
||||||
<CardTitle
|
|
||||||
class="text-sm font-medium text-muted-foreground"
|
|
||||||
>
|
|
||||||
Status Pembayaran
|
|
||||||
</CardTitle>
|
|
||||||
</CardHeader>
|
|
||||||
<CardContent>
|
|
||||||
<div class="flex items-center">
|
|
||||||
<Badge
|
|
||||||
class="text-sm px-2.5 py-0.5 font-semibold"
|
|
||||||
:variant="
|
|
||||||
summary.status === 'open'
|
|
||||||
? 'outline'
|
|
||||||
: 'default'
|
|
||||||
"
|
|
||||||
>
|
|
||||||
{{ summary.status === 'open' ? 'Belum Dibayar' : 'Sudah Dibayar' }}
|
|
||||||
</Badge>
|
|
||||||
</div>
|
|
||||||
<p class="mt-2 text-sm text-muted-foreground">
|
|
||||||
{{ summary.status === 'open' ? 'Akan dibayar otomatis saat periode ditutup' : 'Telah dibayarkan oleh sistem' }}
|
|
||||||
</p>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<Card v-if="payrolls && pagination" class="min-w-0">
|
<Card v-if="payrolls && pagination" class="min-w-0">
|
||||||
<CardContent class="min-w-0 pt-6">
|
<CardContent class="min-w-0 pt-6">
|
||||||
<DataTable
|
<DataTable v-model:search="search" :columns="columns" :data="payrolls.data" :pagination="pagination"
|
||||||
v-model:search="search"
|
:pagination-links="payrolls.links" :sort="currentSort" @sort-change="setSort"
|
||||||
:columns="columns"
|
@filters-reset="resetFilters" />
|
||||||
:data="payrolls.data"
|
|
||||||
:pagination="pagination"
|
|
||||||
:pagination-links="payrolls.links"
|
|
||||||
:sort="currentSort"
|
|
||||||
@sort-change="setSort"
|
|
||||||
@filters-reset="resetFilters"
|
|
||||||
/>
|
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
@ -238,10 +184,7 @@ watch(
|
|||||||
</Empty>
|
</Empty>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<PayrollAdjustmentModal
|
<PayrollAdjustmentModal v-if="can('payroll.adjust')" v-model:open="adjustmentModalOpen"
|
||||||
v-if="can('payroll.adjust')"
|
:payroll="currentAdjustingPayroll" :adjustment-types="adjustmentTypes" />
|
||||||
v-model:open="adjustmentModalOpen"
|
</AdminLayout>
|
||||||
:payroll="currentAdjustingPayroll"
|
|
||||||
:adjustment-types="adjustmentTypes"
|
|
||||||
/> </AdminLayout>
|
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@ -1,8 +1,9 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { useForm, router } from '@inertiajs/vue3';
|
import { useForm, router } from '@inertiajs/vue3';
|
||||||
import { Save, Pencil, Trash } from '@lucide/vue';
|
import { Pencil, Save, Trash2 } from '@lucide/vue';
|
||||||
import { ref, watch } from 'vue';
|
import { ref, watch } from 'vue';
|
||||||
import { toast } from 'vue-sonner';
|
import { toast } from 'vue-sonner';
|
||||||
|
import ConfirmDialog from '@/components/ConfirmDialog.vue';
|
||||||
import { RupiahInput } from '@/components/form/rupiah-input';
|
import { RupiahInput } from '@/components/form/rupiah-input';
|
||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
import {
|
import {
|
||||||
@ -21,6 +22,11 @@ import {
|
|||||||
import { Label } from '@/components/ui/label';
|
import { Label } from '@/components/ui/label';
|
||||||
import { RadioGroup, RadioGroupItem } from '@/components/ui/radio-group';
|
import { RadioGroup, RadioGroupItem } from '@/components/ui/radio-group';
|
||||||
import { Textarea } from '@/components/ui/textarea';
|
import { Textarea } from '@/components/ui/textarea';
|
||||||
|
import {
|
||||||
|
Tooltip,
|
||||||
|
TooltipContent,
|
||||||
|
TooltipTrigger,
|
||||||
|
} from '@/components/ui/tooltip';
|
||||||
import { FIELD_LIMITS } from '@/lib/field-limits';
|
import { FIELD_LIMITS } from '@/lib/field-limits';
|
||||||
import { formErrors } from '@/lib/form';
|
import { formErrors } from '@/lib/form';
|
||||||
import { parseRupiah } from '@/lib/rupiah';
|
import { parseRupiah } from '@/lib/rupiah';
|
||||||
@ -40,6 +46,8 @@ const form = useForm<PayrollAdjustmentFormData>({
|
|||||||
});
|
});
|
||||||
|
|
||||||
const editingAdjustment = ref<PayrollAdjustmentItem | null>(null);
|
const editingAdjustment = ref<PayrollAdjustmentItem | null>(null);
|
||||||
|
const deleteConfirmOpen = ref(false);
|
||||||
|
const deleteTarget = ref<PayrollAdjustmentItem | null>(null);
|
||||||
|
|
||||||
function resetForm() {
|
function resetForm() {
|
||||||
form.reset();
|
form.reset();
|
||||||
@ -94,7 +102,6 @@ function submit() {
|
|||||||
preserveScroll: true,
|
preserveScroll: true,
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
cancelEdit();
|
cancelEdit();
|
||||||
toast.success('Penyesuaian berhasil diperbarui.');
|
|
||||||
},
|
},
|
||||||
onError: () => {
|
onError: () => {
|
||||||
toast.error('Gagal memperbarui penyesuaian.');
|
toast.error('Gagal memperbarui penyesuaian.');
|
||||||
@ -105,7 +112,6 @@ function submit() {
|
|||||||
preserveScroll: true,
|
preserveScroll: true,
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
resetForm();
|
resetForm();
|
||||||
toast.success('Penyesuaian berhasil ditambahkan.');
|
|
||||||
},
|
},
|
||||||
onError: () => {
|
onError: () => {
|
||||||
toast.error('Gagal menambahkan penyesuaian.');
|
toast.error('Gagal menambahkan penyesuaian.');
|
||||||
@ -126,26 +132,47 @@ function submit() {
|
|||||||
<div class="space-y-4">
|
<div class="space-y-4">
|
||||||
<h3 class="text-sm font-semibold">Daftar Penyesuaian Saat Ini</h3>
|
<h3 class="text-sm font-semibold">Daftar Penyesuaian Saat Ini</h3>
|
||||||
<div class="max-h-[300px] overflow-y-auto space-y-2 pr-1">
|
<div class="max-h-[300px] overflow-y-auto space-y-2 pr-1">
|
||||||
<div v-if="!payroll?.adjustments || payroll.adjustments.length === 0" class="text-sm text-muted-foreground py-4 text-center">
|
<div v-if="!payroll?.adjustments || payroll.adjustments.length === 0"
|
||||||
|
class="text-sm text-muted-foreground py-4 text-center">
|
||||||
Belum ada penyesuaian.
|
Belum ada penyesuaian.
|
||||||
</div>
|
</div>
|
||||||
<div v-else v-for="adj in payroll.adjustments" :key="adj.id" class="flex items-center justify-between p-3 rounded-lg border bg-muted/40">
|
<div v-else v-for="adj in payroll.adjustments" :key="adj.id"
|
||||||
|
class="flex items-center justify-between p-3 rounded-lg border"
|
||||||
|
:class="adj.type === 'bonus' ? 'border-primary/20 bg-primary/5' : 'border-destructive/20 bg-destructive/5'">
|
||||||
<div class="space-y-1">
|
<div class="space-y-1">
|
||||||
<div class="flex items-center gap-2">
|
<div class="flex items-center gap-2">
|
||||||
<span class="text-[10px] font-bold uppercase px-1.5 py-0.5 rounded" :class="adj.type === 'bonus' ? 'bg-primary/10 text-primary' : 'bg-destructive/10 text-destructive'">
|
<span class="text-sm font-bold"
|
||||||
{{ adj.type_label }}
|
:class="adj.type === 'bonus' ? 'text-primary' : 'text-destructive'">
|
||||||
|
{{ adj.amount_formatted }}
|
||||||
</span>
|
</span>
|
||||||
<span class="text-sm font-bold">{{ adj.amount_formatted }}</span>
|
|
||||||
</div>
|
</div>
|
||||||
<p class="text-xs text-muted-foreground">{{ adj.description }}</p>
|
<p class="text-xs text-muted-foreground">{{ adj.description }}</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex items-center gap-1">
|
<div class="flex items-center gap-1">
|
||||||
<Button variant="ghost" size="icon" class="size-8 text-muted-foreground hover:text-foreground" @click="startEdit(adj)">
|
<Tooltip>
|
||||||
|
<TooltipTrigger as-child>
|
||||||
|
<Button variant="ghost" size="icon"
|
||||||
|
class="size-8 text-muted-foreground hover:text-foreground"
|
||||||
|
@click="startEdit(adj)">
|
||||||
<Pencil class="size-4" />
|
<Pencil class="size-4" />
|
||||||
</Button>
|
</Button>
|
||||||
<Button variant="ghost" size="icon" class="size-8 text-destructive hover:bg-destructive/10" @click="deleteAdj(adj)">
|
</TooltipTrigger>
|
||||||
<Trash class="size-4" />
|
<TooltipContent class="z-210">
|
||||||
|
<p>Ubah</p>
|
||||||
|
</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger as-child>
|
||||||
|
<Button variant="ghost" size="icon"
|
||||||
|
class="size-8 text-destructive hover:bg-destructive/10"
|
||||||
|
@click="deleteTarget = adj; deleteConfirmOpen = true">
|
||||||
|
<Trash2 class="size-4" />
|
||||||
</Button>
|
</Button>
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent class="z-210">
|
||||||
|
<p>Hapus</p>
|
||||||
|
</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -161,8 +188,10 @@ function submit() {
|
|||||||
<Field>
|
<Field>
|
||||||
<FieldLabel required>Jenis</FieldLabel>
|
<FieldLabel required>Jenis</FieldLabel>
|
||||||
<RadioGroup v-model="form.type" class="flex gap-4 pt-1">
|
<RadioGroup v-model="form.type" class="flex gap-4 pt-1">
|
||||||
<div v-for="option in adjustmentTypes" :key="option.value" class="flex items-center gap-2">
|
<div v-for="option in adjustmentTypes" :key="option.value"
|
||||||
<RadioGroupItem :id="`type-${option.value}`" :value="option.value" required/>
|
class="flex items-center gap-2">
|
||||||
|
<RadioGroupItem :id="`type-${option.value}`" :value="option.value"
|
||||||
|
required />
|
||||||
<Label :for="`type-${option.value}`" class="font-normal">
|
<Label :for="`type-${option.value}`" class="font-normal">
|
||||||
{{ option.label }}
|
{{ option.label }}
|
||||||
</Label>
|
</Label>
|
||||||
@ -180,7 +209,8 @@ function submit() {
|
|||||||
<Field>
|
<Field>
|
||||||
<FieldLabel for="description" required>Keterangan</FieldLabel>
|
<FieldLabel for="description" required>Keterangan</FieldLabel>
|
||||||
<Textarea id="description" v-model="form.description" rows="3"
|
<Textarea id="description" v-model="form.description" rows="3"
|
||||||
:maxlength="FIELD_LIMITS.description" placeholder="Contoh: Kinerja Bagus" required/>
|
:maxlength="FIELD_LIMITS.description" placeholder="Contoh: Kinerja Bagus"
|
||||||
|
required />
|
||||||
<FieldError :errors="formErrors(form, 'description')" />
|
<FieldError :errors="formErrors(form, 'description')" />
|
||||||
</Field>
|
</Field>
|
||||||
</FieldGroup>
|
</FieldGroup>
|
||||||
@ -200,4 +230,8 @@ function submit() {
|
|||||||
</div>
|
</div>
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
|
|
||||||
|
<ConfirmDialog v-model:open="deleteConfirmOpen" title="Hapus Penyesuaian?"
|
||||||
|
:description="`Penyesuaian ${deleteTarget?.type_label} ${deleteTarget?.amount_formatted} akan dihapus.`"
|
||||||
|
confirm-label="Hapus" cancel-label="Batal" destructive @confirm="deleteAdj(deleteTarget!)" />
|
||||||
</template>
|
</template>
|
||||||
@ -1,9 +1,9 @@
|
|||||||
import type { ColumnDef } from '@tanstack/vue-table';
|
import type { ColumnDef } from '@tanstack/vue-table';
|
||||||
import { h } from 'vue';
|
import { h } from 'vue';
|
||||||
import DataTableActions from '@/components/admin/finance/payroll/data-table-actions.vue';
|
|
||||||
import { DataTableColumnHeader } from '@/components/data-table';
|
import { DataTableColumnHeader } from '@/components/data-table';
|
||||||
import { Badge } from '@/components/ui/badge';
|
import { Badge } from '@/components/ui/badge';
|
||||||
import type { PayrollListItem } from '@/types/payroll';
|
import type { PayrollListItem } from '@/types/payroll';
|
||||||
|
import DataTableActions from './data-table-actions.vue';
|
||||||
|
|
||||||
function statusVariant(status: string): 'default' | 'secondary' | 'destructive' | 'outline' {
|
function statusVariant(status: string): 'default' | 'secondary' | 'destructive' | 'outline' {
|
||||||
return status === 'paid' ? 'secondary' : 'outline';
|
return status === 'paid' ? 'secondary' : 'outline';
|
||||||
@ -5,7 +5,7 @@ import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip
|
|||||||
import { useCan } from '@/composables/useCan';
|
import { useCan } from '@/composables/useCan';
|
||||||
import type { PayrollListItem } from '@/types/payroll';
|
import type { PayrollListItem } from '@/types/payroll';
|
||||||
|
|
||||||
const props = defineProps<{
|
defineProps<{
|
||||||
payroll: PayrollListItem;
|
payroll: PayrollListItem;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
@ -9,7 +9,6 @@
|
|||||||
use App\Http\Controllers\Admin\Finance\EmployeeAdvanceController;
|
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\Finance\PayrollController;
|
use App\Http\Controllers\Admin\Finance\PayrollController;
|
||||||
use App\Http\Controllers\Admin\Finance\PayrollPeriodController;
|
|
||||||
use App\Http\Controllers\Admin\Hr\AttendanceController;
|
use App\Http\Controllers\Admin\Hr\AttendanceController;
|
||||||
use App\Http\Controllers\Admin\Hr\EmployeeController;
|
use App\Http\Controllers\Admin\Hr\EmployeeController;
|
||||||
use App\Http\Controllers\Admin\Hr\LeaveRequestController;
|
use App\Http\Controllers\Admin\Hr\LeaveRequestController;
|
||||||
@ -421,7 +420,7 @@
|
|||||||
Route::prefix('payroll')->name('payroll.')
|
Route::prefix('payroll')->name('payroll.')
|
||||||
->middleware('permission:'.Permission::PAYROLL_VIEW->value)
|
->middleware('permission:'.Permission::PAYROLL_VIEW->value)
|
||||||
->group(function () {
|
->group(function () {
|
||||||
Route::get('/', [PayrollPeriodController::class, 'index'])->name('index');
|
Route::get('/', [PayrollController::class, 'index'])->name('index');
|
||||||
|
|
||||||
Route::post('{payroll}/adjustments', [PayrollController::class, 'storeAdjustment'])
|
Route::post('{payroll}/adjustments', [PayrollController::class, 'storeAdjustment'])
|
||||||
->middleware('permission:'.Permission::PAYROLL_ADJUST->value)
|
->middleware('permission:'.Permission::PAYROLL_ADJUST->value)
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user