feat: implement cash transaction management with deposit and withdrawal requests, enhance UI components for better user experience
This commit is contained in:
parent
256b13ad73
commit
802c4e36c0
@ -5,9 +5,9 @@
|
||||
use App\Http\Controllers\Concerns\FlashesEntityMessage;
|
||||
use App\Http\Controllers\Concerns\ParsesDataTableQuery;
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Http\Requests\Admin\Finance\DepositCashRequest;
|
||||
use App\Http\Requests\Admin\Finance\Cash\DepositRequest;
|
||||
use App\Http\Requests\Admin\Finance\Cash\WithdrawRequest;
|
||||
use App\Http\Requests\Admin\Finance\UpdateCashTransactionRequest;
|
||||
use App\Http\Requests\Admin\Finance\WithdrawCashRequest;
|
||||
use App\Models\CashTransaction;
|
||||
use App\Services\Finance\CashService;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
@ -26,16 +26,19 @@ public function __construct(
|
||||
public function index(Request $request): Response
|
||||
{
|
||||
$tableQuery = $this->parseDataTableQuery($request);
|
||||
$referenceType = $request->string('reference_type')->toString();
|
||||
$cashAccount = $this->cashService->getDefaultAccount();
|
||||
|
||||
return Inertia::render('admin/finance/cash/Index', [
|
||||
'cashAccount' => $cashAccount,
|
||||
'transactions' => $this->cashService->paginateForIndex($cashAccount, $tableQuery),
|
||||
'filters' => $this->dataTableFilters($tableQuery),
|
||||
'transactions' => $this->cashService->paginateForIndex($cashAccount, $tableQuery, $referenceType),
|
||||
'filters' => $this->dataTableFilters($tableQuery, [
|
||||
'reference_type' => $referenceType,
|
||||
]),
|
||||
]);
|
||||
}
|
||||
|
||||
public function deposit(DepositCashRequest $request): RedirectResponse
|
||||
public function deposit(DepositRequest $request): RedirectResponse
|
||||
{
|
||||
$cashAccount = $this->cashService->getDefaultAccount();
|
||||
|
||||
@ -46,7 +49,7 @@ public function deposit(DepositCashRequest $request): RedirectResponse
|
||||
return redirect()->route('admin.finance.cash.index');
|
||||
}
|
||||
|
||||
public function withdraw(WithdrawCashRequest $request): RedirectResponse
|
||||
public function withdraw(WithdrawRequest $request): RedirectResponse
|
||||
{
|
||||
$cashAccount = $this->cashService->getDefaultAccount();
|
||||
|
||||
|
||||
@ -1,12 +1,12 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Requests\Admin\Finance;
|
||||
namespace App\Http\Requests\Admin\Finance\Cash;
|
||||
|
||||
use App\Enums\Permission;
|
||||
use App\Http\Requests\Concerns\ValidatesMediaUploads;
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
|
||||
class DepositCashRequest extends FormRequest
|
||||
class DepositRequest extends FormRequest
|
||||
{
|
||||
use ValidatesMediaUploads;
|
||||
|
||||
@ -1,12 +1,12 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Requests\Admin\Finance;
|
||||
namespace App\Http\Requests\Admin\Finance\Cash;
|
||||
|
||||
use App\Enums\Permission;
|
||||
use App\Http\Requests\Concerns\ValidatesMediaUploads;
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
|
||||
class WithdrawCashRequest extends FormRequest
|
||||
class WithdrawRequest extends FormRequest
|
||||
{
|
||||
use ValidatesMediaUploads;
|
||||
|
||||
@ -21,6 +21,7 @@
|
||||
'balance_after_formatted',
|
||||
'reference_label',
|
||||
'is_incoming',
|
||||
'source_badge_class',
|
||||
'created_at_formatted',
|
||||
'created_by_name',
|
||||
])]
|
||||
@ -93,6 +94,20 @@ public function isIncoming(): Attribute
|
||||
);
|
||||
}
|
||||
|
||||
public function sourceBadgeClass(): Attribute
|
||||
{
|
||||
return Attribute::make(
|
||||
get: fn () => match ($this->reference_type) {
|
||||
null => 'bg-blue-100 text-blue-700 hover:bg-blue-100',
|
||||
Expense::class => 'bg-red-100 text-red-700 hover:bg-red-100',
|
||||
EmployeeAdvance::class => 'bg-amber-100 text-amber-700 hover:bg-amber-100',
|
||||
Payroll::class => 'bg-green-100 text-green-700 hover:bg-green-100',
|
||||
Order::class => 'bg-purple-100 text-purple-700 hover:bg-purple-100',
|
||||
default => 'bg-gray-100 text-gray-700 hover:bg-gray-100',
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
public function referenceLabel(): Attribute
|
||||
{
|
||||
return Attribute::make(
|
||||
|
||||
@ -32,7 +32,7 @@ public function getDefaultAccount(): CashAccount
|
||||
/**
|
||||
* @param array{search: string, sort: string, direction: 'asc'|'desc'} $tableQuery
|
||||
*/
|
||||
public function paginateForIndex(CashAccount $cashAccount, array $tableQuery): LengthAwarePaginator
|
||||
public function paginateForIndex(CashAccount $cashAccount, array $tableQuery, string $referenceType = ''): LengthAwarePaginator
|
||||
{
|
||||
$query = CashTransaction::query()
|
||||
->with(['createdBy.profile', 'reference', 'media'])
|
||||
@ -42,6 +42,13 @@ public function paginateForIndex(CashAccount $cashAccount, array $tableQuery): L
|
||||
$query->where(function (Builder $query) use ($search): void {
|
||||
$query->where('description', 'like', "%{$search}%");
|
||||
});
|
||||
})
|
||||
->when($referenceType !== '', function (Builder $query) use ($referenceType): void {
|
||||
if ($referenceType === 'manual') {
|
||||
$query->whereNull('reference_type');
|
||||
} else {
|
||||
$query->where('reference_type', $referenceType);
|
||||
}
|
||||
});
|
||||
|
||||
$this->applySorting($query, $tableQuery['sort'], $tableQuery['direction']);
|
||||
|
||||
@ -2,8 +2,6 @@
|
||||
import { Head } from '@inertiajs/vue3';
|
||||
import { ArrowDownCircle, ArrowUpCircle, Wallet } from '@lucide/vue';
|
||||
import { computed, ref, watch } from 'vue';
|
||||
import CashTransactionFormModal from '@/components/admin/finance/cash/CashTransactionFormModal.vue';
|
||||
import { createColumns } from '@/components/admin/finance/cash/columns';
|
||||
import { DataTable } from '@/components/data-table';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
||||
@ -11,7 +9,9 @@ import { useCan } from '@/composables/useCan';
|
||||
import { useDataTableQuery, useDataTableQuerySync } from '@/composables/useDataTableQuery';
|
||||
import AdminLayout from '@/layouts/AdminLayout.vue';
|
||||
import type { CashAccount, CashTransactionListItem, PaginatedCashTransactions } from '@/types/cash';
|
||||
import type { DataTableSort } from '@/types/data-table';
|
||||
import type { DataTableFilterDef, DataTableSort } from '@/types/data-table';
|
||||
import CashTransactionFormModal from './form/CashTransactionFormModal.vue';
|
||||
import { createColumns } from './table/columns';
|
||||
|
||||
const props = defineProps<{
|
||||
cashAccount: CashAccount;
|
||||
@ -20,6 +20,7 @@ const props = defineProps<{
|
||||
search: string;
|
||||
sort?: string;
|
||||
direction?: 'asc' | 'desc';
|
||||
reference_type?: string;
|
||||
};
|
||||
}>();
|
||||
|
||||
@ -29,9 +30,10 @@ const formModalOpen = ref(false);
|
||||
const formModalMode = ref<'deposit' | 'withdrawal'>('deposit');
|
||||
const editingTransaction = ref<CashTransactionListItem | null>(null);
|
||||
|
||||
const { query, setSearch, setSort, resetFilters, syncFromServer } = useDataTableQuery({
|
||||
const { query, setSearch, setSort, setFilter, resetFilters, syncFromServer } = useDataTableQuery({
|
||||
url: '/admin/finance/cash',
|
||||
initial: { ...props.filters },
|
||||
filterKeys: ['reference_type'],
|
||||
});
|
||||
|
||||
useDataTableQuerySync(() => props.filters, syncFromServer);
|
||||
@ -49,6 +51,25 @@ const currentSort = computed<DataTableSort | null>(() => {
|
||||
};
|
||||
});
|
||||
|
||||
const filterDefs = computed<DataTableFilterDef[]>(() => [
|
||||
{
|
||||
key: 'reference_type',
|
||||
label: 'Sumber',
|
||||
type: 'select',
|
||||
options: [
|
||||
{ value: 'manual', label: 'Setor/Tarik Kas' },
|
||||
{ value: 'App\\Models\\Expense', label: 'Pengeluaran' },
|
||||
{ value: 'App\\Models\\EmployeeAdvance', label: 'Kasbon Pegawai' },
|
||||
{ value: 'App\\Models\\Payroll', label: 'Gaji Pegawai' },
|
||||
{ value: 'App\\Models\\Order', label: 'Pesanan' },
|
||||
],
|
||||
},
|
||||
]);
|
||||
|
||||
const filterValues = computed(() => ({
|
||||
reference_type: query.value.reference_type ?? '',
|
||||
}));
|
||||
|
||||
const pagination = computed(() => ({
|
||||
currentPage: props.transactions.current_page,
|
||||
perPage: props.transactions.per_page,
|
||||
@ -119,17 +140,14 @@ watch(
|
||||
|
||||
<Card class="min-w-0">
|
||||
<CardContent class="min-w-0 pt-6">
|
||||
<DataTable v-model:search="search" :columns="columns" :data="transactions.data"
|
||||
:pagination="pagination" :pagination-links="transactions.links" :sort="currentSort"
|
||||
@sort-change="setSort" @filters-reset="resetFilters" />
|
||||
<DataTable v-model:search="search" :columns="columns" :data="transactions.data" :pagination="pagination"
|
||||
:pagination-links="transactions.links" :sort="currentSort" :filter-defs="filterDefs"
|
||||
:filter-values="filterValues" @sort-change="setSort" @filter-change="setFilter"
|
||||
@filters-reset="resetFilters" />
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<CashTransactionFormModal
|
||||
v-if="can('cash.deposit') || can('cash.withdraw') || can('cash.update')"
|
||||
v-model:open="formModalOpen"
|
||||
:transaction="editingTransaction"
|
||||
:mode="formModalMode"
|
||||
/>
|
||||
<CashTransactionFormModal v-if="can('cash.deposit') || can('cash.withdraw') || can('cash.update')"
|
||||
v-model:open="formModalOpen" :transaction="editingTransaction" :mode="formModalMode" />
|
||||
</AdminLayout>
|
||||
</template>
|
||||
|
||||
@ -168,9 +168,8 @@ const placeholder = computed(() =>
|
||||
</Field>
|
||||
<Field>
|
||||
<FieldLabel for="cash-description" required>Keterangan</FieldLabel>
|
||||
<Textarea id="cash-description" v-model="form.description"
|
||||
:placeholder="placeholder" rows="3"
|
||||
:maxlength="FIELD_LIMITS.description" />
|
||||
<Textarea id="cash-description" v-model="form.description" :placeholder="placeholder"
|
||||
rows="3" :maxlength="FIELD_LIMITS.description" />
|
||||
<FieldError :errors="formErrors(form, 'description')" />
|
||||
</Field>
|
||||
<ImageUploadField id="cash-photos" v-model="photoFile" label="Foto Bukti" required
|
||||
@ -1,10 +1,10 @@
|
||||
import type { ColumnDef } from '@tanstack/vue-table';
|
||||
import { h } from 'vue';
|
||||
import DataTableActions from '@/components/admin/finance/cash/data-table-actions.vue';
|
||||
import { DataTableColumnHeader } from '@/components/data-table';
|
||||
import MediaThumbnailCell from '@/components/media/MediaThumbnailCell.vue';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import type { CashTransactionListItem } from '@/types/cash';
|
||||
import DataTableActions from './data-table-actions.vue';
|
||||
|
||||
export function createColumns(onEdit: (transaction: CashTransactionListItem) => void): ColumnDef<CashTransactionListItem>[] {
|
||||
return [
|
||||
@ -17,15 +17,11 @@ export function createColumns(onEdit: (transaction: CashTransactionListItem) =>
|
||||
accessorKey: 'reference_label',
|
||||
enableSorting: true,
|
||||
header: () => h(DataTableColumnHeader, { title: 'Sumber', column: 'reference_type' }),
|
||||
cell: ({ row }) => {
|
||||
const isIncoming = row.original.is_incoming;
|
||||
|
||||
return h(
|
||||
Badge,
|
||||
{ variant: isIncoming ? 'default' : 'secondary' },
|
||||
() => row.original.reference_label,
|
||||
);
|
||||
},
|
||||
cell: ({ row }) => h(
|
||||
Badge,
|
||||
{ class: row.original.source_badge_class },
|
||||
() => row.original.reference_label,
|
||||
),
|
||||
},
|
||||
{
|
||||
accessorKey: 'amount_formatted',
|
||||
@ -13,6 +13,7 @@ export type CashTransactionListItem = {
|
||||
reference_type: string | null;
|
||||
reference_label: string;
|
||||
is_incoming: boolean;
|
||||
source_badge_class: string;
|
||||
amount: number;
|
||||
amount_formatted: string;
|
||||
balance_after: number;
|
||||
|
||||
Loading…
Reference in New Issue
Block a user