feat: update purchase management to support decimal quantities and improve data handling
This commit is contained in:
parent
b85bbb1ba4
commit
f128ace2ea
@ -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'],
|
||||||
|
|||||||
@ -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',
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -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 [
|
||||||
|
|||||||
@ -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();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
@ -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;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@ -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>
|
||||||
|
|||||||
@ -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>
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user