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'); }); }); });