feat: add variants endpoint and update product/raw material models for variant handling
This commit is contained in:
parent
ff638cf9d1
commit
ce901c2c8b
@ -8,6 +8,7 @@
|
||||
use App\Models\Product;
|
||||
use App\Services\Admin\Master\CategoryService;
|
||||
use App\Services\Admin\Master\Product\ProductService;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Inertia\Inertia;
|
||||
use Inertia\Response;
|
||||
@ -124,4 +125,11 @@ public function resubmit(Product $product): RedirectResponse
|
||||
|
||||
return back();
|
||||
}
|
||||
|
||||
public function variants(Product $product): JsonResponse
|
||||
{
|
||||
return response()->json([
|
||||
'variants' => $this->service->getVariants($product),
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
@ -7,6 +7,7 @@
|
||||
use App\Http\Requests\PaginatedRequest;
|
||||
use App\Models\RawMaterial;
|
||||
use App\Services\Admin\Master\RawMaterial\RawMaterialService;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Inertia\Inertia;
|
||||
use Inertia\Response;
|
||||
@ -80,4 +81,11 @@ public function toggleStatus(RawMaterial $rawMaterial): RedirectResponse
|
||||
|
||||
return back();
|
||||
}
|
||||
|
||||
public function variants(RawMaterial $rawMaterial): JsonResponse
|
||||
{
|
||||
return response()->json([
|
||||
'variants' => $this->service->getVariants($rawMaterial),
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
@ -34,13 +34,17 @@ public function getNames(): Collection
|
||||
|
||||
public function paginated(int $perPage = 25, string $search = '', string $sort = 'created_at', string $direction = 'desc', array $filters = []): LengthAwarePaginator
|
||||
{
|
||||
$paginator = Product::query()
|
||||
$stockSumQuery = '(SELECT IFNULL(SUM(stock), 0) FROM product_variants WHERE product_variants.product_id = products.id AND product_variants.deleted_at IS NULL)';
|
||||
$rejectSumQuery = '(SELECT IFNULL(SUM(reject_stock), 0) FROM product_variants WHERE product_variants.product_id = products.id AND product_variants.deleted_at IS NULL)';
|
||||
$retailSumQuery = '(SELECT IFNULL(SUM(retail_stock), 0) FROM product_variants WHERE product_variants.product_id = products.id AND product_variants.deleted_at IS NULL)';
|
||||
|
||||
return Product::query()
|
||||
->select(['id', 'name', 'slug', 'description', 'status', 'rejection_reason', 'is_featured'])
|
||||
->with([
|
||||
'categories:id,name',
|
||||
'productVariants:id,product_id,name,stock,reject_stock,retail_stock',
|
||||
'productVariants.productPrices:id,variant_id,type,price',
|
||||
])
|
||||
->with(['categories:id,name'])
|
||||
->withCount('productVariants as variants_count')
|
||||
->selectRaw("{$stockSumQuery} as total_stock")
|
||||
->selectRaw("{$rejectSumQuery} as total_reject_stock")
|
||||
->selectRaw("{$retailSumQuery} as total_retail_stock")
|
||||
->when($search, fn ($q) => $q->where('name', 'like', "%{$search}%")
|
||||
->orWhereHas('productVariants', fn ($vq) => $vq->where('name', 'like', "%{$search}%"))
|
||||
)
|
||||
@ -50,24 +54,27 @@ public function paginated(int $perPage = 25, string $search = '', string $sort =
|
||||
$cq->where('categories.id', $categoryId);
|
||||
}))
|
||||
->when($filters['featured'] ?? null, fn ($q, $featured) => $q->where('is_featured', $featured === 'true'))
|
||||
->when(($filters['stock'] ?? null) === 'empty', function ($q) {
|
||||
$q->whereRaw('(SELECT IFNULL(SUM(stock), 0) FROM product_variants WHERE product_variants.product_id = products.id AND product_variants.deleted_at IS NULL) = 0');
|
||||
->when(($filters['stock'] ?? null) === 'empty', function ($q) use ($stockSumQuery) {
|
||||
$q->whereRaw("{$stockSumQuery} = 0");
|
||||
})
|
||||
->when(($filters['stock'] ?? null) === 'low', function ($q) {
|
||||
$q->whereRaw('(SELECT IFNULL(SUM(stock), 0) FROM product_variants WHERE product_variants.product_id = products.id AND product_variants.deleted_at IS NULL) BETWEEN 1 AND 9');
|
||||
->when(($filters['stock'] ?? null) === 'low', function ($q) use ($stockSumQuery) {
|
||||
$q->whereRaw("{$stockSumQuery} BETWEEN 1 AND 9");
|
||||
})
|
||||
->orderBy($sort, $direction)
|
||||
->paginate($perPage);
|
||||
}
|
||||
|
||||
$paginator->getCollection()->each(function ($product) {
|
||||
$product->productVariants->each(function ($variant) {
|
||||
public function getVariants(Product $product): Collection
|
||||
{
|
||||
return $product->productVariants()
|
||||
->select(['id', 'product_id', 'name', 'stock', 'reject_stock', 'retail_stock'])
|
||||
->with(['productPrices:id,variant_id,type,price'])
|
||||
->get()
|
||||
->each(function (ProductVariant $variant) {
|
||||
$media = $variant->getMedia('images');
|
||||
$variant->photo_urls = $media->map(fn ($m) => $this->s3Service->getTemporaryUrl($m->getPath()))->toArray();
|
||||
$variant->photo_conversion_urls = $media->map(fn ($m) => $this->s3Service->getTemporaryUrl($m->getPath('thumb')))->toArray();
|
||||
});
|
||||
});
|
||||
|
||||
return $paginator;
|
||||
}
|
||||
|
||||
public function store(array $data): Product
|
||||
|
||||
@ -27,11 +27,14 @@ public function getNames(): Collection
|
||||
|
||||
public function paginated(int $perPage = 25, string $search = '', string $sort = 'created_at', string $direction = 'desc', array $filters = []): LengthAwarePaginator
|
||||
{
|
||||
$paginator = RawMaterial::query()
|
||||
$stockSumQuery = '(SELECT IFNULL(SUM(stock), 0) FROM raw_material_prices WHERE raw_material_prices.raw_material_id = raw_materials.id AND raw_material_prices.deleted_at IS NULL)';
|
||||
$valueSumQuery = '(SELECT IFNULL(SUM(price * stock), 0) FROM raw_material_prices WHERE raw_material_prices.raw_material_id = raw_materials.id AND raw_material_prices.deleted_at IS NULL)';
|
||||
|
||||
return RawMaterial::query()
|
||||
->select(['id', 'name', 'unit', 'is_active'])
|
||||
->with([
|
||||
'rawMaterialPrices:id,raw_material_id,variant,price,stock',
|
||||
])
|
||||
->withCount('rawMaterialPrices as variants_count')
|
||||
->selectRaw("{$stockSumQuery} as total_stock")
|
||||
->selectRaw("{$valueSumQuery} as total_value")
|
||||
->when($search, fn ($q) => $q->where('name', 'like', "%{$search}%")
|
||||
->orWhereHas('rawMaterialPrices', fn ($vq) => $vq->where('variant', 'like', "%{$search}%"))
|
||||
)
|
||||
@ -39,17 +42,22 @@ public function paginated(int $perPage = 25, string $search = '', string $sort =
|
||||
->when(($filters['is_active'] ?? null) !== null && ($filters['is_active'] ?? null) !== '', function ($q) use ($filters) {
|
||||
$q->where('is_active', $filters['is_active'] === 'true');
|
||||
})
|
||||
->when(($filters['stock'] ?? null) === 'empty', function ($q) {
|
||||
$q->whereRaw('(SELECT IFNULL(SUM(stock), 0) FROM raw_material_prices WHERE raw_material_prices.raw_material_id = raw_materials.id AND raw_material_prices.deleted_at IS NULL) = 0');
|
||||
->when(($filters['stock'] ?? null) === 'empty', function ($q) use ($stockSumQuery) {
|
||||
$q->whereRaw("{$stockSumQuery} = 0");
|
||||
})
|
||||
->when(($filters['stock'] ?? null) === 'low', function ($q) {
|
||||
$q->whereRaw('(SELECT IFNULL(SUM(stock), 0) FROM raw_material_prices WHERE raw_material_prices.raw_material_id = raw_materials.id AND raw_material_prices.deleted_at IS NULL) BETWEEN 0.0001 AND 9.9999');
|
||||
->when(($filters['stock'] ?? null) === 'low', function ($q) use ($stockSumQuery) {
|
||||
$q->whereRaw("{$stockSumQuery} BETWEEN 0.0001 AND 9.9999");
|
||||
})
|
||||
->orderBy($sort, $direction)
|
||||
->paginate($perPage);
|
||||
}
|
||||
|
||||
$paginator->getCollection()->each(function ($rawMaterial) {
|
||||
$rawMaterial->rawMaterialPrices->each(function ($price) {
|
||||
public function getVariants(RawMaterial $rawMaterial): Collection
|
||||
{
|
||||
return $rawMaterial->rawMaterialPrices()
|
||||
->select(['id', 'raw_material_id', 'variant', 'price', 'stock'])
|
||||
->get()
|
||||
->each(function (RawMaterialPrice $price) {
|
||||
$media = $price->getFirstMedia('images');
|
||||
if ($media) {
|
||||
$price->photo_url = $this->s3Service->getTemporaryUrl($media->getPath());
|
||||
@ -59,9 +67,6 @@ public function paginated(int $perPage = 25, string $search = '', string $sort =
|
||||
$price->photo_conversion_url = null;
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
return $paginator;
|
||||
}
|
||||
|
||||
public function store(array $data): RawMaterial
|
||||
|
||||
@ -38,7 +38,11 @@ export type Product = {
|
||||
id: number;
|
||||
name: string;
|
||||
}[];
|
||||
product_variants: ProductVariant[];
|
||||
variants_count: number;
|
||||
total_stock: number;
|
||||
total_reject_stock: number;
|
||||
total_retail_stock: number;
|
||||
product_variants?: ProductVariant[];
|
||||
};
|
||||
|
||||
function getStatusLabel(status: string): string {
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
import { Link, router } from '@inertiajs/react';
|
||||
import { Head } from '@inertiajs/react';
|
||||
import { Plus } from 'lucide-react';
|
||||
import { useMemo, useState } from 'react';
|
||||
import { useCallback, useMemo, useState } from 'react';
|
||||
import { CardTable } from '@/components/data-display';
|
||||
import { DeleteConfirmDialog } from '@/components/dialogs';
|
||||
import type { ImagePreviewItem } from '@/components/dialogs';
|
||||
@ -36,6 +36,7 @@ import {
|
||||
approve as productApprove,
|
||||
reject as productReject,
|
||||
resubmit as productResubmit,
|
||||
variants as productVariants,
|
||||
} from '@/routes/admin/master/products';
|
||||
import {
|
||||
destroy as variantDestroy,
|
||||
@ -76,7 +77,9 @@ export default function ProductIndex({ products, categories, productNames, filte
|
||||
variant: ProductVariant;
|
||||
} | null>(null);
|
||||
const [rejecting, setRejecting] = useState<Product | null>(null);
|
||||
const expand = useCardTableExpand(true);
|
||||
const [loadedVariants, setLoadedVariants] = useState<Record<number, ProductVariant[]>>({});
|
||||
const [loadingVariants, setLoadingVariants] = useState<Record<number, boolean>>({});
|
||||
const expand = useCardTableExpand(false);
|
||||
|
||||
const pagination = {
|
||||
current_page: products.current_page,
|
||||
@ -106,20 +109,43 @@ export default function ProductIndex({ products, categories, productNames, filte
|
||||
[productNames],
|
||||
);
|
||||
|
||||
const fetchVariants = useCallback((product: Product) => {
|
||||
if (loadedVariants[product.id] || loadingVariants[product.id]) {
|
||||
return;
|
||||
}
|
||||
|
||||
setLoadingVariants((prev) => ({ ...prev, [product.id]: true }));
|
||||
|
||||
fetch(productVariants.url(product.id))
|
||||
.then((res) => res.json())
|
||||
.then((data) => {
|
||||
setLoadedVariants((prev) => ({
|
||||
...prev,
|
||||
[product.id]: data.variants ?? [],
|
||||
}));
|
||||
})
|
||||
.catch(() => {
|
||||
setLoadedVariants((prev) => ({ ...prev, [product.id]: [] }));
|
||||
})
|
||||
.finally(() => {
|
||||
setLoadingVariants((prev) => ({ ...prev, [product.id]: false }));
|
||||
});
|
||||
}, [loadedVariants, loadingVariants]);
|
||||
|
||||
const allPreviewItems: ImagePreviewItem[] = useMemo(
|
||||
() =>
|
||||
products.data.flatMap((product) =>
|
||||
(product.product_variants ?? [])
|
||||
Object.entries(loadedVariants).flatMap(([productId, variants]) =>
|
||||
variants
|
||||
.filter((v) => v.photo_urls?.length > 0)
|
||||
.map((v) => ({
|
||||
id: `${product.id}-${v.id}`,
|
||||
id: `${productId}-${v.id}`,
|
||||
src: v.photo_urls[0],
|
||||
sources: v.photo_urls,
|
||||
title: v.name,
|
||||
description: `Stok Bagus: ${v.formatted_stock} | Stok Reject: ${v.formatted_reject_stock} | Stok Ecer: ${v.formatted_retail_stock}`,
|
||||
}))
|
||||
),
|
||||
[products.data],
|
||||
[loadedVariants],
|
||||
);
|
||||
|
||||
const selectedCategory = useMemo(
|
||||
@ -313,7 +339,14 @@ export default function ProductIndex({ products, categories, productNames, filte
|
||||
data={products.data}
|
||||
getItemKey={(p) => p.id}
|
||||
expandedKeys={expand.expandedKeys}
|
||||
onToggleExpand={expand.toggleExpand}
|
||||
onToggleExpand={(key) => {
|
||||
const p = products.data.find((r) => r.id === key);
|
||||
const isCurrentlyExpanded = expand.expandedKeys === 'all' || expand.expandedKeys.has(key);
|
||||
if (p && !isCurrentlyExpanded) {
|
||||
fetchVariants(p);
|
||||
}
|
||||
expand.toggleExpand(key);
|
||||
}}
|
||||
searchValue={search}
|
||||
onSearchChange={handleSearchChange}
|
||||
|
||||
@ -351,6 +384,8 @@ export default function ProductIndex({ products, categories, productNames, filte
|
||||
renderSubContent={(product) => (
|
||||
<VariantSubRow
|
||||
product={product}
|
||||
variants={loadedVariants[product.id] ?? []}
|
||||
isLoading={loadingVariants[product.id] ?? false}
|
||||
allPreviewItems={allPreviewItems}
|
||||
onEditVariant={(p, v) => {
|
||||
router.visit(
|
||||
|
||||
@ -62,16 +62,10 @@ export function ProductCardRow({
|
||||
}: ProductCardRowParams) {
|
||||
const { can, hasRole } = useCan();
|
||||
const isVerifier = hasRole('developer') || hasRole('owner');
|
||||
const variants = product.product_variants ?? [];
|
||||
const totalStock = variants.reduce((sum, v) => sum + (v.stock ?? 0), 0);
|
||||
const totalReject = variants.reduce(
|
||||
(sum, v) => sum + (v.reject_stock ?? 0),
|
||||
0,
|
||||
);
|
||||
const totalRetail = variants.reduce(
|
||||
(sum, v) => sum + (v.retail_stock ?? 0),
|
||||
0,
|
||||
);
|
||||
const variantCount = product.variants_count ?? 0;
|
||||
const totalStock = product.total_stock ?? 0;
|
||||
const totalReject = product.total_reject_stock ?? 0;
|
||||
const totalRetail = product.total_retail_stock ?? 0;
|
||||
const totalAll = totalStock + totalReject + totalRetail;
|
||||
|
||||
const isPending = product.status === 'pending';
|
||||
@ -132,7 +126,7 @@ export function ProductCardRow({
|
||||
|
||||
<div className="mt-2 flex flex-wrap items-center gap-3 text-xs text-muted-foreground">
|
||||
<span className="inline-flex items-center rounded-md bg-muted px-2 py-1 font-medium text-foreground">
|
||||
{variants.length} varian
|
||||
{variantCount} varian
|
||||
</span>
|
||||
<span>
|
||||
Bagus:{' '}
|
||||
|
||||
@ -21,11 +21,15 @@ import { TransferStockDialog } from './transfer-stock-dialog';
|
||||
|
||||
export function VariantSubRow({
|
||||
product,
|
||||
variants: loadedVariants,
|
||||
isLoading,
|
||||
allPreviewItems,
|
||||
onEditVariant,
|
||||
onDeleteVariantClick,
|
||||
}: {
|
||||
product: Product;
|
||||
variants: ProductVariant[];
|
||||
isLoading: boolean;
|
||||
allPreviewItems: ImagePreviewItem[];
|
||||
onEditVariant: (product: Product, variant: ProductVariant) => void;
|
||||
onDeleteVariantClick: (product: Product, variant: ProductVariant) => void;
|
||||
@ -34,7 +38,6 @@ export function VariantSubRow({
|
||||
const isVerifier = hasRole('developer') || hasRole('owner');
|
||||
const isPending = product.status === 'pending';
|
||||
const isRejected = product.status === 'rejected';
|
||||
const variants = product.product_variants ?? [];
|
||||
const [transferVariant, setTransferVariant] = useState<{
|
||||
product: Product;
|
||||
variant: ProductVariant;
|
||||
@ -75,7 +78,16 @@ export function VariantSubRow({
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{variants.length === 0 ? (
|
||||
{isLoading ? (
|
||||
<TableRow>
|
||||
<TableCell
|
||||
colSpan={canAnyAction ? 8 : 7}
|
||||
className="text-center text-muted-foreground"
|
||||
>
|
||||
Memuat varian...
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
) : loadedVariants.length === 0 ? (
|
||||
<TableRow>
|
||||
<TableCell
|
||||
colSpan={canAnyAction ? 8 : 7}
|
||||
@ -85,7 +97,7 @@ export function VariantSubRow({
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
) : (
|
||||
variants.map((variant, index) => (
|
||||
loadedVariants.map((variant, index) => (
|
||||
<TableRow key={variant.id}>
|
||||
<TableCell className="text-center">
|
||||
{index + 1}
|
||||
|
||||
@ -13,7 +13,10 @@ export type RawMaterial = {
|
||||
name: string;
|
||||
unit: string;
|
||||
is_active: boolean;
|
||||
raw_material_prices: RawMaterialVariant[];
|
||||
variants_count: number;
|
||||
total_stock: number;
|
||||
total_value: number;
|
||||
raw_material_prices?: RawMaterialVariant[];
|
||||
};
|
||||
|
||||
export type RawMaterialVariantForEdit = {
|
||||
|
||||
@ -1,10 +1,10 @@
|
||||
import { Head, Link, router } from '@inertiajs/react';
|
||||
import { Plus } from 'lucide-react';
|
||||
import { useMemo, useState } from 'react';
|
||||
import { useCallback, useMemo, useState } from 'react';
|
||||
import { CardTable } from '@/components/data-display';
|
||||
import { FilterPopover } from '@/components/data-display';
|
||||
import { DeleteConfirmDialog } from '@/components/dialogs';
|
||||
import type { ImagePreviewItem } from '@/components/dialogs';
|
||||
import { FilterPopover } from '@/components/data-display';
|
||||
import { useCardTableExpand } from '@/components/hooks/use-card-table-expand';
|
||||
import { PageHeader } from '@/components/layout';
|
||||
import { Button } from '@/components/ui/button';
|
||||
@ -31,6 +31,7 @@ import {
|
||||
edit as rawMaterialEdit,
|
||||
index as rawMaterialIndex,
|
||||
toggleStatus,
|
||||
variants as rawMaterialVariants,
|
||||
} from '@/routes/admin/master/raw-materials';
|
||||
import {
|
||||
destroy as variantDestroy
|
||||
@ -62,7 +63,9 @@ export default function RawMaterialIndex({ rawMaterials, rawMaterialNames, filte
|
||||
rawMaterial: RawMaterial;
|
||||
variant: RawMaterialVariant;
|
||||
} | null>(null);
|
||||
const expand = useCardTableExpand(true);
|
||||
const [loadedVariants, setLoadedVariants] = useState<Record<number, RawMaterialVariant[]>>({});
|
||||
const [loadingVariants, setLoadingVariants] = useState<Record<number, boolean>>({});
|
||||
const expand = useCardTableExpand(false);
|
||||
|
||||
const pagination = {
|
||||
current_page: rawMaterials.current_page,
|
||||
@ -92,19 +95,42 @@ export default function RawMaterialIndex({ rawMaterials, rawMaterialNames, filte
|
||||
[rawMaterialNames],
|
||||
);
|
||||
|
||||
const fetchVariants = useCallback((rawMaterial: RawMaterial) => {
|
||||
if (loadedVariants[rawMaterial.id] || loadingVariants[rawMaterial.id]) {
|
||||
return;
|
||||
}
|
||||
|
||||
setLoadingVariants((prev) => ({ ...prev, [rawMaterial.id]: true }));
|
||||
|
||||
fetch(rawMaterialVariants.url(rawMaterial.id))
|
||||
.then((res) => res.json())
|
||||
.then((data) => {
|
||||
setLoadedVariants((prev) => ({
|
||||
...prev,
|
||||
[rawMaterial.id]: data.variants ?? [],
|
||||
}));
|
||||
})
|
||||
.catch(() => {
|
||||
setLoadedVariants((prev) => ({ ...prev, [rawMaterial.id]: [] }));
|
||||
})
|
||||
.finally(() => {
|
||||
setLoadingVariants((prev) => ({ ...prev, [rawMaterial.id]: false }));
|
||||
});
|
||||
}, [loadedVariants, loadingVariants]);
|
||||
|
||||
const allPreviewItems: ImagePreviewItem[] = useMemo(
|
||||
() =>
|
||||
rawMaterials.data.flatMap((rm) =>
|
||||
(rm.raw_material_prices ?? [])
|
||||
Object.entries(loadedVariants).flatMap(([rmId, variants]) =>
|
||||
variants
|
||||
.filter((v) => v.photo_url)
|
||||
.map((v) => ({
|
||||
id: `${rm.id}-${v.id}`,
|
||||
id: `${rmId}-${v.id}`,
|
||||
src: v.photo_url,
|
||||
title: v.variant,
|
||||
description: `Stok: ${v.formatted_stock}`,
|
||||
}))
|
||||
),
|
||||
[rawMaterials.data],
|
||||
[loadedVariants],
|
||||
);
|
||||
|
||||
function handleDelete() {
|
||||
@ -230,7 +256,16 @@ export default function RawMaterialIndex({ rawMaterials, rawMaterialNames, filte
|
||||
data={rawMaterials.data}
|
||||
getItemKey={(rm) => rm.id}
|
||||
expandedKeys={expand.expandedKeys}
|
||||
onToggleExpand={expand.toggleExpand}
|
||||
onToggleExpand={(key) => {
|
||||
const rm = rawMaterials.data.find((r) => r.id === key);
|
||||
const isCurrentlyExpanded = expand.expandedKeys === 'all' || expand.expandedKeys.has(key);
|
||||
|
||||
if (rm && !isCurrentlyExpanded) {
|
||||
fetchVariants(rm);
|
||||
}
|
||||
|
||||
expand.toggleExpand(key);
|
||||
}}
|
||||
searchValue={search}
|
||||
onSearchChange={handleSearchChange}
|
||||
|
||||
@ -264,6 +299,8 @@ export default function RawMaterialIndex({ rawMaterials, rawMaterialNames, filte
|
||||
renderSubContent={(rawMaterial) => (
|
||||
<RawMaterialVariantSubRow
|
||||
rawMaterial={rawMaterial}
|
||||
variants={loadedVariants[rawMaterial.id] ?? []}
|
||||
isLoading={loadingVariants[rawMaterial.id] ?? false}
|
||||
allPreviewItems={allPreviewItems}
|
||||
/>
|
||||
)}
|
||||
|
||||
@ -28,15 +28,9 @@ export function RawMaterialCardRow({
|
||||
toggleStatusUrl,
|
||||
}: RawMaterialCardRowParams) {
|
||||
const { can } = useCan();
|
||||
const variants = rawMaterial.raw_material_prices ?? [];
|
||||
const totalStock = variants.reduce(
|
||||
(sum, v) => sum + (Number(v.stock) || 0),
|
||||
0,
|
||||
);
|
||||
const totalValue = variants.reduce(
|
||||
(sum, v) => sum + (Number(v.price) || 0) * (Number(v.stock) || 0),
|
||||
0,
|
||||
);
|
||||
const variantCount = rawMaterial.variants_count ?? 0;
|
||||
const totalStock = rawMaterial.total_stock ?? 0;
|
||||
const totalValue = rawMaterial.total_value ?? 0;
|
||||
|
||||
return (
|
||||
<Card className="overflow-hidden">
|
||||
@ -65,7 +59,7 @@ export function RawMaterialCardRow({
|
||||
|
||||
<div className="mt-2 flex flex-wrap items-center gap-3 text-xs text-muted-foreground">
|
||||
<span className="inline-flex items-center rounded-md bg-muted px-2 py-1 font-medium text-foreground">
|
||||
{variants.length} varian
|
||||
{variantCount} varian
|
||||
</span>
|
||||
<span>
|
||||
Total Stok:{' '}
|
||||
|
||||
@ -1,10 +1,10 @@
|
||||
import { router } from '@inertiajs/react';
|
||||
import { Pencil, Trash2 } from 'lucide-react';
|
||||
import { useState } from 'react';
|
||||
import { RowActions } from '@/components/data-display';
|
||||
import { ConfirmDialog } from '@/components/dialogs';
|
||||
import { ImagePreviewButton } from '@/components/dialogs';
|
||||
import type { ImagePreviewItem } from '@/components/dialogs';
|
||||
import { RowActions } from '@/components/data-display';
|
||||
import {
|
||||
Table,
|
||||
TableBody,
|
||||
@ -24,13 +24,16 @@ import type { RawMaterial, RawMaterialVariant } from '../columns';
|
||||
|
||||
export function RawMaterialVariantSubRow({
|
||||
rawMaterial,
|
||||
variants: loadedVariants,
|
||||
isLoading,
|
||||
allPreviewItems,
|
||||
}: {
|
||||
rawMaterial: RawMaterial;
|
||||
variants: RawMaterialVariant[];
|
||||
isLoading: boolean;
|
||||
allPreviewItems: ImagePreviewItem[];
|
||||
}) {
|
||||
const { can } = useCan();
|
||||
const variants = rawMaterial.raw_material_prices ?? [];
|
||||
const [deletingVariant, setDeletingVariant] =
|
||||
useState<RawMaterialVariant | null>(null);
|
||||
|
||||
@ -72,7 +75,16 @@ export function RawMaterialVariantSubRow({
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{variants.length === 0 ? (
|
||||
{isLoading ? (
|
||||
<TableRow>
|
||||
<TableCell
|
||||
colSpan={canAnyAction ? 6 : 5}
|
||||
className="text-center text-muted-foreground"
|
||||
>
|
||||
Memuat varian...
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
) : loadedVariants.length === 0 ? (
|
||||
<TableRow>
|
||||
<TableCell
|
||||
colSpan={canAnyAction ? 6 : 5}
|
||||
@ -82,7 +94,7 @@ export function RawMaterialVariantSubRow({
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
) : (
|
||||
variants.map((variant, index) => (
|
||||
loadedVariants.map((variant, index) => (
|
||||
<TableRow key={variant.id}>
|
||||
<TableCell className="text-center">
|
||||
{index + 1}
|
||||
|
||||
@ -45,6 +45,7 @@
|
||||
Route::post('products/{product}/approve', [ProductController::class, 'approve'])->name('products.approve')->middleware('permission:products.update');
|
||||
Route::post('products/{product}/reject', [ProductController::class, 'reject'])->name('products.reject')->middleware('permission:products.update');
|
||||
Route::post('products/{product}/resubmit', [ProductController::class, 'resubmit'])->name('products.resubmit')->middleware('permission:products.update');
|
||||
Route::get('products/{product}/variants', [ProductController::class, 'variants'])->name('products.variants')->middleware('permission:products.view');
|
||||
Route::delete('products/{product}/variants/{variant}', [ProductVariantController::class, 'destroy'])->name('products.variants.destroy')->middleware('permission:products.delete');
|
||||
Route::get('products/{product}/variants/{variant}/edit', [ProductVariantController::class, 'edit'])->name('products.variants.edit')->middleware('permission:products.update');
|
||||
Route::put('products/{product}/variants/{variant}', [ProductVariantController::class, 'update'])->name('products.variants.update')->middleware('permission:products.update');
|
||||
|
||||
Loading…
Reference in New Issue
Block a user