refactor: remove unused inventory stock warning components and service logic
This commit is contained in:
parent
62e6995191
commit
ce553f9aed
@ -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,
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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();
|
||||
}
|
||||
}
|
||||
@ -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>
|
||||
@ -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">
|
||||
|
||||
@ -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">
|
||||
|
||||
Loading…
Reference in New Issue
Block a user