Update dependencies and enhance out-of-stock product management. Upgrade @lucide/vue to version 1.18.0 in package.json and package-lock.json. Implement outOfStockGroups method in ProductService and RawMaterialService to retrieve out-of-stock items. Add MasterOutOfStockCatalogSection component for displaying out-of-stock products in the admin interface, and integrate it into the products and raw materials index pages.
This commit is contained in:
parent
e77f0ebc8a
commit
c757b3c9b2
@ -32,6 +32,7 @@ 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),
|
'products' => $this->productService->paginateForIndex($tableQuery, $isActive),
|
||||||
|
'outOfStockGroups' => $this->productService->outOfStockGroups(),
|
||||||
'filters' => $this->dataTableFilters($tableQuery, [
|
'filters' => $this->dataTableFilters($tableQuery, [
|
||||||
'is_active' => $isActive,
|
'is_active' => $isActive,
|
||||||
]),
|
]),
|
||||||
|
|||||||
@ -32,6 +32,7 @@ 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),
|
'rawMaterials' => $this->rawMaterialService->paginateForIndex($tableQuery, $isActive),
|
||||||
|
'outOfStockGroups' => $this->rawMaterialService->outOfStockGroups(),
|
||||||
'filters' => $this->dataTableFilters($tableQuery, [
|
'filters' => $this->dataTableFilters($tableQuery, [
|
||||||
'is_active' => $isActive,
|
'is_active' => $isActive,
|
||||||
]),
|
]),
|
||||||
|
|||||||
@ -20,6 +20,52 @@ public function __construct(
|
|||||||
private readonly MediaService $mediaService,
|
private readonly MediaService $mediaService,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return list<array{
|
||||||
|
* id: int,
|
||||||
|
* name: string,
|
||||||
|
* edit_url: string,
|
||||||
|
* variants: list<array{
|
||||||
|
* id: int,
|
||||||
|
* name: string,
|
||||||
|
* stock_formatted: string,
|
||||||
|
* images: list<array<string, mixed>>,
|
||||||
|
* }>,
|
||||||
|
* }>
|
||||||
|
*/
|
||||||
|
public function outOfStockGroups(): array
|
||||||
|
{
|
||||||
|
return ProductVariant::query()
|
||||||
|
->with(['product', 'media'])
|
||||||
|
->where('stock', '<=', 0)
|
||||||
|
->whereHas('product', fn (Builder $query) => $query->where('is_active', true))
|
||||||
|
->get()
|
||||||
|
->sortBy([
|
||||||
|
fn (ProductVariant $variant) => $variant->product->name,
|
||||||
|
fn (ProductVariant $variant) => $variant->name,
|
||||||
|
])
|
||||||
|
->groupBy('product_id')
|
||||||
|
->map(function ($variants, $productId) {
|
||||||
|
$product = $variants->first()->product;
|
||||||
|
|
||||||
|
return [
|
||||||
|
'id' => (int) $productId,
|
||||||
|
'name' => $product->name,
|
||||||
|
'edit_url' => route('admin.master.products.edit', $productId),
|
||||||
|
'variants' => $variants->map(function (ProductVariant $variant): array {
|
||||||
|
return [
|
||||||
|
'id' => $variant->id,
|
||||||
|
'name' => $variant->name,
|
||||||
|
'stock_formatted' => number_format($variant->stock, 0, ',', '.').' pcs',
|
||||||
|
'images' => MediaPresenter::collection($variant, 'images'),
|
||||||
|
];
|
||||||
|
})->values()->all(),
|
||||||
|
];
|
||||||
|
})
|
||||||
|
->values()
|
||||||
|
->all();
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param array{search: string, sort: string, direction: 'asc'|'desc'} $tableQuery
|
* @param array{search: string, sort: string, direction: 'asc'|'desc'} $tableQuery
|
||||||
*/
|
*/
|
||||||
|
|||||||
@ -18,6 +18,64 @@ public function __construct(
|
|||||||
private readonly MediaService $mediaService,
|
private readonly MediaService $mediaService,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return list<array{
|
||||||
|
* id: int,
|
||||||
|
* name: string,
|
||||||
|
* unit_label: string,
|
||||||
|
* edit_url: string,
|
||||||
|
* variants: list<array{
|
||||||
|
* id: int,
|
||||||
|
* name: string,
|
||||||
|
* stock_formatted: string,
|
||||||
|
* images: list<array<string, mixed>>,
|
||||||
|
* }>,
|
||||||
|
* }>
|
||||||
|
*/
|
||||||
|
public function outOfStockGroups(): array
|
||||||
|
{
|
||||||
|
return RawMaterialPrice::query()
|
||||||
|
->with(['rawMaterial', 'media'])
|
||||||
|
->where('stock', '<=', 0)
|
||||||
|
->get()
|
||||||
|
->sortBy([
|
||||||
|
fn (RawMaterialPrice $price) => $price->rawMaterial->name,
|
||||||
|
fn (RawMaterialPrice $price) => $price->variant,
|
||||||
|
])
|
||||||
|
->groupBy('raw_material_id')
|
||||||
|
->map(function ($prices, $rawMaterialId) {
|
||||||
|
$material = $prices->first()->rawMaterial;
|
||||||
|
|
||||||
|
return [
|
||||||
|
'id' => (int) $rawMaterialId,
|
||||||
|
'name' => $material->name,
|
||||||
|
'unit_label' => $material->unit->label(),
|
||||||
|
'edit_url' => route('admin.master.raw-materials.edit', $rawMaterialId),
|
||||||
|
'variants' => $prices->map(function (RawMaterialPrice $price) use ($material): array {
|
||||||
|
return [
|
||||||
|
'id' => $price->id,
|
||||||
|
'name' => $price->variant,
|
||||||
|
'stock_formatted' => $this->formatStockAmount((float) $price->stock, $material->unit->abbreviation()),
|
||||||
|
'images' => MediaPresenter::collection($price, 'images'),
|
||||||
|
];
|
||||||
|
})->values()->all(),
|
||||||
|
];
|
||||||
|
})
|
||||||
|
->values()
|
||||||
|
->all();
|
||||||
|
}
|
||||||
|
|
||||||
|
private function formatStockAmount(float $amount, ?string $unit = null): string
|
||||||
|
{
|
||||||
|
$formatted = rtrim(rtrim(number_format($amount, 4, ',', '.'), '0'), ',');
|
||||||
|
|
||||||
|
if ($unit === null) {
|
||||||
|
return $formatted;
|
||||||
|
}
|
||||||
|
|
||||||
|
return "{$formatted} {$unit}";
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param array{search: string, sort: string, direction: 'asc'|'desc'} $tableQuery
|
* @param array{search: string, sort: string, direction: 'asc'|'desc'} $tableQuery
|
||||||
*/
|
*/
|
||||||
|
|||||||
8
package-lock.json
generated
8
package-lock.json
generated
@ -12,7 +12,7 @@
|
|||||||
"@fullcalendar/vue3": "^6.1.20",
|
"@fullcalendar/vue3": "^6.1.20",
|
||||||
"@inertiajs/vite": "^3.0.0",
|
"@inertiajs/vite": "^3.0.0",
|
||||||
"@inertiajs/vue3": "^3.0.0",
|
"@inertiajs/vue3": "^3.0.0",
|
||||||
"@lucide/vue": "^1.17.0",
|
"@lucide/vue": "^1.18.0",
|
||||||
"@point-of-sale/receipt-printer-encoder": "^3.0.3",
|
"@point-of-sale/receipt-printer-encoder": "^3.0.3",
|
||||||
"@point-of-sale/webbluetooth-receipt-printer": "^2.0.0",
|
"@point-of-sale/webbluetooth-receipt-printer": "^2.0.0",
|
||||||
"@point-of-sale/webserial-receipt-printer": "^2.0.0",
|
"@point-of-sale/webserial-receipt-printer": "^2.0.0",
|
||||||
@ -2618,9 +2618,9 @@
|
|||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/@lucide/vue": {
|
"node_modules/@lucide/vue": {
|
||||||
"version": "1.17.0",
|
"version": "1.18.0",
|
||||||
"resolved": "https://registry.npmjs.org/@lucide/vue/-/vue-1.17.0.tgz",
|
"resolved": "https://registry.npmjs.org/@lucide/vue/-/vue-1.18.0.tgz",
|
||||||
"integrity": "sha512-6Q1ZHgr5FbmJzKWe5BxlNdjLj2lbmuH1zwDtVzUJofX0w9UREwKgq4F4jwKqFYyyIS4Rj3FiJvDi2k6djukmmw==",
|
"integrity": "sha512-DmnUpDB85PlMZ+ofjZLcKq3JoJnaD1bk7SIj9xwUvqerfNqA6hCLa0/m3gIybH6rdrErABbqvTD8yYJdNqiZ3Q==",
|
||||||
"license": "ISC",
|
"license": "ISC",
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"vue": ">=3.0.1"
|
"vue": ">=3.0.1"
|
||||||
|
|||||||
@ -40,7 +40,7 @@
|
|||||||
"@fullcalendar/vue3": "^6.1.20",
|
"@fullcalendar/vue3": "^6.1.20",
|
||||||
"@inertiajs/vite": "^3.0.0",
|
"@inertiajs/vite": "^3.0.0",
|
||||||
"@inertiajs/vue3": "^3.0.0",
|
"@inertiajs/vue3": "^3.0.0",
|
||||||
"@lucide/vue": "^1.17.0",
|
"@lucide/vue": "^1.18.0",
|
||||||
"@point-of-sale/receipt-printer-encoder": "^3.0.3",
|
"@point-of-sale/receipt-printer-encoder": "^3.0.3",
|
||||||
"@point-of-sale/webbluetooth-receipt-printer": "^2.0.0",
|
"@point-of-sale/webbluetooth-receipt-printer": "^2.0.0",
|
||||||
"@point-of-sale/webserial-receipt-printer": "^2.0.0",
|
"@point-of-sale/webserial-receipt-printer": "^2.0.0",
|
||||||
|
|||||||
@ -2,15 +2,18 @@
|
|||||||
import { Package } from '@lucide/vue';
|
import { Package } from '@lucide/vue';
|
||||||
import type { MediaItem } from '@/types/media';
|
import type { MediaItem } from '@/types/media';
|
||||||
|
|
||||||
defineProps<{
|
withDefaults(defineProps<{
|
||||||
title: string;
|
title: string;
|
||||||
coverImage?: MediaItem;
|
coverImage?: MediaItem;
|
||||||
}>();
|
showCover?: boolean;
|
||||||
|
}>(), {
|
||||||
|
showCover: true,
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<article class="mb-4 break-inside-avoid overflow-hidden rounded-lg border bg-card shadow-sm">
|
<article class="mb-4 break-inside-avoid overflow-hidden rounded-lg border bg-card shadow-sm">
|
||||||
<div class="relative aspect-4/3 overflow-hidden bg-muted/30">
|
<div v-if="showCover" class="relative aspect-4/3 overflow-hidden bg-muted/30">
|
||||||
<img
|
<img
|
||||||
v-if="coverImage"
|
v-if="coverImage"
|
||||||
:src="coverImage.thumb_url"
|
:src="coverImage.thumb_url"
|
||||||
|
|||||||
@ -0,0 +1,103 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { router } from '@inertiajs/vue3';
|
||||||
|
import { ChevronDown, PackageX } from '@lucide/vue';
|
||||||
|
import { computed, ref } from 'vue';
|
||||||
|
import PosCatalogCard from '@/components/admin/manage/PosCatalogCard.vue';
|
||||||
|
import PosCatalogVariantThumb from '@/components/admin/manage/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 = defineProps<{
|
||||||
|
title: string;
|
||||||
|
description?: string;
|
||||||
|
groups: MasterOutOfStockGroup[];
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const open = ref(true);
|
||||||
|
|
||||||
|
const variantCount = computed(() => props.groups.reduce(
|
||||||
|
(total, group) => total + group.variants.length,
|
||||||
|
0,
|
||||||
|
));
|
||||||
|
|
||||||
|
function visitEdit(url: string): void {
|
||||||
|
router.visit(url);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<Collapsible v-model:open="open">
|
||||||
|
<Card>
|
||||||
|
<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">
|
||||||
|
<PackageX class="size-4 shrink-0 text-red-600 dark:text-red-400" />
|
||||||
|
<h3 class="font-semibold leading-tight">
|
||||||
|
{{ title }}
|
||||||
|
</h3>
|
||||||
|
<Badge v-if="variantCount > 0" variant="destructive">
|
||||||
|
{{ 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">
|
||||||
|
<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 text-red-600 dark:text-red-400">
|
||||||
|
{{ variant.stock_formatted }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</PosCatalogCard>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Empty v-else class="py-8">
|
||||||
|
<EmptyHeader>
|
||||||
|
<EmptyTitle>Tidak ada stok habis</EmptyTitle>
|
||||||
|
<EmptyDescription>
|
||||||
|
Semua varian masih memiliki stok tersedia.
|
||||||
|
</EmptyDescription>
|
||||||
|
</EmptyHeader>
|
||||||
|
</Empty>
|
||||||
|
</CardContent>
|
||||||
|
</CollapsibleContent>
|
||||||
|
</Card>
|
||||||
|
</Collapsible>
|
||||||
|
</template>
|
||||||
19
resources/js/components/ui/collapsible/Collapsible.vue
Normal file
19
resources/js/components/ui/collapsible/Collapsible.vue
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import type { CollapsibleRootEmits, CollapsibleRootProps } from "reka-ui"
|
||||||
|
import { CollapsibleRoot, useForwardPropsEmits } from "reka-ui"
|
||||||
|
|
||||||
|
const props = defineProps<CollapsibleRootProps>()
|
||||||
|
const emits = defineEmits<CollapsibleRootEmits>()
|
||||||
|
|
||||||
|
const forwarded = useForwardPropsEmits(props, emits)
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<CollapsibleRoot
|
||||||
|
v-slot="slotProps"
|
||||||
|
data-slot="collapsible"
|
||||||
|
v-bind="forwarded"
|
||||||
|
>
|
||||||
|
<slot v-bind="slotProps" />
|
||||||
|
</CollapsibleRoot>
|
||||||
|
</template>
|
||||||
@ -0,0 +1,15 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import type { CollapsibleContentProps } from "reka-ui"
|
||||||
|
import { CollapsibleContent } from "reka-ui"
|
||||||
|
|
||||||
|
const props = defineProps<CollapsibleContentProps>()
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<CollapsibleContent
|
||||||
|
data-slot="collapsible-content"
|
||||||
|
v-bind="props"
|
||||||
|
>
|
||||||
|
<slot />
|
||||||
|
</CollapsibleContent>
|
||||||
|
</template>
|
||||||
@ -0,0 +1,15 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import type { CollapsibleTriggerProps } from "reka-ui"
|
||||||
|
import { CollapsibleTrigger } from "reka-ui"
|
||||||
|
|
||||||
|
const props = defineProps<CollapsibleTriggerProps>()
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<CollapsibleTrigger
|
||||||
|
data-slot="collapsible-trigger"
|
||||||
|
v-bind="props"
|
||||||
|
>
|
||||||
|
<slot />
|
||||||
|
</CollapsibleTrigger>
|
||||||
|
</template>
|
||||||
3
resources/js/components/ui/collapsible/index.ts
Normal file
3
resources/js/components/ui/collapsible/index.ts
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
export { default as Collapsible } from "./Collapsible.vue"
|
||||||
|
export { default as CollapsibleContent } from "./CollapsibleContent.vue"
|
||||||
|
export { default as CollapsibleTrigger } from "./CollapsibleTrigger.vue"
|
||||||
14
resources/js/lib/stock.ts
Normal file
14
resources/js/lib/stock.ts
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
export function formatStockAmount(value: number | string, unit?: string): string {
|
||||||
|
const numericValue = typeof value === 'string' ? Number.parseFloat(value) : value;
|
||||||
|
|
||||||
|
if (!Number.isFinite(numericValue)) {
|
||||||
|
return unit ? `0 ${unit}` : '0';
|
||||||
|
}
|
||||||
|
|
||||||
|
const formatted = numericValue.toLocaleString('id-ID', {
|
||||||
|
minimumFractionDigits: 0,
|
||||||
|
maximumFractionDigits: 4,
|
||||||
|
});
|
||||||
|
|
||||||
|
return unit ? `${formatted} ${unit}` : formatted;
|
||||||
|
}
|
||||||
@ -2,6 +2,7 @@
|
|||||||
import { Head, Link } from '@inertiajs/vue3';
|
import { Head, Link } from '@inertiajs/vue3';
|
||||||
import { Plus } from '@lucide/vue';
|
import { Plus } from '@lucide/vue';
|
||||||
import { computed, ref, watch } from 'vue';
|
import { computed, ref, watch } from 'vue';
|
||||||
|
import MasterOutOfStockCatalogSection from '@/components/admin/master/MasterOutOfStockCatalogSection.vue';
|
||||||
import ProductGroupedTable from '@/components/admin/master/products/ProductGroupedTable.vue';
|
import ProductGroupedTable from '@/components/admin/master/products/ProductGroupedTable.vue';
|
||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
import { Card, CardContent } from '@/components/ui/card';
|
import { Card, CardContent } from '@/components/ui/card';
|
||||||
@ -9,10 +10,11 @@ import { useCan } from '@/composables/useCan';
|
|||||||
import { useDataTableQuery, useDataTableQuerySync } from '@/composables/useDataTableQuery';
|
import { useDataTableQuery, useDataTableQuerySync } from '@/composables/useDataTableQuery';
|
||||||
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 { PaginatedProducts } from '@/types/product';
|
import type { PaginatedProducts, ProductOutOfStockGroup } from '@/types/product';
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
products: PaginatedProducts;
|
products: PaginatedProducts;
|
||||||
|
outOfStockGroups: ProductOutOfStockGroup[];
|
||||||
filters: {
|
filters: {
|
||||||
search: string;
|
search: string;
|
||||||
sort?: string;
|
sort?: string;
|
||||||
@ -93,6 +95,12 @@ watch(
|
|||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<MasterOutOfStockCatalogSection
|
||||||
|
title="Produk Stok Habis"
|
||||||
|
description="Hanya produk aktif · klik kartu atau varian untuk edit"
|
||||||
|
:groups="outOfStockGroups"
|
||||||
|
/>
|
||||||
|
|
||||||
<Card class="min-w-0">
|
<Card class="min-w-0">
|
||||||
<CardContent class="min-w-0">
|
<CardContent class="min-w-0">
|
||||||
<ProductGroupedTable v-model:search="search" :products="products.data" :first-item="firstItem"
|
<ProductGroupedTable v-model:search="search" :products="products.data" :first-item="firstItem"
|
||||||
|
|||||||
@ -2,6 +2,7 @@
|
|||||||
import { Head, Link } from '@inertiajs/vue3';
|
import { Head, Link } from '@inertiajs/vue3';
|
||||||
import { Plus } from '@lucide/vue';
|
import { Plus } from '@lucide/vue';
|
||||||
import { computed, ref, watch } from 'vue';
|
import { computed, ref, watch } from 'vue';
|
||||||
|
import MasterOutOfStockCatalogSection from '@/components/admin/master/MasterOutOfStockCatalogSection.vue';
|
||||||
import RawMaterialGroupedTable from '@/components/admin/master/raw-materials/RawMaterialGroupedTable.vue';
|
import RawMaterialGroupedTable from '@/components/admin/master/raw-materials/RawMaterialGroupedTable.vue';
|
||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
import { Card, CardContent } from '@/components/ui/card';
|
import { Card, CardContent } from '@/components/ui/card';
|
||||||
@ -9,10 +10,12 @@ import { useCan } from '@/composables/useCan';
|
|||||||
import { useDataTableQuery, useDataTableQuerySync } from '@/composables/useDataTableQuery';
|
import { useDataTableQuery, useDataTableQuerySync } from '@/composables/useDataTableQuery';
|
||||||
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 { MasterOutOfStockGroup } from '@/types/master-inventory';
|
||||||
import type { PaginatedRawMaterials } from '@/types/raw-material';
|
import type { PaginatedRawMaterials } from '@/types/raw-material';
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
rawMaterials: PaginatedRawMaterials;
|
rawMaterials: PaginatedRawMaterials;
|
||||||
|
outOfStockGroups: MasterOutOfStockGroup[];
|
||||||
filters: {
|
filters: {
|
||||||
search: string;
|
search: string;
|
||||||
sort?: string;
|
sort?: string;
|
||||||
@ -93,6 +96,12 @@ watch(
|
|||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<MasterOutOfStockCatalogSection
|
||||||
|
title="Bahan Baku Stok Habis"
|
||||||
|
description="Klik kartu atau varian untuk membuka halaman edit"
|
||||||
|
:groups="outOfStockGroups"
|
||||||
|
/>
|
||||||
|
|
||||||
<Card class="min-w-0">
|
<Card class="min-w-0">
|
||||||
<CardContent class="min-w-0">
|
<CardContent class="min-w-0">
|
||||||
<RawMaterialGroupedTable v-model:search="search" :materials="rawMaterials.data" :first-item="firstItem"
|
<RawMaterialGroupedTable v-model:search="search" :materials="rawMaterials.data" :first-item="firstItem"
|
||||||
|
|||||||
16
resources/js/types/master-inventory.ts
Normal file
16
resources/js/types/master-inventory.ts
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
import type { MediaItem } from '@/types/media';
|
||||||
|
|
||||||
|
export type MasterOutOfStockVariant = {
|
||||||
|
id: number;
|
||||||
|
name: string;
|
||||||
|
stock_formatted: string;
|
||||||
|
images?: MediaItem[];
|
||||||
|
};
|
||||||
|
|
||||||
|
export type MasterOutOfStockGroup = {
|
||||||
|
id: number;
|
||||||
|
name: string;
|
||||||
|
edit_url: string;
|
||||||
|
unit_label?: string;
|
||||||
|
variants: MasterOutOfStockVariant[];
|
||||||
|
};
|
||||||
@ -1,5 +1,8 @@
|
|||||||
|
import type { MasterOutOfStockGroup } from '@/types/master-inventory';
|
||||||
import type { MediaItem, MediaUploadState } from '@/types/media';
|
import type { MediaItem, MediaUploadState } from '@/types/media';
|
||||||
|
|
||||||
|
export type { MasterOutOfStockGroup as ProductOutOfStockGroup };
|
||||||
|
|
||||||
export type EnumOption = {
|
export type EnumOption = {
|
||||||
value: string;
|
value: string;
|
||||||
label: string;
|
label: string;
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user