feat: update purchase management to support decimal quantities and improve data handling

This commit is contained in:
Yoga Pangestu 2026-08-20 11:12:38 +07:00
parent b85bbb1ba4
commit f128ace2ea
7 changed files with 123 additions and 80 deletions

View File

@ -31,11 +31,11 @@ public function rules(): array
'variants' => [Rule::requiredUnless(fn () => $this->input('mode') === 'existing'), 'array', 'min:1'], 'variants' => [Rule::requiredUnless(fn () => $this->input('mode') === 'existing'), 'array', 'min:1'],
'variants.*.variant' => [Rule::requiredUnless(fn () => $this->input('mode') === 'existing'), 'string', 'max:200'], 'variants.*.variant' => [Rule::requiredUnless(fn () => $this->input('mode') === 'existing'), 'string', 'max:200'],
'variants.*.price' => [Rule::requiredUnless(fn () => $this->input('mode') === 'existing'), 'integer', 'min:0'], 'variants.*.price' => [Rule::requiredUnless(fn () => $this->input('mode') === 'existing'), 'integer', 'min:0'],
'variants.*.stock' => [Rule::requiredUnless(fn () => $this->input('mode') === 'existing'), 'integer', 'min:0'], 'variants.*.stock' => [Rule::requiredUnless(fn () => $this->input('mode') === 'existing'), 'numeric', 'min:0'],
'variants.*.photo_key' => [Rule::requiredUnless(fn () => $this->input('mode') === 'existing'), 'string', 'max:500'], 'variants.*.photo_key' => [Rule::requiredUnless(fn () => $this->input('mode') === 'existing'), 'string', 'max:500'],
'existing_items' => [Rule::requiredIf(fn () => $this->input('mode') === 'existing'), 'array', 'min:1'], 'existing_items' => [Rule::requiredIf(fn () => $this->input('mode') === 'existing'), 'array', 'min:1'],
'existing_items.*.raw_material_price_id' => ['required', 'integer', Rule::exists('raw_material_prices', 'id')], 'existing_items.*.raw_material_price_id' => ['required', 'integer', Rule::exists('raw_material_prices', 'id')],
'existing_items.*.quantity' => ['required', 'integer', 'min:1'], 'existing_items.*.quantity' => ['required', 'numeric', 'min:0.01'],
'existing_items.*.unit_price' => ['required', 'integer', 'min:0'], 'existing_items.*.unit_price' => ['required', 'integer', 'min:0'],
'supplier_id' => ['required', 'integer', Rule::exists('suppliers', 'id')], 'supplier_id' => ['required', 'integer', Rule::exists('suppliers', 'id')],
'discount' => ['nullable', 'integer', 'min:0'], 'discount' => ['nullable', 'integer', 'min:0'],

View File

@ -19,9 +19,9 @@ class PurchaseItem extends Model
protected function casts(): array protected function casts(): array
{ {
return [ return [
'quantity' => 'integer', 'quantity' => 'decimal:2',
'unit_price' => 'integer', 'unit_price' => 'integer',
'subtotal' => 'integer', 'subtotal' => 'decimal:2',
]; ];
} }

View File

