refactor: remove unused inventory stock warning components and service logic

This commit is contained in:
Yoga Pangestu 2026-06-28 18:11:26 +07:00
parent 62e6995191
commit ce553f9aed
6 changed files with 0 additions and 272 deletions

View File

@ -9,7 +9,6 @@
use App\Http\Requests\Admin\ToggleStatusRequest;
use App\Models\Product;
use App\Services\Master\CategoryService;
use App\Services\Master\MasterInventoryService;
use App\Services\Master\ProductService;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
@ -23,7 +22,6 @@ class ProductController extends Controller
public function __construct(
private readonly ProductService $productService,
private readonly CategoryService $categoryService,
private readonly MasterInventoryService $masterInventoryService,
) {}
public function index(Request $request): Response
@ -36,7 +34,6 @@ public function index(Request $request): Response
return Inertia::render('admin/master/products/Index', [
'products' => $this->productService->paginateForIndex($tableQuery, $isActive, $categoryId, $stockStatus),
'categories' => $this->categoryService->getSelectOptions(),
'outOfStockGroups' => $this->masterInventoryService->outOfStockProductGroups(),
'filters' => $this->dataTableFilters($tableQuery, [
'is_active' => $isActive,
'category_id' => $categoryId,

View File

@ -9,7 +9,6 @@
use App\Http\Requests\Admin\Master\RawMaterialRequest;
use App\Http\Requests\Admin\ToggleStatusRequest;
use App\Models\RawMaterial;
use App\Services\Master\MasterInventoryService;
use App\Services\Master\RawMaterialService;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
@ -22,7 +21,6 @@ class RawMaterialController extends Controller
public function __construct(
private readonly RawMaterialService $rawMaterialService,
private readonly MasterInventoryService $masterInventoryService,
) {}
public function index(Request $request): Response
@ -33,8 +31,6 @@ public function index(Request $request): Response
return Inertia::render('admin/master/raw-materials/Index', [
'rawMaterials' => $this->rawMaterialService->paginateForIndex($tableQuery, $isActive, $stockStatus),
'outOfStockGroups' => $this->masterInventoryService->outOfStockRawMaterialGroups(),
'lowStockGroups' => $this->masterInventoryService->lowStockRawMaterialGroups(),
'filters' => $this->dataTableFilters($tableQuery, [
'is_active' => $isActive,
'stock_status' => $stockStatus,

View File

@ -1,108 +0,0 @@
<?php
namespace App\Services\Master;
use App\Enums\RawMaterialUnit;
use App\Models\Product;
use App\Models\ProductVariant;
use App\Models\RawMaterial;
use App\Models\RawMaterialPrice;
use App\Support\Media\MediaPresenter;
use Illuminate\Database\Eloquent\Builder;
class MasterInventoryService
{
public function outOfStockProductGroups(int $limit = 50): array
{
return Product::query()
->where('is_active', true)
->whereHas('variants', fn (Builder $query) => $query->where('stock', '<=', 0))
->with([
'variants' => fn ($query) => $query
->where('stock', '<=', 0)
->with('media')
->orderBy('name'),
])
->orderBy('name')
->limit($limit)
->get()
->map(fn (Product $product) => [
'id' => $product->id,
'name' => $product->name,
'edit_url' => route('admin.master.products.edit', $product),
'variants' => $product->variants->map(fn (ProductVariant $variant) => [
'id' => $variant->id,
'name' => $variant->name,
'stock_formatted' => $variant->stock_formatted,
'images' => MediaPresenter::collection($variant, 'images'),
])->values()->all(),
])
->filter(fn (array $group) => $group['variants'] !== [])
->values()
->all();
}
public function outOfStockRawMaterialGroups(int $limit = 50): array
{
return $this->rawMaterialGroupsWithStockFilter(
fn (Builder $query) => $query->where('stock', '<=', 0),
$limit,
);
}
public function lowStockRawMaterialGroups(int $limit = 50): array
{
$minStock = [
RawMaterialUnit::YARD->value => 20,
RawMaterialUnit::METER->value => 10,
RawMaterialUnit::KILOGRAM->value => 5,
];
return $this->rawMaterialGroupsWithStockFilter(
function (Builder $query) use ($minStock): void {
$query->where('stock', '>', 0)->where(function (Builder $query) use ($minStock): void {
foreach (RawMaterialUnit::cases() as $unit) {
$query->orWhere(function (Builder $query) use ($unit, $minStock): void {
$query->whereHas(
'rawMaterial',
fn (Builder $rawMaterialQuery) => $rawMaterialQuery->where('unit', $unit),
)->where('stock', '<', $minStock[$unit->value]);
});
}
});
},
$limit,
);
}
private function rawMaterialGroupsWithStockFilter(callable $priceConstraint, int $limit): array
{
return RawMaterial::query()
->where('is_active', true)
->whereHas('prices', $priceConstraint)
->with([
'prices' => function ($query) use ($priceConstraint): void {
$priceConstraint($query);
$query->with('media')->orderBy('variant');
},
])
->orderBy('name')
->limit($limit)
->get()
->map(fn (RawMaterial $rawMaterial) => [
'id' => $rawMaterial->id,
'name' => $rawMaterial->name,
'edit_url' => route('admin.master.raw_materials.edit', $rawMaterial),
'unit_label' => $rawMaterial->unit_label,
'variants' => $rawMaterial->prices->map(fn (RawMaterialPrice $price) => [
'id' => $price->id,
'name' => $price->variant,
'stock_formatted' => $price->stock_formatted,
'images' => MediaPresenter::collection($price, 'images'),
])->values()->all(),
])
->filter(fn (array $group) => $group['variants'] !== [])
->values()
->all();
}
}

View File

@ -1,127 +0,0 @@
<script setup lang="ts">
import { router } from '@inertiajs/vue3';
import { ChevronDown, PackageX, TriangleAlert } from '@lucide/vue';
import { computed, ref } from 'vue';
import PosCatalogCard from '@/components/catalog/PosCatalogCard.vue';
import PosCatalogVariantThumb from '@/components/catalog/PosCatalogVariantThumb.vue';
import { Badge } from '@/components/ui/badge';
import { Card, CardContent } from '@/components/ui/card';
import {
Collapsible,
CollapsibleContent,
CollapsibleTrigger,
} from '@/components/ui/collapsible';
import {
Empty,
EmptyDescription,
EmptyHeader,
EmptyTitle,
} from '@/components/ui/empty';
import type { MasterOutOfStockGroup } from '@/types/master-inventory';
const props = withDefaults(defineProps<{
title: string;
description?: string;
groups: MasterOutOfStockGroup[];
severity?: 'danger' | 'warning';
emptyTitle?: string;
emptyDescription?: string;
}>(), {
severity: 'danger',
emptyTitle: 'Tidak ada stok habis',
emptyDescription: 'Semua varian masih memiliki stok tersedia.',
});
const open = ref(false);
const variantCount = computed(() => props.groups.reduce(
(total, group) => total + group.variants.length,
0,
));
const iconClass = computed(() => (
props.severity === 'warning'
? 'text-amber-600 dark:text-amber-400'
: 'text-red-600 dark:text-red-400'
));
const stockTextClass = computed(() => (
props.severity === 'warning'
? 'text-amber-600 dark:text-amber-400'
: 'text-red-600 dark:text-red-400'
));
const badgeVariant = computed(() => (
props.severity === 'warning' ? 'outline' : 'destructive'
));
function visitEdit(url: string): void {
router.visit(url);
}
</script>
<template>
<Collapsible v-model:open="open">
<Card class="py-0">
<CollapsibleTrigger
class="flex w-full items-center justify-between gap-3 px-6 py-4 text-left transition-colors hover:bg-muted/30">
<div class="min-w-0 space-y-1">
<div class="flex items-center gap-2">
<TriangleAlert v-if="severity === 'warning'" class="size-4 shrink-0" :class="iconClass" />
<PackageX v-else class="size-4 shrink-0" :class="iconClass" />
<h3 class="font-semibold leading-tight">
{{ title }}
</h3>
<Badge v-if="variantCount > 0" :variant="badgeVariant">
{{ variantCount }}
</Badge>
</div>
<p v-if="description" class="text-xs text-muted-foreground">
{{ description }}
</p>
</div>
<ChevronDown class="size-4 shrink-0 text-muted-foreground transition-transform duration-200"
:class="open ? 'rotate-180' : ''" />
</CollapsibleTrigger>
<CollapsibleContent>
<CardContent class="pt-0 pb-6">
<div v-if="groups.length" class="columns-1 gap-4 sm:columns-2 xl:columns-3">
<PosCatalogCard v-for="group in groups" :key="group.id" :title="group.name" :show-cover="false"
class="cursor-pointer transition-shadow hover:shadow-md" @click="visitEdit(group.edit_url)">
<template v-if="group.unit_label" #header-extra>
<Badge variant="secondary" class="mt-1.5">
{{ group.unit_label }}
</Badge>
</template>
<div v-for="variant in group.variants" :key="variant.id"
class="flex items-center gap-2.5 px-3 py-2.5 transition-colors hover:bg-muted/30"
@click.stop="visitEdit(group.edit_url)">
<PosCatalogVariantThumb :items="variant.images" />
<div class="min-w-0 flex-1">
<p class="truncate text-sm font-medium">
{{ variant.name }}
</p>
<p class="text-xs font-medium tabular-nums" :class="stockTextClass">
{{ variant.stock_formatted }}
</p>
</div>
</div>
</PosCatalogCard>
</div>
<Empty v-else class="py-8">
<EmptyHeader>
<EmptyTitle>{{ emptyTitle }}</EmptyTitle>
<EmptyDescription>
{{ emptyDescription }}
</EmptyDescription>
</EmptyHeader>
</Empty>
</CardContent>
</CollapsibleContent>
</Card>
</Collapsible>
</template>

View File

@ -11,8 +11,6 @@ import { StockStatus } from '@/constants/stock-status';
import AdminLayout from '@/layouts/AdminLayout.vue';
import type { DataTableFilterDef } from '@/types/data-table';
import type { CategoryOption, PaginatedProducts } from '@/types/product';
import type { MasterOutOfStockGroup } from '@/types/master-inventory';
import MasterOutOfStockCatalogSection from '@/components/master/MasterOutOfStockCatalogSection.vue';
import { Head } from '@inertiajs/vue3';
import { computed, ref, watch } from 'vue';
import { index, create } from '@/routes/admin/master/products';
@ -21,7 +19,6 @@ import ProductGroupedTable from './table/ProductGroupedTable.vue';
const props = defineProps<{
products: PaginatedProducts;
categories: CategoryOption[];
outOfStockGroups: MasterOutOfStockGroup[];
filters: {
search: string;
sort?: string;
@ -122,12 +119,6 @@ watch(
/>
</div>
<MasterOutOfStockCatalogSection
v-if="outOfStockGroups.length > 0"
title="Produk Stok Habis"
description="Varian produk aktif dengan stok bagus 0 pcs."
:groups="outOfStockGroups"
/>
<Card class="min-w-0">
<CardContent class="min-w-0">

View File

@ -11,8 +11,6 @@ import { StockStatus } from '@/constants/stock-status';
import AdminLayout from '@/layouts/AdminLayout.vue';
import type { DataTableFilterDef } from '@/types/data-table';
import type { PaginatedRawMaterials } from '@/types/raw-material';
import type { MasterOutOfStockGroup } from '@/types/master-inventory';
import MasterOutOfStockCatalogSection from '@/components/master/MasterOutOfStockCatalogSection.vue';
import { Head } from '@inertiajs/vue3';
import { computed, ref, watch } from 'vue';
import { index, create } from '@/routes/admin/master/raw_materials';
@ -20,8 +18,6 @@ import RawMaterialGroupedTable from './table/RawMaterialGroupedTable.vue';
const props = defineProps<{
rawMaterials: PaginatedRawMaterials;
outOfStockGroups: MasterOutOfStockGroup[];
lowStockGroups: MasterOutOfStockGroup[];
filters: {
search: string;
sort?: string;
@ -112,23 +108,6 @@ watch(
/>
</div>
<div v-if="outOfStockGroups.length > 0 || lowStockGroups.length > 0" class="space-y-4">
<MasterOutOfStockCatalogSection
v-if="outOfStockGroups.length > 0"
title="Bahan Baku Stok Habis"
description="Varian bahan baku aktif dengan stok 0."
:groups="outOfStockGroups"
/>
<MasterOutOfStockCatalogSection
v-if="lowStockGroups.length > 0"
title="Bahan Baku Stok Menipis"
description="Varian bahan baku aktif di bawah batas minimum stok."
severity="warning"
:groups="lowStockGroups"
empty-title="Tidak ada stok menipis"
empty-description="Semua varian bahan baku masih di atas batas minimum."
/>
</div>
<Card class="min-w-0">
<CardContent class="min-w-0">