From 887b811cee922a5c785ade24ddd83a2a6fc5adb3 Mon Sep 17 00:00:00 2001 From: Yoga Pangestu Date: Wed, 10 Jun 2026 15:02:14 +0700 Subject: [PATCH] Refactor admin controllers for categories, products, and raw materials. Introduce new Master namespace for CategoryController, ProductController, and RawMaterialController, enhancing organization and maintainability. Migrate existing logic to dedicated service classes for improved separation of concerns and code clarity. Remove outdated controllers to streamline the codebase. --- .../Controllers/Admin/CategoryController.php | 97 ----- .../Admin/Hr/EmployeeController.php | 207 +---------- .../Admin/Master/CategoryController.php | 59 +++ .../Admin/Master/ProductController.php | 100 ++++++ .../Admin/Master/RawMaterialController.php | 95 +++++ .../Controllers/Admin/ProductController.php | 338 ------------------ .../Admin/RawMaterialController.php | 278 -------------- .../Admin/{ => Master}/CategoryRequest.php | 2 +- .../Admin/{ => Master}/ProductRequest.php | 2 +- .../Admin/{ => Master}/RawMaterialRequest.php | 3 +- app/Models/Employee.php | 16 +- app/Models/ProductPrice.php | 24 ++ app/Models/RawMaterial.php | 17 + app/Models/RawMaterialPrice.php | 35 ++ app/Models/User.php | 15 + app/Models/UserProfile.php | 16 +- app/Services/Hr/EmployeeService.php | 172 +++++++++ app/Services/Master/CategoryService.php | 63 ++++ app/Services/Master/ProductService.php | 181 ++++++++++ app/Services/Master/RawMaterialService.php | 133 +++++++ .../{ => admin}/hr/EmployeeForm.vue | 0 .../{ => admin}/hr/ResetPasswordDialog.vue | 0 .../{ => admin}/hr/employees/columns.ts | 56 ++- .../hr/employees/data-table-actions.vue | 4 +- .../hr/employees/employee-status-toggle.vue | 0 .../master}/categories/CategoryFormModal.vue | 0 .../{ => admin/master}/categories/columns.ts | 2 +- .../master}/categories/data-table-actions.vue | 0 .../master}/products/ProductForm.vue | 0 .../master}/products/ProductGroupedTable.vue | 115 ++---- .../master}/products/data-table-actions.vue | 10 +- .../products/product-status-toggle.vue | 10 +- .../master}/raw-materials/RawMaterialForm.vue | 0 .../raw-materials/RawMaterialGroupedTable.vue | 91 ++--- .../raw-materials/data-table-actions.vue | 10 +- .../raw-material-status-toggle.vue | 10 +- resources/js/components/products/columns.ts | 128 ------- resources/js/lib/products.ts | 28 -- resources/js/lib/raw-materials.ts | 30 -- .../js/pages/admin/hr/employees/Create.vue | 12 +- .../js/pages/admin/hr/employees/Edit.vue | 31 +- .../js/pages/admin/hr/employees/Index.vue | 21 +- .../admin/{ => master}/categories/Index.vue | 4 +- .../admin/{ => master}/products/Create.vue | 9 +- .../admin/{ => master}/products/Edit.vue | 34 +- .../admin/{ => master}/products/Index.vue | 35 +- .../{ => master}/raw-materials/Create.vue | 9 +- .../admin/{ => master}/raw-materials/Edit.vue | 22 +- .../{ => master}/raw-materials/Index.vue | 35 +- resources/js/types/employee.ts | 55 ++- resources/js/types/product.ts | 46 +-- resources/js/types/raw-material.ts | 50 +-- routes/web.php | 22 +- 53 files changed, 1210 insertions(+), 1522 deletions(-) delete mode 100644 app/Http/Controllers/Admin/CategoryController.php create mode 100644 app/Http/Controllers/Admin/Master/CategoryController.php create mode 100644 app/Http/Controllers/Admin/Master/ProductController.php create mode 100644 app/Http/Controllers/Admin/Master/RawMaterialController.php delete mode 100644 app/Http/Controllers/Admin/ProductController.php delete mode 100644 app/Http/Controllers/Admin/RawMaterialController.php rename app/Http/Requests/Admin/{ => Master}/CategoryRequest.php (93%) rename app/Http/Requests/Admin/{ => Master}/ProductRequest.php (97%) rename app/Http/Requests/Admin/{ => Master}/RawMaterialRequest.php (94%) create mode 100644 app/Services/Hr/EmployeeService.php create mode 100644 app/Services/Master/CategoryService.php create mode 100644 app/Services/Master/ProductService.php create mode 100644 app/Services/Master/RawMaterialService.php rename resources/js/components/{ => admin}/hr/EmployeeForm.vue (100%) rename resources/js/components/{ => admin}/hr/ResetPasswordDialog.vue (100%) rename resources/js/components/{ => admin}/hr/employees/columns.ts (71%) rename resources/js/components/{ => admin}/hr/employees/data-table-actions.vue (91%) rename resources/js/components/{ => admin}/hr/employees/employee-status-toggle.vue (100%) rename resources/js/components/{ => admin/master}/categories/CategoryFormModal.vue (100%) rename resources/js/components/{ => admin/master}/categories/columns.ts (89%) rename resources/js/components/{ => admin/master}/categories/data-table-actions.vue (100%) rename resources/js/components/{ => admin/master}/products/ProductForm.vue (100%) rename resources/js/components/{ => admin/master}/products/ProductGroupedTable.vue (58%) rename resources/js/components/{ => admin/master}/products/data-table-actions.vue (86%) rename resources/js/components/{ => admin/master}/products/product-status-toggle.vue (84%) rename resources/js/components/{ => admin/master}/raw-materials/RawMaterialForm.vue (100%) rename resources/js/components/{ => admin/master}/raw-materials/RawMaterialGroupedTable.vue (59%) rename resources/js/components/{ => admin/master}/raw-materials/data-table-actions.vue (87%) rename resources/js/components/{ => admin/master}/raw-materials/raw-material-status-toggle.vue (83%) delete mode 100644 resources/js/components/products/columns.ts delete mode 100644 resources/js/lib/products.ts delete mode 100644 resources/js/lib/raw-materials.ts rename resources/js/pages/admin/{ => master}/categories/Index.vue (95%) rename resources/js/pages/admin/{ => master}/products/Create.vue (81%) rename resources/js/pages/admin/{ => master}/products/Edit.vue (56%) rename resources/js/pages/admin/{ => master}/products/Index.vue (70%) rename resources/js/pages/admin/{ => master}/raw-materials/Create.vue (79%) rename resources/js/pages/admin/{ => master}/raw-materials/Edit.vue (66%) rename resources/js/pages/admin/{ => master}/raw-materials/Index.vue (69%) diff --git a/app/Http/Controllers/Admin/CategoryController.php b/app/Http/Controllers/Admin/CategoryController.php deleted file mode 100644 index 2dc29d4..0000000 --- a/app/Http/Controllers/Admin/CategoryController.php +++ /dev/null @@ -1,97 +0,0 @@ -parseDataTableQuery($request); - $search = $tableQuery['search']; - $sort = $tableQuery['sort']; - $direction = $tableQuery['direction']; - - $query = Category::query() - ->when($search !== '', function (Builder $query) use ($search): void { - $query->where(function (Builder $query) use ($search): void { - $query->where('name', 'like', "%{$search}%"); - }); - }); - - $this->applySorting($query, $sort, $direction); - - $categories = $query - ->paginate(10) - ->withQueryString() - ->through(fn (Category $category) => $this->transformCategory($category)); - - return Inertia::render('admin/categories/Index', [ - 'categories' => $categories, - 'filters' => $this->dataTableFilters($tableQuery), - ]); - } - - public function store(CategoryRequest $request): RedirectResponse - { - Category::create($request->validated()); - - Inertia::flash('success', 'Kategori berhasil ditambahkan.'); - - return redirect()->route('admin.master.categories.index'); - } - - public function update(CategoryRequest $request, Category $category): RedirectResponse - { - $validated = $request->validated(); - $category->name = $validated['name']; - $category->save(); - - Inertia::flash('success', 'Kategori berhasil diperbarui.'); - - return redirect()->route('admin.master.categories.index'); - } - - public function destroy(Category $category): RedirectResponse - { - $category->delete(); - - Inertia::flash('success', 'Kategori berhasil dihapus.'); - - return redirect()->route('admin.master.categories.index'); - } - - private function applySorting(Builder $query, string $sort, string $direction): void - { - if (in_array($sort, ['name', 'slug'], true)) { - $query->orderBy($sort, $direction); - - return; - } - - $query->latest(); - } - - /** - * @return array - */ - private function transformCategory(Category $category): array - { - return [ - 'id' => $category->id, - 'name' => $category->name, - 'slug' => $category->slug, - ]; - } -} diff --git a/app/Http/Controllers/Admin/Hr/EmployeeController.php b/app/Http/Controllers/Admin/Hr/EmployeeController.php index 13205bf..8072ab9 100644 --- a/app/Http/Controllers/Admin/Hr/EmployeeController.php +++ b/app/Http/Controllers/Admin/Hr/EmployeeController.php @@ -8,14 +8,10 @@ use App\Http\Controllers\Concerns\ParsesDataTableQuery; use App\Http\Controllers\Controller; use App\Http\Requests\Admin\Hr\EmployeeRequest; -use App\Models\Employee; use App\Models\User; -use App\Models\UserProfile; -use Illuminate\Database\Eloquent\Builder; +use App\Services\Hr\EmployeeService; use Illuminate\Http\RedirectResponse; use Illuminate\Http\Request; -use Illuminate\Support\Facades\DB; -use Illuminate\Support\Facades\Hash; use Inertia\Inertia; use Inertia\Response; @@ -23,42 +19,17 @@ class EmployeeController extends Controller { use ParsesDataTableQuery; + public function __construct( + private readonly EmployeeService $employeeService, + ) {} + public function index(Request $request): Response { $tableQuery = $this->parseDataTableQuery($request); - $search = $tableQuery['search']; - $sort = $tableQuery['sort']; - $direction = $tableQuery['direction']; - $employmentStatus = $request->string('employment_status')->toString(); - $query = User::query() - ->with(['profile', 'employee', 'roles']) - ->whereHas('employee') - ->when($search !== '', function ($query) use ($search): void { - $query->where(function ($query) use ($search): void { - $query->where('email', 'like', "%{$search}%") - ->orWhere('username', 'like', "%{$search}%") - ->orWhereHas('profile', function ($query) use ($search): void { - $query->where('full_name', 'like', "%{$search}%") - ->orWhere('phone_number', 'like', "%{$search}%"); - }); - }); - }) - ->when( - $employmentStatus !== '', - fn ($query) => $query->whereHas('employee', fn ($query) => $query->where('employment_status', $employmentStatus)) - ); - - $this->applySorting($query, $sort, $direction); - - $employees = $query - ->paginate(10) - ->withQueryString() - ->through(fn (User $user) => $this->transformEmployee($user)); - return Inertia::render('admin/hr/employees/Index', [ - 'employees' => $employees, + 'employees' => $this->employeeService->paginateForIndex($tableQuery, $employmentStatus), 'filters' => $this->dataTableFilters($tableQuery, [ 'employment_status' => $employmentStatus, ]), @@ -77,33 +48,7 @@ public function create(): Response public function store(EmployeeRequest $request): RedirectResponse { - $validated = $request->validated(); - - DB::transaction(function () use ($validated): void { - $user = User::create([ - 'email' => $validated['email'], - 'username' => $validated['username'], - 'password' => Hash::make(config('auth.password_default')), - ]); - - UserProfile::create([ - 'user_id' => $user->id, - 'full_name' => $validated['full_name'], - 'phone_number' => $validated['phone_number'], - 'gender' => $validated['gender'], - 'birth_date' => $validated['birth_date'], - 'address' => $validated['address'], - ]); - - Employee::create([ - 'user_id' => $user->id, - 'join_date' => $validated['join_date'], - 'employment_status' => $validated['employment_status'], - 'base_salary' => $validated['base_salary'], - ]); - - $user->syncRoles([$validated['role']]); - }); + $this->employeeService->create($request->validated()); Inertia::flash('success', 'Pegawai berhasil ditambahkan.'); @@ -118,35 +63,13 @@ public function edit(User $user): Response 'genders' => Gender::selectOptions(), 'employmentStatuses' => EmploymentStatus::selectOptions(), 'roles' => Role::assignableSelectOptions(), - 'employee' => $this->transformEmployeeForForm($user), + 'employee' => $user, ]); } public function update(EmployeeRequest $request, User $user): RedirectResponse { - $validated = $request->validated(); - $employee = $user->employee; - - DB::transaction(function () use ($validated, $user, $employee): void { - $user->email = $validated['email']; - $user->username = $validated['username']; - $user->save(); - - $profile = $user->profile; - $profile->full_name = $validated['full_name']; - $profile->phone_number = $validated['phone_number']; - $profile->gender = $validated['gender']; - $profile->birth_date = $validated['birth_date']; - $profile->address = $validated['address']; - $profile->save(); - - $employee->join_date = $validated['join_date']; - $employee->employment_status = $validated['employment_status']; - $employee->base_salary = $validated['base_salary']; - $employee->save(); - - $user->syncRoles([$validated['role']]); - }); + $this->employeeService->update($user, $request->validated()); Inertia::flash('success', 'Data pegawai berhasil diperbarui.'); @@ -159,12 +82,7 @@ public function toggleStatus(Request $request, User $user): RedirectResponse 'is_active' => ['required', 'boolean'], ]); - $user->is_active = $validated['is_active']; - $user->save(); - - if (! $validated['is_active']) { - DB::table('sessions')->where('user_id', $user->id)->delete(); - } + $this->employeeService->toggleStatus($user, $validated['is_active']); Inertia::flash('success', 'Status pegawai berhasil diperbarui.'); @@ -173,10 +91,7 @@ public function toggleStatus(Request $request, User $user): RedirectResponse public function resetPassword(User $user): RedirectResponse { - $user->password = config('auth.password_default'); - $user->save(); - - DB::table('sessions')->where('user_id', $user->id)->delete(); + $this->employeeService->resetPassword($user); Inertia::flash('success', 'Kata sandi berhasil direset. Pengguna telah logout dari semua sesi.'); @@ -185,108 +100,10 @@ public function resetPassword(User $user): RedirectResponse public function destroy(User $user): RedirectResponse { - DB::transaction(function () use ($user): void { - $user->employee?->delete(); - $user->profile?->delete(); - $user->delete(); - }); + $this->employeeService->delete($user); Inertia::flash('success', 'Pegawai berhasil dihapus.'); return redirect()->route('admin.hr.employees.index'); } - - private function applySorting(Builder $query, string $sort, string $direction): void - { - $employeeSorts = [ - 'join_date', - 'base_salary', - 'employment_status', - ]; - - if (in_array($sort, $employeeSorts, true)) { - $query->orderBy( - Employee::select($sort) - ->whereColumn('employees.user_id', 'users.id') - ->limit(1), - $direction - ); - - return; - } - - if ($sort === 'full_name') { - $query->orderBy( - UserProfile::select('full_name') - ->whereColumn('user_profiles.user_id', 'users.id') - ->limit(1), - $direction - ); - - return; - } - - if ($sort === 'email') { - $query->orderBy('email', $direction); - - return; - } - - $query->latest(); - } - - /** - * @return array - */ - private function transformEmployeeForForm(User $user): array - { - $employee = $user->employee; - $profile = $user->profile; - - return [ - 'id' => $user->id, - 'email' => $user->email, - 'username' => $user->username, - 'full_name' => $profile->full_name, - 'phone_number' => $profile->phone_number, - 'gender' => $profile->gender?->value, - 'birth_date' => $profile->birth_date?->format('Y-m-d'), - 'address' => $profile->address, - 'join_date' => $employee->join_date?->format('Y-m-d'), - 'employment_status' => $employee->employment_status?->value, - 'base_salary' => $employee->base_salary, - 'role' => $user->roles->first()?->name, - ]; - } - - /** - * @return array - */ - private function transformEmployee(User $user): array - { - $employee = $user->employee; - $profile = $user->profile; - $role = $user->roles->first(); - - return [ - 'id' => $user->id, - 'join_date' => $employee->join_date_formatted, - 'resign_date' => $employee->resign_date_formatted, - 'employment_status' => $employee->employment_status?->value, - 'employment_status_label' => $employee->employment_status?->label(), - 'base_salary' => $employee->base_salary, - 'base_salary_formatted' => $employee->base_salary_formatted, - 'email' => $user->email, - 'username' => $user->username, - 'is_active' => $user->is_active, - 'full_name' => $profile->full_name, - 'phone_number' => $profile->phone_number, - 'gender' => $profile->gender?->value, - 'gender_label' => $profile->gender?->label(), - 'birth_date' => $profile->birth_date_formatted, - 'address' => $profile->address, - 'role' => $role?->name, - 'role_label' => $role ? Role::from($role->name)->label() : null, - ]; - } } diff --git a/app/Http/Controllers/Admin/Master/CategoryController.php b/app/Http/Controllers/Admin/Master/CategoryController.php new file mode 100644 index 0000000..57cdb65 --- /dev/null +++ b/app/Http/Controllers/Admin/Master/CategoryController.php @@ -0,0 +1,59 @@ +parseDataTableQuery($request); + + return Inertia::render('admin/master/categories/Index', [ + 'categories' => $this->categoryService->paginateForIndex($tableQuery), + 'filters' => $this->dataTableFilters($tableQuery), + ]); + } + + public function store(CategoryRequest $request): RedirectResponse + { + $this->categoryService->create($request->validated()); + + Inertia::flash('success', 'Kategori berhasil ditambahkan.'); + + return redirect()->route('admin.master.categories.index'); + } + + public function update(CategoryRequest $request, Category $category): RedirectResponse + { + $this->categoryService->update($category, $request->validated()); + + Inertia::flash('success', 'Kategori berhasil diperbarui.'); + + return redirect()->route('admin.master.categories.index'); + } + + public function destroy(Category $category): RedirectResponse + { + $this->categoryService->delete($category); + + Inertia::flash('success', 'Kategori berhasil dihapus.'); + + return redirect()->route('admin.master.categories.index'); + } +} diff --git a/app/Http/Controllers/Admin/Master/ProductController.php b/app/Http/Controllers/Admin/Master/ProductController.php new file mode 100644 index 0000000..6eb5a8a --- /dev/null +++ b/app/Http/Controllers/Admin/Master/ProductController.php @@ -0,0 +1,100 @@ +parseDataTableQuery($request); + $isActive = $request->string('is_active')->toString(); + + return Inertia::render('admin/master/products/Index', [ + 'products' => $this->productService->paginateForIndex($tableQuery, $isActive), + 'filters' => $this->dataTableFilters($tableQuery, [ + 'is_active' => $isActive, + ]), + ]); + } + + public function create(): Response + { + return Inertia::render('admin/master/products/Create', [ + 'categories' => $this->productService->categoryOptions(), + 'priceTypes' => PriceType::selectOptions(), + ]); + } + + public function store(ProductRequest $request): RedirectResponse + { + $this->productService->create($request->validated()); + + Inertia::flash('success', 'Produk berhasil ditambahkan.'); + + return redirect()->route('admin.master.products.index'); + } + + public function edit(Product $product): Response + { + $product->load([ + 'categories', + 'variants' => fn ($query) => $query + ->with(['prices' => fn ($query) => $query->orderBy('type')]) + ->orderBy('created_at'), + ]); + + return Inertia::render('admin/master/products/Edit', [ + 'categories' => $this->productService->categoryOptions(), + 'priceTypes' => PriceType::selectOptions(), + 'product' => $product, + ]); + } + + public function update(ProductRequest $request, Product $product): RedirectResponse + { + $this->productService->update($product, $request->validated()); + + Inertia::flash('success', 'Produk berhasil diperbarui.'); + + return redirect()->route('admin.master.products.index'); + } + + public function toggleStatus(Request $request, Product $product): RedirectResponse + { + $validated = $request->validate([ + 'is_active' => ['required', 'boolean'], + ]); + + $this->productService->toggleStatus($product, $validated['is_active']); + + Inertia::flash('success', 'Status produk berhasil diperbarui.'); + + return back(); + } + + public function destroy(Product $product): RedirectResponse + { + $this->productService->delete($product); + + Inertia::flash('success', 'Produk berhasil dihapus.'); + + return redirect()->route('admin.master.products.index'); + } +} diff --git a/app/Http/Controllers/Admin/Master/RawMaterialController.php b/app/Http/Controllers/Admin/Master/RawMaterialController.php new file mode 100644 index 0000000..07ce9ae --- /dev/null +++ b/app/Http/Controllers/Admin/Master/RawMaterialController.php @@ -0,0 +1,95 @@ +parseDataTableQuery($request); + $isActive = $request->string('is_active')->toString(); + + return Inertia::render('admin/master/raw-materials/Index', [ + 'rawMaterials' => $this->rawMaterialService->paginateForIndex($tableQuery, $isActive), + 'filters' => $this->dataTableFilters($tableQuery, [ + 'is_active' => $isActive, + ]), + ]); + } + + public function create(): Response + { + return Inertia::render('admin/master/raw-materials/Create', [ + 'units' => RawMaterialUnit::selectOptions(), + ]); + } + + public function store(RawMaterialRequest $request): RedirectResponse + { + $this->rawMaterialService->create($request->validated()); + + Inertia::flash('success', 'Bahan baku berhasil ditambahkan.'); + + return redirect()->route('admin.master.raw-materials.index'); + } + + public function edit(RawMaterial $rawMaterial): Response + { + $rawMaterial->load([ + 'prices' => fn ($query) => $query->orderBy('created_at'), + ]); + + return Inertia::render('admin/master/raw-materials/Edit', [ + 'units' => RawMaterialUnit::selectOptions(), + 'rawMaterial' => $rawMaterial, + ]); + } + + public function update(RawMaterialRequest $request, RawMaterial $rawMaterial): RedirectResponse + { + $this->rawMaterialService->update($rawMaterial, $request->validated()); + + Inertia::flash('success', 'Bahan baku berhasil diperbarui.'); + + return redirect()->route('admin.master.raw-materials.index'); + } + + public function toggleStatus(Request $request, RawMaterial $rawMaterial): RedirectResponse + { + $validated = $request->validate([ + 'is_active' => ['required', 'boolean'], + ]); + + $this->rawMaterialService->toggleStatus($rawMaterial, $validated['is_active']); + + Inertia::flash('success', 'Status bahan baku berhasil diperbarui.'); + + return back(); + } + + public function destroy(RawMaterial $rawMaterial): RedirectResponse + { + $this->rawMaterialService->delete($rawMaterial); + + Inertia::flash('success', 'Bahan baku berhasil dihapus.'); + + return redirect()->route('admin.master.raw-materials.index'); + } +} diff --git a/app/Http/Controllers/Admin/ProductController.php b/app/Http/Controllers/Admin/ProductController.php deleted file mode 100644 index a8c1537..0000000 --- a/app/Http/Controllers/Admin/ProductController.php +++ /dev/null @@ -1,338 +0,0 @@ -parseDataTableQuery($request); - $search = $tableQuery['search']; - $sort = $tableQuery['sort']; - $direction = $tableQuery['direction']; - $isActive = $request->string('is_active')->toString(); - - $query = Product::query() - ->with([ - 'categories', - 'variants' => fn ($query) => $query - ->with(['prices' => fn ($query) => $query->orderBy('type')]) - ->orderBy('created_at'), - ]) - ->when($search !== '', function (Builder $query) use ($search): void { - $query->where(function (Builder $query) use ($search): void { - $query->where('name', 'like', "%{$search}%") - ->orWhere('slug', 'like', "%{$search}%") - ->orWhere('description', 'like', "%{$search}%") - ->orWhereHas('categories', fn (Builder $query) => $query->where('name', 'like', "%{$search}%")) - ->orWhereHas('variants', fn (Builder $query) => $query->where('name', 'like', "%{$search}%")); - }); - }) - ->when( - $isActive !== '', - fn (Builder $query) => $query->where('is_active', $isActive === '1') - ); - - $this->applySorting($query, $sort, $direction); - - $products = $query - ->paginate(10) - ->withQueryString(); - - $rows = $products->getCollection() - ->flatMap(fn (Product $product, int $index) => $this->flattenProductForTable($product, $index, $products->firstItem() ?? 1)) - ->values() - ->all(); - - return Inertia::render('admin/products/Index', [ - 'rows' => $rows, - 'pagination' => [ - 'current_page' => $products->currentPage(), - 'last_page' => $products->lastPage(), - 'per_page' => $products->perPage(), - 'total' => $products->total(), - 'links' => $products->linkCollection()->toArray(), - ], - 'filters' => $this->dataTableFilters($tableQuery, [ - 'is_active' => $isActive, - ]), - ]); - } - - public function create(): Response - { - return Inertia::render('admin/products/Create', [ - 'categories' => $this->categoryOptions(), - 'priceTypes' => PriceType::selectOptions(), - ]); - } - - public function store(ProductRequest $request): RedirectResponse - { - $validated = $request->validated(); - - DB::transaction(function () use ($validated): void { - $product = Product::create([ - 'name' => $validated['name'], - 'description' => $validated['description'] ?? null, - 'is_active' => true, - ]); - - $product->categories()->sync($validated['category_ids']); - - foreach ($validated['variants'] as $variantData) { - $this->createVariant($product, $variantData); - } - }); - - Inertia::flash('success', 'Produk berhasil ditambahkan.'); - - return redirect()->route('admin.master.products.index'); - } - - public function edit(Product $product): Response - { - $product->load([ - 'categories', - 'variants' => fn ($query) => $query - ->with(['prices' => fn ($query) => $query->orderBy('type')]) - ->orderBy('created_at'), - ]); - - return Inertia::render('admin/products/Edit', [ - 'categories' => $this->categoryOptions(), - 'priceTypes' => PriceType::selectOptions(), - 'product' => $this->transformProductForForm($product), - ]); - } - - public function update(ProductRequest $request, Product $product): RedirectResponse - { - $validated = $request->validated(); - - DB::transaction(function () use ($validated, $product): void { - $product->name = $validated['name']; - $product->description = $validated['description'] ?? null; - $product->save(); - - $product->categories()->sync($validated['category_ids']); - - $submittedVariantIds = collect($validated['variants']) - ->pluck('id') - ->filter() - ->map(fn ($id) => (int) $id) - ->all(); - - $product->variants() - ->whereNotIn('id', $submittedVariantIds) - ->get() - ->each(fn (ProductVariant $variant) => $variant->delete()); - - foreach ($validated['variants'] as $variantData) { - if (! empty($variantData['id'])) { - $variant = $product->variants()->findOrFail($variantData['id']); - $variant->name = $variantData['name']; - $variant->stock = $variantData['stock']; - $variant->save(); - $this->syncVariantPrices($variant, $variantData['prices']); - - continue; - } - - $this->createVariant($product, $variantData); - } - }); - - Inertia::flash('success', 'Produk berhasil diperbarui.'); - - return redirect()->route('admin.master.products.index'); - } - - public function toggleStatus(Request $request, Product $product): RedirectResponse - { - $validated = $request->validate([ - 'is_active' => ['required', 'boolean'], - ]); - - $product->is_active = $validated['is_active']; - $product->save(); - - Inertia::flash('success', 'Status produk berhasil diperbarui.'); - - return back(); - } - - public function destroy(Product $product): RedirectResponse - { - DB::transaction(function () use ($product): void { - $product->variants()->delete(); - $product->categories()->detach(); - $product->delete(); - }); - - Inertia::flash('success', 'Produk berhasil dihapus.'); - - return redirect()->route('admin.master.products.index'); - } - - private function applySorting(Builder $query, string $sort, string $direction): void - { - if (in_array($sort, ['name', 'slug', 'is_active'], true)) { - $query->orderBy($sort, $direction); - - return; - } - - $query->latest(); - } - - /** - * @return list - */ - private function categoryOptions(): array - { - return Category::query() - ->orderBy('name') - ->get(['id', 'name']) - ->map(fn (Category $category) => [ - 'value' => $category->id, - 'label' => $category->name, - ]) - ->all(); - } - - /** - * @param array $variantData - */ - private function createVariant(Product $product, array $variantData): ProductVariant - { - $variant = $product->variants()->create([ - 'name' => $variantData['name'], - 'stock' => $variantData['stock'], - ]); - - $this->syncVariantPrices($variant, $variantData['prices']); - - return $variant; - } - - /** - * @param list $prices - */ - private function syncVariantPrices(ProductVariant $variant, array $prices): void - { - foreach ($prices as $priceData) { - ProductPrice::updateOrCreate( - [ - 'variant_id' => $variant->id, - 'type' => $priceData['type'], - ], - [ - 'price' => $priceData['price'], - ], - ); - } - } - - /** - * @return list> - */ - private function flattenProductForTable(Product $product, int $productIndex, int $firstItem): array - { - $variants = $product->variants->isNotEmpty() - ? $product->variants - : collect([null]); - - $variantCount = $variants->count(); - - return $variants - ->values() - ->map(function ($variant, int $variantIndex) use ($product, $productIndex, $firstItem, $variantCount) { - return [ - 'row_id' => $variant - ? "{$product->id}-{$variant->id}" - : "{$product->id}-empty", - 'product_id' => $product->id, - 'product_name' => $product->name, - 'categories' => $product->categories - ->map(fn (Category $category) => [ - 'id' => $category->id, - 'name' => $category->name, - ]) - ->values() - ->all(), - 'is_active' => $product->is_active, - 'is_first_variant' => $variantIndex === 0, - 'variant_count' => $variantCount, - 'product_row_number' => $firstItem + $productIndex, - 'variant_id' => $variant?->id, - 'variant_name' => $variant?->name, - 'stock' => $variant?->stock, - 'prices' => $variant - ? $variant->prices - ->map(fn (ProductPrice $price) => [ - 'type' => $price->type->value, - 'type_label' => $price->type->label(), - 'price' => $price->price, - 'price_formatted' => $this->formatPrice($price->price), - ]) - ->values() - ->all() - : [], - ]; - }) - ->all(); - } - - /** - * @return array - */ - private function transformProductForForm(Product $product): array - { - return [ - 'id' => $product->id, - 'name' => $product->name, - 'description' => $product->description, - 'category_ids' => $product->categories->pluck('id')->all(), - 'variants' => $product->variants - ->map(fn (ProductVariant $variant) => [ - 'id' => $variant->id, - 'name' => $variant->name, - 'stock' => $variant->stock, - 'prices' => collect(PriceType::cases()) - ->mapWithKeys(function (PriceType $type) use ($variant) { - $price = $variant->prices->firstWhere('type', $type); - - return [ - $type->value => $price ? (string) (int) $price->price : '', - ]; - }) - ->all(), - ]) - ->values() - ->all(), - ]; - } - - private function formatPrice(float|string $price): string - { - return 'Rp '.number_format((float) $price, 0, ',', '.'); - } -} diff --git a/app/Http/Controllers/Admin/RawMaterialController.php b/app/Http/Controllers/Admin/RawMaterialController.php deleted file mode 100644 index 80fc710..0000000 --- a/app/Http/Controllers/Admin/RawMaterialController.php +++ /dev/null @@ -1,278 +0,0 @@ -parseDataTableQuery($request); - $search = $tableQuery['search']; - $sort = $tableQuery['sort']; - $direction = $tableQuery['direction']; - $isActive = $request->string('is_active')->toString(); - - $query = RawMaterial::query() - ->with([ - 'prices' => fn ($query) => $query->orderBy('created_at'), - ]) - ->when($search !== '', function (Builder $query) use ($search): void { - $query->where(function (Builder $query) use ($search): void { - $query->where('name', 'like', "%{$search}%") - ->orWhereHas('prices', fn (Builder $query) => $query->where('variant', 'like', "%{$search}%")); - }); - }) - ->when( - $isActive !== '', - fn (Builder $query) => $query->where('is_active', $isActive === '1') - ); - - $this->applySorting($query, $sort, $direction); - - $rawMaterials = $query - ->paginate(10) - ->withQueryString(); - - $rows = $rawMaterials->getCollection() - ->flatMap(fn (RawMaterial $rawMaterial, int $index) => $this->flattenRawMaterialForTable( - $rawMaterial, - $index, - $rawMaterials->firstItem() ?? 1, - )) - ->values() - ->all(); - - return Inertia::render('admin/raw-materials/Index', [ - 'rows' => $rows, - 'pagination' => [ - 'current_page' => $rawMaterials->currentPage(), - 'last_page' => $rawMaterials->lastPage(), - 'per_page' => $rawMaterials->perPage(), - 'total' => $rawMaterials->total(), - 'links' => $rawMaterials->linkCollection()->toArray(), - ], - 'filters' => $this->dataTableFilters($tableQuery, [ - 'is_active' => $isActive, - ]), - ]); - } - - public function create(): Response - { - return Inertia::render('admin/raw-materials/Create', [ - 'units' => RawMaterialUnit::selectOptions(), - ]); - } - - public function store(RawMaterialRequest $request): RedirectResponse - { - $validated = $request->validated(); - - DB::transaction(function () use ($validated): void { - $rawMaterial = RawMaterial::create([ - 'name' => $validated['name'], - 'unit' => $validated['unit'], - ]); - - foreach ($validated['prices'] as $priceData) { - $this->createPrice($rawMaterial, $priceData); - } - }); - - Inertia::flash('success', 'Bahan baku berhasil ditambahkan.'); - - return redirect()->route('admin.master.raw-materials.index'); - } - - public function edit(RawMaterial $rawMaterial): Response - { - $rawMaterial->load([ - 'prices' => fn ($query) => $query->orderBy('created_at'), - ]); - - return Inertia::render('admin/raw-materials/Edit', [ - 'units' => RawMaterialUnit::selectOptions(), - 'rawMaterial' => $this->transformRawMaterialForForm($rawMaterial), - ]); - } - - public function update(RawMaterialRequest $request, RawMaterial $rawMaterial): RedirectResponse - { - $validated = $request->validated(); - - DB::transaction(function () use ($validated, $rawMaterial): void { - $rawMaterial->name = $validated['name']; - $rawMaterial->unit = $validated['unit']; - $rawMaterial->save(); - - $submittedPriceIds = collect($validated['prices']) - ->pluck('id') - ->filter() - ->map(fn ($id) => (int) $id) - ->all(); - - $rawMaterial->prices() - ->whereNotIn('id', $submittedPriceIds) - ->get() - ->each(fn (RawMaterialPrice $price) => $price->delete()); - - foreach ($validated['prices'] as $priceData) { - if (! empty($priceData['id'])) { - $price = $rawMaterial->prices()->findOrFail($priceData['id']); - $price->variant = $priceData['variant']; - $price->price = $priceData['price']; - $price->stock = $priceData['stock']; - $price->save(); - - continue; - } - - $this->createPrice($rawMaterial, $priceData); - } - }); - - Inertia::flash('success', 'Bahan baku berhasil diperbarui.'); - - return redirect()->route('admin.master.raw-materials.index'); - } - - public function toggleStatus(Request $request, RawMaterial $rawMaterial): RedirectResponse - { - $validated = $request->validate([ - 'is_active' => ['required', 'boolean'], - ]); - - $rawMaterial->is_active = $validated['is_active']; - $rawMaterial->save(); - - Inertia::flash('success', 'Status bahan baku berhasil diperbarui.'); - - return back(); - } - - public function destroy(RawMaterial $rawMaterial): RedirectResponse - { - DB::transaction(function () use ($rawMaterial): void { - $rawMaterial->prices()->delete(); - $rawMaterial->delete(); - }); - - Inertia::flash('success', 'Bahan baku berhasil dihapus.'); - - return redirect()->route('admin.master.raw-materials.index'); - } - - private function applySorting(Builder $query, string $sort, string $direction): void - { - if (in_array($sort, ['name', 'unit', 'is_active'], true)) { - $query->orderBy($sort, $direction); - - return; - } - - $query->latest(); - } - - /** - * @param array $priceData - */ - private function createPrice(RawMaterial $rawMaterial, array $priceData): RawMaterialPrice - { - return $rawMaterial->prices()->create([ - 'variant' => $priceData['variant'], - 'price' => $priceData['price'], - 'stock' => $priceData['stock'], - ]); - } - - /** - * @return list> - */ - private function flattenRawMaterialForTable(RawMaterial $rawMaterial, int $materialIndex, int $firstItem): array - { - $prices = $rawMaterial->prices->isNotEmpty() - ? $rawMaterial->prices - : collect([null]); - - $variantCount = $prices->count(); - - return $prices - ->values() - ->map(function ($price, int $variantIndex) use ($rawMaterial, $materialIndex, $firstItem, $variantCount) { - return [ - 'row_id' => $price - ? "{$rawMaterial->id}-{$price->id}" - : "{$rawMaterial->id}-empty", - 'raw_material_id' => $rawMaterial->id, - 'raw_material_name' => $rawMaterial->name, - 'unit' => $rawMaterial->unit->value, - 'unit_label' => $rawMaterial->unit->label(), - 'unit_abbreviation' => $rawMaterial->unit->abbreviation(), - 'is_active' => $rawMaterial->is_active, - 'is_first_variant' => $variantIndex === 0, - 'variant_count' => $variantCount, - 'raw_material_row_number' => $firstItem + $materialIndex, - 'price_id' => $price?->id, - 'variant' => $price?->variant, - 'stock' => $price?->stock, - 'stock_formatted' => $price ? $this->formatStock($price->stock, $rawMaterial->unit) : null, - 'price' => $price?->price, - 'price_formatted' => $price ? $this->formatPrice($price->price) : null, - ]; - }) - ->all(); - } - - /** - * @return array - */ - private function transformRawMaterialForForm(RawMaterial $rawMaterial): array - { - return [ - 'id' => $rawMaterial->id, - 'name' => $rawMaterial->name, - 'unit' => $rawMaterial->unit->value, - 'prices' => $rawMaterial->prices - ->map(fn (RawMaterialPrice $price) => [ - 'id' => $price->id, - 'variant' => $price->variant, - 'price' => (string) (int) $price->price, - 'stock' => $this->formatStockInput($price->stock), - ]) - ->values() - ->all(), - ]; - } - - private function formatPrice(float|string $price): string - { - return 'Rp '.number_format((float) $price, 0, ',', '.'); - } - - private function formatStock(float|string $stock, RawMaterialUnit $unit): string - { - $formatted = rtrim(rtrim(number_format((float) $stock, 4, ',', '.'), '0'), ','); - - return "{$formatted} {$unit->abbreviation()}"; - } - - private function formatStockInput(float|string $stock): string - { - return rtrim(rtrim(number_format((float) $stock, 4, '.', ''), '0'), '.'); - } -} diff --git a/app/Http/Requests/Admin/CategoryRequest.php b/app/Http/Requests/Admin/Master/CategoryRequest.php similarity index 93% rename from app/Http/Requests/Admin/CategoryRequest.php rename to app/Http/Requests/Admin/Master/CategoryRequest.php index cc3f55c..e8ea83a 100644 --- a/app/Http/Requests/Admin/CategoryRequest.php +++ b/app/Http/Requests/Admin/Master/CategoryRequest.php @@ -1,6 +1,6 @@ $this->join_date?->format('Y-m-d'), + ); + } + + public function employmentStatusLabel(): Attribute + { + return Attribute::make( + get: fn () => $this->employment_status?->label(), + ); + } + public function user(): BelongsTo { return $this->belongsTo(User::class); diff --git a/app/Models/ProductPrice.php b/app/Models/ProductPrice.php index 549b561..ccb646e 100644 --- a/app/Models/ProductPrice.php +++ b/app/Models/ProductPrice.php @@ -3,11 +3,14 @@ namespace App\Models; use App\Enums\PriceType; +use Illuminate\Database\Eloquent\Attributes\Appends; use Illuminate\Database\Eloquent\Attributes\Guarded; +use Illuminate\Database\Eloquent\Casts\Attribute; use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Relations\BelongsTo; #[Guarded(['id'])] +#[Appends(['price_formatted', 'price_input', 'type_label'])] class ProductPrice extends Model { protected function casts(): array @@ -18,6 +21,27 @@ protected function casts(): array ]; } + public function priceFormatted(): Attribute + { + return Attribute::make( + get: fn () => 'Rp '.number_format((float) $this->price, 0, ',', '.'), + ); + } + + public function priceInput(): Attribute + { + return Attribute::make( + get: fn () => (string) (int) $this->price, + ); + } + + public function typeLabel(): Attribute + { + return Attribute::make( + get: fn () => $this->type->label(), + ); + } + public function variant(): BelongsTo { return $this->belongsTo(ProductVariant::class, 'variant_id'); diff --git a/app/Models/RawMaterial.php b/app/Models/RawMaterial.php index a1013dc..159cdac 100644 --- a/app/Models/RawMaterial.php +++ b/app/Models/RawMaterial.php @@ -3,14 +3,17 @@ namespace App\Models; use App\Enums\RawMaterialUnit; +use Illuminate\Database\Eloquent\Attributes\Appends; use Illuminate\Database\Eloquent\Attributes\Guarded; use Illuminate\Database\Eloquent\Attributes\Scope; use Illuminate\Database\Eloquent\Builder; +use Illuminate\Database\Eloquent\Casts\Attribute; use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Relations\HasMany; use Illuminate\Database\Eloquent\SoftDeletes; #[Guarded(['id'])] +#[Appends(['unit_label', 'unit_abbreviation'])] class RawMaterial extends Model { use SoftDeletes; @@ -35,6 +38,20 @@ public function inactive(Builder $query): void $query->where('is_active', false); } + public function unitLabel(): Attribute + { + return Attribute::make( + get: fn () => $this->unit->label(), + ); + } + + public function unitAbbreviation(): Attribute + { + return Attribute::make( + get: fn () => $this->unit->abbreviation(), + ); + } + public function prices(): HasMany { return $this->hasMany(RawMaterialPrice::class); diff --git a/app/Models/RawMaterialPrice.php b/app/Models/RawMaterialPrice.php index 0a8c822..2e4fe66 100644 --- a/app/Models/RawMaterialPrice.php +++ b/app/Models/RawMaterialPrice.php @@ -2,12 +2,15 @@ namespace App\Models; +use Illuminate\Database\Eloquent\Attributes\Appends; use Illuminate\Database\Eloquent\Attributes\Guarded; +use Illuminate\Database\Eloquent\Casts\Attribute; use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Relations\BelongsTo; use Illuminate\Database\Eloquent\SoftDeletes; #[Guarded(['id'])] +#[Appends(['price_formatted', 'stock_formatted', 'price_input', 'stock_input'])] class RawMaterialPrice extends Model { use SoftDeletes; @@ -20,6 +23,38 @@ protected function casts(): array ]; } + public function priceFormatted(): Attribute + { + return Attribute::make( + get: fn () => 'Rp '.number_format((float) $this->price, 0, ',', '.'), + ); + } + + public function stockFormatted(): Attribute + { + return Attribute::make( + get: function () { + $formatted = rtrim(rtrim(number_format((float) $this->stock, 4, ',', '.'), '0'), ','); + + return "{$formatted} {$this->rawMaterial->unit->abbreviation()}"; + }, + ); + } + + public function priceInput(): Attribute + { + return Attribute::make( + get: fn () => (string) (int) $this->price, + ); + } + + public function stockInput(): Attribute + { + return Attribute::make( + get: fn () => rtrim(rtrim(number_format((float) $this->stock, 4, '.', ''), '0'), '.'), + ); + } + public function rawMaterial(): BelongsTo { return $this->belongsTo(RawMaterial::class); diff --git a/app/Models/User.php b/app/Models/User.php index 617ff03..941a389 100644 --- a/app/Models/User.php +++ b/app/Models/User.php @@ -2,10 +2,13 @@ namespace App\Models; +use App\Enums\Role; +use Illuminate\Database\Eloquent\Attributes\Appends; use Illuminate\Database\Eloquent\Attributes\Guarded; use Illuminate\Database\Eloquent\Attributes\Hidden; use Illuminate\Database\Eloquent\Attributes\Scope; use Illuminate\Database\Eloquent\Builder; +use Illuminate\Database\Eloquent\Casts\Attribute; use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Relations\HasOne; use Illuminate\Database\Eloquent\SoftDeletes; @@ -15,6 +18,7 @@ #[Guarded(['id'])] #[Hidden(['password', 'remember_token'])] +#[Appends(['role_label'])] class User extends Authenticatable { use HasFactory, HasRoles, Notifiable, SoftDeletes; @@ -40,6 +44,17 @@ public function inactive(Builder $query): void $query->where('is_active', false); } + public function roleLabel(): Attribute + { + return Attribute::make( + get: function () { + $role = $this->roles->first(); + + return $role ? Role::from($role->name)->label() : null; + }, + ); + } + public function profile(): HasOne { return $this->hasOne(UserProfile::class); diff --git a/app/Models/UserProfile.php b/app/Models/UserProfile.php index 4955642..1663189 100644 --- a/app/Models/UserProfile.php +++ b/app/Models/UserProfile.php @@ -12,7 +12,7 @@ use Illuminate\Database\Eloquent\SoftDeletes; #[Guarded(['id'])] -#[Appends(['birth_date_formatted'])] +#[Appends(['birth_date_formatted', 'birth_date_input', 'gender_label'])] class UserProfile extends Model { use SoftDeletes; @@ -32,6 +32,20 @@ protected function birthDateFormatted(): Attribute ); } + protected function birthDateInput(): Attribute + { + return Attribute::make( + get: fn () => $this->birth_date?->format('Y-m-d'), + ); + } + + protected function genderLabel(): Attribute + { + return Attribute::make( + get: fn () => $this->gender?->label(), + ); + } + public function user(): BelongsTo { return $this->belongsTo(User::class); diff --git a/app/Services/Hr/EmployeeService.php b/app/Services/Hr/EmployeeService.php new file mode 100644 index 0000000..e333ca2 --- /dev/null +++ b/app/Services/Hr/EmployeeService.php @@ -0,0 +1,172 @@ +with(['profile', 'employee', 'roles']) + ->whereHas('employee') + ->when($tableQuery['search'] !== '', function ($query) use ($tableQuery): void { + $search = $tableQuery['search']; + $query->where(function ($query) use ($search): void { + $query->where('email', 'like', "%{$search}%") + ->orWhere('username', 'like', "%{$search}%") + ->orWhereHas('profile', function ($query) use ($search): void { + $query->where('full_name', 'like', "%{$search}%") + ->orWhere('phone_number', 'like', "%{$search}%"); + }); + }); + }) + ->when( + $employmentStatus !== '', + fn ($query) => $query->whereHas('employee', fn ($query) => $query->where('employment_status', $employmentStatus)) + ); + + $this->applySorting($query, $tableQuery['sort'], $tableQuery['direction']); + + return $query + ->paginate(10) + ->withQueryString(); + } + + /** + * @param array $validated + */ + public function create(array $validated): void + { + DB::transaction(function () use ($validated): void { + $user = User::create([ + 'email' => $validated['email'], + 'username' => $validated['username'], + 'password' => Hash::make(config('auth.password_default')), + ]); + + UserProfile::create([ + 'user_id' => $user->id, + 'full_name' => $validated['full_name'], + 'phone_number' => $validated['phone_number'], + 'gender' => $validated['gender'], + 'birth_date' => $validated['birth_date'], + 'address' => $validated['address'], + ]); + + Employee::create([ + 'user_id' => $user->id, + 'join_date' => $validated['join_date'], + 'employment_status' => $validated['employment_status'], + 'base_salary' => $validated['base_salary'], + ]); + + $user->syncRoles([$validated['role']]); + }); + } + + /** + * @param array $validated + */ + public function update(User $user, array $validated): void + { + $employee = $user->employee; + + DB::transaction(function () use ($validated, $user, $employee): void { + $user->email = $validated['email']; + $user->username = $validated['username']; + $user->save(); + + $profile = $user->profile; + $profile->full_name = $validated['full_name']; + $profile->phone_number = $validated['phone_number']; + $profile->gender = $validated['gender']; + $profile->birth_date = $validated['birth_date']; + $profile->address = $validated['address']; + $profile->save(); + + $employee->join_date = $validated['join_date']; + $employee->employment_status = $validated['employment_status']; + $employee->base_salary = $validated['base_salary']; + $employee->save(); + + $user->syncRoles([$validated['role']]); + }); + } + + public function toggleStatus(User $user, bool $isActive): void + { + $user->is_active = $isActive; + $user->save(); + + if (! $isActive) { + DB::table('sessions')->where('user_id', $user->id)->delete(); + } + } + + public function resetPassword(User $user): void + { + $user->password = config('auth.password_default'); + $user->save(); + + DB::table('sessions')->where('user_id', $user->id)->delete(); + } + + public function delete(User $user): void + { + DB::transaction(function () use ($user): void { + $user->employee?->delete(); + $user->profile?->delete(); + $user->delete(); + }); + } + + private function applySorting(Builder $query, string $sort, string $direction): void + { + $employeeSorts = [ + 'join_date', + 'base_salary', + 'employment_status', + ]; + + if (in_array($sort, $employeeSorts, true)) { + $query->orderBy( + Employee::select($sort) + ->whereColumn('employees.user_id', 'users.id') + ->limit(1), + $direction + ); + + return; + } + + if ($sort === 'full_name') { + $query->orderBy( + UserProfile::select('full_name') + ->whereColumn('user_profiles.user_id', 'users.id') + ->limit(1), + $direction + ); + + return; + } + + if ($sort === 'email') { + $query->orderBy('email', $direction); + + return; + } + + $query->latest(); + } +} diff --git a/app/Services/Master/CategoryService.php b/app/Services/Master/CategoryService.php new file mode 100644 index 0000000..9184ea7 --- /dev/null +++ b/app/Services/Master/CategoryService.php @@ -0,0 +1,63 @@ +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}%"); + }); + }); + + $this->applySorting($query, $tableQuery['sort'], $tableQuery['direction']); + + return $query + ->paginate(10) + ->withQueryString(); + } + + /** + * @param array $validated + */ + public function create(array $validated): void + { + Category::create($validated); + } + + /** + * @param array $validated + */ + public function update(Category $category, array $validated): void + { + $category->name = $validated['name']; + $category->save(); + } + + public function delete(Category $category): void + { + $category->delete(); + } + + private function applySorting(Builder $query, string $sort, string $direction): void + { + if (in_array($sort, ['name', 'slug'], true)) { + $query->orderBy($sort, $direction); + + return; + } + + $query->latest(); + } +} diff --git a/app/Services/Master/ProductService.php b/app/Services/Master/ProductService.php new file mode 100644 index 0000000..bb6a1ae --- /dev/null +++ b/app/Services/Master/ProductService.php @@ -0,0 +1,181 @@ +with([ + 'categories', + 'variants' => fn ($query) => $query + ->with(['prices' => fn ($query) => $query->orderBy('type')]) + ->orderBy('created_at'), + ]) + ->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('slug', 'like', "%{$search}%") + ->orWhere('description', 'like', "%{$search}%") + ->orWhereHas('categories', fn (Builder $query) => $query->where('name', 'like', "%{$search}%")) + ->orWhereHas('variants', fn (Builder $query) => $query->where('name', 'like', "%{$search}%")); + }); + }) + ->when( + $isActive !== '', + fn (Builder $query) => $query->where('is_active', $isActive === '1') + ); + + $this->applySorting($query, $tableQuery['sort'], $tableQuery['direction']); + + return $query + ->paginate(10) + ->withQueryString(); + } + + /** + * @return list + */ + public function categoryOptions(): array + { + return Category::query() + ->orderBy('name') + ->get(['id', 'name']) + ->map(fn (Category $category) => [ + 'value' => $category->id, + 'label' => $category->name, + ]) + ->all(); + } + + /** + * @param array $validated + */ + public function create(array $validated): void + { + DB::transaction(function () use ($validated): void { + $product = Product::create([ + 'name' => $validated['name'], + 'description' => $validated['description'] ?? null, + 'is_active' => true, + ]); + + $product->categories()->sync($validated['category_ids']); + + foreach ($validated['variants'] as $variantData) { + $this->createVariant($product, $variantData); + } + }); + } + + /** + * @param array $validated + */ + public function update(Product $product, array $validated): void + { + DB::transaction(function () use ($validated, $product): void { + $product->name = $validated['name']; + $product->description = $validated['description'] ?? null; + $product->save(); + + $product->categories()->sync($validated['category_ids']); + + $submittedVariantIds = collect($validated['variants']) + ->pluck('id') + ->filter() + ->map(fn ($id) => (int) $id) + ->all(); + + $product->variants() + ->whereNotIn('id', $submittedVariantIds) + ->get() + ->each(fn (ProductVariant $variant) => $variant->delete()); + + foreach ($validated['variants'] as $variantData) { + if (! empty($variantData['id'])) { + $variant = $product->variants()->findOrFail($variantData['id']); + $variant->name = $variantData['name']; + $variant->stock = $variantData['stock']; + $variant->save(); + $this->syncVariantPrices($variant, $variantData['prices']); + + continue; + } + + $this->createVariant($product, $variantData); + } + }); + } + + public function toggleStatus(Product $product, bool $isActive): void + { + $product->is_active = $isActive; + $product->save(); + } + + public function delete(Product $product): void + { + DB::transaction(function () use ($product): void { + $product->variants()->delete(); + $product->categories()->detach(); + $product->delete(); + }); + } + + private function applySorting(Builder $query, string $sort, string $direction): void + { + if (in_array($sort, ['name', 'slug', 'is_active'], true)) { + $query->orderBy($sort, $direction); + + return; + } + + $query->latest(); + } + + /** + * @param array $variantData + */ + private function createVariant(Product $product, array $variantData): ProductVariant + { + $variant = $product->variants()->create([ + 'name' => $variantData['name'], + 'stock' => $variantData['stock'], + ]); + + $this->syncVariantPrices($variant, $variantData['prices']); + + return $variant; + } + + /** + * @param list $prices + */ + private function syncVariantPrices(ProductVariant $variant, array $prices): void + { + foreach ($prices as $priceData) { + ProductPrice::updateOrCreate( + [ + 'variant_id' => $variant->id, + 'type' => $priceData['type'], + ], + [ + 'price' => $priceData['price'], + ], + ); + } + } +} diff --git a/app/Services/Master/RawMaterialService.php b/app/Services/Master/RawMaterialService.php new file mode 100644 index 0000000..d19820f --- /dev/null +++ b/app/Services/Master/RawMaterialService.php @@ -0,0 +1,133 @@ +with([ + 'prices' => fn ($query) => $query + ->orderBy('created_at') + ->with('rawMaterial:id,unit'), + ]) + ->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}%") + ->orWhereHas('prices', fn (Builder $query) => $query->where('variant', 'like', "%{$search}%")); + }); + }) + ->when( + $isActive !== '', + fn (Builder $query) => $query->where('is_active', $isActive === '1') + ); + + $this->applySorting($query, $tableQuery['sort'], $tableQuery['direction']); + + return $query + ->paginate(10) + ->withQueryString(); + } + + /** + * @param array $validated + */ + public function create(array $validated): void + { + DB::transaction(function () use ($validated): void { + $rawMaterial = RawMaterial::create([ + 'name' => $validated['name'], + 'unit' => $validated['unit'], + ]); + + foreach ($validated['prices'] as $priceData) { + $this->createPrice($rawMaterial, $priceData); + } + }); + } + + /** + * @param array $validated + */ + public function update(RawMaterial $rawMaterial, array $validated): void + { + DB::transaction(function () use ($validated, $rawMaterial): void { + $rawMaterial->name = $validated['name']; + $rawMaterial->unit = $validated['unit']; + $rawMaterial->save(); + + $submittedPriceIds = collect($validated['prices']) + ->pluck('id') + ->filter() + ->map(fn ($id) => (int) $id) + ->all(); + + $rawMaterial->prices() + ->whereNotIn('id', $submittedPriceIds) + ->get() + ->each(fn (RawMaterialPrice $price) => $price->delete()); + + foreach ($validated['prices'] as $priceData) { + if (! empty($priceData['id'])) { + $price = $rawMaterial->prices()->findOrFail($priceData['id']); + $price->variant = $priceData['variant']; + $price->price = $priceData['price']; + $price->stock = $priceData['stock']; + $price->save(); + + continue; + } + + $this->createPrice($rawMaterial, $priceData); + } + }); + } + + public function toggleStatus(RawMaterial $rawMaterial, bool $isActive): void + { + $rawMaterial->is_active = $isActive; + $rawMaterial->save(); + } + + public function delete(RawMaterial $rawMaterial): void + { + DB::transaction(function () use ($rawMaterial): void { + $rawMaterial->prices()->delete(); + $rawMaterial->delete(); + }); + } + + private function applySorting(Builder $query, string $sort, string $direction): void + { + if (in_array($sort, ['name', 'unit', 'is_active'], true)) { + $query->orderBy($sort, $direction); + + return; + } + + $query->latest(); + } + + /** + * @param array $priceData + */ + private function createPrice(RawMaterial $rawMaterial, array $priceData): RawMaterialPrice + { + return $rawMaterial->prices()->create([ + 'variant' => $priceData['variant'], + 'price' => $priceData['price'], + 'stock' => $priceData['stock'], + ]); + } +} diff --git a/resources/js/components/hr/EmployeeForm.vue b/resources/js/components/admin/hr/EmployeeForm.vue similarity index 100% rename from resources/js/components/hr/EmployeeForm.vue rename to resources/js/components/admin/hr/EmployeeForm.vue diff --git a/resources/js/components/hr/ResetPasswordDialog.vue b/resources/js/components/admin/hr/ResetPasswordDialog.vue similarity index 100% rename from resources/js/components/hr/ResetPasswordDialog.vue rename to resources/js/components/admin/hr/ResetPasswordDialog.vue diff --git a/resources/js/components/hr/employees/columns.ts b/resources/js/components/admin/hr/employees/columns.ts similarity index 71% rename from resources/js/components/hr/employees/columns.ts rename to resources/js/components/admin/hr/employees/columns.ts index c0d2dba..387121f 100644 --- a/resources/js/components/hr/employees/columns.ts +++ b/resources/js/components/admin/hr/employees/columns.ts @@ -1,34 +1,17 @@ import type { ColumnDef } from '@tanstack/vue-table'; import { h } from 'vue'; import { DataTableColumnHeader } from '@/components/data-table'; -import DataTableActions from '@/components/hr/employees/data-table-actions.vue'; -import EmployeeStatusToggle from '@/components/hr/employees/employee-status-toggle.vue'; +import DataTableActions from '@/components/admin/hr/employees/data-table-actions.vue'; +import EmployeeStatusToggle from '@/components/admin/hr/employees/employee-status-toggle.vue'; import { Badge } from '@/components/ui/badge'; import type { EmployeeListItem } from '@/types/employee'; -function formatDate(value: string | null): string { - if (!value) { - return '-'; - } - - const date = new Date(value); - - if (Number.isNaN(date.getTime())) { - return value; - } - - return date.toLocaleDateString('id-ID', { - day: 'numeric', - month: 'short', - year: 'numeric', - }); -} - export const columns: ColumnDef[] = [ { - accessorKey: 'full_name', + accessorKey: 'profile.full_name', enableSorting: true, header: () => h(DataTableColumnHeader, { title: 'Nama', column: 'full_name' }), + cell: ({ row }) => row.original.profile?.full_name ?? '-', }, { id: 'account', @@ -49,62 +32,63 @@ export const columns: ColumnDef[] = [ : '-', }, { - accessorKey: 'phone_number', + accessorKey: 'profile.phone_number', enableSorting: false, header: () => h(DataTableColumnHeader, { title: 'Telepon', column: 'phone_number' }), - cell: ({ row }) => row.original.phone_number ?? '-', + cell: ({ row }) => row.original.profile?.phone_number ?? '-', }, { - accessorKey: 'gender_label', + accessorKey: 'profile.gender_label', enableSorting: false, header: () => h(DataTableColumnHeader, { title: 'Jenis Kelamin', column: 'gender' }), - cell: ({ row }) => row.original.gender_label ?? '-', + cell: ({ row }) => row.original.profile?.gender_label ?? '-', }, { - accessorKey: 'birth_date', + accessorKey: 'profile.birth_date_formatted', enableSorting: false, header: () => h(DataTableColumnHeader, { title: 'Tanggal Lahir', column: 'birth_date' }), - cell: ({ row }) => formatDate(row.original.birth_date), + cell: ({ row }) => row.original.profile?.birth_date_formatted ?? '-', }, { - accessorKey: 'address', + accessorKey: 'profile.address', enableSorting: false, header: () => h(DataTableColumnHeader, { title: 'Alamat', column: 'address' }), cell: ({ row }) => h( 'span', - { class: 'block max-w-[200px] truncate', title: row.original.address ?? undefined }, - row.original.address ?? '-', + { class: 'block max-w-[200px] truncate', title: row.original.profile?.address ?? undefined }, + row.original.profile?.address ?? '-', ), }, { id: 'employment_dates', - accessorKey: 'join_date', + accessorKey: 'employee.join_date_formatted', enableSorting: true, header: () => h(DataTableColumnHeader, { title: 'Masa Kepegawaian', column: 'join_date' }), cell: ({ row }) => h('div', { class: 'space-y-0.5 text-sm' }, [ h('div', {}, [ h('span', { class: 'text-muted-foreground' }, 'Bergabung: '), - formatDate(row.original.join_date), + row.original.employee?.join_date_formatted ?? '-', ]), h('div', {}, [ h('span', { class: 'text-muted-foreground' }, 'Resign: '), - row.original.resign_date ?? '-', + row.original.employee?.resign_date_formatted ?? '-', ]), ]), }, { - accessorKey: 'employment_status_label', + accessorKey: 'employee.employment_status_label', enableSorting: true, header: () => h(DataTableColumnHeader, { title: 'Status Kepegawaian', column: 'employment_status' }), + cell: ({ row }) => row.original.employee?.employment_status_label ?? '-', }, { - accessorKey: 'base_salary_formatted', + accessorKey: 'employee.base_salary_formatted', enableSorting: true, header: () => h(DataTableColumnHeader, { title: 'Gaji Pokok', column: 'base_salary', }), - cell: ({ row }) => row.original.base_salary_formatted, + cell: ({ row }) => row.original.employee?.base_salary_formatted ?? '-', }, { id: 'status_toggle', diff --git a/resources/js/components/hr/employees/data-table-actions.vue b/resources/js/components/admin/hr/employees/data-table-actions.vue similarity index 91% rename from resources/js/components/hr/employees/data-table-actions.vue rename to resources/js/components/admin/hr/employees/data-table-actions.vue index ac52557..2ffc166 100644 --- a/resources/js/components/hr/employees/data-table-actions.vue +++ b/resources/js/components/admin/hr/employees/data-table-actions.vue @@ -94,7 +94,7 @@ function resetPassword() { v-if="can('employees.reset-password')" v-model:open="resetPasswordConfirmOpen" title="Reset kata sandi pegawai?" - :description="`Kata sandi ${employee.full_name ?? 'pegawai'} akan direset ke kata sandi default sistem. Pengguna akan otomatis logout dari semua sesi aktif.`" + :description="`Kata sandi ${employee.profile?.full_name ?? 'pegawai'} akan direset ke kata sandi default sistem. Pengguna akan otomatis logout dari semua sesi aktif.`" confirm-label="Reset Kata Sandi" cancel-label="Batal" :loading="resetPasswordProcessing" @confirm="resetPassword" /> @@ -102,6 +102,6 @@ function resetPassword() { v-if="can('employees.delete')" v-model:open="deleteConfirmOpen" title="Hapus pegawai?" - :description="`Data pegawai ${employee.full_name ?? ''} akan dihapus secara permanen beserta akun pengguna terkait. Tindakan ini tidak dapat dibatalkan.`" + :description="`Data pegawai ${employee.profile?.full_name ?? ''} akan dihapus secara permanen beserta akun pengguna terkait. Tindakan ini tidak dapat dibatalkan.`" confirm-label="Hapus" cancel-label="Batal" destructive :loading="deleteProcessing" @confirm="destroyEmployee" /> diff --git a/resources/js/components/hr/employees/employee-status-toggle.vue b/resources/js/components/admin/hr/employees/employee-status-toggle.vue similarity index 100% rename from resources/js/components/hr/employees/employee-status-toggle.vue rename to resources/js/components/admin/hr/employees/employee-status-toggle.vue diff --git a/resources/js/components/categories/CategoryFormModal.vue b/resources/js/components/admin/master/categories/CategoryFormModal.vue similarity index 100% rename from resources/js/components/categories/CategoryFormModal.vue rename to resources/js/components/admin/master/categories/CategoryFormModal.vue diff --git a/resources/js/components/categories/columns.ts b/resources/js/components/admin/master/categories/columns.ts similarity index 89% rename from resources/js/components/categories/columns.ts rename to resources/js/components/admin/master/categories/columns.ts index 62b50d8..f4b1490 100644 --- a/resources/js/components/categories/columns.ts +++ b/resources/js/components/admin/master/categories/columns.ts @@ -1,6 +1,6 @@ import type { ColumnDef } from '@tanstack/vue-table'; import { h } from 'vue'; -import DataTableActions from '@/components/categories/data-table-actions.vue'; +import DataTableActions from '@/components/admin/master/categories/data-table-actions.vue'; import { DataTableColumnHeader } from '@/components/data-table'; import type { CategoryListItem } from '@/types/category'; diff --git a/resources/js/components/categories/data-table-actions.vue b/resources/js/components/admin/master/categories/data-table-actions.vue similarity index 100% rename from resources/js/components/categories/data-table-actions.vue rename to resources/js/components/admin/master/categories/data-table-actions.vue diff --git a/resources/js/components/products/ProductForm.vue b/resources/js/components/admin/master/products/ProductForm.vue similarity index 100% rename from resources/js/components/products/ProductForm.vue rename to resources/js/components/admin/master/products/ProductForm.vue diff --git a/resources/js/components/products/ProductGroupedTable.vue b/resources/js/components/admin/master/products/ProductGroupedTable.vue similarity index 58% rename from resources/js/components/products/ProductGroupedTable.vue rename to resources/js/components/admin/master/products/ProductGroupedTable.vue index 5f54067..df18a5e 100644 --- a/resources/js/components/products/ProductGroupedTable.vue +++ b/resources/js/components/admin/master/products/ProductGroupedTable.vue @@ -1,8 +1,8 @@ diff --git a/resources/js/pages/admin/products/Index.vue b/resources/js/pages/admin/master/products/Index.vue similarity index 70% rename from resources/js/pages/admin/products/Index.vue rename to resources/js/pages/admin/master/products/Index.vue index 1c28883..00fd6da 100644 --- a/resources/js/pages/admin/products/Index.vue +++ b/resources/js/pages/admin/master/products/Index.vue @@ -2,18 +2,17 @@ import { Head, Link } from '@inertiajs/vue3'; import { Plus } from '@lucide/vue'; import { computed, ref, watch } from 'vue'; -import ProductGroupedTable from '@/components/products/ProductGroupedTable.vue'; +import ProductGroupedTable from '@/components/admin/master/products/ProductGroupedTable.vue'; 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 { DataTableFilterDef } from '@/types/data-table'; -import type { ProductPagination, ProductTableRow } from '@/types/product'; +import type { PaginatedProducts } from '@/types/product'; const props = defineProps<{ - rows: ProductTableRow[]; - pagination: ProductPagination; + products: PaginatedProducts; filters: { search: string; sort?: string; @@ -50,14 +49,16 @@ const filterValues = computed(() => ({ })); const tablePagination = computed(() => ({ - currentPage: props.pagination.current_page, - perPage: props.pagination.per_page, - lastPage: props.pagination.last_page, - total: props.pagination.total, + currentPage: props.products.current_page, + perPage: props.products.per_page, + lastPage: props.products.last_page, + total: props.products.total, })); -const productCountOnPage = computed(() => ( - new Set(props.rows.map((row) => row.product_id)).size +const firstItem = computed(() => ( + props.products.data.length > 0 + ? (props.products.current_page - 1) * props.products.per_page + 1 + : 0 )); watch(search, (value) => { @@ -94,17 +95,9 @@ watch( - + diff --git a/resources/js/pages/admin/raw-materials/Create.vue b/resources/js/pages/admin/master/raw-materials/Create.vue similarity index 79% rename from resources/js/pages/admin/raw-materials/Create.vue rename to resources/js/pages/admin/master/raw-materials/Create.vue index 2a76400..6640908 100644 --- a/resources/js/pages/admin/raw-materials/Create.vue +++ b/resources/js/pages/admin/master/raw-materials/Create.vue @@ -1,7 +1,7 @@ @@ -39,12 +44,7 @@ const initialData = computed(() => ({ - + diff --git a/resources/js/pages/admin/raw-materials/Index.vue b/resources/js/pages/admin/master/raw-materials/Index.vue similarity index 69% rename from resources/js/pages/admin/raw-materials/Index.vue rename to resources/js/pages/admin/master/raw-materials/Index.vue index 4c43904..e4d4103 100644 --- a/resources/js/pages/admin/raw-materials/Index.vue +++ b/resources/js/pages/admin/master/raw-materials/Index.vue @@ -2,18 +2,17 @@ import { Head, Link } from '@inertiajs/vue3'; import { Plus } from '@lucide/vue'; import { computed, ref, watch } from 'vue'; -import RawMaterialGroupedTable from '@/components/raw-materials/RawMaterialGroupedTable.vue'; +import RawMaterialGroupedTable from '@/components/admin/master/raw-materials/RawMaterialGroupedTable.vue'; 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 { DataTableFilterDef } from '@/types/data-table'; -import type { RawMaterialPagination, RawMaterialTableRow } from '@/types/raw-material'; +import type { PaginatedRawMaterials } from '@/types/raw-material'; const props = defineProps<{ - rows: RawMaterialTableRow[]; - pagination: RawMaterialPagination; + rawMaterials: PaginatedRawMaterials; filters: { search: string; sort?: string; @@ -50,14 +49,16 @@ const filterValues = computed(() => ({ })); const tablePagination = computed(() => ({ - currentPage: props.pagination.current_page, - perPage: props.pagination.per_page, - lastPage: props.pagination.last_page, - total: props.pagination.total, + currentPage: props.rawMaterials.current_page, + perPage: props.rawMaterials.per_page, + lastPage: props.rawMaterials.last_page, + total: props.rawMaterials.total, })); -const materialCountOnPage = computed(() => ( - new Set(props.rows.map((row) => row.raw_material_id)).size +const firstItem = computed(() => ( + props.rawMaterials.data.length > 0 + ? (props.rawMaterials.current_page - 1) * props.rawMaterials.per_page + 1 + : 0 )); watch(search, (value) => { @@ -94,17 +95,9 @@ watch( - + diff --git a/resources/js/types/employee.ts b/resources/js/types/employee.ts index ef1027b..6971940 100644 --- a/resources/js/types/employee.ts +++ b/resources/js/types/employee.ts @@ -3,40 +3,39 @@ export type EnumOption = { label: string; }; -export type EmployeeEditItem = { - id: number; - email: string | null; - username: string | null; - full_name: string | null; - phone_number: string | null; - gender: string | null; - birth_date: string | null; - address: string | null; - join_date: string | null; - employment_status: string | null; - base_salary: number | null; - role: string | null; -}; - -export type EmployeeListItem = { - id: number; - join_date: string | null; - resign_date: string | null; - employment_status: string; - employment_status_label: string; - base_salary: number; - base_salary_formatted: string; - email: string | null; - username: string | null; - is_active: boolean; +export type EmployeeProfile = { full_name: string | null; phone_number: string | null; gender: string | null; gender_label: string | null; - birth_date: string | null; + birth_date_formatted: string | null; + birth_date_input: string | null; address: string | null; - role: string | null; +}; + +export type EmployeeRecord = { + join_date_formatted: string | null; + resign_date_formatted: string | null; + join_date_input: string | null; + employment_status: string | null; + employment_status_label: string | null; + base_salary: number | null; + base_salary_formatted: string; +}; + +export type EmployeeRole = { + name: string; +}; + +export type EmployeeListItem = { + id: number; + email: string | null; + username: string | null; + is_active: boolean; role_label: string | null; + profile: EmployeeProfile; + employee: EmployeeRecord; + roles: EmployeeRole[]; }; export type EmployeeFormData = { diff --git a/resources/js/types/product.ts b/resources/js/types/product.ts index 6b75c3d..178c27e 100644 --- a/resources/js/types/product.ts +++ b/resources/js/types/product.ts @@ -13,6 +13,7 @@ export type ProductPriceItem = { type_label: string; price: string; price_formatted: string; + price_input: string; }; export type ProductCategoryItem = { @@ -20,28 +21,19 @@ export type ProductCategoryItem = { name: string; }; -export type ProductTableGroup = { - product_id: number; - product_row_number: number; - product_name: string; - categories: ProductCategoryItem[]; - is_active: boolean; - variants: ProductTableRow[]; +export type ProductVariantItem = { + id: number; + name: string; + stock: number; + prices: ProductPriceItem[]; }; -export type ProductTableRow = { - row_id: string; - product_id: number; - product_name: string; - categories: ProductCategoryItem[]; +export type ProductListItem = { + id: number; + name: string; is_active: boolean; - is_first_variant: boolean; - variant_count: number; - product_row_number: number; - variant_id: number | null; - variant_name: string | null; - stock: number | null; - prices: ProductPriceItem[]; + categories: ProductCategoryItem[]; + variants: ProductVariantItem[]; }; export type ProductVariantFormItem = { @@ -52,19 +44,6 @@ export type ProductVariantFormItem = { prices: Record; }; -export type ProductEditItem = { - id: number; - name: string; - description: string | null; - category_ids: number[]; - variants: Array<{ - id: number; - name: string; - stock: number; - prices: Record; - }>; -}; - export type ProductFormData = { name: string; description: string; @@ -87,7 +66,8 @@ export type ProductFilters = { is_active?: string; }; -export type ProductPagination = { +export type PaginatedProducts = { + data: ProductListItem[]; current_page: number; last_page: number; per_page: number; diff --git a/resources/js/types/raw-material.ts b/resources/js/types/raw-material.ts index 84191b7..91dc4ff 100644 --- a/resources/js/types/raw-material.ts +++ b/resources/js/types/raw-material.ts @@ -3,34 +3,25 @@ export type EnumOption = { label: string; }; -export type RawMaterialTableGroup = { - raw_material_id: number; - raw_material_row_number: number; - raw_material_name: string; - unit: string; - unit_label: string; - unit_abbreviation: string; - is_active: boolean; - variants: RawMaterialTableRow[]; +export type RawMaterialPrice = { + id: number; + variant: string; + stock: string; + stock_formatted: string; + price: string; + price_formatted: string; + price_input: string; + stock_input: string; }; -export type RawMaterialTableRow = { - row_id: string; - raw_material_id: number; - raw_material_name: string; +export type RawMaterialListItem = { + id: number; + name: string; unit: string; unit_label: string; unit_abbreviation: string; is_active: boolean; - is_first_variant: boolean; - variant_count: number; - raw_material_row_number: number; - price_id: number | null; - variant: string | null; - stock: string | null; - stock_formatted: string | null; - price: string | null; - price_formatted: string | null; + prices: RawMaterialPrice[]; }; export type RawMaterialPriceFormItem = { @@ -41,18 +32,6 @@ export type RawMaterialPriceFormItem = { stock: string; }; -export type RawMaterialEditItem = { - id: number; - name: string; - unit: string; - prices: Array<{ - id: number; - variant: string; - price: string; - stock: string; - }>; -}; - export type RawMaterialFormData = { name: string; unit: string; @@ -64,7 +43,8 @@ export type RawMaterialFormData = { }>; }; -export type RawMaterialPagination = { +export type PaginatedRawMaterials = { + data: RawMaterialListItem[]; current_page: number; last_page: number; per_page: number; diff --git a/routes/web.php b/routes/web.php index c6a5ed1..655ef86 100644 --- a/routes/web.php +++ b/routes/web.php @@ -1,11 +1,11 @@ group(function () { Route::post('{user}/reset-password', [EmployeeController::class, 'resetPassword']) ->middleware('permission:'.Permission::EMPLOYEES_RESET_PASSWORD->value) - ->name('employees.reset-password'); + ->name('reset-password'); Route::patch('{user}/toggle-status', [EmployeeController::class, 'toggleStatus']) ->middleware('permission:'.Permission::EMPLOYEES_TOGGLE_STATUS->value) - ->name('employees.toggle-status'); + ->name('toggle-status'); Route::get('create', [EmployeeController::class, 'create']) ->middleware('permission:'.Permission::EMPLOYEES_CREATE->value) - ->name('employees.create'); + ->name('create'); Route::post('employees', [EmployeeController::class, 'store']) ->middleware('permission:'.Permission::EMPLOYEES_CREATE->value) - ->name('employees.store'); + ->name('store'); Route::get('{user}/edit', [EmployeeController::class, 'edit']) ->middleware('permission:'.Permission::EMPLOYEES_UPDATE->value) - ->name('employees.edit'); + ->name('edit'); Route::put('{user}', [EmployeeController::class, 'update']) ->middleware('permission:'.Permission::EMPLOYEES_UPDATE->value) - ->name('employees.update'); + ->name('update'); Route::delete('{user}', [EmployeeController::class, 'destroy']) ->middleware('permission:'.Permission::EMPLOYEES_DELETE->value) - ->name('employees.destroy'); + ->name('destroy'); - Route::get('/', [EmployeeController::class, 'index'])->name('employees.index'); + Route::get('/', [EmployeeController::class, 'index'])->name('index'); }); }); });