@ -157,7 +157,7 @@ public function getForEdit(Purchase $purchase): array
)->toArray(); )->toArray();
$existingQuantities = $items->mapWithKeys(fn (PurchaseItem $item) => [ $existingQuantities = $items->mapWithKeys(fn (PurchaseItem $item) => [
(int) $item->raw_material_price_id => (int) $item->quantity, (int) $item->raw_material_price_id => (string) $item->quantity,
])->all(); ])->all();
return [ return [
@ -194,7 +194,7 @@ private function storeFromExisting(array $data): Purchase
$subtotal = 0; $subtotal = 0;
$itemRows = collect($data['existing_items'])->map(function ($item) use ($now, &$subtotal) { $itemRows = collect($data['existing_items'])->map(function ($item) use ($now, &$subtotal) {
$itemSubtotal = (int) ($item['unit_price'] * $item['quantity']); $itemSubtotal = $item['unit_price'] * $item['quantity'];
$subtotal += $itemSubtotal; $subtotal += $itemSubtotal;
return [ return [
@ -267,7 +267,7 @@ private function storeNew(array $data): Purchase
$subtotal = 0; $subtotal = 0;
$now = now(); $now = now();
$priceRows = collect($data['variants'])->map(function ($v) use ($rawMaterial, $now, &$subtotal) { $priceRows = collect($data['variants'])->map(function ($v) use ($rawMaterial, $now, &$subtotal) {
$itemSubtotal = (int) ($v['price'] * $v['stock']); $itemSubtotal = $v['price'] * $v['stock'];
$subtotal += $itemSubtotal; $subtotal += $itemSubtotal;
return [ return [
@ -325,7 +325,7 @@ private function storeNew(array $data): Purchase
'user_id' => auth()->id(), 'user_id' => auth()->id(),
'quantity' => $v['stock'], 'quantity' => $v['stock'],
'unit_price' => $v['price'], 'unit_price' => $v['price'],
'subtotal' => (int) ($v['price'] * $v['stock']), 'subtotal' => $v['price'] * $v['stock'],
'created_at' => $now, 'created_at' => $now,
'updated_at' => $now, 'updated_at' => $now,
]; ];
@ -378,7 +378,7 @@ public function update(Purchase $purchase, array $data): Purchase
if (($data['mode'] ?? 'new') === 'existing') { if (($data['mode'] ?? 'new') === 'existing') {
$itemRows = collect($data['existing_items'])->map(function ($item) use ($now, &$subtotal) { $itemRows = collect($data['existing_items'])->map(function ($item) use ($now, &$subtotal) {
$itemSubtotal = (int) ($item['unit_price'] * $item['quantity']); $itemSubtotal = $item['unit_price'] * $item['quantity'];
$subtotal += $itemSubtotal; $subtotal += $itemSubtotal;
return [ return [
@ -429,7 +429,7 @@ public function update(Purchase $purchase, array $data): Purchase
} }
if ($price) { if ($price) {
$price->increment('stock', (int) $v['stock']); $price->increment('stock', $v['stock']);
$price->update(['price' => $v['price']]); $price->update(['price' => $v['price']]);
} else { } else {
$price = $rawMaterial->rawMaterialPrices()->create([ $price = $rawMaterial->rawMaterialPrices()->create([
@ -449,7 +449,7 @@ public function update(Purchase $purchase, array $data): Purchase
); );
} }
$itemSubtotal = (int) ($v['price'] * $v['stock']); $itemSubtotal = $v['price'] * $v['stock'];
$subtotal += $itemSubtotal; $subtotal += $itemSubtotal;
return [ return [

View File

@ -0,0 +1,30 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::table('purchase_items', function (Blueprint $table) {
$table->decimal('quantity', 10, 2)->default(0)->change();
$table->decimal('subtotal', 14, 2)->default(0)->change();
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::table('purchase_items', function (Blueprint $table) {
$table->unsignedInteger('quantity')->default(0)->change();
$table->unsignedInteger('subtotal')->default(0)->change();
});
}
};

View File

@ -10,12 +10,12 @@ export type PurchaseDraftData = {
variants: Array<{ variants: Array<{
variant: string; variant: string;
price: number; price: number;
stock: number; stock: string;
photo_keys?: string[]; photo_keys?: string[];
}>; }>;
mode?: 'new' | 'existing'; mode?: 'new' | 'existing';
selectedMaterialName?: string; selectedMaterialName?: string;
quantities?: Record<string, number>; quantities?: Record<string, string>;
photo?: string; photo?: string;
}; };

View File

@ -17,7 +17,6 @@ import { ConfirmDialog } from '@/components/dialogs';
import { FileUpload, FileUploadMultiple } from '@/components/inputs'; import { FileUpload, FileUploadMultiple } from '@/components/inputs';
import { ImagePreviewModal } from '@/components/dialogs'; import { ImagePreviewModal } from '@/components/dialogs';
import { InputError } from '@/components/ui'; import { InputError } from '@/components/ui';
import { NumberInput } from '@/components/inputs';
import { RupiahInput } from '@/components/inputs'; import { RupiahInput } from '@/components/inputs';
import { Button } from '@/components/ui/button'; import { Button } from '@/components/ui/button';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
@ -53,7 +52,7 @@ import type { PurchaseCreateData, RawMaterialVariant } from './columns';
type VariantState = { type VariantState = {
variant: string; variant: string;
price: number; price: number;
stock: number; stock: string;
photo: string | null; photo: string | null;
photoUrl: string | null; photoUrl: string | null;
uploading: boolean; uploading: boolean;
@ -65,9 +64,9 @@ type CartLine = {
title: string; title: string;
subtitle: string; subtitle: string;
price: number; price: number;
quantity: number; quantity: string;
onAdjust: (delta: number) => void; onAdjust: (delta: number) => void;
onSet: (value: number) => void; onSet: (value: string) => void;
onRemove: () => void; onRemove: () => void;
}; };
@ -89,7 +88,7 @@ export default function PurchaseCreate({ suppliers, rawMaterials }: Props) {
return draft.variants.map((v) => ({ return draft.variants.map((v) => ({
variant: v.variant, variant: v.variant,
price: v.price, price: v.price,
stock: v.stock, stock: String(v.stock),
photo: v.photo ?? null, photo: v.photo ?? null,
photoUrl: v.photo ? getTemporaryUrl(v.photo) : null, photoUrl: v.photo ? getTemporaryUrl(v.photo) : null,
uploading: false, uploading: false,
@ -100,7 +99,7 @@ export default function PurchaseCreate({ suppliers, rawMaterials }: Props) {
{ {
variant: '', variant: '',
price: 0, price: 0,
stock: 0, stock: '0',
photo: null, photo: null,
photoUrl: null, photoUrl: null,
uploading: false, uploading: false,
@ -132,11 +131,11 @@ export default function PurchaseCreate({ suppliers, rawMaterials }: Props) {
const [fetchedVariants, setFetchedVariants] = useState<Map<number, RawMaterialVariant[]>>(new Map()); const [fetchedVariants, setFetchedVariants] = useState<Map<number, RawMaterialVariant[]>>(new Map());
const [loadingVariants, setLoadingVariants] = useState(false); const [loadingVariants, setLoadingVariants] = useState(false);
const [quantities, setQuantities] = useState<Record<number, number>>(() => const [quantities, setQuantities] = useState<Record<number, string>>(() =>
Object.fromEntries( Object.fromEntries(
Object.entries(draft?.quantities ?? {}).map(([id, qty]) => [ Object.entries(draft?.quantities ?? {}).map(([id, qty]) => [
Number(id), Number(id),
qty, String(qty),
]), ]),
), ),
); );
@ -213,14 +212,14 @@ export default function PurchaseCreate({ suppliers, rawMaterials }: Props) {
}, [fetchedVariants, rawMaterials]); }, [fetchedVariants, rawMaterials]);
const newSubtotal = variants.reduce( const newSubtotal = variants.reduce(
(sum, v) => sum + Number(v.price) * Number(v.stock), (sum, v) => sum + Number(v.price) * (parseFloat(v.stock) || 0),
0, 0,
); );
const existingSubtotal = Object.entries(quantities).reduce( const existingSubtotal = Object.entries(quantities).reduce(
(sum, [priceId, quantity]) => { (sum, [priceId, quantity]) => {
const price = priceMap.get(Number(priceId)); const price = priceMap.get(Number(priceId));
return sum + (price ? price.price * quantity : 0); return sum + (price ? price.price * (parseFloat(quantity) || 0) : 0);
}, },
0, 0,
); );
@ -233,7 +232,7 @@ export default function PurchaseCreate({ suppliers, rawMaterials }: Props) {
{ {
variant: '', variant: '',
price: 0, price: 0,
stock: 0, stock: '0',
photo: null, photo: null,
photoUrl: null, photoUrl: null,
uploading: false, uploading: false,
@ -341,17 +340,17 @@ export default function PurchaseCreate({ suppliers, rawMaterials }: Props) {
.finally(() => setLoadingVariants(false)); .finally(() => setLoadingVariants(false));
}, [selectedMaterial, mode, fetchedVariants]); }, [selectedMaterial, mode, fetchedVariants]);
const updateQuantity = useCallback((priceId: number, value: number) => { const updateQuantity = useCallback((priceId: number, value: string) => {
setQuantities((prev) => ({ setQuantities((prev) => ({
...prev, ...prev,
[priceId]: Math.max(0, value), [priceId]: value,
})); }));
}, []); }, []);
const incrementQuantity = useCallback((priceId: number, amount: number) => { const incrementQuantity = useCallback((priceId: number, amount: number) => {
setQuantities((prev) => ({ setQuantities((prev) => ({
...prev, ...prev,
[priceId]: Math.max(0, (prev[priceId] ?? 0) + amount), [priceId]: String(Math.max(0, (parseFloat(prev[priceId]) || 0) + amount)),
})); }));
}, []); }, []);
@ -360,7 +359,7 @@ export default function PurchaseCreate({ suppliers, rawMaterials }: Props) {
const updated = [...prev]; const updated = [...prev];
updated[index] = { updated[index] = {
...updated[index], ...updated[index],
stock: Math.max(0, Number(updated[index].stock) + amount), stock: String(Math.max(0, (parseFloat(updated[index].stock) || 0) + amount)),
}; };
return updated; return updated;
@ -372,7 +371,7 @@ export default function PurchaseCreate({ suppliers, rawMaterials }: Props) {
const lines: CartLine[] = []; const lines: CartLine[] = [];
for (const [priceId, quantity] of Object.entries(quantities)) { for (const [priceId, quantity] of Object.entries(quantities)) {
if (quantity <= 0) { if ((parseFloat(quantity) || 0) <= 0) {
continue; continue;
} }
@ -390,7 +389,7 @@ export default function PurchaseCreate({ suppliers, rawMaterials }: Props) {
quantity, quantity,
onAdjust: (delta) => incrementQuantity(id, delta), onAdjust: (delta) => incrementQuantity(id, delta),
onSet: (value) => updateQuantity(id, value), onSet: (value) => updateQuantity(id, value),
onRemove: () => updateQuantity(id, 0), onRemove: () => updateQuantity(id, '0'),
}); });
} }
} }
@ -410,7 +409,7 @@ export default function PurchaseCreate({ suppliers, rawMaterials }: Props) {
onSet: (value) => updateVariant(index, 'stock', value), onSet: (value) => updateVariant(index, 'stock', value),
onRemove: () => removeVariant(index), onRemove: () => removeVariant(index),
})) }))
.filter((line) => line.quantity > 0); .filter((line) => (parseFloat(line.quantity) || 0) > 0);
})(); })();
function formatQuantity(value: number): string { function formatQuantity(value: number): string {
@ -433,7 +432,7 @@ export default function PurchaseCreate({ suppliers, rawMaterials }: Props) {
existing_items: Object.entries(quantities) existing_items: Object.entries(quantities)
.map(([priceId, quantity]) => ({ .map(([priceId, quantity]) => ({
raw_material_price_id: Number(priceId), raw_material_price_id: Number(priceId),
quantity: Number(quantity), quantity: parseFloat(quantity) || 0,
unit_price: priceMap.get(Number(priceId))?.price ?? 0, unit_price: priceMap.get(Number(priceId))?.price ?? 0,
})) }))
.filter((item) => item.quantity > 0), .filter((item) => item.quantity > 0),
@ -447,7 +446,7 @@ export default function PurchaseCreate({ suppliers, rawMaterials }: Props) {
variants: variantsRef.current.map((v) => ({ variants: variantsRef.current.map((v) => ({
variant: v.variant, variant: v.variant,
price: Number(v.price), price: Number(v.price),
stock: Number(v.stock), stock: parseFloat(v.stock) || 0,
photo_key: v.photo, photo_key: v.photo,
})), })),
}; };
@ -727,17 +726,19 @@ export default function PurchaseCreate({ suppliers, rawMaterials }: Props) {
* *
</span> </span>
</Label> </Label>
<NumberInput <Input
type="text"
inputMode="decimal"
value={ value={
variant.stock variant.stock
} }
onValueChange={( onChange={(
val, e,
) => ) =>
updateVariant( updateVariant(
variantIndex, variantIndex,
'stock', 'stock',
val, e.target.value,
) )
} }
/> />
@ -902,12 +903,14 @@ export default function PurchaseCreate({ suppliers, rawMaterials }: Props) {
price.id price.id
} }
className={ className={
(quantities[ (parseFloat(
price quantities[
.id price
] ?? .id
0) > ] ??
0 '0',
) >
0)
? 'flex flex-wrap items-center justify-between gap-2 rounded-lg border border-primary p-3' ? 'flex flex-wrap items-center justify-between gap-2 rounded-lg border border-primary p-3'
: 'flex flex-wrap items-center justify-between gap-2 rounded-lg border p-3' : 'flex flex-wrap items-center justify-between gap-2 rounded-lg border p-3'
} }
@ -958,11 +961,13 @@ export default function PurchaseCreate({ suppliers, rawMaterials }: Props) {
size="icon" size="icon"
disabled={ disabled={
!( !(
quantities[ parseFloat(
price quantities[
.id price
] ?? .id
0 ] ??
'0',
) > 0
) )
} }
onClick={() => onClick={() =>
@ -974,21 +979,23 @@ export default function PurchaseCreate({ suppliers, rawMaterials }: Props) {
> >
<Minus className="h-4 w-4" /> <Minus className="h-4 w-4" />
</Button> </Button>
<NumberInput <Input
type="text"
inputMode="decimal"
className="w-24 text-center" className="w-24 text-center"
value={ value={
quantities[ quantities[
price price
.id .id
] ?? ] ??
0 '0'
} }
onValueChange={( onChange={(
val, e,
) => ) =>
updateQuantity( updateQuantity(
price.id, price.id,
val, e.target.value,
) )
} }
/> />
@ -1166,7 +1173,7 @@ export default function PurchaseCreate({ suppliers, rawMaterials }: Props) {
(mode === 'existing' (mode === 'existing'
? Object.values( ? Object.values(
quantities, quantities,
).every((q) => q <= 0) ).every((q) => (parseFloat(q) || 0) <= 0)
: !name) : !name)
} }
> >
@ -1263,7 +1270,7 @@ export default function PurchaseCreate({ suppliers, rawMaterials }: Props) {
variant="outline" variant="outline"
size="icon-sm" size="icon-sm"
disabled={ disabled={
item.quantity <= 0 (parseFloat(item.quantity) || 0) <= 0
} }
onClick={() => onClick={() =>
item.onAdjust(-1) item.onAdjust(-1)
@ -1271,10 +1278,12 @@ export default function PurchaseCreate({ suppliers, rawMaterials }: Props) {
> >
<Minus className="h-4 w-4" /> <Minus className="h-4 w-4" />
</Button> </Button>
<NumberInput <Input
type="text"
inputMode="decimal"
className="w-20 text-center" className="w-20 text-center"
value={item.quantity} value={item.quantity}
onValueChange={item.onSet} onChange={(e) => item.onSet(e.target.value)}
/> />
<Button <Button
type="button" type="button"
@ -1290,7 +1299,7 @@ export default function PurchaseCreate({ suppliers, rawMaterials }: Props) {
<div className="text-right"> <div className="text-right">
<span className="text-xs font-medium text-muted-foreground"> <span className="text-xs font-medium text-muted-foreground">
{formatCurrency( {formatCurrency(
item.price * item.quantity, item.price * (parseFloat(item.quantity) || 0),
)} )}
</span> </span>
</div> </div>

View File

@ -14,7 +14,6 @@ import { ConfirmDialog } from '@/components/dialogs';
import { FileUploadMultiple } from '@/components/inputs'; import { FileUploadMultiple } from '@/components/inputs';
import { ImagePreviewModal } from '@/components/dialogs'; import { ImagePreviewModal } from '@/components/dialogs';
import { InputError } from '@/components/ui'; import { InputError } from '@/components/ui';
import { NumberInput } from '@/components/inputs';
import { RupiahInput } from '@/components/inputs'; import { RupiahInput } from '@/components/inputs';
import { Button } from '@/components/ui/button'; import { Button } from '@/components/ui/button';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
@ -27,6 +26,7 @@ import {
ComboboxList, ComboboxList,
} from '@/components/ui/combobox'; } from '@/components/ui/combobox';
import { Label } from '@/components/ui/label'; import { Label } from '@/components/ui/label';
import { Input } from '@/components/ui/input';
import { import {
Sheet, Sheet,
SheetContent, SheetContent,
@ -50,9 +50,9 @@ type CartLine = {
title: string; title: string;
subtitle: string; subtitle: string;
price: number; price: number;
quantity: number; quantity: string;
onAdjust: (delta: number) => void; onAdjust: (delta: number) => void;
onSet: (value: number) => void; onSet: (value: string) => void;
onRemove: () => void; onRemove: () => void;
}; };
@ -85,11 +85,11 @@ export default function PurchaseEdit({
}); });
const [uploading, setUploading] = useState(false); const [uploading, setUploading] = useState(false);
const [quantities, setQuantities] = useState<Record<number, number>>(() => const [quantities, setQuantities] = useState<Record<number, string>>(() =>
Object.fromEntries( Object.fromEntries(
Object.entries(purchase.existing_quantities).map(([id, qty]) => [ Object.entries(purchase.existing_quantities).map(([id, qty]) => [
Number(id), Number(id),
qty, String(qty),
]), ]),
), ),
); );
@ -158,7 +158,7 @@ export default function PurchaseEdit({
(sum, [priceId, quantity]) => { (sum, [priceId, quantity]) => {
const price = priceMap.get(Number(priceId)); const price = priceMap.get(Number(priceId));
return sum + (price ? price.price * quantity : 0); return sum + (price ? price.price * (parseFloat(quantity) || 0) : 0);
}, },
0, 0,
); );
@ -193,17 +193,17 @@ export default function PurchaseEdit({
const isMultiMaterial = purchasedVariantsByMaterial.size > 1; const isMultiMaterial = purchasedVariantsByMaterial.size > 1;
const updateQuantity = useCallback((priceId: number, value: number) => { const updateQuantity = useCallback((priceId: number, value: string) => {
setQuantities((prev) => ({ setQuantities((prev) => ({
...prev, ...prev,
[priceId]: Math.max(0, value), [priceId]: value,
})); }));
}, []); }, []);
const incrementQuantity = useCallback((priceId: number, amount: number) => { const incrementQuantity = useCallback((priceId: number, amount: number) => {
setQuantities((prev) => ({ setQuantities((prev) => ({
...prev, ...prev,
[priceId]: Math.max(0, (prev[priceId] ?? 0) + amount), [priceId]: String(Math.max(0, (parseFloat(prev[priceId]) || 0) + amount)),
})); }));
}, []); }, []);
@ -211,7 +211,7 @@ export default function PurchaseEdit({
const lines: CartLine[] = []; const lines: CartLine[] = [];
for (const [priceId, quantity] of Object.entries(quantities)) { for (const [priceId, quantity] of Object.entries(quantities)) {
if (quantity <= 0) { if ((parseFloat(quantity) || 0) <= 0) {
continue; continue;
} }
@ -229,7 +229,7 @@ export default function PurchaseEdit({
quantity, quantity,
onAdjust: (delta) => incrementQuantity(id, delta), onAdjust: (delta) => incrementQuantity(id, delta),
onSet: (value) => updateQuantity(id, value), onSet: (value) => updateQuantity(id, value),
onRemove: () => updateQuantity(id, 0), onRemove: () => updateQuantity(id, '0'),
}); });
} }
} }
@ -252,7 +252,7 @@ export default function PurchaseEdit({
existing_items: Object.entries(quantities) existing_items: Object.entries(quantities)
.map(([priceId, quantity]) => ({ .map(([priceId, quantity]) => ({
raw_material_price_id: Number(priceId), raw_material_price_id: Number(priceId),
quantity: Number(quantity), quantity: parseFloat(quantity) || 0,
unit_price: priceMap.get(Number(priceId))?.price ?? 0, unit_price: priceMap.get(Number(priceId))?.price ?? 0,
})) }))
.filter((item) => item.quantity > 0), .filter((item) => item.quantity > 0),
@ -338,7 +338,7 @@ export default function PurchaseEdit({
variant="outline" variant="outline"
size="icon" size="icon"
disabled={ disabled={
(quantities[price.id] ?? 0) <= 0 (parseFloat(quantities[price.id] ?? '0') || 0) <= 0
} }
onClick={() => onClick={() =>
incrementQuantity(price.id, -1) incrementQuantity(price.id, -1)
@ -346,11 +346,13 @@ export default function PurchaseEdit({
> >
<Minus className="h-4 w-4" /> <Minus className="h-4 w-4" />
</Button> </Button>
<NumberInput <Input
type="text"
inputMode="decimal"
className="w-24 text-center" className="w-24 text-center"
value={quantities[price.id] ?? 0} value={quantities[price.id] ?? '0'}
onValueChange={(val) => onChange={(e) =>
updateQuantity(price.id, val) updateQuantity(price.id, e.target.value)
} }
/> />
<Button <Button
@ -517,7 +519,7 @@ export default function PurchaseEdit({
!supplierId || !supplierId ||
Object.values( Object.values(
quantities, quantities,
).every((q) => q <= 0) ).every((q) => (parseFloat(q) || 0) <= 0)
} }
> >
{processing {processing
@ -613,7 +615,7 @@ export default function PurchaseEdit({
variant="outline" variant="outline"
size="icon-sm" size="icon-sm"
disabled={ disabled={
item.quantity <= 0 (parseFloat(item.quantity) || 0) <= 0
} }
onClick={() => onClick={() =>
item.onAdjust(-1) item.onAdjust(-1)
@ -621,10 +623,12 @@ export default function PurchaseEdit({
> >
<Minus className="h-4 w-4" /> <Minus className="h-4 w-4" />
</Button> </Button>
<NumberInput <Input
type="text"
inputMode="decimal"
className="w-20 text-center" className="w-20 text-center"
value={item.quantity} value={item.quantity}
onValueChange={item.onSet} onChange={(e) => item.onSet(e.target.value)}
/> />
<Button <Button
type="button" type="button"
@ -640,7 +644,7 @@ export default function PurchaseEdit({
<div className="text-right"> <div className="text-right">
<span className="text-xs font-medium text-muted-foreground"> <span className="text-xs font-medium text-muted-foreground">
{formatCurrency( {formatCurrency(
item.price * item.quantity, item.price * (parseFloat(item.quantity) || 0),
)} )}
</span> </span>
</div> </div>