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