refactor: consolidate payroll management by merging PayrollPeriodController into PayrollController, enhancing data handling and UI components for improved functionality

This commit is contained in:
Yoga Pangestu 2026-06-21 01:02:12 +07:00
parent 50eaced126
commit 0803cbeb96
8 changed files with 127 additions and 217 deletions

View File

@ -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 index(Request $request): Response
{
$tableQuery = $this->parseDataTableQuery($request);
$periodId = $request->integer('period_id') ?: null;
$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 public function storeAdjustment(PayrollAdjustmentRequest $request, Payroll $payroll): RedirectResponse
{ {
$user = $request->user();
if ($user && ! $user->hasAnyRole(['owner', 'developer', 'direktur'])) {
abort(403, 'Anda tidak memiliki akses untuk menambah penyesuaian gaji.');
}
$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);

View File

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

View File

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

View File

@ -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" ? 'default'
:variant=" : 'secondary'
currentPeriod.status === 'open' ">
? 'default'
: '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,70 +147,28 @@ 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" Total Gaji Periode
> </CardTitle>
<CardTitle <Banknote class="size-4 text-muted-foreground" />
class="text-sm font-medium text-muted-foreground" </CardHeader>
> <CardContent>
Total Gaji Periode <div class="text-3xl font-bold tracking-tight">
</CardTitle> {{ summary.total_amount_formatted }}
<Banknote class="size-4 text-muted-foreground" /> </div>
</CardHeader> <p class="mt-1 text-sm text-muted-foreground">
<CardContent> {{ summary.total_count }} slip gaji
<div class="text-3xl font-bold tracking-tight"> </p>
{{ summary.total_amount_formatted }} </CardContent>
</div> </Card>
<p class="mt-1 text-sm text-muted-foreground">
{{ summary.total_count }} slip gaji
</p>
</CardContent>
</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>

View File

@ -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>
<Pencil class="size-4" /> <TooltipTrigger as-child>
</Button> <Button variant="ghost" size="icon"
<Button variant="ghost" size="icon" class="size-8 text-destructive hover:bg-destructive/10" @click="deleteAdj(adj)"> class="size-8 text-muted-foreground hover:text-foreground"
<Trash class="size-4" /> @click="startEdit(adj)">
</Button> <Pencil class="size-4" />
</Button>
</TooltipTrigger>
<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>
</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>
@ -173,14 +202,15 @@ function submit() {
<Field> <Field>
<FieldLabel for="amount" required>Jumlah</FieldLabel> <FieldLabel for="amount" required>Jumlah</FieldLabel>
<RupiahInput id="amount" v-model="form.amount" required/> <RupiahInput id="amount" v-model="form.amount" required />
<FieldError :errors="formErrors(form, 'amount')" /> <FieldError :errors="formErrors(form, 'amount')" />
</Field> </Field>
<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>

View File

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

View File

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

View File

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