feat: add customer management functionality and update navigation
- Introduced CustomerController for handling customer operations. - Added resourceful routes for customers in the web routes. - Updated DatabaseSeeder to include CustomerSeeder for initial data population. - Modified sidebar navigation to link to the customers index.
This commit is contained in:
parent
8edeaf0db7
commit
5c1ef6a7b0
52
app/Http/Controllers/Admin/Master/CustomerController.php
Normal file
52
app/Http/Controllers/Admin/Master/CustomerController.php
Normal file
@ -0,0 +1,52 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Admin\Master;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Http\Requests\Admin\Master\CustomerRequest;
|
||||
use App\Models\Customer;
|
||||
use App\Services\Admin\Master\CustomerService;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Inertia\Inertia;
|
||||
use Inertia\Response;
|
||||
|
||||
class CustomerController extends Controller
|
||||
{
|
||||
public function __construct(
|
||||
private CustomerService $service
|
||||
) {}
|
||||
|
||||
public function index(): Response
|
||||
{
|
||||
return Inertia::render('admin/master/customer/index', [
|
||||
'customers' => $this->service->getAll(),
|
||||
]);
|
||||
}
|
||||
|
||||
public function store(CustomerRequest $request): RedirectResponse
|
||||
{
|
||||
$this->service->create($request->validated());
|
||||
|
||||
Inertia::flash('toast', ['type' => 'success', 'message' => 'Customer berhasil ditambahkan.']);
|
||||
|
||||
return to_route('admin.master.customers.index');
|
||||
}
|
||||
|
||||
public function update(CustomerRequest $request, Customer $customer): RedirectResponse
|
||||
{
|
||||
$this->service->update($customer, $request->validated());
|
||||
|
||||
Inertia::flash('toast', ['type' => 'success', 'message' => 'Customer berhasil diperbarui.']);
|
||||
|
||||
return to_route('admin.master.customers.index');
|
||||
}
|
||||
|
||||
public function destroy(Customer $customer): RedirectResponse
|
||||
{
|
||||
$this->service->delete($customer);
|
||||
|
||||
Inertia::flash('toast', ['type' => 'success', 'message' => 'Customer berhasil dihapus.']);
|
||||
|
||||
return to_route('admin.master.customers.index');
|
||||
}
|
||||
}
|
||||
34
app/Http/Requests/Admin/Master/CustomerRequest.php
Normal file
34
app/Http/Requests/Admin/Master/CustomerRequest.php
Normal file
@ -0,0 +1,34 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Requests\Admin\Master;
|
||||
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
use Illuminate\Validation\Rule;
|
||||
|
||||
class CustomerRequest extends FormRequest
|
||||
{
|
||||
public function authorize(): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
public function rules(): array
|
||||
{
|
||||
$customer = $this->route('customer');
|
||||
|
||||
return [
|
||||
'name' => ['required', 'string', 'max:200', Rule::unique('customers', 'name')->ignore($customer)],
|
||||
'phone_number' => ['nullable', 'string', 'max:20', 'digits_between:1,20'],
|
||||
'address' => ['nullable', 'string'],
|
||||
];
|
||||
}
|
||||
|
||||
public function attributes(): array
|
||||
{
|
||||
return [
|
||||
'name' => 'nama',
|
||||
'phone_number' => 'nomor telepon',
|
||||
'address' => 'alamat',
|
||||
];
|
||||
}
|
||||
}
|
||||
31
app/Services/Admin/Master/CustomerService.php
Normal file
31
app/Services/Admin/Master/CustomerService.php
Normal file
@ -0,0 +1,31 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services\Admin\Master;
|
||||
|
||||
use App\Models\Customer;
|
||||
use Illuminate\Database\Eloquent\Collection;
|
||||
|
||||
class CustomerService
|
||||
{
|
||||
public function getAll(): Collection
|
||||
{
|
||||
return Customer::latest()->get();
|
||||
}
|
||||
|
||||
public function create(array $data): Customer
|
||||
{
|
||||
return Customer::create($data);
|
||||
}
|
||||
|
||||
public function update(Customer $customer, array $data): Customer
|
||||
{
|
||||
$customer->update($data);
|
||||
|
||||
return $customer;
|
||||
}
|
||||
|
||||
public function delete(Customer $customer): bool
|
||||
{
|
||||
return $customer->delete();
|
||||
}
|
||||
}
|
||||
14
database/seeders/CustomerSeeder.php
Normal file
14
database/seeders/CustomerSeeder.php
Normal file
@ -0,0 +1,14 @@
|
||||
<?php
|
||||
|
||||
namespace Database\Seeders;
|
||||
|
||||
use App\Models\Customer;
|
||||
use Illuminate\Database\Seeder;
|
||||
|
||||
class CustomerSeeder extends Seeder
|
||||
{
|
||||
public function run(): void
|
||||
{
|
||||
Customer::factory()->count(100)->create();
|
||||
}
|
||||
}
|
||||
@ -18,6 +18,7 @@ public function run(): void
|
||||
UserSeeder::class,
|
||||
CategorySeeder::class,
|
||||
SupplierSeeder::class,
|
||||
CustomerSeeder::class,
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
@ -36,6 +36,7 @@ import {
|
||||
import { useCurrentUrl } from '@/hooks/use-current-url';
|
||||
import { dashboard } from '@/routes';
|
||||
import { index as categoriesIndex } from '@/routes/admin/master/categories';
|
||||
import { index as customersIndex } from '@/routes/admin/master/customers';
|
||||
import { index as suppliersIndex } from '@/routes/admin/master/suppliers';
|
||||
|
||||
type NavMenuItem = { title: string; href: string; icon: LucideIcon };
|
||||
@ -57,7 +58,7 @@ const masterItems: NavMenuItem[] = [
|
||||
{ title: 'Produk', href: '#', icon: Package },
|
||||
{ title: 'Bahan Baku', href: '#', icon: Boxes },
|
||||
{ title: 'Supplier', href: suppliersIndex.url(), icon: Truck },
|
||||
{ title: 'Customer', href: '#', icon: Users },
|
||||
{ title: 'Customer', href: customersIndex.url(), icon: Users },
|
||||
];
|
||||
|
||||
const kelolaItems: NavMenuItem[] = [
|
||||
|
||||
160
resources/js/pages/admin/master/customer/columns.tsx
Normal file
160
resources/js/pages/admin/master/customer/columns.tsx
Normal file
@ -0,0 +1,160 @@
|
||||
import type { ColumnDef } from '@tanstack/react-table';
|
||||
import { ArrowUpDown, Pencil, Trash2 } from 'lucide-react';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import {
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
TooltipProvider,
|
||||
TooltipTrigger,
|
||||
} from '@/components/ui/tooltip';
|
||||
|
||||
export type Customer = {
|
||||
id: number;
|
||||
name: string;
|
||||
phone_number: string | null;
|
||||
address: string | null;
|
||||
};
|
||||
|
||||
type CreateColumnsParams = {
|
||||
handleEdit: (customer: Customer) => void;
|
||||
handleDeleteClick: (customer: Customer) => void;
|
||||
};
|
||||
|
||||
export function createCustomerColumns(
|
||||
params: CreateColumnsParams,
|
||||
): ColumnDef<Customer>[] {
|
||||
const { handleEdit, handleDeleteClick } = params;
|
||||
|
||||
return [
|
||||
{
|
||||
id: 'no',
|
||||
header: () => <span className="block text-center">No</span>,
|
||||
cell: ({ row }) => (
|
||||
<span className="block text-center">
|
||||
{row.index + 1}
|
||||
</span>
|
||||
),
|
||||
meta: {
|
||||
className: 'w-[50px] text-center',
|
||||
headerClassName: 'w-[50px] text-center',
|
||||
},
|
||||
},
|
||||
{
|
||||
accessorKey: 'name',
|
||||
header: ({ column }) => (
|
||||
<Button
|
||||
variant="ghost"
|
||||
className="-ml-3 h-8"
|
||||
onClick={() =>
|
||||
column.toggleSorting(
|
||||
column.getIsSorted() === 'asc',
|
||||
)
|
||||
}
|
||||
>
|
||||
<span>Nama</span>
|
||||
<ArrowUpDown className="ml-2 h-4 w-4" />
|
||||
</Button>
|
||||
),
|
||||
cell: ({ row }) => (
|
||||
<span className="font-medium">
|
||||
{row.getValue('name') as string}
|
||||
</span>
|
||||
),
|
||||
},
|
||||
{
|
||||
accessorKey: 'phone_number',
|
||||
header: ({ column }) => (
|
||||
<Button
|
||||
variant="ghost"
|
||||
className="-ml-3 h-8"
|
||||
onClick={() =>
|
||||
column.toggleSorting(
|
||||
column.getIsSorted() === 'asc',
|
||||
)
|
||||
}
|
||||
>
|
||||
<span>No. Telepon</span>
|
||||
<ArrowUpDown className="ml-2 h-4 w-4" />
|
||||
</Button>
|
||||
),
|
||||
cell: ({ row }) => (
|
||||
<span>
|
||||
{row.getValue('phone_number') as string ?? '-'}
|
||||
</span>
|
||||
),
|
||||
},
|
||||
{
|
||||
accessorKey: 'address',
|
||||
header: ({ column }) => (
|
||||
<Button
|
||||
variant="ghost"
|
||||
className="-ml-3 h-8"
|
||||
onClick={() =>
|
||||
column.toggleSorting(
|
||||
column.getIsSorted() === 'asc',
|
||||
)
|
||||
}
|
||||
>
|
||||
<span>Alamat</span>
|
||||
<ArrowUpDown className="ml-2 h-4 w-4" />
|
||||
</Button>
|
||||
),
|
||||
cell: ({ row }) => (
|
||||
<span className="max-w-[200px] truncate block">
|
||||
{row.getValue('address') as string ?? '-'}
|
||||
</span>
|
||||
),
|
||||
},
|
||||
{
|
||||
id: 'actions',
|
||||
header: () => <span className="block text-center">Aksi</span>,
|
||||
meta: {
|
||||
className: 'w-[100px] text-center',
|
||||
headerClassName: 'w-[100px] text-center',
|
||||
},
|
||||
cell: ({ row }) => {
|
||||
const customer = row.original;
|
||||
|
||||
return (
|
||||
<TooltipProvider>
|
||||
<div className="flex items-center justify-center gap-1">
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
onClick={() =>
|
||||
handleEdit(customer)
|
||||
}
|
||||
>
|
||||
<Pencil className="h-4 w-4" />
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="top">
|
||||
Edit
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
onClick={() =>
|
||||
handleDeleteClick(customer)
|
||||
}
|
||||
>
|
||||
<Trash2 className="h-4 w-4 text-destructive" />
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="top">
|
||||
Hapus
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</div>
|
||||
</TooltipProvider>
|
||||
);
|
||||
},
|
||||
},
|
||||
];
|
||||
}
|
||||
254
resources/js/pages/admin/master/customer/index.tsx
Normal file
254
resources/js/pages/admin/master/customer/index.tsx
Normal file
@ -0,0 +1,254 @@
|
||||
import { Form, Head, router } from '@inertiajs/react';
|
||||
import { Plus } from 'lucide-react';
|
||||
import { useState } from 'react';
|
||||
import InputError from '@/components/input-error';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from '@/components/ui/dialog';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { Label } from '@/components/ui/label';
|
||||
import { destroy, index as customerIndex, store, update } from '@/routes/admin/master/customers';
|
||||
import { createCustomerColumns } from './columns';
|
||||
import type { Customer } from './columns';
|
||||
import { ConfirmDialog } from '@/components/confirm-dialog';
|
||||
import { DataTable } from '@/components/data-table';
|
||||
|
||||
type Props = {
|
||||
customers: Customer[];
|
||||
};
|
||||
|
||||
export default function CustomerIndex({ customers }: Props) {
|
||||
const [createOpen, setCreateOpen] = useState(false);
|
||||
const [editing, setEditing] = useState<Customer | null>(null);
|
||||
const [deleting, setDeleting] = useState<Customer | null>(null);
|
||||
|
||||
function handleDelete() {
|
||||
if (!deleting) {
|
||||
return;
|
||||
}
|
||||
|
||||
router.delete(destroy(deleting.id), {
|
||||
onSuccess: () => setDeleting(null),
|
||||
});
|
||||
}
|
||||
|
||||
const columns = createCustomerColumns({
|
||||
handleEdit: (customer) => setEditing(customer),
|
||||
handleDeleteClick: (customer) => setDeleting(customer),
|
||||
});
|
||||
|
||||
return (
|
||||
<>
|
||||
<Head title="Customer" />
|
||||
|
||||
<div className="flex h-full flex-1 flex-col gap-6 overflow-x-auto p-4 md:p-6">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<h2 className="text-2xl font-semibold tracking-tight">
|
||||
Customer
|
||||
</h2>
|
||||
</div>
|
||||
<Dialog open={createOpen} onOpenChange={setCreateOpen}>
|
||||
<Button asChild>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setCreateOpen(true)}
|
||||
>
|
||||
<Plus className="h-4 w-4" />
|
||||
Tambah
|
||||
</button>
|
||||
</Button>
|
||||
<DialogContent>
|
||||
<Form action={store()} resetOnSuccess onSuccess={() => setCreateOpen(false)}>
|
||||
{({ errors, processing }) => {
|
||||
|
||||
return (
|
||||
<>
|
||||
<DialogHeader>
|
||||
<DialogTitle>Tambah Customer</DialogTitle>
|
||||
</DialogHeader>
|
||||
<div className="grid gap-4 py-4">
|
||||
<div className="grid gap-2">
|
||||
<Label htmlFor="name">
|
||||
Nama{' '} <span className="text-destructive">*</span>
|
||||
</Label>
|
||||
<Input
|
||||
id="name"
|
||||
name="name"
|
||||
placeholder="Masukkan nama customer"
|
||||
/>
|
||||
<InputError message={errors.name} />
|
||||
</div>
|
||||
<div className="grid gap-2">
|
||||
<Label htmlFor="phone_number">
|
||||
No. Telepon
|
||||
</Label>
|
||||
<Input
|
||||
id="phone_number"
|
||||
name="phone_number"
|
||||
type="tel"
|
||||
inputMode="numeric"
|
||||
pattern="[0-9]*"
|
||||
placeholder="Masukkan nomor telepon"
|
||||
/>
|
||||
<InputError message={errors.phone_number} />
|
||||
</div>
|
||||
<div className="grid gap-2">
|
||||
<Label htmlFor="address">
|
||||
Alamat
|
||||
</Label>
|
||||
<Input
|
||||
id="address"
|
||||
name="address"
|
||||
placeholder="Masukkan alamat"
|
||||
/>
|
||||
<InputError message={errors.address} />
|
||||
</div>
|
||||
</div>
|
||||
<DialogFooter>
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
onClick={() => setCreateOpen(false)}
|
||||
>
|
||||
Batal
|
||||
</Button>
|
||||
<Button
|
||||
type='submit'
|
||||
disabled={processing}
|
||||
>
|
||||
{processing
|
||||
? 'Menyimpan...'
|
||||
: 'Simpan'}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</>
|
||||
);
|
||||
}}
|
||||
</Form>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</div>
|
||||
|
||||
<DataTable
|
||||
columns={columns}
|
||||
data={customers}
|
||||
searchKey="name"
|
||||
searchPlaceholder="Cari customer..."
|
||||
emptyText="Belum ada data customer."
|
||||
/>
|
||||
|
||||
<Dialog
|
||||
open={editing !== null}
|
||||
onOpenChange={(open) => {
|
||||
if (!open) {
|
||||
setEditing(null);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<DialogContent>
|
||||
{editing && (
|
||||
<Form action={update(editing.id)} resetOnSuccess onSuccess={() => setEditing(null)}>
|
||||
{({ errors, processing }) => {
|
||||
|
||||
return (
|
||||
<>
|
||||
<DialogHeader>
|
||||
<DialogTitle>Edit Customer</DialogTitle>
|
||||
</DialogHeader>
|
||||
<div className="grid gap-4 py-4">
|
||||
<div className="grid gap-2">
|
||||
<Label htmlFor="edit-name">Nama{' '} <span className="text-destructive">*</span></Label>
|
||||
<Input
|
||||
id="edit-name"
|
||||
name="name"
|
||||
placeholder="Masukkan nama customer"
|
||||
defaultValue={editing.name}
|
||||
/>
|
||||
<InputError message={errors.name} />
|
||||
</div>
|
||||
<div className="grid gap-2">
|
||||
<Label htmlFor="edit-phone_number">No. Telepon</Label>
|
||||
<Input
|
||||
id="edit-phone_number"
|
||||
name="phone_number"
|
||||
type="tel"
|
||||
inputMode="numeric"
|
||||
pattern="[0-9]*"
|
||||
placeholder="Masukkan nomor telepon"
|
||||
defaultValue={editing.phone_number ?? ''}
|
||||
/>
|
||||
<InputError message={errors.phone_number} />
|
||||
</div>
|
||||
<div className="grid gap-2">
|
||||
<Label htmlFor="edit-address">Alamat</Label>
|
||||
<Input
|
||||
id="edit-address"
|
||||
name="address"
|
||||
placeholder="Masukkan alamat"
|
||||
defaultValue={editing.address ?? ''}
|
||||
/>
|
||||
<InputError message={errors.address} />
|
||||
</div>
|
||||
</div>
|
||||
<DialogFooter>
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
onClick={() => {
|
||||
setEditing(null);
|
||||
}}
|
||||
>
|
||||
Batal
|
||||
</Button>
|
||||
<Button
|
||||
type="submit"
|
||||
disabled={processing}
|
||||
>
|
||||
{processing
|
||||
? 'Menyimpan...'
|
||||
: 'Simpan'}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</>
|
||||
);
|
||||
}}
|
||||
</Form>
|
||||
)}
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
<ConfirmDialog
|
||||
open={deleting !== null}
|
||||
onOpenChange={(open) => {
|
||||
if (!open) {
|
||||
setDeleting(null);
|
||||
}
|
||||
}}
|
||||
title="Hapus Customer"
|
||||
description={`Apakah Anda yakin ingin menghapus customer "${deleting?.name}"? Tindakan ini tidak dapat dibatalkan.`}
|
||||
confirmLabel="Hapus"
|
||||
onConfirm={handleDelete}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
CustomerIndex.layout = {
|
||||
breadcrumbs: [
|
||||
{
|
||||
title: 'Master',
|
||||
href: customerIndex(),
|
||||
},
|
||||
{
|
||||
title: 'Customer',
|
||||
href: customerIndex(),
|
||||
},
|
||||
],
|
||||
};
|
||||
@ -1,6 +1,7 @@
|
||||
<?php
|
||||
|
||||
use App\Http\Controllers\Admin\Master\CategoryController;
|
||||
use App\Http\Controllers\Admin\Master\CustomerController;
|
||||
use App\Http\Controllers\Admin\Master\SupplierController;
|
||||
use Illuminate\Support\Facades\Route;
|
||||
|
||||
@ -12,6 +13,7 @@
|
||||
Route::prefix('admin/master')->name('admin.master.')->group(function () {
|
||||
Route::resource('categories', CategoryController::class)->except(['show', 'create', 'edit']);
|
||||
Route::resource('suppliers', SupplierController::class)->except(['show', 'create', 'edit']);
|
||||
Route::resource('customers', CustomerController::class)->except(['show', 'create', 'edit']);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
181
tests/Feature/Admin/Master/CustomerTest.php
Normal file
181
tests/Feature/Admin/Master/CustomerTest.php
Normal file
@ -0,0 +1,181 @@
|
||||
<?php
|
||||
|
||||
use App\Models\Customer;
|
||||
use App\Models\User;
|
||||
use Inertia\Testing\AssertableInertia as Assert;
|
||||
|
||||
test('guests are redirected to the login page', function () {
|
||||
$response = $this->get(route('admin.master.customers.index'));
|
||||
$response->assertRedirect(route('login'));
|
||||
});
|
||||
|
||||
test('authenticated users can visit the customer index page', function () {
|
||||
$user = User::factory()->create();
|
||||
$this->actingAs($user);
|
||||
|
||||
$response = $this->get(route('admin.master.customers.index'));
|
||||
$response->assertOk();
|
||||
});
|
||||
|
||||
test('customer index page displays customers', function () {
|
||||
$user = User::factory()->create();
|
||||
$this->actingAs($user);
|
||||
|
||||
$customers = Customer::factory()->count(3)->create();
|
||||
|
||||
$response = $this->get(route('admin.master.customers.index'));
|
||||
$response->assertOk();
|
||||
$response->assertInertia(fn (Assert $page) => $page
|
||||
->component('admin/master/customer/index')
|
||||
->has('customers', 3)
|
||||
);
|
||||
});
|
||||
|
||||
test('customer can be created', function () {
|
||||
$user = User::factory()->create();
|
||||
$this->actingAs($user);
|
||||
|
||||
$response = $this->post(route('admin.master.customers.store'), [
|
||||
'name' => 'Customer Test',
|
||||
'phone_number' => '08123456789',
|
||||
'address' => 'Jl. Test No. 1',
|
||||
]);
|
||||
|
||||
$response
|
||||
->assertSessionHasNoErrors()
|
||||
->assertRedirect(route('admin.master.customers.index'));
|
||||
|
||||
$this->assertDatabaseHas('customers', [
|
||||
'name' => 'Customer Test',
|
||||
'phone_number' => '08123456789',
|
||||
'address' => 'Jl. Test No. 1',
|
||||
]);
|
||||
});
|
||||
|
||||
test('customer can be created without optional fields', function () {
|
||||
$user = User::factory()->create();
|
||||
$this->actingAs($user);
|
||||
|
||||
$response = $this->post(route('admin.master.customers.store'), [
|
||||
'name' => 'Customer Minimal',
|
||||
]);
|
||||
|
||||
$response
|
||||
->assertSessionHasNoErrors()
|
||||
->assertRedirect(route('admin.master.customers.index'));
|
||||
|
||||
$this->assertDatabaseHas('customers', [
|
||||
'name' => 'Customer Minimal',
|
||||
]);
|
||||
});
|
||||
|
||||
test('customer name is required', function () {
|
||||
$user = User::factory()->create();
|
||||
$this->actingAs($user);
|
||||
|
||||
$response = $this->post(route('admin.master.customers.store'), [
|
||||
'name' => '',
|
||||
]);
|
||||
|
||||
$response->assertSessionHasErrors('name');
|
||||
});
|
||||
|
||||
test('customer name must not exceed 200 characters', function () {
|
||||
$user = User::factory()->create();
|
||||
$this->actingAs($user);
|
||||
|
||||
$response = $this->post(route('admin.master.customers.store'), [
|
||||
'name' => str_repeat('a', 201),
|
||||
]);
|
||||
|
||||
$response->assertSessionHasErrors('name');
|
||||
});
|
||||
|
||||
test('customer name must be unique', function () {
|
||||
$user = User::factory()->create();
|
||||
$this->actingAs($user);
|
||||
|
||||
Customer::factory()->create(['name' => 'Existing Customer']);
|
||||
|
||||
$response = $this->post(route('admin.master.customers.store'), [
|
||||
'name' => 'Existing Customer',
|
||||
]);
|
||||
|
||||
$response->assertSessionHasErrors('name');
|
||||
});
|
||||
|
||||
test('customer phone_number must be numeric', function () {
|
||||
$user = User::factory()->create();
|
||||
$this->actingAs($user);
|
||||
|
||||
$response = $this->post(route('admin.master.customers.store'), [
|
||||
'name' => 'Customer With Letters',
|
||||
'phone_number' => 'abc123',
|
||||
]);
|
||||
|
||||
$response->assertSessionHasErrors('phone_number');
|
||||
});
|
||||
|
||||
test('customer can be updated', function () {
|
||||
$user = User::factory()->create();
|
||||
$this->actingAs($user);
|
||||
|
||||
$customer = Customer::factory()->create();
|
||||
|
||||
$response = $this->put(route('admin.master.customers.update', $customer), [
|
||||
'name' => 'Customer Updated',
|
||||
'phone_number' => '0987654321',
|
||||
'address' => 'Jl. Updated No. 2',
|
||||
]);
|
||||
|
||||
$response
|
||||
->assertSessionHasNoErrors()
|
||||
->assertRedirect(route('admin.master.customers.index'));
|
||||
|
||||
$customer->refresh();
|
||||
expect($customer->name)->toBe('Customer Updated');
|
||||
expect($customer->phone_number)->toBe('0987654321');
|
||||
expect($customer->address)->toBe('Jl. Updated No. 2');
|
||||
});
|
||||
|
||||
test('customer name can be updated to itself', function () {
|
||||
$user = User::factory()->create();
|
||||
$this->actingAs($user);
|
||||
|
||||
$customer = Customer::factory()->create(['name' => 'My Customer']);
|
||||
|
||||
$response = $this->put(route('admin.master.customers.update', $customer), [
|
||||
'name' => 'My Customer',
|
||||
]);
|
||||
|
||||
$response->assertSessionHasNoErrors();
|
||||
});
|
||||
|
||||
test('customer update name must be unique excluding itself', function () {
|
||||
$user = User::factory()->create();
|
||||
$this->actingAs($user);
|
||||
|
||||
$customer = Customer::factory()->create(['name' => 'First']);
|
||||
Customer::factory()->create(['name' => 'Second']);
|
||||
|
||||
$response = $this->put(route('admin.master.customers.update', $customer), [
|
||||
'name' => 'Second',
|
||||
]);
|
||||
|
||||
$response->assertSessionHasErrors('name');
|
||||
});
|
||||
|
||||
test('customer can be deleted', function () {
|
||||
$user = User::factory()->create();
|
||||
$this->actingAs($user);
|
||||
|
||||
$customer = Customer::factory()->create();
|
||||
|
||||
$response = $this->delete(route('admin.master.customers.destroy', $customer));
|
||||
|
||||
$response
|
||||
->assertSessionHasNoErrors()
|
||||
->assertRedirect(route('admin.master.customers.index'));
|
||||
|
||||
$this->assertSoftDeleted('customers', ['id' => $customer->id]);
|
||||
});
|
||||
Loading…
Reference in New Issue
Block a user