feat: implement employee advance payment functionality with history tracking
This commit is contained in:
parent
81b5db77e2
commit
54ad2edaf7
@ -4,6 +4,7 @@
|
|||||||
|
|
||||||
use App\Enums\EmployeeAdvanceStatus;
|
use App\Enums\EmployeeAdvanceStatus;
|
||||||
use App\Http\Controllers\Controller;
|
use App\Http\Controllers\Controller;
|
||||||
|
use App\Http\Requests\Admin\Finance\EmployeeAdvancePaymentRequest;
|
||||||
use App\Http\Requests\Admin\Finance\EmployeeAdvanceRequest;
|
use App\Http\Requests\Admin\Finance\EmployeeAdvanceRequest;
|
||||||
use App\Http\Requests\PaginatedRequest;
|
use App\Http\Requests\PaginatedRequest;
|
||||||
use App\Models\EmployeeAdvance;
|
use App\Models\EmployeeAdvance;
|
||||||
@ -68,11 +69,11 @@ public function approve(EmployeeAdvance $employeeAdvance): RedirectResponse
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function pay(EmployeeAdvance $employeeAdvance): RedirectResponse
|
public function pay(EmployeeAdvancePaymentRequest $request, EmployeeAdvance $employeeAdvance): RedirectResponse
|
||||||
{
|
{
|
||||||
return $this->handleAction(
|
return $this->handleAction(
|
||||||
fn () => $this->service->pay($employeeAdvance),
|
fn () => $this->service->pay($employeeAdvance, $request->validated('amount')),
|
||||||
'Kasbon berhasil dibayar.',
|
'Pembayaran kasbon berhasil.',
|
||||||
'admin.finance.employee-advances.index'
|
'admin.finance.employee-advances.index'
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -0,0 +1,37 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Requests\Admin\Finance;
|
||||||
|
|
||||||
|
use App\Concerns\CurrencyStripping;
|
||||||
|
use Illuminate\Foundation\Http\FormRequest;
|
||||||
|
use Override;
|
||||||
|
|
||||||
|
class EmployeeAdvancePaymentRequest extends FormRequest
|
||||||
|
{
|
||||||
|
use CurrencyStripping;
|
||||||
|
|
||||||
|
public function authorize(): bool
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
#[Override]
|
||||||
|
public function prepareForValidation(): void
|
||||||
|
{
|
||||||
|
$this->merge($this->stripCurrencyDot($this->all(), 'amount'));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function rules(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'amount' => ['required', 'integer', 'min:1'],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function attributes(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'amount' => 'jumlah bayar',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -11,9 +11,10 @@
|
|||||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||||
use Illuminate\Database\Eloquent\Model;
|
use Illuminate\Database\Eloquent\Model;
|
||||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||||
|
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||||
use Illuminate\Database\Eloquent\SoftDeletes;
|
use Illuminate\Database\Eloquent\SoftDeletes;
|
||||||
|
|
||||||
#[Appends(['formatted_amount', 'formatted_created_at', 'formatted_due_date', 'formatted_paid_amount', 'status_label'])]
|
#[Appends(['formatted_amount', 'formatted_created_at', 'formatted_due_date', 'formatted_paid_amount', 'formatted_remaining_amount', 'status_label'])]
|
||||||
#[Guarded(['id'])]
|
#[Guarded(['id'])]
|
||||||
class EmployeeAdvance extends Model
|
class EmployeeAdvance extends Model
|
||||||
{
|
{
|
||||||
@ -59,6 +60,13 @@ protected function formattedPaidAmount(): Attribute
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected function formattedRemainingAmount(): Attribute
|
||||||
|
{
|
||||||
|
return Attribute::make(
|
||||||
|
get: fn () => 'Rp '.number_format($this->amount - $this->paid_amount, 0, ',', '.'),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
protected function statusLabel(): Attribute
|
protected function statusLabel(): Attribute
|
||||||
{
|
{
|
||||||
return Attribute::make(
|
return Attribute::make(
|
||||||
@ -106,6 +114,11 @@ public function employee(): BelongsTo
|
|||||||
return $this->belongsTo(Employee::class);
|
return $this->belongsTo(Employee::class);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function payments(): HasMany
|
||||||
|
{
|
||||||
|
return $this->hasMany(EmployeeAdvancePayment::class);
|
||||||
|
}
|
||||||
|
|
||||||
public function paidBy(): BelongsTo
|
public function paidBy(): BelongsTo
|
||||||
{
|
{
|
||||||
return $this->belongsTo(User::class, 'paid_by_id');
|
return $this->belongsTo(User::class, 'paid_by_id');
|
||||||
|
|||||||
54
app/Models/EmployeeAdvancePayment.php
Normal file
54
app/Models/EmployeeAdvancePayment.php
Normal file
@ -0,0 +1,54 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Models;
|
||||||
|
|
||||||
|
use Illuminate\Database\Eloquent\Attributes\Appends;
|
||||||
|
use Illuminate\Database\Eloquent\Attributes\Guarded;
|
||||||
|
use Illuminate\Database\Eloquent\Casts\Attribute;
|
||||||
|
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||||
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||||
|
|
||||||
|
#[Appends(['formatted_amount', 'formatted_paid_at'])]
|
||||||
|
#[Guarded(['id'])]
|
||||||
|
class EmployeeAdvancePayment extends Model
|
||||||
|
{
|
||||||
|
use HasFactory;
|
||||||
|
|
||||||
|
protected function casts(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'amount' => 'integer',
|
||||||
|
'paid_at' => 'datetime',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function formattedAmount(): Attribute
|
||||||
|
{
|
||||||
|
return Attribute::make(
|
||||||
|
get: fn() => 'Rp ' . number_format($this->amount, 0, ',', '.'),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function formattedPaidAt(): Attribute
|
||||||
|
{
|
||||||
|
return Attribute::make(
|
||||||
|
get: fn() => $this->paid_at?->translatedFormat('l, d F Y'),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function employeeAdvance(): BelongsTo
|
||||||
|
{
|
||||||
|
return $this->belongsTo(EmployeeAdvance::class);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function paidBy(): BelongsTo
|
||||||
|
{
|
||||||
|
return $this->belongsTo(User::class, 'paid_by_id');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function cashTransaction(): BelongsTo
|
||||||
|
{
|
||||||
|
return $this->belongsTo(CashTransaction::class);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -4,11 +4,13 @@
|
|||||||
|
|
||||||
use App\Enums\EmployeeAdvanceStatus;
|
use App\Enums\EmployeeAdvanceStatus;
|
||||||
use App\Models\EmployeeAdvance;
|
use App\Models\EmployeeAdvance;
|
||||||
|
use App\Models\EmployeeAdvancePayment;
|
||||||
use App\Services\Concerns\HandlesCashTransactions;
|
use App\Services\Concerns\HandlesCashTransactions;
|
||||||
use App\Services\NotificationService;
|
use App\Services\NotificationService;
|
||||||
use Illuminate\Contracts\Pagination\LengthAwarePaginator;
|
use Illuminate\Contracts\Pagination\LengthAwarePaginator;
|
||||||
use Illuminate\Support\Collection;
|
use Illuminate\Support\Collection;
|
||||||
use Illuminate\Support\Facades\DB;
|
use Illuminate\Support\Facades\DB;
|
||||||
|
use Illuminate\Validation\ValidationException;
|
||||||
|
|
||||||
class EmployeeAdvanceService
|
class EmployeeAdvanceService
|
||||||
{
|
{
|
||||||
@ -22,7 +24,7 @@ private function canViewAll(): bool
|
|||||||
public function getAll(array $filters = []): Collection
|
public function getAll(array $filters = []): Collection
|
||||||
{
|
{
|
||||||
return EmployeeAdvance::select(['id', 'employee_id', 'amount', 'paid_amount', 'description', 'due_date', 'status', 'created_at'])
|
return EmployeeAdvance::select(['id', 'employee_id', 'amount', 'paid_amount', 'description', 'due_date', 'status', 'created_at'])
|
||||||
->with(['employee.user.userProfile'])
|
->with(['employee.user.userProfile', 'payments.paidBy.userProfile'])
|
||||||
->when(! $this->canViewAll(), fn ($q) => $q->whereHas('employee', fn ($eq) => $eq->where('user_id', auth()->id())))
|
->when(! $this->canViewAll(), fn ($q) => $q->whereHas('employee', fn ($eq) => $eq->where('user_id', auth()->id())))
|
||||||
->latest()
|
->latest()
|
||||||
->get();
|
->get();
|
||||||
@ -32,7 +34,7 @@ public function paginated(int $perPage = 25, string $search = '', string $sort =
|
|||||||
{
|
{
|
||||||
return EmployeeAdvance::query()
|
return EmployeeAdvance::query()
|
||||||
->select(['id', 'employee_id', 'amount', 'paid_amount', 'description', 'due_date', 'status', 'created_at'])
|
->select(['id', 'employee_id', 'amount', 'paid_amount', 'description', 'due_date', 'status', 'created_at'])
|
||||||
->with(['employee.user.userProfile'])
|
->with(['employee.user.userProfile', 'payments.paidBy.userProfile'])
|
||||||
->when(! $this->canViewAll(), fn ($q) => $q->whereHas('employee', fn ($eq) => $eq->where('user_id', auth()->id())))
|
->when(! $this->canViewAll(), fn ($q) => $q->whereHas('employee', fn ($eq) => $eq->where('user_id', auth()->id())))
|
||||||
->when($filters['status'] ?? null, fn ($q) => $q->where('status', $filters['status']))
|
->when($filters['status'] ?? null, fn ($q) => $q->where('status', $filters['status']))
|
||||||
->when($search, fn ($q) => $q->where('description', 'like', "%{$search}%"))
|
->when($search, fn ($q) => $q->where('description', 'like', "%{$search}%"))
|
||||||
@ -117,29 +119,51 @@ public function approve(EmployeeAdvance $employeeAdvance): EmployeeAdvance
|
|||||||
return $employeeAdvance;
|
return $employeeAdvance;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function pay(EmployeeAdvance $employeeAdvance): EmployeeAdvance
|
public function pay(EmployeeAdvance $employeeAdvance, int $amount): EmployeeAdvance
|
||||||
{
|
{
|
||||||
$employeeAdvance = DB::transaction(function () use ($employeeAdvance) {
|
$remaining = $employeeAdvance->amount - $employeeAdvance->paid_amount;
|
||||||
|
|
||||||
|
if ($amount > $remaining) {
|
||||||
|
throw ValidationException::withMessages([
|
||||||
|
'amount' => 'Jumlah bayar melebihi sisa kasbon.',
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
$employeeAdvance = DB::transaction(function () use ($employeeAdvance, $amount) {
|
||||||
$cashTransaction = $this->creditCash(
|
$cashTransaction = $this->creditCash(
|
||||||
amount: $employeeAdvance->amount,
|
amount: $amount,
|
||||||
description: 'Pembayaran kasbon: '.$employeeAdvance->description,
|
description: 'Pembayaran kasbon: '.$employeeAdvance->description,
|
||||||
);
|
);
|
||||||
|
|
||||||
$employeeAdvance->update([
|
EmployeeAdvancePayment::create([
|
||||||
'status' => EmployeeAdvanceStatus::PAID,
|
'employee_advance_id' => $employeeAdvance->id,
|
||||||
'paid_by_id' => auth()->id(),
|
'paid_by_id' => auth()->id(),
|
||||||
'paid_amount' => $employeeAdvance->amount,
|
'cash_transaction_id' => $cashTransaction->id,
|
||||||
|
'amount' => $amount,
|
||||||
'paid_at' => now(),
|
'paid_at' => now(),
|
||||||
'repayment_cash_transaction_id' => $cashTransaction->id,
|
]);
|
||||||
|
|
||||||
|
$newPaidAmount = $employeeAdvance->paid_amount + $amount;
|
||||||
|
$isFullyPaid = $newPaidAmount >= $employeeAdvance->amount;
|
||||||
|
|
||||||
|
$employeeAdvance->update([
|
||||||
|
'paid_amount' => $newPaidAmount,
|
||||||
|
'status' => $isFullyPaid ? EmployeeAdvanceStatus::PAID : $employeeAdvance->status,
|
||||||
|
'paid_by_id' => $isFullyPaid ? auth()->id() : $employeeAdvance->paid_by_id,
|
||||||
|
'paid_at' => $isFullyPaid ? now() : $employeeAdvance->paid_at,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
return $employeeAdvance;
|
return $employeeAdvance;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
$notificationBody = $employeeAdvance->status === EmployeeAdvanceStatus::PAID
|
||||||
|
? 'Kasbon sebesar Rp '.number_format($employeeAdvance->amount, 0, ',', '.').' telah dibayar lunas'.' oleh '.auth()->user()->full_name.'.'
|
||||||
|
: 'Pembayaran kasbon sebesar Rp '.number_format($amount, 0, ',', '.').' oleh '.auth()->user()->full_name.'. Sisa: Rp '.number_format($employeeAdvance->amount - $employeeAdvance->paid_amount, 0, ',', '.').'.';
|
||||||
|
|
||||||
NotificationService::notify(
|
NotificationService::notify(
|
||||||
roles: ['Owner', 'Developer', 'Direktur', 'Admin Toko'],
|
roles: ['Owner', 'Developer', 'Direktur', 'Admin Toko'],
|
||||||
title: 'Kasbon Dibayar',
|
title: $employeeAdvance->status === EmployeeAdvanceStatus::PAID ? 'Kasbon Dibayar' : 'Pembayaran Kasbon',
|
||||||
body: 'Kasbon sebesar Rp '.number_format($employeeAdvance->amount, 0, ',', '.').' telah dibayar lunas'.' oleh '.auth()->user()->full_name.'.',
|
body: $notificationBody,
|
||||||
url: route('admin.finance.employee-advances.index'),
|
url: route('admin.finance.employee-advances.index'),
|
||||||
additionalUser: $employeeAdvance->employee->user ?? null,
|
additionalUser: $employeeAdvance->employee->user ?? null,
|
||||||
);
|
);
|
||||||
|
|||||||
@ -1,7 +1,21 @@
|
|||||||
import type { ColumnDef } from '@tanstack/react-table';
|
|
||||||
import { CheckCircle, CircleDollarSign, Pencil, Trash2 } from 'lucide-react';
|
|
||||||
import { RowActions } from '@/components/row-actions';
|
import { RowActions } from '@/components/row-actions';
|
||||||
import { Badge } from '@/components/ui/badge';
|
import { Badge } from '@/components/ui/badge';
|
||||||
|
import type { ColumnDef } from '@tanstack/react-table';
|
||||||
|
import { CheckCircle, CircleDollarSign, History, Pencil, Trash2 } from 'lucide-react';
|
||||||
|
|
||||||
|
export type EmployeeAdvancePayment = {
|
||||||
|
id: number;
|
||||||
|
amount: number;
|
||||||
|
formatted_amount: string;
|
||||||
|
description: string | null;
|
||||||
|
paid_at: string;
|
||||||
|
formatted_paid_at: string;
|
||||||
|
paid_by: {
|
||||||
|
user_profile: {
|
||||||
|
full_name: string;
|
||||||
|
};
|
||||||
|
} | null;
|
||||||
|
};
|
||||||
|
|
||||||
export type EmployeeAdvance = {
|
export type EmployeeAdvance = {
|
||||||
id: number;
|
id: number;
|
||||||
@ -9,6 +23,8 @@ export type EmployeeAdvance = {
|
|||||||
formatted_amount: string;
|
formatted_amount: string;
|
||||||
paid_amount: number;
|
paid_amount: number;
|
||||||
formatted_paid_amount: string;
|
formatted_paid_amount: string;
|
||||||
|
remaining_amount: number;
|
||||||
|
formatted_remaining_amount: string;
|
||||||
description: string;
|
description: string;
|
||||||
due_date: string;
|
due_date: string;
|
||||||
formatted_due_date: string;
|
formatted_due_date: string;
|
||||||
@ -23,6 +39,7 @@ export type EmployeeAdvance = {
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
payments: EmployeeAdvancePayment[];
|
||||||
};
|
};
|
||||||
|
|
||||||
function getStatusBadge(status: string) {
|
function getStatusBadge(status: string) {
|
||||||
@ -63,6 +80,7 @@ type CreateColumnsParams = {
|
|||||||
handleDeleteClick: (employeeAdvance: EmployeeAdvance) => void;
|
handleDeleteClick: (employeeAdvance: EmployeeAdvance) => void;
|
||||||
handleApprove: (employeeAdvance: EmployeeAdvance) => void;
|
handleApprove: (employeeAdvance: EmployeeAdvance) => void;
|
||||||
handlePay: (employeeAdvance: EmployeeAdvance) => void;
|
handlePay: (employeeAdvance: EmployeeAdvance) => void;
|
||||||
|
handleShowPayments: (employeeAdvance: EmployeeAdvance) => void;
|
||||||
can: (permission: string) => boolean;
|
can: (permission: string) => boolean;
|
||||||
authUserId: number;
|
authUserId: number;
|
||||||
};
|
};
|
||||||
@ -70,7 +88,7 @@ type CreateColumnsParams = {
|
|||||||
export function createEmployeeAdvanceColumns(
|
export function createEmployeeAdvanceColumns(
|
||||||
params: CreateColumnsParams,
|
params: CreateColumnsParams,
|
||||||
): ColumnDef<EmployeeAdvance>[] {
|
): ColumnDef<EmployeeAdvance>[] {
|
||||||
const { handleEdit, handleDeleteClick, handleApprove, handlePay, can, authUserId } =
|
const { handleEdit, handleDeleteClick, handleApprove, handlePay, handleShowPayments, can, authUserId } =
|
||||||
params;
|
params;
|
||||||
|
|
||||||
return [
|
return [
|
||||||
@ -98,11 +116,27 @@ export function createEmployeeAdvanceColumns(
|
|||||||
accessorKey: 'formatted_amount',
|
accessorKey: 'formatted_amount',
|
||||||
header: () => <span>Jumlah</span>,
|
header: () => <span>Jumlah</span>,
|
||||||
cell: ({ row }) => (
|
cell: ({ row }) => (
|
||||||
<span className="font-medium text-red-600">
|
<span className="font-medium">
|
||||||
- {row.getValue('formatted_amount') as string}
|
{row.getValue('formatted_amount') as string}
|
||||||
</span>
|
</span>
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
accessorKey: 'formatted_remaining_amount',
|
||||||
|
header: () => <span>Sisa</span>,
|
||||||
|
cell: ({ row }) => {
|
||||||
|
const employeeAdvance = row.original;
|
||||||
|
if (employeeAdvance.status === 'paid') {
|
||||||
|
return <span className="text-green-600">Lunas</span>;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<span className="font-medium text-orange-600">
|
||||||
|
{row.getValue('formatted_remaining_amount') as string}
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
},
|
||||||
{
|
{
|
||||||
accessorKey: 'description',
|
accessorKey: 'description',
|
||||||
header: () => <span>Keterangan</span>,
|
header: () => <span>Keterangan</span>,
|
||||||
@ -161,6 +195,12 @@ export function createEmployeeAdvanceColumns(
|
|||||||
employeeAdvance.status === 'approved',
|
employeeAdvance.status === 'approved',
|
||||||
onClick: () => handlePay(employeeAdvance),
|
onClick: () => handlePay(employeeAdvance),
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
label: 'Riwayat',
|
||||||
|
icon: <History className="h-4 w-4" />,
|
||||||
|
show: employeeAdvance.payments.length > 0,
|
||||||
|
onClick: () => handleShowPayments(employeeAdvance),
|
||||||
|
},
|
||||||
{
|
{
|
||||||
label: 'Edit',
|
label: 'Edit',
|
||||||
icon: <Pencil className="h-4 w-4" />,
|
icon: <Pencil className="h-4 w-4" />,
|
||||||
|
|||||||
@ -8,8 +8,16 @@ import InputError from '@/components/input-error';
|
|||||||
import { PageHeader } from '@/components/page-header';
|
import { PageHeader } from '@/components/page-header';
|
||||||
import { RupiahInput } from '@/components/rupiah-input';
|
import { RupiahInput } from '@/components/rupiah-input';
|
||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
|
import {
|
||||||
|
Dialog,
|
||||||
|
DialogContent,
|
||||||
|
DialogDescription,
|
||||||
|
DialogHeader,
|
||||||
|
DialogTitle,
|
||||||
|
} from '@/components/ui/dialog';
|
||||||
import { Input } from '@/components/ui/input';
|
import { Input } from '@/components/ui/input';
|
||||||
import { Label } from '@/components/ui/label';
|
import { Label } from '@/components/ui/label';
|
||||||
|
import { Separator } from '@/components/ui/separator';
|
||||||
import {
|
import {
|
||||||
Select,
|
Select,
|
||||||
SelectContent,
|
SelectContent,
|
||||||
@ -66,6 +74,7 @@ export default function EmployeeAdvanceIndex({
|
|||||||
const [deleting, setDeleting] = useState<EmployeeAdvance | null>(null);
|
const [deleting, setDeleting] = useState<EmployeeAdvance | null>(null);
|
||||||
const [approving, setApproving] = useState<EmployeeAdvance | null>(null);
|
const [approving, setApproving] = useState<EmployeeAdvance | null>(null);
|
||||||
const [paying, setPaying] = useState<EmployeeAdvance | null>(null);
|
const [paying, setPaying] = useState<EmployeeAdvance | null>(null);
|
||||||
|
const [viewingPayments, setViewingPayments] = useState<EmployeeAdvance | null>(null);
|
||||||
const [dueDate, setDueDate] = useState<Date | undefined>(undefined);
|
const [dueDate, setDueDate] = useState<Date | undefined>(undefined);
|
||||||
const [editingDueDate, setEditingDueDate] = useState<Date | undefined>(
|
const [editingDueDate, setEditingDueDate] = useState<Date | undefined>(
|
||||||
undefined,
|
undefined,
|
||||||
@ -125,25 +134,12 @@ export default function EmployeeAdvanceIndex({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function handlePay() {
|
|
||||||
if (!paying) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
router.post(
|
|
||||||
pay(paying.id),
|
|
||||||
{},
|
|
||||||
{
|
|
||||||
onSuccess: () => setPaying(null),
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const columns = createEmployeeAdvanceColumns({
|
const columns = createEmployeeAdvanceColumns({
|
||||||
handleEdit: (employeeAdvance) => setEditing(employeeAdvance),
|
handleEdit: (employeeAdvance) => setEditing(employeeAdvance),
|
||||||
handleDeleteClick: (employeeAdvance) => setDeleting(employeeAdvance),
|
handleDeleteClick: (employeeAdvance) => setDeleting(employeeAdvance),
|
||||||
handleApprove: (employeeAdvance) => setApproving(employeeAdvance),
|
handleApprove: (employeeAdvance) => setApproving(employeeAdvance),
|
||||||
handlePay: (employeeAdvance) => setPaying(employeeAdvance),
|
handlePay: (employeeAdvance) => setPaying(employeeAdvance),
|
||||||
|
handleShowPayments: (employeeAdvance) => setViewingPayments(employeeAdvance),
|
||||||
can,
|
can,
|
||||||
authUserId: auth.user.id,
|
authUserId: auth.user.id,
|
||||||
});
|
});
|
||||||
@ -383,20 +379,128 @@ export default function EmployeeAdvanceIndex({
|
|||||||
onConfirm={handleApprove}
|
onConfirm={handleApprove}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<DeleteConfirmDialog
|
<FormDialog
|
||||||
target={paying}
|
open={paying !== null}
|
||||||
onOpenChange={(open) => {
|
onOpenChange={(open) => {
|
||||||
if (!open) {
|
if (!open) {
|
||||||
setPaying(null);
|
setPaying(null);
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
title="Bayar Kasbon"
|
title="Bayar Kasbon"
|
||||||
description={(advance) =>
|
action={paying ? pay(paying.id) : ''}
|
||||||
`Apakah Anda yakin ingin membayar kasbon "${advance.description}" sebesar ${advance.formatted_amount}? Saldo kas akan dikembalikan.`
|
resetOnSuccess
|
||||||
|
onSuccess={() => setPaying(null)}
|
||||||
|
>
|
||||||
|
{({ errors }) =>
|
||||||
|
paying && (
|
||||||
|
<>
|
||||||
|
<div className="mb-4 rounded-md bg-muted p-3 text-sm">
|
||||||
|
<p>
|
||||||
|
Sisa:{' '}
|
||||||
|
<span className="font-medium">
|
||||||
|
{paying.formatted_remaining_amount}
|
||||||
|
</span>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div className="grid gap-2">
|
||||||
|
<Label>
|
||||||
|
Jumlah Bayar{' '}
|
||||||
|
<span className="text-destructive">
|
||||||
|
*
|
||||||
|
</span>
|
||||||
|
</Label>
|
||||||
|
<RupiahInput
|
||||||
|
name="amount"
|
||||||
|
defaultValue={paying.remaining_amount}
|
||||||
|
/>
|
||||||
|
<InputError message={errors.amount} />
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)
|
||||||
}
|
}
|
||||||
confirmLabel="Bayar"
|
</FormDialog>
|
||||||
onConfirm={handlePay}
|
|
||||||
/>
|
<Dialog
|
||||||
|
open={viewingPayments !== null}
|
||||||
|
onOpenChange={(open) => {
|
||||||
|
if (!open) {
|
||||||
|
setViewingPayments(null);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<DialogContent className="sm:max-w-md">
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle>Riwayat Pembayaran</DialogTitle>
|
||||||
|
<DialogDescription>
|
||||||
|
{viewingPayments?.description}
|
||||||
|
</DialogDescription>
|
||||||
|
</DialogHeader>
|
||||||
|
{viewingPayments && (
|
||||||
|
<div className="space-y-4">
|
||||||
|
<div className="flex items-center justify-between text-sm">
|
||||||
|
<span className="text-muted-foreground">
|
||||||
|
Total:
|
||||||
|
</span>
|
||||||
|
<span className="font-medium">
|
||||||
|
{viewingPayments.formatted_amount}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center justify-between text-sm">
|
||||||
|
<span className="text-muted-foreground">
|
||||||
|
Terbayar:
|
||||||
|
</span>
|
||||||
|
<span className="font-medium text-green-600">
|
||||||
|
{viewingPayments.formatted_paid_amount}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center justify-between text-sm">
|
||||||
|
<span className="text-muted-foreground">
|
||||||
|
Sisa:
|
||||||
|
</span>
|
||||||
|
<span className="font-medium text-orange-600">
|
||||||
|
{viewingPayments.formatted_remaining_amount}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Separator />
|
||||||
|
|
||||||
|
{viewingPayments.payments.length === 0 ? (
|
||||||
|
<p className="py-4 text-center text-sm text-muted-foreground">
|
||||||
|
Belum ada pembayaran.
|
||||||
|
</p>
|
||||||
|
) : (
|
||||||
|
<div className="space-y-3">
|
||||||
|
{viewingPayments.payments.map(
|
||||||
|
(payment) => (
|
||||||
|
<div
|
||||||
|
key={payment.id}
|
||||||
|
className="rounded-md border p-3"
|
||||||
|
>
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<span className="text-sm font-medium text-green-600">
|
||||||
|
{payment.formatted_amount}
|
||||||
|
</span>
|
||||||
|
<span className="text-xs text-muted-foreground">
|
||||||
|
{payment.formatted_paid_at}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
{payment.paid_by && (
|
||||||
|
<p className="mt-1 text-xs text-muted-foreground">
|
||||||
|
oleh{' '}
|
||||||
|
{payment.paid_by
|
||||||
|
.user_profile
|
||||||
|
.full_name ?? '-'}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user