feat: add variant search functionality in CuttingCreate and CuttingEdit components
This commit is contained in:
parent
dd9ce7df85
commit
45a970e2f1
@ -1,7 +1,7 @@
|
||||
'use no memo';
|
||||
|
||||
import { Form, Head, Link, usePage } from '@inertiajs/react';
|
||||
import { ArrowLeft, Check, Layers, Plus, ShoppingCart, Trash2 } from 'lucide-react';
|
||||
import { ArrowLeft, Check, Layers, Plus, Search, ShoppingCart, Trash2 } from 'lucide-react';
|
||||
import { useCallback, useMemo, useRef, useState } from 'react';
|
||||
import { toast } from 'sonner';
|
||||
import { ConfirmDialog } from '@/components/dialogs';
|
||||
@ -76,6 +76,7 @@ export default function CuttingCreate({ rawMaterials }: Props) {
|
||||
return [];
|
||||
});
|
||||
const [selectedMaterialName, setSelectedMaterialName] = useState(draft?.selectedMaterialName ?? '');
|
||||
const [variantSearch, setVariantSearch] = useState('');
|
||||
|
||||
const [productName, setProductName] = useState(draft?.productName ?? '');
|
||||
const [sample, setSample] = useState(draft?.sample ?? 0);
|
||||
@ -149,13 +150,22 @@ export default function CuttingCreate({ rawMaterials }: Props) {
|
||||
[rawMaterials, selectedMaterialName],
|
||||
);
|
||||
|
||||
const addVariant = useCallback(
|
||||
(priceId: number) => {
|
||||
if (!selectedMaterial) {
|
||||
return;
|
||||
}
|
||||
const groupedVariants = useMemo(() => {
|
||||
if (!variantSearch) return [];
|
||||
const search = variantSearch.toLowerCase();
|
||||
return rawMaterials
|
||||
.map((rm) => ({
|
||||
...rm,
|
||||
raw_material_prices: rm.raw_material_prices.filter(
|
||||
(p) => p.variant.toLowerCase().includes(search),
|
||||
),
|
||||
}))
|
||||
.filter((rm) => rm.raw_material_prices.length > 0);
|
||||
}, [rawMaterials, variantSearch]);
|
||||
|
||||
const price = selectedMaterial.raw_material_prices.find((p) => p.id === priceId);
|
||||
const addVariant = useCallback(
|
||||
(rawMaterial: (typeof rawMaterials)[number], priceId: number) => {
|
||||
const price = rawMaterial.raw_material_prices.find((p) => p.id === priceId);
|
||||
|
||||
if (!price) {
|
||||
return;
|
||||
@ -174,14 +184,14 @@ export default function CuttingCreate({ rawMaterials }: Props) {
|
||||
material_result: 0,
|
||||
combination_id: null,
|
||||
variant: price.variant,
|
||||
material_name: selectedMaterial.name,
|
||||
unit: selectedMaterial.unit,
|
||||
material_name: rawMaterial.name,
|
||||
unit: rawMaterial.unit,
|
||||
photo_url: price.photo_url,
|
||||
},
|
||||
];
|
||||
});
|
||||
},
|
||||
[selectedMaterial],
|
||||
[],
|
||||
);
|
||||
|
||||
const openComboDialog = useCallback((materialName: string, preSelectPriceId?: number) => {
|
||||
@ -352,9 +362,70 @@ export default function CuttingCreate({ rawMaterials }: Props) {
|
||||
</Combobox>
|
||||
</div>
|
||||
|
||||
{selectedMaterial && selectedMaterial.raw_material_prices.length > 0 && (
|
||||
<div className="grid gap-2">
|
||||
<Label className="text-sm font-medium">Cari Varian</Label>
|
||||
<div className="relative">
|
||||
<Search className="absolute left-2.5 top-2.5 h-4 w-4 text-muted-foreground" />
|
||||
<Input
|
||||
placeholder="Ketik nama varian..."
|
||||
value={variantSearch}
|
||||
onChange={(e) => setVariantSearch(e.target.value)}
|
||||
className="pl-8"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{variantSearch && groupedVariants.length > 0 && (
|
||||
<div className="space-y-4">
|
||||
{groupedVariants.map((rm) => (
|
||||
<div key={rm.id} className="space-y-2">
|
||||
<p className="text-xs font-semibold text-muted-foreground uppercase">{rm.name} ({rm.unit})</p>
|
||||
<div className="space-y-2">
|
||||
{rm.raw_material_prices.map((price) => {
|
||||
const isAdded = materials.some((m) => m.raw_material_price_id === price.id);
|
||||
const addedCount = materials.filter((m) => m.raw_material_price_id === price.id).length;
|
||||
|
||||
return (
|
||||
<div key={price.id} className={isAdded ? 'flex items-center justify-between gap-3 rounded-lg border border-primary p-3' : 'flex items-center justify-between gap-3 rounded-lg border p-3'}>
|
||||
<div className="flex min-w-0 items-center gap-3">
|
||||
{price.photo_url ? (
|
||||
<img src={price.photo_url} alt={price.variant} className="h-10 w-10 shrink-0 rounded-md object-cover" />
|
||||
) : (
|
||||
<div className="flex h-10 w-10 shrink-0 items-center justify-center rounded-md bg-muted text-xs text-muted-foreground">N/A</div>
|
||||
)}
|
||||
<div className="min-w-0">
|
||||
<p className="truncate font-medium">{price.variant}</p>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
Stok: {formatNumber(Number(price.stock))} {rm.unit} · {formatCurrency(price.price)}
|
||||
{addedCount > 0 && ` · ×${addedCount}`}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex shrink-0 items-center gap-1">
|
||||
<Button type="button" variant="outline" size="sm" onClick={() => addVariant(rm, price.id)}>
|
||||
<Plus className="h-4 w-4" />
|
||||
Tambah
|
||||
</Button>
|
||||
<Button type="button" variant="outline" size="sm" onClick={() => openComboDialog(rm.name, price.id)}>
|
||||
<Layers className="h-4 w-4" />
|
||||
Kombinasi
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{variantSearch && groupedVariants.length === 0 && (
|
||||
<p className="text-sm text-muted-foreground">Tidak ada varian ditemukan.</p>
|
||||
)}
|
||||
|
||||
{!variantSearch && selectedMaterial && selectedMaterial.raw_material_prices.length > 0 && (
|
||||
<div className="space-y-2">
|
||||
<Label className="text-sm font-medium">Pilih Varian</Label>
|
||||
<div className="space-y-2">
|
||||
{selectedMaterial.raw_material_prices.map((price) => {
|
||||
const isAdded = materials.some((m) => m.raw_material_price_id === price.id);
|
||||
@ -377,7 +448,7 @@ export default function CuttingCreate({ rawMaterials }: Props) {
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex shrink-0 items-center gap-1">
|
||||
<Button type="button" variant="outline" size="sm" onClick={() => addVariant(price.id)}>
|
||||
<Button type="button" variant="outline" size="sm" onClick={() => addVariant(selectedMaterial, price.id)}>
|
||||
<Plus className="h-4 w-4" />
|
||||
Tambah
|
||||
</Button>
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
'use no memo';
|
||||
|
||||
import { Form, Head, Link, usePage } from '@inertiajs/react';
|
||||
import { ArrowLeft, Check, Layers, Plus, ShoppingCart, Trash2 } from 'lucide-react';
|
||||
import { ArrowLeft, Check, Layers, Plus, Search, ShoppingCart, Trash2 } from 'lucide-react';
|
||||
import { useCallback, useMemo, useRef, useState } from 'react';
|
||||
import { toast } from 'sonner';
|
||||
import { ConfirmDialog } from '@/components/dialogs';
|
||||
@ -86,6 +86,7 @@ export default function CuttingEdit({ cutting, rawMaterials }: Props) {
|
||||
);
|
||||
|
||||
const [selectedMaterialName, setSelectedMaterialName] = useState('');
|
||||
const [variantSearch, setVariantSearch] = useState('');
|
||||
|
||||
const [productName, setProductName] = useState(cutting.product_name);
|
||||
const [sample, setSample] = useState(cutting.sample);
|
||||
@ -130,13 +131,22 @@ export default function CuttingEdit({ cutting, rawMaterials }: Props) {
|
||||
[rawMaterials, selectedMaterialName],
|
||||
);
|
||||
|
||||
const addVariant = useCallback(
|
||||
(priceId: number) => {
|
||||
if (!selectedMaterial) {
|
||||
return;
|
||||
}
|
||||
const groupedVariants = useMemo(() => {
|
||||
if (!variantSearch) return [];
|
||||
const search = variantSearch.toLowerCase();
|
||||
return rawMaterials
|
||||
.map((rm) => ({
|
||||
...rm,
|
||||
raw_material_prices: rm.raw_material_prices.filter(
|
||||
(p) => p.variant.toLowerCase().includes(search),
|
||||
),
|
||||
}))
|
||||
.filter((rm) => rm.raw_material_prices.length > 0);
|
||||
}, [rawMaterials, variantSearch]);
|
||||
|
||||
const price = selectedMaterial.raw_material_prices.find((p) => p.id === priceId);
|
||||
const addVariant = useCallback(
|
||||
(rawMaterial: (typeof rawMaterials)[number], priceId: number) => {
|
||||
const price = rawMaterial.raw_material_prices.find((p) => p.id === priceId);
|
||||
|
||||
if (!price) {
|
||||
return;
|
||||
@ -155,14 +165,14 @@ export default function CuttingEdit({ cutting, rawMaterials }: Props) {
|
||||
material_result: 0,
|
||||
combination_id: null,
|
||||
variant: price.variant,
|
||||
material_name: selectedMaterial.name,
|
||||
unit: selectedMaterial.unit,
|
||||
material_name: rawMaterial.name,
|
||||
unit: rawMaterial.unit,
|
||||
photo_url: price.photo_url,
|
||||
},
|
||||
];
|
||||
});
|
||||
},
|
||||
[selectedMaterial],
|
||||
[],
|
||||
);
|
||||
|
||||
const openComboDialog = useCallback((materialName: string, preSelectPriceId?: number) => {
|
||||
@ -328,9 +338,70 @@ export default function CuttingEdit({ cutting, rawMaterials }: Props) {
|
||||
</Combobox>
|
||||
</div>
|
||||
|
||||
{selectedMaterial && selectedMaterial.raw_material_prices.length > 0 && (
|
||||
<div className="grid gap-2">
|
||||
<Label className="text-sm font-medium">Cari Varian</Label>
|
||||
<div className="relative">
|
||||
<Search className="absolute left-2.5 top-2.5 h-4 w-4 text-muted-foreground" />
|
||||
<Input
|
||||
placeholder="Ketik nama varian..."
|
||||
value={variantSearch}
|
||||
onChange={(e) => setVariantSearch(e.target.value)}
|
||||
className="pl-8"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{variantSearch && groupedVariants.length > 0 && (
|
||||
<div className="space-y-4">
|
||||
{groupedVariants.map((rm) => (
|
||||
<div key={rm.id} className="space-y-2">
|
||||
<p className="text-xs font-semibold text-muted-foreground uppercase">{rm.name} ({rm.unit})</p>
|
||||
<div className="space-y-2">
|
||||
{rm.raw_material_prices.map((price) => {
|
||||
const isAdded = materials.some((m) => m.raw_material_price_id === price.id);
|
||||
const addedCount = materials.filter((m) => m.raw_material_price_id === price.id).length;
|
||||
|
||||
return (
|
||||
<div key={price.id} className={isAdded ? 'flex items-center justify-between gap-3 rounded-lg border border-primary p-3' : 'flex items-center justify-between gap-3 rounded-lg border p-3'}>
|
||||
<div className="flex min-w-0 items-center gap-3">
|
||||
{price.photo_url ? (
|
||||
<img src={price.photo_url} alt={price.variant} className="h-10 w-10 shrink-0 rounded-md object-cover" />
|
||||
) : (
|
||||
<div className="flex h-10 w-10 shrink-0 items-center justify-center rounded-md bg-muted text-xs text-muted-foreground">N/A</div>
|
||||
)}
|
||||
<div className="min-w-0">
|
||||
<p className="truncate font-medium">{price.variant}</p>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
Stok: {formatNumber(Number(price.stock))} {rm.unit} · {formatCurrency(price.price)}
|
||||
{addedCount > 0 && ` · ×${addedCount}`}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex shrink-0 items-center gap-1">
|
||||
<Button type="button" variant="outline" size="sm" onClick={() => addVariant(rm, price.id)}>
|
||||
<Plus className="h-4 w-4" />
|
||||
Tambah
|
||||
</Button>
|
||||
<Button type="button" variant="outline" size="sm" onClick={() => openComboDialog(rm.name, price.id)}>
|
||||
<Layers className="h-4 w-4" />
|
||||
Kombinasi
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{variantSearch && groupedVariants.length === 0 && (
|
||||
<p className="text-sm text-muted-foreground">Tidak ada varian ditemukan.</p>
|
||||
)}
|
||||
|
||||
{!variantSearch && selectedMaterial && selectedMaterial.raw_material_prices.length > 0 && (
|
||||
<div className="space-y-2">
|
||||
<Label className="text-sm font-medium">Pilih Varian</Label>
|
||||
<div className="space-y-2">
|
||||
{selectedMaterial.raw_material_prices.map((price) => {
|
||||
const isAdded = materials.some((m) => m.raw_material_price_id === price.id);
|
||||
@ -353,7 +424,7 @@ export default function CuttingEdit({ cutting, rawMaterials }: Props) {
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex shrink-0 items-center gap-1">
|
||||
<Button type="button" variant="outline" size="sm" onClick={() => addVariant(price.id)}>
|
||||
<Button type="button" variant="outline" size="sm" onClick={() => addVariant(selectedMaterial, price.id)}>
|
||||
<Plus className="h-4 w-4" />
|
||||
Tambah
|
||||
</Button>
|
||||
|
||||
Loading…
Reference in New Issue
Block a user