feat: implement employee advance payment management with new model, migration, and frontend updates
This commit is contained in:
parent
fe1549e0a5
commit
414ce51904
@ -13,6 +13,7 @@
|
|||||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||||
use Illuminate\Database\Eloquent\Model;
|
use Illuminate\Database\Eloquent\Model;
|
||||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||||
|
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||||
|
|
||||||
#[Guarded(['id'])]
|
#[Guarded(['id'])]
|
||||||
#[Appends([
|
#[Appends([
|
||||||
@ -71,6 +72,11 @@ public function verifiedBy(): BelongsTo
|
|||||||
return $this->belongsTo(User::class, 'verified_by_id');
|
return $this->belongsTo(User::class, 'verified_by_id');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function payments(): HasMany
|
||||||
|
{
|
||||||
|
return $this->hasMany(EmployeeAdvancePayment::class)->latest('paid_at');
|
||||||
|
}
|
||||||
|
|
||||||
public function amountFormatted(): Attribute
|
public function amountFormatted(): Attribute
|
||||||
{
|
{
|
||||||
return Attribute::make(
|
return Attribute::make(
|
||||||
|
|||||||
56
app/Models/EmployeeAdvancePayment.php
Normal file
56
app/Models/EmployeeAdvancePayment.php
Normal file
@ -0,0 +1,56 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Models;
|
||||||
|
|
||||||
|
use Illuminate\Database\Eloquent\Attributes\Appends;
|
||||||
|
use Illuminate\Database\Eloquent\Attributes\Guarded;
|
||||||
|
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||||
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||||
|
|
||||||
|
#[Guarded(['id'])]
|
||||||
|
#[Appends([
|
||||||
|
'amount_formatted',
|
||||||
|
'paid_at_formatted',
|
||||||
|
])]
|
||||||
|
class EmployeeAdvancePayment extends Model
|
||||||
|
{
|
||||||
|
use HasFactory;
|
||||||
|
|
||||||
|
protected function casts(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'amount' => 'integer',
|
||||||
|
'paid_at' => 'datetime',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function employeeAdvance(): BelongsTo
|
||||||
|
{
|
||||||
|
return $this->belongsTo(EmployeeAdvance::class);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function paidBy(): BelongsTo
|
||||||
|
{
|
||||||
|
return $this->belongsTo(User::class, 'paid_by_id');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function cashTransaction(): BelongsTo
|
||||||
|
{
|
||||||
|
return $this->belongsTo(CashTransaction::class);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function amountFormatted(): \Illuminate\Database\Eloquent\Casts\Attribute
|
||||||
|
{
|
||||||
|
return \Illuminate\Database\Eloquent\Casts\Attribute::make(
|
||||||
|
get: fn () => 'Rp '.number_format($this->amount, 0, ',', '.'),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function paidAtFormatted(): \Illuminate\Database\Eloquent\Casts\Attribute
|
||||||
|
{
|
||||||
|
return \Illuminate\Database\Eloquent\Casts\Attribute::make(
|
||||||
|
get: fn () => $this->paid_at?->translatedFormat('l, d F Y H:i'),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -5,6 +5,7 @@
|
|||||||
use App\Enums\EmployeeAdvanceStatus;
|
use App\Enums\EmployeeAdvanceStatus;
|
||||||
use App\Enums\Role;
|
use App\Enums\Role;
|
||||||
use App\Models\EmployeeAdvance;
|
use App\Models\EmployeeAdvance;
|
||||||
|
use App\Models\EmployeeAdvancePayment;
|
||||||
use App\Models\User;
|
use App\Models\User;
|
||||||
use App\Services\Concerns\ResolvesAuthenticatedEmployee;
|
use App\Services\Concerns\ResolvesAuthenticatedEmployee;
|
||||||
use App\Services\System\PushNotificationService;
|
use App\Services\System\PushNotificationService;
|
||||||
@ -49,7 +50,7 @@ public function outstandingSummary(?User $user = null): array
|
|||||||
public function paginateForIndex(array $tableQuery, User $user, string $status = ''): LengthAwarePaginator
|
public function paginateForIndex(array $tableQuery, User $user, string $status = ''): LengthAwarePaginator
|
||||||
{
|
{
|
||||||
$query = EmployeeAdvance::query()
|
$query = EmployeeAdvance::query()
|
||||||
->with(['employee.user.profile', 'rejection'])
|
->with(['employee.user.profile', 'rejection', 'payments'])
|
||||||
->when(! $user->hasAnyRole([Role::OWNER->value, Role::DEVELOPER->value, Role::DIREKTUR->value]), 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);
|
||||||
@ -275,6 +276,15 @@ public function pay(EmployeeAdvance $employeeAdvance, User $user, ?int $payAmoun
|
|||||||
'paid_by_id' => $isFullPayment ? $user->id : $employeeAdvance->paid_by_id,
|
'paid_by_id' => $isFullPayment ? $user->id : $employeeAdvance->paid_by_id,
|
||||||
'status' => $isFullPayment ? EmployeeAdvanceStatus::PAID : EmployeeAdvanceStatus::PARTIALLY_PAID,
|
'status' => $isFullPayment ? EmployeeAdvanceStatus::PAID : EmployeeAdvanceStatus::PARTIALLY_PAID,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
EmployeeAdvancePayment::create([
|
||||||
|
'employee_advance_id' => $employeeAdvance->id,
|
||||||
|
'paid_by_id' => $user->id,
|
||||||
|
'cash_transaction_id' => $cashTransaction->id,
|
||||||
|
'amount' => $amountToPay,
|
||||||
|
'description' => $description,
|
||||||
|
'paid_at' => now(),
|
||||||
|
]);
|
||||||
});
|
});
|
||||||
} catch (ValidationException $e) {
|
} catch (ValidationException $e) {
|
||||||
throw $e;
|
throw $e;
|
||||||
|
|||||||
@ -0,0 +1,17 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
use Illuminate\Support\Facades\DB;
|
||||||
|
|
||||||
|
return new class extends Migration
|
||||||
|
{
|
||||||
|
public function up(): void
|
||||||
|
{
|
||||||
|
DB::statement("ALTER TABLE employee_advances MODIFY COLUMN status ENUM('pending','approved','rejected','partially_paid','paid') DEFAULT 'pending'");
|
||||||
|
}
|
||||||
|
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
DB::statement("ALTER TABLE employee_advances MODIFY COLUMN status ENUM('pending','approved','rejected','paid') DEFAULT 'pending'");
|
||||||
|
}
|
||||||
|
};
|
||||||
@ -0,0 +1,31 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
|
||||||
|
return new class extends Migration
|
||||||
|
{
|
||||||
|
public function up(): void
|
||||||
|
{
|
||||||
|
Schema::create('employee_advance_payments', function (Blueprint $table) {
|
||||||
|
$table->id();
|
||||||
|
|
||||||
|
$table->foreignId('employee_advance_id')->constrained()->cascadeOnDelete();
|
||||||
|
$table->foreignId('paid_by_id')->nullable()->constrained('users')->restrictOnDelete();
|
||||||
|
$table->foreignId('cash_transaction_id')->nullable()->unique()->constrained('cash_transactions')->restrictOnDelete();
|
||||||
|
|
||||||
|
$table->unsignedBigInteger('amount');
|
||||||
|
$table->string('description', 100)->nullable();
|
||||||
|
$table->timestamp('paid_at')->useCurrent();
|
||||||
|
|
||||||
|
$table->timestamp('created_at')->useCurrent();
|
||||||
|
$table->timestamp('updated_at')->useCurrent()->useCurrentOnUpdate();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
Schema::dropIfExists('employee_advance_payments');
|
||||||
|
}
|
||||||
|
};
|
||||||
@ -44,6 +44,7 @@ const emit = defineEmits<{
|
|||||||
{{ description }}
|
{{ description }}
|
||||||
</AlertDialogDescription>
|
</AlertDialogDescription>
|
||||||
</AlertDialogHeader>
|
</AlertDialogHeader>
|
||||||
|
<slot name="content" />
|
||||||
<AlertDialogFooter>
|
<AlertDialogFooter>
|
||||||
<AlertDialogCancel :disabled="loading">
|
<AlertDialogCancel :disabled="loading">
|
||||||
{{ cancelLabel }}
|
{{ cancelLabel }}
|
||||||
|
|||||||
@ -7,12 +7,17 @@ defineProps<{
|
|||||||
tooltip?: string;
|
tooltip?: string;
|
||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
|
defineEmits<{
|
||||||
|
click: [];
|
||||||
|
}>();
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<Tooltip>
|
<Tooltip>
|
||||||
<TooltipTrigger as-child>
|
<TooltipTrigger as-child>
|
||||||
<Button variant="ghost" size="icon" class="size-8" :disabled="disabled">
|
<Button variant="ghost" size="icon" class="size-8" :disabled="disabled"
|
||||||
|
@click="$emit('click')">
|
||||||
<Banknote class="size-4" />
|
<Banknote class="size-4" />
|
||||||
<span class="sr-only">{{ tooltip || 'Bayar' }}</span>
|
<span class="sr-only">{{ tooltip || 'Bayar' }}</span>
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
@ -42,6 +42,25 @@ export function createColumns(
|
|||||||
enableSorting: true,
|
enableSorting: true,
|
||||||
header: () => h(DataTableColumnHeader, { title: 'Jumlah', column: 'amount' }),
|
header: () => h(DataTableColumnHeader, { title: 'Jumlah', column: 'amount' }),
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
id: 'payment_status',
|
||||||
|
enableSorting: false,
|
||||||
|
header: () => 'Dibayar / Sisa',
|
||||||
|
cell: ({ row }) => {
|
||||||
|
const { paid_amount_formatted, remaining_amount, remaining_amount_formatted, status } = row.original;
|
||||||
|
|
||||||
|
if (status === 'pending' || status === 'rejected') {
|
||||||
|
return h('span', { class: 'text-muted-foreground text-sm' }, '-');
|
||||||
|
}
|
||||||
|
|
||||||
|
return h('div', { class: 'space-y-0.5' }, [
|
||||||
|
h('div', { class: 'text-sm font-medium' }, paid_amount_formatted),
|
||||||
|
remaining_amount > 0
|
||||||
|
? h('div', { class: 'text-xs text-muted-foreground' }, `Sisa: ${remaining_amount_formatted}`)
|
||||||
|
: h('div', { class: 'text-xs text-green-600 font-medium' }, 'Lunas'),
|
||||||
|
]);
|
||||||
|
},
|
||||||
|
},
|
||||||
{
|
{
|
||||||
accessorKey: 'description',
|
accessorKey: 'description',
|
||||||
enableSorting: true,
|
enableSorting: true,
|
||||||
|
|||||||
@ -64,9 +64,11 @@ function approveEmployeeAdvance() {
|
|||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
approveConfirmOpen.value = false;
|
approveConfirmOpen.value = false;
|
||||||
},
|
},
|
||||||
onError: (errors: any) => {
|
onError: (errors: Record<string, string>) => {
|
||||||
if (errors.system) {
|
const message = errors.system ?? Object.values(errors)[0];
|
||||||
toast.error(errors.system);
|
|
||||||
|
if (message) {
|
||||||
|
toast.error(message);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
onFinish: () => {
|
onFinish: () => {
|
||||||
@ -80,11 +82,13 @@ function payEmployeeAdvance() {
|
|||||||
|
|
||||||
if (!amount || amount <= 0) {
|
if (!amount || amount <= 0) {
|
||||||
payError.value = 'Jumlah pembayaran harus lebih dari 0.';
|
payError.value = 'Jumlah pembayaran harus lebih dari 0.';
|
||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (amount > props.employeeAdvance.remaining_amount) {
|
if (amount > props.employeeAdvance.remaining_amount) {
|
||||||
payError.value = 'Jumlah pembayaran melebihi sisa kasbon.';
|
payError.value = 'Jumlah pembayaran melebihi sisa kasbon.';
|
||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -96,9 +100,11 @@ function payEmployeeAdvance() {
|
|||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
payConfirmOpen.value = false;
|
payConfirmOpen.value = false;
|
||||||
},
|
},
|
||||||
onError: (errors: any) => {
|
onError: (errors: Record<string, string>) => {
|
||||||
if (errors.system) {
|
const message = errors.system ?? Object.values(errors)[0];
|
||||||
toast.error(errors.system);
|
|
||||||
|
if (message) {
|
||||||
|
toast.error(message);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
onFinish: () => {
|
onFinish: () => {
|
||||||
@ -146,8 +152,21 @@ function payEmployeeAdvance() {
|
|||||||
<Field>
|
<Field>
|
||||||
<FieldLabel for="pay-amount" required>Jumlah Pembayaran</FieldLabel>
|
<FieldLabel for="pay-amount" required>Jumlah Pembayaran</FieldLabel>
|
||||||
<RupiahInput id="pay-amount" v-model="payAmount" placeholder="0" />
|
<RupiahInput id="pay-amount" v-model="payAmount" placeholder="0" />
|
||||||
<FieldError v-if="payError" :errors="{ amount: [payError] }" />
|
<FieldError v-if="payError" :errors="[payError]" />
|
||||||
</Field>
|
</Field>
|
||||||
|
|
||||||
|
<div v-if="employeeAdvance.payments && employeeAdvance.payments.length > 0" class="space-y-2">
|
||||||
|
<p class="text-sm font-medium">Riwayat Pembayaran</p>
|
||||||
|
<div class="max-h-40 space-y-2 overflow-y-auto rounded-md border p-2">
|
||||||
|
<div v-for="payment in employeeAdvance.payments" :key="payment.id"
|
||||||
|
class="flex items-center justify-between text-sm">
|
||||||
|
<div>
|
||||||
|
<p class="font-medium">{{ payment.amount_formatted }}</p>
|
||||||
|
<p class="text-xs text-muted-foreground">{{ payment.paid_at_formatted }}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</ConfirmDialog>
|
</ConfirmDialog>
|
||||||
|
|||||||
@ -1,3 +1,15 @@
|
|||||||
|
export type EmployeeAdvancePayment = {
|
||||||
|
id: number;
|
||||||
|
employee_advance_id: number;
|
||||||
|
paid_by_id: number | null;
|
||||||
|
cash_transaction_id: number | null;
|
||||||
|
amount: number;
|
||||||
|
amount_formatted: string;
|
||||||
|
description: string | null;
|
||||||
|
paid_at: string;
|
||||||
|
paid_at_formatted: string;
|
||||||
|
};
|
||||||
|
|
||||||
export type EmployeeAdvanceListItem = {
|
export type EmployeeAdvanceListItem = {
|
||||||
id: number;
|
id: number;
|
||||||
employee_id: number;
|
employee_id: number;
|
||||||
@ -18,6 +30,7 @@ export type EmployeeAdvanceListItem = {
|
|||||||
is_editable: boolean;
|
is_editable: boolean;
|
||||||
can_verify: boolean;
|
can_verify: boolean;
|
||||||
can_pay: boolean;
|
can_pay: boolean;
|
||||||
|
payments?: EmployeeAdvancePayment[];
|
||||||
};
|
};
|
||||||
|
|
||||||
export type EmployeeAdvanceFormData = {
|
export type EmployeeAdvanceFormData = {
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user