Add supplier management features including Supplier model, SupplierController for CRUD operations, and SupplierRequest for validation. Integrate permissions in the Permission and Role enums, and update routes for supplier management. Enhance UI components for supplier listing and management, including a data table and form modal. Update sidebar for supplier navigation.
This commit is contained in:
parent
887b811cee
commit
c21f71563f
@ -22,6 +22,11 @@ enum Permission: string
|
||||
case CATEGORIES_UPDATE = 'categories.update';
|
||||
case CATEGORIES_DELETE = 'categories.delete';
|
||||
|
||||
case SUPPLIERS_VIEW = 'suppliers.view';
|
||||
case SUPPLIERS_CREATE = 'suppliers.create';
|
||||
case SUPPLIERS_UPDATE = 'suppliers.update';
|
||||
case SUPPLIERS_DELETE = 'suppliers.delete';
|
||||
|
||||
case PRODUCTS_VIEW = 'products.view';
|
||||
case PRODUCTS_CREATE = 'products.create';
|
||||
case PRODUCTS_UPDATE = 'products.update';
|
||||
@ -51,6 +56,11 @@ public function label(): string
|
||||
self::CATEGORIES_UPDATE => 'Ubah Kategori',
|
||||
self::CATEGORIES_DELETE => 'Hapus Kategori',
|
||||
|
||||
self::SUPPLIERS_VIEW => 'Lihat Supplier',
|
||||
self::SUPPLIERS_CREATE => 'Tambah Supplier',
|
||||
self::SUPPLIERS_UPDATE => 'Ubah Supplier',
|
||||
self::SUPPLIERS_DELETE => 'Hapus Supplier',
|
||||
|
||||
self::PRODUCTS_VIEW => 'Lihat Produk',
|
||||
self::PRODUCTS_CREATE => 'Tambah Produk',
|
||||
self::PRODUCTS_UPDATE => 'Ubah Produk',
|
||||
@ -73,6 +83,8 @@ public function group(): string
|
||||
self::EMPLOYEES_DELETE, self::EMPLOYEES_RESET_PASSWORD, self::EMPLOYEES_TOGGLE_STATUS => 'Pegawai',
|
||||
self::CATEGORIES_VIEW, self::CATEGORIES_CREATE, self::CATEGORIES_UPDATE,
|
||||
self::CATEGORIES_DELETE => 'Kategori',
|
||||
self::SUPPLIERS_VIEW, self::SUPPLIERS_CREATE, self::SUPPLIERS_UPDATE,
|
||||
self::SUPPLIERS_DELETE => 'Supplier',
|
||||
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,
|
||||
|
||||
@ -48,6 +48,10 @@ public function permissions(): array
|
||||
Permission::CATEGORIES_CREATE,
|
||||
Permission::CATEGORIES_UPDATE,
|
||||
Permission::CATEGORIES_DELETE,
|
||||
Permission::SUPPLIERS_VIEW,
|
||||
Permission::SUPPLIERS_CREATE,
|
||||
Permission::SUPPLIERS_UPDATE,
|
||||
Permission::SUPPLIERS_DELETE,
|
||||
Permission::PRODUCTS_VIEW,
|
||||
Permission::PRODUCTS_CREATE,
|
||||
Permission::PRODUCTS_UPDATE,
|
||||
@ -70,6 +74,10 @@ public function permissions(): array
|
||||
Permission::CATEGORIES_CREATE,
|
||||
Permission::CATEGORIES_UPDATE,
|
||||
Permission::CATEGORIES_DELETE,
|
||||
Permission::SUPPLIERS_VIEW,
|
||||
Permission::SUPPLIERS_CREATE,
|
||||
Permission::SUPPLIERS_UPDATE,
|
||||
Permission::SUPPLIERS_DELETE,
|
||||
Permission::PRODUCTS_VIEW,
|
||||
Permission::PRODUCTS_CREATE,
|
||||
Permission::PRODUCTS_UPDATE,
|
||||
@ -83,6 +91,10 @@ public function permissions(): array
|
||||
Permission::CATEGORIES_CREATE,
|
||||
Permission::CATEGORIES_UPDATE,
|
||||
Permission::CATEGORIES_DELETE,
|
||||
Permission::SUPPLIERS_VIEW,
|
||||
Permission::SUPPLIERS_CREATE,
|
||||
Permission::SUPPLIERS_UPDATE,
|
||||
Permission::SUPPLIERS_DELETE,
|
||||
Permission::RAW_MATERIALS_VIEW,
|
||||
Permission::RAW_MATERIALS_CREATE,
|
||||
Permission::RAW_MATERIALS_UPDATE,
|
||||
@ -93,6 +105,13 @@ public function permissions(): array
|
||||
Permission::DASHBOARD_VIEW,
|
||||
Permission::EMPLOYEES_VIEW,
|
||||
Permission::CATEGORIES_VIEW,
|
||||
Permission::CATEGORIES_CREATE,
|
||||
Permission::CATEGORIES_UPDATE,
|
||||
Permission::CATEGORIES_DELETE,
|
||||
Permission::SUPPLIERS_VIEW,
|
||||
Permission::SUPPLIERS_CREATE,
|
||||
Permission::SUPPLIERS_UPDATE,
|
||||
Permission::SUPPLIERS_DELETE,
|
||||
Permission::PRODUCTS_VIEW,
|
||||
],
|
||||
self::NON_OPERATOR => [
|
||||
|
||||
59
app/Http/Controllers/Admin/Master/SupplierController.php
Normal file
59
app/Http/Controllers/Admin/Master/SupplierController.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\SupplierRequest;
|
||||
use App\Models\Supplier;
|
||||
use App\Services\Master\SupplierService;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Inertia\Inertia;
|
||||
use Inertia\Response;
|
||||
|
||||
class SupplierController extends Controller
|
||||
{
|
||||
use ParsesDataTableQuery;
|
||||
|
||||
public function __construct(
|
||||
private readonly SupplierService $supplierService,
|
||||
) {}
|
||||
|
||||
public function index(Request $request): Response
|
||||
{
|
||||
$tableQuery = $this->parseDataTableQuery($request);
|
||||
|
||||
return Inertia::render('admin/master/suppliers/Index', [
|
||||
'suppliers' => $this->supplierService->paginateForIndex($tableQuery),
|
||||
'filters' => $this->dataTableFilters($tableQuery),
|
||||
]);
|
||||
}
|
||||
|
||||
public function store(SupplierRequest $request): RedirectResponse
|
||||
{
|
||||
$this->supplierService->create($request->validated());
|
||||
|
||||
Inertia::flash('success', 'Supplier berhasil ditambahkan.');
|
||||
|
||||
return redirect()->route('admin.master.suppliers.index');
|
||||
}
|
||||
|
||||
public function update(SupplierRequest $request, Supplier $supplier): RedirectResponse
|
||||
{
|
||||
$this->supplierService->update($supplier, $request->validated());
|
||||
|
||||
Inertia::flash('success', 'Supplier berhasil diperbarui.');
|
||||
|
||||
return redirect()->route('admin.master.suppliers.index');
|
||||
}
|
||||
|
||||
public function destroy(Supplier $supplier): RedirectResponse
|
||||
{
|
||||
$this->supplierService->delete($supplier);
|
||||
|
||||
Inertia::flash('success', 'Supplier berhasil dihapus.');
|
||||
|
||||
return redirect()->route('admin.master.suppliers.index');
|
||||
}
|
||||
}
|
||||
30
app/Http/Requests/Admin/Master/SupplierRequest.php
Normal file
30
app/Http/Requests/Admin/Master/SupplierRequest.php
Normal file
@ -0,0 +1,30 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Requests\Admin\Master;
|
||||
|
||||
use App\Enums\Permission;
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
|
||||
class SupplierRequest extends FormRequest
|
||||
{
|
||||
public function authorize(): bool
|
||||
{
|
||||
$permission = $this->isMethod('POST')
|
||||
? Permission::SUPPLIERS_CREATE
|
||||
: Permission::SUPPLIERS_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/Supplier.php
Normal file
13
app/Models/Supplier.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 Supplier extends Model
|
||||
{
|
||||
use SoftDeletes;
|
||||
}
|
||||
67
app/Services/Master/SupplierService.php
Normal file
67
app/Services/Master/SupplierService.php
Normal file
@ -0,0 +1,67 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services\Master;
|
||||
|
||||
use App\Models\Supplier;
|
||||
use Illuminate\Contracts\Pagination\LengthAwarePaginator;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
|
||||
class SupplierService
|
||||
{
|
||||
/**
|
||||
* @param array{search: string, sort: string, direction: 'asc'|'desc'} $tableQuery
|
||||
*/
|
||||
public function paginateForIndex(array $tableQuery): LengthAwarePaginator
|
||||
{
|
||||
$query = Supplier::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
|
||||
{
|
||||
Supplier::create($validated);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $validated
|
||||
*/
|
||||
public function update(Supplier $supplier, array $validated): void
|
||||
{
|
||||
$supplier->name = $validated['name'];
|
||||
$supplier->phone_number = $validated['phone_number'];
|
||||
$supplier->address = $validated['address'];
|
||||
$supplier->save();
|
||||
}
|
||||
|
||||
public function delete(Supplier $supplier): void
|
||||
{
|
||||
$supplier->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('suppliers', 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('suppliers');
|
||||
}
|
||||
};
|
||||
@ -1,6 +1,6 @@
|
||||
<script setup lang="ts">
|
||||
import { Link, usePage } from '@inertiajs/vue3';
|
||||
import { FolderTree, Layers, LayoutDashboard, Package, Users } from '@lucide/vue';
|
||||
import { FolderTree, Layers, LayoutDashboard, Package, User, Users } from '@lucide/vue';
|
||||
import { computed } from 'vue';
|
||||
import {
|
||||
Sidebar,
|
||||
@ -24,6 +24,7 @@ const isEmployeesActive = computed(() => page.url.startsWith('/admin/hr/employee
|
||||
const isCategoriesActive = computed(() => page.url.startsWith('/admin/master/categories'));
|
||||
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 showMasterMenu = computed(() => (
|
||||
can('categories.view') || can('products.view') || can('raw-materials.view')
|
||||
));
|
||||
@ -88,6 +89,14 @@ const showMasterMenu = computed(() => (
|
||||
</Link>
|
||||
</SidebarMenuButton>
|
||||
</SidebarMenuItem>
|
||||
<SidebarMenuItem v-if="can('raw-materials.view')">
|
||||
<SidebarMenuButton as-child tooltip="Supplier" :is-active="isSuppliersActive">
|
||||
<Link href="/admin/master/suppliers">
|
||||
<User />
|
||||
<span>Supplier</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 { SupplierFormData, SupplierListItem } from '@/types/supplier';
|
||||
|
||||
const open = defineModel<boolean>('open', { default: false });
|
||||
|
||||
const props = defineProps<{
|
||||
supplier?: SupplierListItem | null;
|
||||
}>();
|
||||
|
||||
const isEditing = computed(() => props.supplier != null);
|
||||
|
||||
const form = useForm<SupplierFormData>({
|
||||
name: '',
|
||||
phone_number: '',
|
||||
address: '',
|
||||
});
|
||||
|
||||
function resetForm() {
|
||||
form.reset();
|
||||
form.clearErrors();
|
||||
}
|
||||
|
||||
function populateForm(supplier: SupplierListItem | null | undefined) {
|
||||
resetForm();
|
||||
|
||||
if (!supplier) {
|
||||
return;
|
||||
}
|
||||
|
||||
form.name = supplier.name;
|
||||
form.phone_number = supplier.phone_number;
|
||||
form.address = supplier.address;
|
||||
}
|
||||
|
||||
watch(
|
||||
() => props.supplier,
|
||||
(supplier) => {
|
||||
populateForm(supplier);
|
||||
},
|
||||
);
|
||||
|
||||
watch(open, (isOpen) => {
|
||||
if (isOpen) {
|
||||
populateForm(props.supplier);
|
||||
} 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.supplier) {
|
||||
form.put(`/admin/master/suppliers/${props.supplier.id}`, options);
|
||||
} else {
|
||||
form.post('/admin/master/suppliers', options);
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Dialog v-model:open="open">
|
||||
<DialogContent class="sm:max-w-md">
|
||||
<DialogHeader>
|
||||
<DialogTitle>{{ isEditing ? 'Ubah Supplier' : 'Tambah Supplier' }}</DialogTitle>
|
||||
</DialogHeader>
|
||||
|
||||
<form @submit.prevent="submit">
|
||||
<FieldGroup>
|
||||
<FieldSet class="grid gap-4">
|
||||
<Field>
|
||||
<FieldLabel for="supplier-name" required>Nama</FieldLabel>
|
||||
<Input id="supplier-name" v-model="form.name" type="text" placeholder="Nama supplier"
|
||||
autofocus />
|
||||
<FieldError :errors="form.errors.name ? [form.errors.name] : []" />
|
||||
</Field>
|
||||
<Field>
|
||||
<FieldLabel for="supplier-phone-number" required>Nomor Telepon</FieldLabel>
|
||||
<PhoneNumberInput id="supplier-phone-number" v-model="form.phone_number"
|
||||
placeholder="Nomor telepon supplier" />
|
||||
<FieldError :errors="form.errors.phone_number ? [form.errors.phone_number] : []" />
|
||||
</Field>
|
||||
<Field>
|
||||
<FieldLabel for="supplier-address" required>Alamat</FieldLabel>
|
||||
<Textarea id="supplier-address" v-model="form.address" rows="3"
|
||||
placeholder="Alamat supplier" />
|
||||
<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/suppliers/columns.ts
Normal file
34
resources/js/components/admin/master/suppliers/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/suppliers/data-table-actions.vue';
|
||||
import { DataTableColumnHeader } from '@/components/data-table';
|
||||
import type { SupplierListItem } from '@/types/supplier';
|
||||
|
||||
export function createColumns(onEdit: (supplier: SupplierListItem) => void): ColumnDef<SupplierListItem>[] {
|
||||
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, {
|
||||
supplier: 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 { SupplierListItem } from '@/types/supplier';
|
||||
|
||||
const props = defineProps<{
|
||||
supplier: SupplierListItem;
|
||||
}>();
|
||||
|
||||
const emit = defineEmits<{
|
||||
edit: [supplier: SupplierListItem];
|
||||
}>();
|
||||
|
||||
const { can } = useCan();
|
||||
|
||||
const deleteConfirmOpen = ref(false);
|
||||
const deleteProcessing = ref(false);
|
||||
|
||||
function destroySupplier() {
|
||||
deleteProcessing.value = true;
|
||||
|
||||
router.delete(`/admin/master/suppliers/${props.supplier.id}`, {
|
||||
preserveScroll: true,
|
||||
onSuccess: () => {
|
||||
deleteConfirmOpen.value = false;
|
||||
},
|
||||
onError: () => {
|
||||
toast.error('Gagal menghapus supplier.');
|
||||
},
|
||||
onFinish: () => {
|
||||
deleteProcessing.value = false;
|
||||
},
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex items-center justify-end gap-1">
|
||||
<Tooltip v-if="can('suppliers.update')">
|
||||
<TooltipTrigger as-child>
|
||||
<Button variant="ghost" size="icon" class="size-8" @click="emit('edit', supplier)">
|
||||
<Pencil class="size-4" />
|
||||
<span class="sr-only">Ubah</span>
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>Ubah</TooltipContent>
|
||||
</Tooltip>
|
||||
|
||||
<Tooltip v-if="can('suppliers.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('suppliers.delete')" v-model:open="deleteConfirmOpen" title="Hapus supplier?"
|
||||
:description="`Supplier ${supplier.name} akan dihapus secara permanen. Tindakan ini tidak dapat dibatalkan.`"
|
||||
confirm-label="Hapus" cancel-label="Batal" destructive :loading="deleteProcessing" @confirm="destroySupplier" />
|
||||
</template>
|
||||
108
resources/js/pages/admin/master/suppliers/Index.vue
Normal file
108
resources/js/pages/admin/master/suppliers/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 { 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 { createColumns } from '@/components/admin/master/suppliers/columns';
|
||||
import SupplierFormModal from '@/components/admin/master/suppliers/SupplierFormModal.vue';
|
||||
import type { SupplierListItem, PaginatedSuppliers } from '@/types/supplier';
|
||||
|
||||
const props = defineProps<{
|
||||
suppliers: PaginatedSuppliers;
|
||||
filters: {
|
||||
search: string;
|
||||
sort?: string;
|
||||
direction?: 'asc' | 'desc';
|
||||
};
|
||||
}>();
|
||||
|
||||
const { can } = useCan();
|
||||
const search = ref(props.filters.search ?? '');
|
||||
const formModalOpen = ref(false);
|
||||
const editingSupplier = ref<SupplierListItem | null>(null);
|
||||
|
||||
const { query, setSearch, setSort, resetFilters, syncFromServer } = useDataTableQuery({
|
||||
url: '/admin/master/suppliers',
|
||||
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.suppliers.current_page,
|
||||
perPage: props.suppliers.per_page,
|
||||
lastPage: props.suppliers.last_page,
|
||||
total: props.suppliers.total,
|
||||
}));
|
||||
|
||||
function openCreateModal() {
|
||||
editingSupplier.value = null;
|
||||
formModalOpen.value = true;
|
||||
}
|
||||
|
||||
function openEditModal(supplier: SupplierListItem) {
|
||||
editingSupplier.value = supplier;
|
||||
formModalOpen.value = true;
|
||||
}
|
||||
|
||||
watch(search, (value) => {
|
||||
setSearch(value);
|
||||
});
|
||||
|
||||
watch(
|
||||
() => props.filters.search,
|
||||
(value) => {
|
||||
search.value = value ?? '';
|
||||
},
|
||||
);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
||||
<Head title="Supplier" />
|
||||
|
||||
<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">
|
||||
Supplier
|
||||
</h2>
|
||||
</div>
|
||||
|
||||
<Button v-if="can('suppliers.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="suppliers.data" :pagination="pagination"
|
||||
:pagination-links="suppliers.links" :sort="currentSort" @sort-change="setSort"
|
||||
@filters-reset="resetFilters" />
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<SupplierFormModal v-if="can('suppliers.create') || can('suppliers.update')" v-model:open="formModalOpen"
|
||||
:supplier="editingSupplier" />
|
||||
</AdminLayout>
|
||||
</template>
|
||||
31
resources/js/types/supplier.ts
Normal file
31
resources/js/types/supplier.ts
Normal file
@ -0,0 +1,31 @@
|
||||
export type SupplierListItem = {
|
||||
id: number;
|
||||
name: string;
|
||||
phone_number: string;
|
||||
address: string;
|
||||
};
|
||||
|
||||
export type SupplierFormData = {
|
||||
name: string;
|
||||
phone_number: string;
|
||||
address: string;
|
||||
};
|
||||
|
||||
export type SupplierFilters = {
|
||||
search: string;
|
||||
sort?: string;
|
||||
direction?: 'asc' | 'desc' | null;
|
||||
};
|
||||
|
||||
export type PaginatedSuppliers = {
|
||||
data: SupplierListItem[];
|
||||
current_page: number;
|
||||
last_page: number;
|
||||
per_page: number;
|
||||
total: number;
|
||||
links: Array<{
|
||||
url: string | null;
|
||||
label: string;
|
||||
active: boolean;
|
||||
}>;
|
||||
};
|
||||
@ -6,6 +6,7 @@
|
||||
use App\Http\Controllers\Admin\Master\CategoryController;
|
||||
use App\Http\Controllers\Admin\Master\ProductController;
|
||||
use App\Http\Controllers\Admin\Master\RawMaterialController;
|
||||
use App\Http\Controllers\Admin\Master\SupplierController;
|
||||
use App\Http\Controllers\Auth\LoginController;
|
||||
use App\Http\Controllers\Auth\LogoutController;
|
||||
use Illuminate\Support\Facades\Route;
|
||||
@ -102,6 +103,25 @@
|
||||
|
||||
Route::get('/', [RawMaterialController::class, 'index'])->name('index');
|
||||
});
|
||||
|
||||
Route::prefix('suppliers')->name('suppliers.')
|
||||
->middleware('permission:'.Permission::SUPPLIERS_VIEW->value)
|
||||
->group(function () {
|
||||
Route::get('/', [SupplierController::class, 'index'])->name('index');
|
||||
|
||||
Route::post('/', [SupplierController::class, 'store'])
|
||||
->middleware('permission:'.Permission::SUPPLIERS_CREATE->value)
|
||||
->name('store');
|
||||
|
||||
Route::put('{supplier}', [SupplierController::class, 'update'])
|
||||
->middleware('permission:'.Permission::SUPPLIERS_UPDATE->value)
|
||||
->name('update');
|
||||
|
||||
Route::delete('{supplier}', [SupplierController::class, 'destroy'])
|
||||
->middleware('permission:'.Permission::SUPPLIERS_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