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\Http\Requests\Admin\ToggleStatusRequest;
|
||||||
use App\Models\Product;
|
use App\Models\Product;
|
||||||
use App\Services\Master\CategoryService;
|
use App\Services\Master\CategoryService;
|
||||||
use App\Services\Master\MasterInventoryService;
|
|
||||||
use App\Services\Master\ProductService;
|
use App\Services\Master\ProductService;
|
||||||
use Illuminate\Http\RedirectResponse;
|
use Illuminate\Http\RedirectResponse;
|
||||||
use Illuminate\Http\Request;
|
use Illuminate\Http\Request;
|
||||||
@ -23,7 +22,6 @@ class ProductController extends Controller
|
|||||||
public function __construct(
|
public function __construct(
|
||||||
private readonly ProductService $productService,
|
private readonly ProductService $productService,
|
||||||
private readonly CategoryService $categoryService,
|
private readonly CategoryService $categoryService,
|
||||||
private readonly MasterInventoryService $masterInventoryService,
|
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
public function index(Request $request): Response
|
public function index(Request $request): Response
|
||||||
@ -36,7 +34,6 @@ public function index(Request $request): Response
|
|||||||
return Inertia::render('admin/master/products/Index', [
|
return Inertia::render('admin/master/products/Index', [
|
||||||
'products' => $this->productService->paginateForIndex($tableQuery, $isActive, $categoryId, $stockStatus),
|
'products' => $this->productService->paginateForIndex($tableQuery, $isActive, $categoryId, $stockStatus),
|
||||||
'categories' => $this->categoryService->getSelectOptions(),
|
'categories' => $this->categoryService->getSelectOptions(),
|
||||||
'outOfStockGroups' => $this->masterInventoryService->outOfStockProductGroups(),
|
|
||||||
'filters' => $this->dataTableFilters($tableQuery, [
|
'filters' => $this->dataTableFilters($tableQuery, [
|
||||||
'is_active' => $isActive,
|
'is_active' => $isActive,
|
||||||
'category_id' => $categoryId,
|
'category_id' => $categoryId,
|
||||||
|
|||||||
@ -9,7 +9,6 @@
|
|||||||
use App\Http\Requests\Admin\Master\RawMaterialRequest;
|
use App\Http\Requests\Admin\Master\RawMaterialRequest;
|
||||||
use App\Http\Requests\Admin\ToggleStatusRequest;
|
use App\Http\Requests\Admin\ToggleStatusRequest;
|
||||||
use App\Models\RawMaterial;
|
use App\Models\RawMaterial;
|
||||||
use App\Services\Master\MasterInventoryService;
|
|
||||||
use App\Services\Master\RawMaterialService;
|
use App\Services\Master\RawMaterialService;
|
||||||
use Illuminate\Http\RedirectResponse;
|
use Illuminate\Http\RedirectResponse;
|
||||||
use Illuminate\Http\Request;
|
use Illuminate\Http\Request;
|
||||||
@ -22,7 +21,6 @@ class RawMaterialController extends Controller
|
|||||||
|
|
||||||
public function __construct(
|
public function __construct(
|
||||||
private readonly RawMaterialService $rawMaterialService,
|
private readonly RawMaterialService $rawMaterialService,
|
||||||
private readonly MasterInventoryService $masterInventoryService,
|
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
public function index(Request $request): Response
|
public function index(Request $request): Response
|
||||||
@ -33,8 +31,6 @@ public function index(Request $request): Response
|
|||||||
|
|
||||||
return Inertia::render('admin/master/raw-materials/Index', [
|
return Inertia::render('admin/master/raw-materials/Index', [
|
||||||
'rawMaterials' => $this->rawMaterialService->paginateForIndex($tableQuery, $isActive, $stockStatus),
|
'rawMaterials' => $this->rawMaterialService->paginateForIndex($tableQuery, $isActive, $stockStatus),
|
||||||
'outOfStockGroups' => $this->masterInventoryService->outOfStockRawMaterialGroups(),
|
|
||||||
'lowStockGroups' => $this->masterInventoryService->lowStockRawMaterialGroups(),
|
|
||||||
'filters' => $this->dataTableFilters($tableQuery, [
|
'filters' => $this->dataTableFilters($tableQuery, [
|
||||||
'is_active' => $isActive,
|
'is_active' => $isActive,
|
||||||
'stock_status' => $stockStatus,
|
'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 AdminLayout from '@/layouts/AdminLayout.vue';
|
||||||
import type { DataTableFilterDef } from '@/types/data-table';
|
import type { DataTableFilterDef } from '@/types/data-table';
|
||||||
import type { CategoryOption, PaginatedProducts } from '@/types/product';
|
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 { Head } from '@inertiajs/vue3';
|
||||||
import { computed, ref, watch } from 'vue';
|
import { computed, ref, watch } from 'vue';
|
||||||
import { index, create } from '@/routes/admin/master/products';
|
import { index, create } from '@/routes/admin/master/products';
|
||||||
@ -21,7 +19,6 @@ import ProductGroupedTable from './table/ProductGroupedTable.vue';
|
|||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
products: PaginatedProducts;
|
products: PaginatedProducts;
|
||||||
categories: CategoryOption[];
|
categories: CategoryOption[];
|
||||||
outOfStockGroups: MasterOutOfStockGroup[];
|
|
||||||
filters: {
|
filters: {
|
||||||
search: string;
|
search: string;
|
||||||
sort?: string;
|
sort?: string;
|
||||||
@ -122,12 +119,6 @@ watch(
|
|||||||
/>
|
/>
|
||||||
</div>
|
</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">
|
<Card class="min-w-0">
|
||||||
<CardContent 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 AdminLayout from '@/layouts/AdminLayout.vue';
|
||||||
import type { DataTableFilterDef } from '@/types/data-table';
|
import type { DataTableFilterDef } from '@/types/data-table';
|
||||||
import type { PaginatedRawMaterials } from '@/types/raw-material';
|
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 { Head } from '@inertiajs/vue3';
|
||||||
import { computed, ref, watch } from 'vue';
|
import { computed, ref, watch } from 'vue';
|
||||||
import { index, create } from '@/routes/admin/master/raw_materials';
|
import { index, create } from '@/routes/admin/master/raw_materials';
|
||||||
@ -20,8 +18,6 @@ import RawMaterialGroupedTable from './table/RawMaterialGroupedTable.vue';
|
|||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
rawMaterials: PaginatedRawMaterials;
|
rawMaterials: PaginatedRawMaterials;
|
||||||
outOfStockGroups: MasterOutOfStockGroup[];
|
|
||||||
lowStockGroups: MasterOutOfStockGroup[];
|
|
||||||
filters: {
|
filters: {
|
||||||
search: string;
|
search: string;
|
||||||
sort?: string;
|
sort?: string;
|
||||||
@ -112,23 +108,6 @@ watch(
|
|||||||
/>
|
/>
|
||||||
</div>
|
</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">
|
<Card class="min-w-0">
|
||||||
<CardContent class="min-w-0">
|
<CardContent class="min-w-0">
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user