Add customer management features including Customer model, CustomerController for CRUD operations, and CustomerRequest for validation. Integrate permissions in the Permission and Role enums, and update routes for customer management. Enhance UI components for customer listing and management, including a data table and form modal. Update sidebar for customer navigation.
This commit is contained in:
parent
c21f71563f
commit
426d7ed61c
@ -27,6 +27,11 @@ enum Permission: string
|
||||
case SUPPLIERS_UPDATE = 'suppliers.update';
|
||||
case SUPPLIERS_DELETE = 'suppliers.delete';
|
||||
|
||||
case CUSTOMERS_VIEW = 'customers.view';
|
||||
case CUSTOMERS_CREATE = 'customers.create';
|
||||
case CUSTOMERS_UPDATE = 'customers.update';
|
||||
case CUSTOMERS_DELETE = 'customers.delete';
|
||||
|
||||
case PRODUCTS_VIEW = 'products.view';
|
||||
case PRODUCTS_CREATE = 'products.create';
|
||||
case PRODUCTS_UPDATE = 'products.update';
|
||||
@ -61,6 +66,11 @@ public function label(): string
|
||||
self::SUPPLIERS_UPDATE => 'Ubah Supplier',
|
||||
self::SUPPLIERS_DELETE => 'Hapus Supplier',
|
||||
|
||||
self::CUSTOMERS_VIEW => 'Lihat Pelanggan',
|
||||
self::CUSTOMERS_CREATE => 'Tambah Pelanggan',
|
||||
self::CUSTOMERS_UPDATE => 'Ubah Pelanggan',
|
||||
self::CUSTOMERS_DELETE => 'Hapus Pelanggan',
|
||||
|
||||
self::PRODUCTS_VIEW => 'Lihat Produk',
|
||||
self::PRODUCTS_CREATE => 'Tambah Produk',
|
||||
self::PRODUCTS_UPDATE => 'Ubah Produk',
|
||||
@ -85,6 +95,8 @@ public function group(): string
|
||||
self::CATEGORIES_DELETE => 'Kategori',
|
||||
self::SUPPLIERS_VIEW, self::SUPPLIERS_CREATE, self::SUPPLIERS_UPDATE,
|
||||
self::SUPPLIERS_DELETE => 'Supplier',
|
||||
self::CUSTOMERS_VIEW, self::CUSTOMERS_CREATE, self::CUSTOMERS_UPDATE,
|
||||
self::CUSTOMERS_DELETE => 'Pelanggan',
|
||||
self::PRODUCTS_VIEW, self::PRODUCTS_CREATE, self::PRODUCTS_UPDATE,
|
||||
self::PRODUCTS_DELETE, self::PRODUCTS_TOGGLE_STATUS => 'Produk',
|
||||
self::RAW_MATERIALS_VIEW, self::RAW_MATERIALS_CREATE, self::RAW_MATERIALS_UPDATE,
|
||||
|
||||
@ -52,6 +52,10 @@ public function permissions(): array
|
||||
Permission::SUPPLIERS_CREATE,
|
||||
Permission::SUPPLIERS_UPDATE,
|
||||
Permission::SUPPLIERS_DELETE,
|
||||
Permission::CUSTOMERS_VIEW,
|
||||
Permission::CUSTOMERS_CREATE,
|
||||
Permission::CUSTOMERS_UPDATE,
|
||||
Permission::CUSTOMERS_DELETE,
|
||||
Permission::PRODUCTS_VIEW,
|
||||
Permission::PRODUCTS_CREATE,
|
||||
Permission::PRODUCTS_UPDATE,
|
||||
@ -78,6 +82,10 @@ public function permissions(): array
|
||||
Permission::SUPPLIERS_CREATE,
|
||||
Permission::SUPPLIERS_UPDATE,
|
||||
Permission::SUPPLIERS_DELETE,
|
||||
Permission::CUSTOMERS_VIEW,
|
||||
Permission::CUSTOMERS_CREATE,
|
||||
Permission::CUSTOMERS_UPDATE,
|
||||
Permission::CUSTOMERS_DELETE,
|
||||
Permission::PRODUCTS_VIEW,
|
||||
Permission::PRODUCTS_CREATE,
|
||||
Permission::PRODUCTS_UPDATE,
|
||||
@ -112,6 +120,10 @@ public function permissions(): array
|
||||
Permission::SUPPLIERS_CREATE,
|
||||
Permission::SUPPLIERS_UPDATE,
|
||||
Permission::SUPPLIERS_DELETE,
|
||||
Permission::CUSTOMERS_VIEW,
|
||||
Permission::CUSTOMERS_CREATE,
|
||||
Permission::CUSTOMERS_UPDATE,
|
||||
Permission::CUSTOMERS_DELETE,
|
||||
Permission::PRODUCTS_VIEW,
|
||||
],
|
||||
self::NON_OPERATOR => [
|
||||
|
||||
59
app/Http/Controllers/Admin/Master/CustomerController.php
Normal file
59
app/Http/Controllers/Admin/Master/CustomerController.php
Normal file
@ -0,0 +1,59 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Admin\Master;
|
||||
|
||||
use App\Http\Controllers\Concerns\ParsesDataTableQuery;
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Http\Requests\Admin\Master\CustomerRequest;
|
||||
use App\Models\Customer;
|
||||
use App\Services\Master\CustomerService;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Inertia\Inertia;
|
||||
use Inertia\Response;
|
||||
|
||||
class CustomerController extends Controller
|
||||
{
|
||||
use ParsesDataTableQuery;
|
||||
|
||||
public function __construct(
|
||||
private readonly CustomerService $customerService,
|
||||
) {}
|
||||
|
||||
public function index(Request $request): Response
|
||||
{
|
||||
$tableQuery = $this->parseDataTableQuery($request);
|
||||
|
||||
return Inertia::render('admin/master/customers/Index', [
|
||||
'customers' => $this->customerService->paginateForIndex($tableQuery),
|
||||
'filters' => $this->dataTableFilters($tableQuery),
|
||||
]);
|
||||
}
|
||||
|
||||
public function store(CustomerRequest $request): RedirectResponse
|
||||
{
|
||||
$this->customerService->create($request->validated());
|
||||
|
||||
Inertia::flash('success', 'Customer berhasil ditambahkan.');
|
||||
|
||||
return redirect()->route('admin.master.customers.index');
|
||||
}
|
||||
|
||||
public function update(CustomerRequest $request, Customer $customer): RedirectResponse
|
||||
{
|
||||
$this->customerService->update($customer, $request->validated());
|
||||
|
||||
Inertia::flash('success', 'Customer berhasil diperbarui.');
|
||||
|
||||
return redirect()->route('admin.master.customers.index');
|
||||
}
|
||||
|
||||
public function destroy(Customer $customer): RedirectResponse
|
||||
{
|
||||
$this->customerService->delete($customer);
|
||||
|
||||
Inertia::flash('success', 'Customer berhasil dihapus.');
|
||||
|
||||
return redirect()->route('admin.master.customers.index');
|
||||
}
|
||||
}
|
||||
30
app/Http/Requests/Admin/Master/CustomerRequest.php
Normal file
30
app/Http/Requests/Admin/Master/CustomerRequest.php
Normal file
@ -0,0 +1,30 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Requests\Admin\Master;
|
||||
|
||||
use App\Enums\Permission;
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
|
||||
class CustomerRequest extends FormRequest
|
||||
{
|
||||
public function authorize(): bool
|
||||
{
|
||||
$permission = $this->isMethod('POST')
|
||||
? Permission::CUSTOMERS_CREATE
|
||||
: Permission::CUSTOMERS_UPDATE;
|
||||
|
||||
return $this->user()?->can($permission->value) ?? false;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'name' => ['required', 'string', 'max:200'],
|
||||
'address' => ['required', 'string'],
|
||||
'phone_number' => ['required', 'string', 'max:20'],
|
||||
];
|
||||
}
|
||||
}
|
||||
13
app/Models/Customer.php
Normal file
13
app/Models/Customer.php
Normal file
@ -0,0 +1,13 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Attributes\Guarded;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\SoftDeletes;
|
||||
|
||||
#[Guarded(['id'])]
|
||||
class Customer extends Model
|
||||
{
|
||||
use SoftDeletes;
|
||||
}
|
||||
67
app/Services/Master/CustomerService.php
Normal file
67
app/Services/Master/CustomerService.php
Normal file
@ -0,0 +1,67 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services\Master;
|
||||
|
||||
use App\Models\Customer;
|
||||
use Illuminate\Contracts\Pagination\LengthAwarePaginator;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
|
||||
class CustomerService
|
||||
{
|
||||
/**
|
||||
* @param array{search: string, sort: string, direction: 'asc'|'desc'} $tableQuery
|
||||
*/
|
||||
public function paginateForIndex(array $tableQuery): LengthAwarePaginator
|
||||
{
|
||||
$query = Customer::query()
|
||||
->when($tableQuery['search'] !== '', function (Builder $query) use ($tableQuery): void {
|
||||
$search = $tableQuery['search'];
|
||||
$query->where(function (Builder $query) use ($search): void {
|
||||
$query->where('name', 'like', "%{$search}%")
|
||||
->orWhere('phone_number', 'like', "%{$search}%")
|
||||
->orWhere('address', 'like', "%{$search}%");
|
||||
});
|
||||
});
|
||||
|
||||
$this->applySorting($query, $tableQuery['sort'], $tableQuery['direction']);
|
||||
|
||||
return $query
|
||||
->paginate(10)
|
||||
->withQueryString();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $validated
|
||||
*/
|
||||
public function create(array $validated): void
|
||||
{
|
||||
Customer::create($validated);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $validated
|
||||
*/
|
||||
public function update(Customer $customer, array $validated): void
|
||||
{
|
||||
$customer->name = $validated['name'];
|
||||
$customer->phone_number = $validated['phone_number'];
|
||||
$customer->address = $validated['address'];
|
||||
$customer->save();
|
||||
}
|
||||
|
||||
public function delete(Customer $customer): void
|
||||
{
|
||||
$customer->delete();
|
||||
}
|
||||
|
||||
private function applySorting(Builder $query, string $sort, string $direction): void
|
||||
{
|
||||
if (in_array($sort, ['name', 'phone_number', 'address'], true)) {
|
||||
$query->orderBy($sort, $direction);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$query->latest();
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,34 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('customers', function (Blueprint $table) {
|
||||
$table->id();
|
||||
|
||||
$table->string('name', 200);
|
||||
$table->string('phone_number', 20)->nullable();
|
||||
$table->text('address')->nullable();
|
||||
|
||||
$table->timestamp('created_at')->useCurrent();
|
||||
$table->timestamp('updated_at')->useCurrent()->useCurrentOnUpdate();
|
||||
$table->softDeletes();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('customers');
|
||||
}
|
||||
};
|
||||
@ -1,6 +1,6 @@
|
||||
<script setup lang="ts">
|
||||
import { Link, usePage } from '@inertiajs/vue3';
|
||||
import { FolderTree, Layers, LayoutDashboard, Package, User, Users } from '@lucide/vue';
|
||||
import { FolderTree, Layers, LayoutDashboard, Package, User, UserCheck, Users } from '@lucide/vue';
|
||||
import { computed } from 'vue';
|
||||
import {
|
||||
Sidebar,
|
||||
@ -25,8 +25,10 @@ const isCategoriesActive = computed(() => page.url.startsWith('/admin/master/cat
|
||||
const isProductsActive = computed(() => page.url.startsWith('/admin/master/products'));
|
||||
const isRawMaterialsActive = computed(() => page.url.startsWith('/admin/master/raw-materials'));
|
||||
const isSuppliersActive = computed(() => page.url.startsWith('/admin/master/suppliers'));
|
||||
const isCustomersActive = computed(() => page.url.startsWith('/admin/master/customers'));
|
||||
const showMasterMenu = computed(() => (
|
||||
can('categories.view') || can('products.view') || can('raw-materials.view')
|
||||
|| can('suppliers.view') || can('customers.view')
|
||||
));
|
||||
</script>
|
||||
|
||||
@ -89,7 +91,7 @@ const showMasterMenu = computed(() => (
|
||||
</Link>
|
||||
</SidebarMenuButton>
|
||||
</SidebarMenuItem>
|
||||
<SidebarMenuItem v-if="can('raw-materials.view')">
|
||||
<SidebarMenuItem v-if="can('suppliers.view')">
|
||||
<SidebarMenuButton as-child tooltip="Supplier" :is-active="isSuppliersActive">
|
||||
<Link href="/admin/master/suppliers">
|
||||
<User />
|
||||
@ -97,6 +99,14 @@ const showMasterMenu = computed(() => (
|
||||
</Link>
|
||||
</SidebarMenuButton>
|
||||
</SidebarMenuItem>
|
||||
<SidebarMenuItem v-if="can('customers.view')">
|
||||
<SidebarMenuButton as-child tooltip="Customer" :is-active="isCustomersActive">
|
||||
<Link href="/admin/master/customers">
|
||||
<UserCheck />
|
||||
<span>Customer</span>
|
||||
</Link>
|
||||
</SidebarMenuButton>
|
||||
</SidebarMenuItem>
|
||||
</SidebarMenu>
|
||||
</SidebarGroupContent>
|
||||
</SidebarGroup>
|
||||
|
||||
@ -0,0 +1,134 @@
|
||||
<script setup lang="ts">
|
||||
import { useForm } from '@inertiajs/vue3';
|
||||
import { Save } from '@lucide/vue';
|
||||
import { computed, watch } from 'vue';
|
||||
import { toast } from 'vue-sonner';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from '@/components/ui/dialog';
|
||||
import {
|
||||
Field,
|
||||
FieldError,
|
||||
FieldGroup,
|
||||
FieldLabel,
|
||||
FieldSet,
|
||||
} from '@/components/ui/field';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { PhoneNumberInput } from '@/components/ui/phone-number-input';
|
||||
import { Textarea } from '@/components/ui/textarea';
|
||||
import type { CustomerFormData, CustomerListItem } from '@/types/customer';
|
||||
|
||||
const open = defineModel<boolean>('open', { default: false });
|
||||
|
||||
const props = defineProps<{
|
||||
customer?: CustomerListItem | null;
|
||||
}>();
|
||||
|
||||
const isEditing = computed(() => props.customer != null);
|
||||
|
||||
const form = useForm<CustomerFormData>({
|
||||
name: '',
|
||||
phone_number: '',
|
||||
address: '',
|
||||
});
|
||||
|
||||
function resetForm() {
|
||||
form.reset();
|
||||
form.clearErrors();
|
||||
}
|
||||
|
||||
function populateForm(customer: CustomerListItem | null | undefined) {
|
||||
resetForm();
|
||||
|
||||
if (!customer) {
|
||||
return;
|
||||
}
|
||||
|
||||
form.name = customer.name;
|
||||
form.phone_number = customer.phone_number;
|
||||
form.address = customer.address;
|
||||
}
|
||||
|
||||
watch(
|
||||
() => props.customer,
|
||||
(customer) => {
|
||||
populateForm(customer);
|
||||
},
|
||||
);
|
||||
|
||||
watch(open, (isOpen) => {
|
||||
if (isOpen) {
|
||||
populateForm(props.customer);
|
||||
} else {
|
||||
resetForm();
|
||||
}
|
||||
});
|
||||
|
||||
function submit() {
|
||||
const options = {
|
||||
preserveScroll: true,
|
||||
onSuccess: () => {
|
||||
open.value = false;
|
||||
},
|
||||
onError: () => {
|
||||
toast.error('Gagal menyimpan data. Periksa kembali formulir.');
|
||||
},
|
||||
};
|
||||
|
||||
if (isEditing.value && props.customer) {
|
||||
form.put(`/admin/master/customers/${props.customer.id}`, options);
|
||||
} else {
|
||||
form.post('/admin/master/customers', options);
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Dialog v-model:open="open">
|
||||
<DialogContent class="sm:max-w-md">
|
||||
<DialogHeader>
|
||||
<DialogTitle>{{ isEditing ? 'Ubah Customer' : 'Tambah Customer' }}</DialogTitle>
|
||||
</DialogHeader>
|
||||
|
||||
<form @submit.prevent="submit">
|
||||
<FieldGroup>
|
||||
<FieldSet class="grid gap-4">
|
||||
<Field>
|
||||
<FieldLabel for="customer-name" required>Nama</FieldLabel>
|
||||
<Input id="customer-name" v-model="form.name" type="text" placeholder="Nama customer"
|
||||
autofocus />
|
||||
<FieldError :errors="form.errors.name ? [form.errors.name] : []" />
|
||||
</Field>
|
||||
<Field>
|
||||
<FieldLabel for="customer-phone-number" required>Nomor Telepon</FieldLabel>
|
||||
<PhoneNumberInput id="customer-phone-number" v-model="form.phone_number"
|
||||
placeholder="Nomor telepon customer" />
|
||||
<FieldError :errors="form.errors.phone_number ? [form.errors.phone_number] : []" />
|
||||
</Field>
|
||||
<Field>
|
||||
<FieldLabel for="customer-address" required>Alamat</FieldLabel>
|
||||
<Textarea id="customer-address" v-model="form.address" rows="3"
|
||||
placeholder="Alamat customer" />
|
||||
<FieldError :errors="form.errors.address ? [form.errors.address] : []" />
|
||||
</Field>
|
||||
</FieldSet>
|
||||
</FieldGroup>
|
||||
|
||||
<DialogFooter class="mt-6">
|
||||
<Button type="button" variant="outline" :disabled="form.processing" @click="open = false">
|
||||
Batal
|
||||
</Button>
|
||||
<Button type="submit" :disabled="form.processing">
|
||||
<Save class="size-4" />
|
||||
{{ form.processing ? 'Menyimpan...' : 'Simpan' }}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</form>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</template>
|
||||
34
resources/js/components/admin/master/customers/columns.ts
Normal file
34
resources/js/components/admin/master/customers/columns.ts
Normal file
@ -0,0 +1,34 @@
|
||||
import type { ColumnDef } from '@tanstack/vue-table';
|
||||
import { h } from 'vue';
|
||||
import DataTableActions from '@/components/admin/master/customers/data-table-actions.vue';
|
||||
import { DataTableColumnHeader } from '@/components/data-table';
|
||||
import type { CustomerListItem } from '@/types/customer';
|
||||
|
||||
export function createColumns(onEdit: (customer: CustomerListItem) => void): ColumnDef<CustomerListItem>[] {
|
||||
return [
|
||||
{
|
||||
accessorKey: 'name',
|
||||
enableSorting: true,
|
||||
header: () => h(DataTableColumnHeader, { title: 'Nama', column: 'name' }),
|
||||
},
|
||||
{
|
||||
accessorKey: 'phone_number',
|
||||
enableSorting: false,
|
||||
header: () => h(DataTableColumnHeader, { title: 'Telepon', column: 'phone_number' }),
|
||||
},
|
||||
{
|
||||
accessorKey: 'address',
|
||||
enableSorting: false,
|
||||
header: () => h(DataTableColumnHeader, { title: 'Alamat', column: 'address' }),
|
||||
},
|
||||
{
|
||||
id: 'actions',
|
||||
enableSorting: false,
|
||||
enableHiding: false,
|
||||
cell: ({ row }) => h(DataTableActions, {
|
||||
customer: row.original,
|
||||
onEdit: () => onEdit(row.original),
|
||||
}),
|
||||
},
|
||||
];
|
||||
}
|
||||
@ -0,0 +1,70 @@
|
||||
<script setup lang="ts">
|
||||
import { router } from '@inertiajs/vue3';
|
||||
import { Pencil, Trash2 } from '@lucide/vue';
|
||||
import { ref } from 'vue';
|
||||
import { toast } from 'vue-sonner';
|
||||
import ConfirmDialog from '@/components/ConfirmDialog.vue';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip';
|
||||
import { useCan } from '@/composables/useCan';
|
||||
import type { CustomerListItem } from '@/types/customer';
|
||||
|
||||
const props = defineProps<{
|
||||
customer: CustomerListItem;
|
||||
}>();
|
||||
|
||||
const emit = defineEmits<{
|
||||
edit: [customer: CustomerListItem];
|
||||
}>();
|
||||
|
||||
const { can } = useCan();
|
||||
|
||||
const deleteConfirmOpen = ref(false);
|
||||
const deleteProcessing = ref(false);
|
||||
|
||||
function destroyCustomer() {
|
||||
deleteProcessing.value = true;
|
||||
|
||||
router.delete(`/admin/master/customers/${props.customer.id}`, {
|
||||
preserveScroll: true,
|
||||
onSuccess: () => {
|
||||
deleteConfirmOpen.value = false;
|
||||
},
|
||||
onError: () => {
|
||||
toast.error('Gagal menghapus customer.');
|
||||
},
|
||||
onFinish: () => {
|
||||
deleteProcessing.value = false;
|
||||
},
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex items-center justify-end gap-1">
|
||||
<Tooltip v-if="can('customers.update')">
|
||||
<TooltipTrigger as-child>
|
||||
<Button variant="ghost" size="icon" class="size-8" @click="emit('edit', customer)">
|
||||
<Pencil class="size-4" />
|
||||
<span class="sr-only">Ubah</span>
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>Ubah</TooltipContent>
|
||||
</Tooltip>
|
||||
|
||||
<Tooltip v-if="can('customers.delete')">
|
||||
<TooltipTrigger as-child>
|
||||
<Button variant="ghost" size="icon" class="text-destructive hover:text-destructive size-8"
|
||||
@click="deleteConfirmOpen = true">
|
||||
<Trash2 class="size-4" />
|
||||
<span class="sr-only">Hapus</span>
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>Hapus</TooltipContent>
|
||||
</Tooltip>
|
||||
</div>
|
||||
|
||||
<ConfirmDialog v-if="can('customers.delete')" v-model:open="deleteConfirmOpen" title="Hapus customer?"
|
||||
:description="`Customer ${customer.name} akan dihapus secara permanen. Tindakan ini tidak dapat dibatalkan.`"
|
||||
confirm-label="Hapus" cancel-label="Batal" destructive :loading="deleteProcessing" @confirm="destroyCustomer" />
|
||||
</template>
|
||||
108
resources/js/pages/admin/master/customers/Index.vue
Normal file
108
resources/js/pages/admin/master/customers/Index.vue
Normal file
@ -0,0 +1,108 @@
|
||||
<script setup lang="ts">
|
||||
import { Head } from '@inertiajs/vue3';
|
||||
import { Plus } from '@lucide/vue';
|
||||
import { computed, ref, watch } from 'vue';
|
||||
import { createColumns } from '@/components/admin/master/customers/columns';
|
||||
import { DataTable } from '@/components/data-table';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Card, CardContent } from '@/components/ui/card';
|
||||
import { useCan } from '@/composables/useCan';
|
||||
import { useDataTableQuery, useDataTableQuerySync } from '@/composables/useDataTableQuery';
|
||||
import AdminLayout from '@/layouts/AdminLayout.vue';
|
||||
import type { DataTableSort } from '@/types/data-table';
|
||||
import CustomerFormModal from '@/components/admin/master/customers/CustomerFormModal.vue';
|
||||
import type { CustomerListItem, PaginatedCustomers } from '@/types/customer';
|
||||
|
||||
const props = defineProps<{
|
||||
customers: PaginatedCustomers;
|
||||
filters: {
|
||||
search: string;
|
||||
sort?: string;
|
||||
direction?: 'asc' | 'desc';
|
||||
};
|
||||
}>();
|
||||
|
||||
const { can } = useCan();
|
||||
const search = ref(props.filters.search ?? '');
|
||||
const formModalOpen = ref(false);
|
||||
const editingCustomer = ref<CustomerListItem | null>(null);
|
||||
|
||||
const { query, setSearch, setSort, resetFilters, syncFromServer } = useDataTableQuery({
|
||||
url: '/admin/master/customers',
|
||||
initial: { ...props.filters },
|
||||
});
|
||||
|
||||
useDataTableQuerySync(() => props.filters, syncFromServer);
|
||||
|
||||
const columns = computed(() => createColumns(openEditModal));
|
||||
|
||||
const currentSort = computed<DataTableSort | null>(() => {
|
||||
if (!query.value.sort || !query.value.direction) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return {
|
||||
column: query.value.sort,
|
||||
direction: query.value.direction,
|
||||
};
|
||||
});
|
||||
|
||||
const pagination = computed(() => ({
|
||||
currentPage: props.customers.current_page,
|
||||
perPage: props.customers.per_page,
|
||||
lastPage: props.customers.last_page,
|
||||
total: props.customers.total,
|
||||
}));
|
||||
|
||||
function openCreateModal() {
|
||||
editingCustomer.value = null;
|
||||
formModalOpen.value = true;
|
||||
}
|
||||
|
||||
function openEditModal(customer: CustomerListItem) {
|
||||
editingCustomer.value = customer;
|
||||
formModalOpen.value = true;
|
||||
}
|
||||
|
||||
watch(search, (value) => {
|
||||
setSearch(value);
|
||||
});
|
||||
|
||||
watch(
|
||||
() => props.filters.search,
|
||||
(value) => {
|
||||
search.value = value ?? '';
|
||||
},
|
||||
);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
||||
<Head title="Customer" />
|
||||
|
||||
<AdminLayout>
|
||||
<div class="flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between">
|
||||
<div class="space-y-1">
|
||||
<h2 class="text-2xl font-bold tracking-tight">
|
||||
Customer
|
||||
</h2>
|
||||
</div>
|
||||
|
||||
<Button v-if="can('customers.create')" class="shrink-0 self-start sm:self-center" @click="openCreateModal">
|
||||
<Plus class="size-4" />
|
||||
Tambah
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<Card class="min-w-0">
|
||||
<CardContent class="min-w-0">
|
||||
<DataTable v-model:search="search" :columns="columns" :data="customers.data" :pagination="pagination"
|
||||
:pagination-links="customers.links" :sort="currentSort" @sort-change="setSort"
|
||||
@filters-reset="resetFilters" />
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<CustomerFormModal v-if="can('customers.create') || can('customers.update')" v-model:open="formModalOpen"
|
||||
:customer="editingCustomer" />
|
||||
</AdminLayout>
|
||||
</template>
|
||||
31
resources/js/types/customers.ts
Normal file
31
resources/js/types/customers.ts
Normal file
@ -0,0 +1,31 @@
|
||||
export type CustomerListItem = {
|
||||
id: number;
|
||||
name: string;
|
||||
phone_number: string;
|
||||
address: string;
|
||||
};
|
||||
|
||||
export type CustomerFormData = {
|
||||
name: string;
|
||||
phone_number: string;
|
||||
address: string;
|
||||
};
|
||||
|
||||
export type CustomerFilters = {
|
||||
search: string;
|
||||
sort?: string;
|
||||
direction?: 'asc' | 'desc' | null;
|
||||
};
|
||||
|
||||
export type PaginatedCustomers = {
|
||||
data: CustomerListItem[];
|
||||
current_page: number;
|
||||
last_page: number;
|
||||
per_page: number;
|
||||
total: number;
|
||||
links: Array<{
|
||||
url: string | null;
|
||||
label: string;
|
||||
active: boolean;
|
||||
}>;
|
||||
};
|
||||
@ -4,6 +4,7 @@
|
||||
use App\Http\Controllers\Admin\DashboardController;
|
||||
use App\Http\Controllers\Admin\Hr\EmployeeController;
|
||||
use App\Http\Controllers\Admin\Master\CategoryController;
|
||||
use App\Http\Controllers\Admin\Master\CustomerController;
|
||||
use App\Http\Controllers\Admin\Master\ProductController;
|
||||
use App\Http\Controllers\Admin\Master\RawMaterialController;
|
||||
use App\Http\Controllers\Admin\Master\SupplierController;
|
||||
@ -122,6 +123,24 @@
|
||||
->name('destroy');
|
||||
});
|
||||
|
||||
Route::prefix('customers')->name('customers.')
|
||||
->middleware('permission:'.Permission::CUSTOMERS_VIEW->value)
|
||||
->group(function () {
|
||||
Route::get('/', [CustomerController::class, 'index'])->name('index');
|
||||
|
||||
Route::post('/', [CustomerController::class, 'store'])
|
||||
->middleware('permission:'.Permission::CUSTOMERS_CREATE->value)
|
||||
->name('store');
|
||||
|
||||
Route::put('{customer}', [CustomerController::class, 'update'])
|
||||
->middleware('permission:'.Permission::CUSTOMERS_UPDATE->value)
|
||||
->name('update');
|
||||
|
||||
Route::delete('{customer}', [CustomerController::class, 'destroy'])
|
||||
->middleware('permission:'.Permission::CUSTOMERS_DELETE->value)
|
||||
->name('destroy');
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
Route::prefix('hr')->name('hr.')->middleware('permission:'.Permission::EMPLOYEES_VIEW->value)->group(function () {
|
||||
|
||||
Loading…
Reference in New Issue
Block a user