一键导入
feature-custom-non-crud
Workflow untuk fitur non-standar seperti Dashboard, Settings, User Management, atau Halaman Khusus.
用 Codex 或 Claude 帮你安装 复制这段 Prompt,粘贴到 Codex、Claude 或其他助手里,让它检查 Skill 页面并帮你完成安装。
菜单
Workflow untuk fitur non-standar seperti Dashboard, Settings, User Management, atau Halaman Khusus.
用 Codex 或 Claude 帮你安装 复制这段 Prompt,粘贴到 Codex、Claude 或其他助手里,让它检查 Skill 页面并帮你完成安装。
| name | Feature Custom / Non-CRUD |
| description | Workflow untuk fitur non-standar seperti Dashboard, Settings, User Management, atau Halaman Khusus. |
Gunakan skill ini untuk halaman atau fitur yang tidak mengikuti pola CRUD standar.
📚 Referensi: Ikuti pola dari modul User dan Permission
📄 Analisis lengkap: Lihat implementasi diapp/Http/Controllers/UserController.phpdanPermissionController.php🪖 Catatan Helmet: Untuk meta tags, tetap gunakan importreact-helmet-async, tetapi resolver project mengarahkannya ke shim lokalresources/js/lib/react-helmet-async.tsx. Jangan menambah dependency upstream lagi tanpa verifikasi kompatibilitas React 19.
Fitur ini NON-CRUD jika:
├── ❌ TIDAK ada model baru untuk di-CRUD
├── ✅ Bekerja dengan model existing (relasi/linking)
├── ✅ Custom UI (dashboard, matrix, wizard, management)
├── ✅ Routing TIDAK pakai Route::resource
└── ✅ Operations: sync, bulk update, linking, aggregation
Contoh Non-CRUD: users (link user to employee), permissions (bulk assign), dashboard (aggregation)
Bukan Non-CRUD: employees, customers (ini Complex CRUD)
| Tool | Kapan Digunakan |
|---|---|
mcp_laravel-boost_database-schema | Lihat model/tabel existing untuk relasi |
mcp_laravel-boost_list-routes | Lihat routes existing, plan custom routes |
mcp_laravel-boost_search-docs | Cari dokumentasi Laravel ecosystem yang relevan dengan backend project |
mcp_context7_resolve-library-id(...) + mcp_context7_query-docs(...) | Cari dokumentasi React Query, react-router-dom, atau package non-Laravel lain |
mcp_laravel-boost_tinker | Test relationships dan business logic |
activate_shadcn_ui_component_and_block_listing | Cari dashboard/UI blocks dan komponen tersedia |
activate_shadcn_ui_code_retrieval | Aktifkan pengambilan source block/komponen UI |
mcp_shadcn-ui_get_block | Ambil source complex UI blocks |
mcp_shadcn-ui_get_component | Ambil source komponen UI (combobox, tree-view) |
mcp_shadcn-ui_get_component_demo | Lihat demo penggunaan komponen UI bila perlu |
read_file | Baca file referensi User/Permission; paralelkan bila perlu |
Terdapat 2 pattern utama untuk Non-CRUD modules:
Karakteristik:
Kapan Gunakan:
Gunakan Pattern A jika:
├── Ada parent-child relationship (hasOne, hasMany)
├── Child creation/update HARUS via parent
├── Ada conditional logic (create vs update)
└── Perlu transaction safety
Contoh Use Cases:
Karakteristik:
Kapan Gunakan:
Gunakan Pattern B jika:
├── Many-to-many relationship (belongsToMany)
├── Bulk assignment/sync needed
├── Matrix view atau tree hierarchy
└── Read + bulk update only (no create)
Contoh Use Cases:
Backend:
├── app/Actions/{Features}/
│ └── Sync{Feature}For{Parent}Action.php # Business logic untuk sync
├── app/Http/Controllers/{Feature}Controller.php
├── app/Http/Requests/{Features}/
│ └── Update{Feature}Request.php # Conditional validation
├── app/Http/Resources/{Features}/
│ └── {Feature}Resource.php # Simple resource
└── routes/api/{feature}.php
Frontend:
├── resources/js/pages/{features}/index.tsx
└── resources/js/components/{features}/
├── {Feature}Form.tsx # Reusable form component
└── {Parent}SearchModal.tsx # Optional: untuk search parent
Tests:
├── tests/Feature/{Feature}ControllerTest.php
└── tests/Unit/Requests/{Features}/Update{Feature}RequestTest.php
Backend:
├── app/Http/Controllers/{Feature}Controller.php
│ └── index() method returns JSON data
└── routes/api/{feature}.php
Frontend:
├── resources/js/pages/{features}/index.tsx
└── resources/js/components/{features}/
├── {Parent}Selector.tsx # Select parent entity
└── {Feature}Manager.tsx # Matrix/tree view
Tests:
└── tests/Feature/{Feature}ControllerTest.php
<?php
namespace App\Actions\Users;
use App\Models\Employee;
use App\Models\User;
use Illuminate\Support\Facades\DB;
/**
* Create or update a user for a given employee.
*
* This action handles the business logic for syncing user data
* with an employee, including conditional password updates.
*/
class SyncUserForEmployeeAction
{
public function execute(Employee $employee, array $data): User
{
return DB::transaction(function () use ($employee, $data) {
$existingUser = $employee->user;
$userData = [
'name' => $data['name'],
'email' => $data['email'],
];
// Only update password if provided (optional for updates)
if (!empty($data['password'])) {
$userData['password'] = $data['password'];
}
if ($existingUser) {
// Update existing user
$existingUser->update($userData);
$user = $existingUser;
} else {
// Create new user and link to employee
$user = User::create($userData);
$employee->update(['user_id' => $user->id]);
}
return $user;
});
}
}
Key Points:
<?php
namespace App\Http\Requests\Users;
use App\Models\Employee;
use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Validation\Rule;
class UpdateUserRequest extends FormRequest
{
public function authorize(): bool
{
return true;
}
public function rules(): array
{
/** @var Employee $employee */
$employee = $this->route('employee');
$existingUser = $employee->user;
return [
'name' => ['required', 'string', 'max:255'],
'email' => [
'required',
'email',
Rule::unique('users')->ignore($existingUser?->id),
],
// Password is required for NEW users, optional for updates
'password' => [
$existingUser ? 'nullable' : 'required',
'string',
'min:8',
],
];
}
}
Key Points:
<?php
namespace App\Http\Controllers;
use App\Actions\Users\SyncUserForEmployeeAction;
use App\Http\Requests\Users\UpdateUserRequest;
use App\Http\Resources\Users\UserResource;
use App\Models\Employee;
use Illuminate\Http\JsonResponse;
/**
* Controller for user management operations.
*
* Handles user creation, updates, and linking users to employees.
* Note: Frontend page routing ditangani oleh react-router-dom.
*/
class UserController extends Controller
{
/**
* Get user data for an employee.
*
* Returns the user linked to the employee, or null if no user is linked.
*/
public function getUserByEmployee(Employee $employee): JsonResponse
{
$user = $employee->user;
return response()->json([
'user' => $user ? new UserResource($user) : null,
'employee' => [
'name' => $employee->name,
'email' => $employee->email,
],
]);
}
/**
* Create or update user for an employee.
*/
public function updateUser(
UpdateUserRequest $request,
Employee $employee
): JsonResponse {
$user = (new SyncUserForEmployeeAction())->execute(
$employee,
$request->validated()
);
return response()->json([
'message' => 'User updated successfully.',
'user' => new UserResource($user),
]);
}
}
Key Points:
<?php
// routes/api/users.php
// Frontend routing ditangani oleh react-router-dom di app-routes.tsx
use App\Http\Controllers\UserController;
use Illuminate\Support\Facades\Route;
// API routes (nested under parent resource)
Route::middleware('permission:user,true')->group(function () {
Route::get('employees/{employee}/user', [UserController::class, 'getUserByEmployee']);
Route::post('employees/{employee}/user', [UserController::class, 'updateUser']);
});
Key Points:
/api/employees/{employee}/user)app-routes.tsx, BUKAN di sini<?php
namespace App\Http\Resources\Users;
use Illuminate\Http\Request;
use Illuminate\Http\Resources\Json\JsonResource;
class UserResource extends JsonResource
{
public function toArray(Request $request): array
{
return [
'id' => $this->id,
'name' => $this->name,
'email' => $this->email,
'created_at' => $this->created_at?->toISOString(),
'updated_at' => $this->updated_at?->toISOString(),
];
}
}
Key Points:
'use client';
import { useUserManagement } from '@/hooks/useUserManagement';
import { UserForm } from '@/components/users/UserForm';
import AppLayout from '@/layouts/app-layout';
import { Helmet } from 'react-helmet-async';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import { FormProvider } from 'react-hook-form';
const breadcrumbs = [
{ label: 'Home', href: '/' },
{ label: 'User Management' },
];
export default function UsersPage() {
const {
form,
loading,
userExists,
errors,
handleSaveUser,
} = useUserManagement();
return (
<AppLayout breadcrumbs={breadcrumbs}>
<Helmet><title>User Management</title></Helmet>
<div className="flex h-full flex-1 flex-col gap-4 p-4">
<Card>
<CardHeader>
<CardTitle>User Management</CardTitle>
</CardHeader>
<CardContent>
<FormProvider {...form}>
<UserForm
loading={loading}
userExists={userExists}
errors={errors}
onSave={handleSaveUser}
/>
</FormProvider>
</CardContent>
</Card>
</div>
</AppLayout>
);
}
Key Points:
createEntityCrudPage)import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label';
import { Zap } from 'lucide-react';
import { useFormContext } from 'react-hook-form';
interface UserFormProps {
loading: boolean;
userExists: boolean;
errors: {
name?: string[];
email?: string[];
password?: string[];
};
onSave: () => void;
}
export function UserForm({
loading,
userExists,
errors,
onSave,
}: UserFormProps) {
const { register } = useFormContext();
return (
<div className="max-w-md space-y-4">
<div className="space-y-2">
<Label htmlFor="name">User Name</Label>
<Input
id="name"
type="text"
{...register('name')}
placeholder="Enter user name"
disabled={loading}
/>
{errors.name && (
<p className="text-sm text-destructive">{errors.name[0]}</p>
)}
</div>
<div className="space-y-2">
<Label htmlFor="email">User Email</Label>
<Input
id="email"
type="email"
{...register('email')}
placeholder="Enter user email"
disabled={loading}
/>
{errors.email && (
<p className="text-sm text-destructive">
{errors.email[0]}
</p>
)}
</div>
<div className="space-y-2">
<Label htmlFor="password">
User Password{' '}
{!userExists && <span className="text-destructive">*</span>}
</Label>
<Input
id="password"
type="password"
{...register('password')}
placeholder={
userExists
? 'Leave empty to keep current password'
: 'Enter password (required for new user)'
}
disabled={loading}
/>
{errors.password && (
<p className="text-sm text-destructive">
{errors.password[0]}
</p>
)}
</div>
<Button onClick={onSave} disabled={loading} className="w-full">
{loading && <Zap className="mr-2 h-4 w-4 animate-spin" />}
Save Changes
</Button>
</div>
);
}
Key Points:
import { useForm } from 'react-hook-form';
import { useState } from 'react';
import axios from '@/lib/axios';
export function useUserManagement() {
const form = useForm();
const [loading, setLoading] = useState(false);
const [userExists, setUserExists] = useState(false);
const [errors, setErrors] = useState({});
const fetchUser = async (employeeId: number) => {
// Fetch user data via API
const response = await fetch(`/api/employees/${employeeId}/user`);
const data = await response.json();
if (data.user) {
form.reset(data.user);
setUserExists(true);
}
};
const handleSaveUser = async () => {
const employeeId = form.getValues('employee_id');
setLoading(true);
try {
const response = await axios.post(
`/api/employees/${employeeId}/user`,
form.getValues()
);
setErrors({});
} catch (error: unknown) {
if (axios.isAxiosError(error) && error.response?.data?.errors) {
setErrors(error.response.data.errors);
}
} finally {
setLoading(false);
}
};
return {
form,
loading,
userExists,
errors,
fetchUser,
handleSaveUser,
};
}
Key Points:
<?php
namespace App\Http\Controllers;
use App\Models\Permission;
use Illuminate\Http\JsonResponse;
/**
* Controller for permission management operations.
* Note: Frontend page rendering ditangani oleh react-router-dom.
*/
class PermissionController extends Controller
{
/**
* Get all permissions as JSON.
*/
public function index(): JsonResponse
{
$permissions = Permission::orderBy('id')->get();
return response()->json([
'permissions' => $permissions,
]);
}
}
Key Points:
<?php
// routes/api/permissions.php
// Frontend routing ditangani oleh react-router-dom di app-routes.tsx
use App\Http\Controllers\PermissionController;
use App\Http\Controllers\EmployeeController;
use Illuminate\Support\Facades\Route;
// API routes
Route::middleware('permission:permission,true')->group(function () {
Route::get('permissions', [PermissionController::class, 'index']);
Route::get('employees/{employee}/permissions', [EmployeeController::class, 'permissions']);
Route::post('employees/{employee}/permissions', [EmployeeController::class, 'syncPermissions']);
});
Key Points:
'use client';
import { useForm, FormProvider } from 'react-hook-form';
import { usePermissionManagement } from '@/hooks/usePermissionManagement';
import { EmployeeSelector } from '@/components/permissions/EmployeeSelector';
import { PermissionManager } from '@/components/permissions/PermissionManager';
import AppLayout from '@/layouts/app-layout';
import { Helmet } from 'react-helmet-async';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import { useEffect } from 'react';
import { useQuery } from '@tanstack/react-query';
import axios from '@/lib/axios';
export default function PermissionsPage() {
// Fetch permissions via React Query (bukan Inertia props)
const { data: permissionsData } = useQuery({
queryKey: ['permissions'],
queryFn: async () => {
const { data } = await axios.get('/api/permissions');
return data.permissions;
},
});
const permissions = permissionsData ?? [];
const form = useForm();
const {
loading,
selectedPermissions,
setSelectedPermissions,
fetchPermissions,
updatePermissions,
} = usePermissionManagement();
const selectedEmployeeId = form.watch('employee_id');
useEffect(() => {
if (selectedEmployeeId) {
fetchPermissions(selectedEmployeeId);
} else {
setSelectedPermissions([]);
}
}, [selectedEmployeeId, fetchPermissions, setSelectedPermissions]);
const handleSave = () => {
updatePermissions(selectedEmployeeId, selectedPermissions);
};
return (
<AppLayout>
<Helmet><title>Permissions</title></Helmet>
<div className="flex h-full flex-1 flex-col gap-4 p-4">
<Card>
<CardHeader>
<CardTitle>Permissions Hierarchy</CardTitle>
</CardHeader>
<CardContent className="space-y-6">
<div className="max-w-md space-y-4">
<FormProvider {...form}>
<form className="space-y-4">
<EmployeeSelector />
</form>
</FormProvider>
</div>
{selectedEmployeeId && (
<PermissionManager
permissions={permissions}
selectedPermissions={selectedPermissions}
onSelectionChange={setSelectedPermissions}
onSave={handleSave}
loading={loading}
/>
)}
</CardContent>
</Card>
</div>
</AppLayout>
);
}
Key Points:
import AsyncSelectField from '@/components/common/AsyncSelectField';
import { useFormContext } from 'react-hook-form';
export function EmployeeSelector() {
const { control } = useFormContext();
return (
<AsyncSelectField
name="employee_id"
label="Select Employee"
url="/api/employees"
placeholder="Search for an employee..."
labelFn={(item) => item.name}
valueFn={(item) => String(item.id)}
/>
);
}
Key Points:
import { TreeView } from '@/components/tree/tree-view';
import { Button } from '@/components/ui/button';
import { Permission } from '@/types/permission';
import { Zap } from 'lucide-react';
interface PermissionManagerProps {
permissions: Permission[];
selectedPermissions: number[];
onSelectionChange: (ids: number[]) => void;
onSave: () => void;
loading: boolean;
}
export function PermissionManager({
permissions,
selectedPermissions,
onSelectionChange,
onSave,
loading,
}: PermissionManagerProps) {
return (
<div className="space-y-4">
<div className="flex items-center justify-between">
<h3 className="text-lg font-medium">Permissions</h3>
<Button
onClick={onSave}
disabled={loading}
data-testid="save-permissions-btn"
>
{loading && <Zap className="mr-2 h-4 w-4 animate-spin" />}
Save Changes
</Button>
</div>
<div className="rounded-md border p-4">
<TreeView
data={permissions}
selectedIds={selectedPermissions}
onSelectionChange={onSelectionChange}
/>
</div>
</div>
);
}
Key Points:
<?php
use App\Models\Employee;
use App\Models\Permission;
use App\Models\User;
use Illuminate\Foundation\Testing\RefreshDatabase;
use function Pest\Laravel\assertDatabaseHas;
use function Pest\Laravel\getJson;
use function Pest\Laravel\postJson;
use Laravel\Sanctum\Sanctum;
uses(RefreshDatabase::class)->group('users');
/**
* Helper function to create a user with permissions.
*
* This pattern is STANDARD for non-CRUD modules that require auth + permissions.
*/
function createUserWithUserPageAccess(array $permissionNames = []): User
{
$user = User::factory()->create();
$employee = Employee::factory()->create(['user_id' => $user->id]);
if (!empty($permissionNames)) {
$permissions = [];
foreach ($permissionNames as $name) {
$permissions[] = Permission::firstOrCreate(
['name' => $name],
['display_name' => ucwords(str_replace('.', ' ', $name))]
)->id;
}
$employee->permissions()->sync($permissions);
}
return $user;
}
describe('Get User By Employee API (unauthenticated)', function () {
test('unauthenticated user cannot access user API', function () {
$employee = Employee::factory()->create();
$response = getJson("/api/employees/{$employee->id}/user");
$response->assertUnauthorized();
});
});
describe('Get User By Employee API', function () {
beforeEach(function () {
$user = createUserWithUserPageAccess(['user']);
Sanctum::actingAs($user);
});
test('returns null user when employee has no linked user', function () {
$employee = Employee::factory()->create(['user_id' => null]);
$response = getJson("/api/employees/{$employee->id}/user");
$response->assertOk()
->assertJson([
'user' => null,
'employee' => [
'name' => $employee->name,
'email' => $employee->email,
],
]);
});
test('returns user data when employee has linked user', function () {
$linkedUser = User::factory()->create();
$employee = Employee::factory()->create(['user_id' => $linkedUser->id]);
$response = getJson("/api/employees/{$employee->id}/user");
$response->assertOk()
->assertJson([
'user' => [
'id' => $linkedUser->id,
'name' => $linkedUser->name,
'email' => $linkedUser->email,
],
]);
});
});
describe('Update User API', function () {
beforeEach(function () {
$user = createUserWithUserPageAccess(['user']);
Sanctum::actingAs($user);
});
test('creates new user for employee without linked user', function () {
$employee = Employee::factory()->create(['user_id' => null]);
$response = postJson("/api/employees/{$employee->id}/user", [
'name' => 'New User',
'email' => 'newuser@example.com',
'password' => 'password123',
]);
$response->assertOk()
->assertJson([
'message' => 'User updated successfully.',
'user' => [
'name' => 'New User',
'email' => 'newuser@example.com',
],
]);
assertDatabaseHas('users', [
'name' => 'New User',
'email' => 'newuser@example.com',
]);
$employee->refresh();
expect($employee->user_id)->not->toBeNull();
});
test('validates unique email constraint', function () {
$otherUser = User::factory()->create(['email' => 'existing@example.com']);
$employee = Employee::factory()->create(['user_id' => null]);
$response = postJson("/api/employees/{$employee->id}/user", [
'name' => 'New User',
'email' => 'existing@example.com',
'password' => 'password123',
]);
$response->assertUnprocessable()
->assertJsonValidationErrors(['email']);
});
});
Key Points:
beforeEach() untuk reusable setup<?php
use App\Models\Employee;
use App\Models\Permission;
use App\Models\User;
use Illuminate\Foundation\Testing\RefreshDatabase;
use function Pest\Laravel\getJson;
use Laravel\Sanctum\Sanctum;
uses(RefreshDatabase::class)->group('permissions');
function createUserWithPermissionPageAccess(array $permissionNames = []): User
{
$user = User::factory()->create();
$employee = Employee::factory()->create(['user_id' => $user->id]);
if (!empty($permissionNames)) {
$permissions = [];
foreach ($permissionNames as $name) {
$permissions[] = Permission::firstOrCreate(
['name' => $name],
['display_name' => ucwords(str_replace('.', ' ', $name))]
)->id;
}
$employee->permissions()->sync($permissions);
}
return $user;
}
describe('Permission API Access', function () {
test('authenticated user with permission can access permissions API', function () {
$user = createUserWithPermissionPageAccess(['permission']);
Sanctum::actingAs($user);
$response = getJson('/api/permissions');
$response->assertOk()
->assertJsonStructure([
'permissions' => [['id', 'name', 'display_name']],
]);
});
test('permissions API returns all permissions ordered by id', function () {
Permission::factory()->create(['name' => 'test.permission.1']);
Permission::factory()->create(['name' => 'test.permission.2']);
$user = createUserWithPermissionPageAccess(['permission']);
Sanctum::actingAs($user);
$response = getJson('/api/permissions');
$response->assertOk();
$permissions = $response->json('permissions');
expect($permissions)->toBeArray();
foreach ($permissions as $permission) {
expect($permission)->toHaveKeys(['id', 'name', 'display_name']);
}
});
});
Key Points:
| Context | Pattern | Example (User) | Example (Permission) |
|---|---|---|---|
| Controller Class | PascalCase | UserController | PermissionController |
| Action Class | PascalCase, Verb+Entity+Parent | SyncUserForEmployeeAction | - |
| Request Class | PascalCase | UpdateUserRequest | - |
| Component | PascalCase | UserForm | PermissionManager |
| Type | Pattern | Example (User) | Example (Permission) |
|---|---|---|---|
| Route File | kebab-case (di routes/api/) | users.php | permissions.php |
| Component File | PascalCase | UserForm.tsx | EmployeeSelector.tsx |
| Hook File | camelCase | useUserManagement.ts | usePermissionManagement.ts |
| Type | Pattern | Example (User) | Example (Permission) |
|---|---|---|---|
| Frontend Route | kebab-case | /users | /permissions |
| API Nested Route | kebab-case | /api/employees/{employee}/user | /api/employees/{employee}/permissions |
| Route Parameter | camelCase | {employee} | {employee} |
| Permission Name | snake_case | user | permission |
Identifikasi Pattern
Design Endpoints
Pattern A (User):
- GET /api/{parents}/{parent}/{feature} (fetch)
- POST /api/{parents}/{parent}/{feature} (sync)
Pattern B (Permission):
- GET /{features} (page dengan bundled data)
- GET /api/{parents}/{parent}/{features} (fetch)
- POST /api/{parents}/{parent}/{features} (sync)
Baca Referensi
# WAJIB baca file referensi sebelum coding
read_file(filePath: "/absolute/path/to/project/app/Http/Controllers/UserController.php", startLine: 1, endLine: 220)
read_file(filePath: "/absolute/path/to/project/app/Http/Controllers/PermissionController.php", startLine: 1, endLine: 220)
read_file(filePath: "/absolute/path/to/project/tests/Feature/UserControllerTest.php", startLine: 1, endLine: 260)
Action (jika ada business logic kompleks)
// Create action class
app/Actions/{Features}/Sync{Feature}For{Parent}Action.php
Request (conditional validation)
app/Http/Requests/{Features}/Update{Feature}Request.php
Controller
app/Http/Controllers/{Feature}Controller.php
Routes
routes/api/{feature}.php
Resource (if needed)
app/Http/Resources/{Features}/{Feature}Resource.php
Controller
app/Http/Controllers/{Feature}Controller.php
Routes
routes/api/{feature}.php
Create Custom Hook
resources/js/hooks/use{Feature}Management.ts
Create Form Component
resources/js/components/{features}/{Feature}Form.tsx
Create Main Page
resources/js/pages/{features}/index.tsx
Create Components
resources/js/components/{features}/{Parent}Selector.tsx
resources/js/components/{features}/{Feature}Manager.tsx
Create Main Page
resources/js/pages/{features}/index.tsx
Feature Tests
tests/Feature/{Feature}ControllerTest.php
Unit Tests (jika ada Request custom)
tests/Unit/Requests/{Features}/Update{Feature}RequestTest.php
// turbo-all
./vendor/bin/sail test --filter={Feature}
Gunakan mcp_laravel-boost_list-routes untuk verify routes terdaftar.
list-routesPattern A (Parent-Child):
├── {Parent} → Employee, Customer, Organization
├── {Feature} → User, Contact, Setting
├── {feature} → user, contact, setting
└── {features} → users, contacts, settings
Pattern B (Matrix/Bulk):
├── {Parent} → Employee, User, Product
├── {Feature} → Permission, Role, Tag
├── {feature} → permission, role, tag
└── {features} → permissions, roles, tags
Use Actions for Complex Logic
Conditional Validation
Nested Routes
Custom Hooks
Component Composition
Error Handling
Helper Functions
Organization
Coverage
Issue: Validation failing on update
Solution: Ensure Rule::unique()->ignore() is used correctly
Issue: Permission middleware not working
Solution: Check helper creates permissions with correct names
Issue: Frontend not receiving data
Solution: Verify API endpoint returns JSON correctly, check React Query config
Issue: Nested route not found
Solution: Check route parameter name matches (camelCase)
# Check database relationships
mcp_laravel-boost_database-schema()
# Test relationships in tinker
mcp_laravel-boost_tinker(code: "
\$employee = App\\Models\\Employee::first();
\$employee->user;
\$employee->permissions;
")
# Find UI components
activate_shadcn_ui_component_and_block_listing()
activate_shadcn_ui_code_retrieval()
mcp_shadcn-ui_get_component(componentName: "combobox")
mcp_shadcn-ui_get_component_demo(componentName: "combobox")
Always read before implementation:
app/Http/Controllers/UserController.phpapp/Http/Controllers/PermissionController.phptests/Feature/UserControllerTest.phpresources/js/pages/users/index.tsxresources/js/pages/permissions/index.tsxPanduan refactor khusus Backend (Laravel)
Panduan refactor E2E test per modul menggunakan shared test factories, termasuk pertimbangan refactoring frontend/backend agar testing konsisten.
Panduan refactor khusus Frontend (React SPA)
Panduan membuat unit test, feature test, dan e2e test sesuai pola codebase.
Panduan membuat migration, seeder, dan factory sesuai standar Laravel.
Workflow untuk membuat fitur CRUD kompleks dengan relasi foreign key, filter multi-field, dan komponen frontend terpisah.