refactor: update stock and quantity fields to use integer type across models, requests, factories, and migrations; introduce NumberInput component for consistent numeric input handling
This commit is contained in:
parent
9637671713
commit
38b4a31c6b
@ -42,11 +42,11 @@ public function rules(): array
|
||||
'variants' => ['required_unless:mode,existing', 'array', 'min:1'],
|
||||
'variants.*.variant' => ['required_unless:mode,existing', 'string', 'max:200'],
|
||||
'variants.*.price' => ['required_unless:mode,existing', 'integer', 'min:0'],
|
||||
'variants.*.stock' => ['required_unless:mode,existing', 'numeric', 'min:0'],
|
||||
'variants.*.stock' => ['required_unless:mode,existing', 'integer', 'min:0'],
|
||||
'variants.*.photo_key' => ['required_unless:mode,existing', 'string', 'max:500'],
|
||||
'existing_items' => ['required_if:mode,existing', 'array', 'min:1'],
|
||||
'existing_items.*.raw_material_price_id' => ['required', 'integer', 'exists:raw_material_prices,id'],
|
||||
'existing_items.*.quantity' => ['required', 'numeric', 'min:0.0001'],
|
||||
'existing_items.*.quantity' => ['required', 'integer', 'min:1'],
|
||||
'existing_items.*.unit_price' => ['required', 'integer', 'min:0'],
|
||||
'supplier_id' => ['required', 'integer', 'exists:suppliers,id'],
|
||||
'discount' => ['nullable', 'integer', 'min:0'],
|
||||
|
||||
@ -35,7 +35,7 @@ public function rules(): array
|
||||
'variants.*.id' => ['nullable', 'integer'],
|
||||
'variants.*.variant' => ['required', 'string', 'max:200'],
|
||||
'variants.*.price' => ['required', 'integer', 'min:0'],
|
||||
'variants.*.stock' => ['required', 'numeric', 'min:0'],
|
||||
'variants.*.stock' => ['required', 'integer', 'min:0'],
|
||||
'variants.*.photo_key' => ['required', 'string', 'max:500'],
|
||||
];
|
||||
}
|
||||
|
||||
@ -24,7 +24,7 @@ public function rules(): array
|
||||
return [
|
||||
'variant' => ['required', 'string', 'max:200'],
|
||||
'price' => ['required', 'integer', 'min:0'],
|
||||
'stock' => ['required', 'numeric', 'min:0'],
|
||||
'stock' => ['required', 'integer', 'min:0'],
|
||||
'photo_key' => ['required', 'string', 'max:500'],
|
||||
];
|
||||
}
|
||||
|
||||
@ -16,7 +16,7 @@ class CuttingMaterial extends Model
|
||||
protected function casts(): array
|
||||
{
|
||||
return [
|
||||
'material_usage' => 'decimal:2',
|
||||
'material_usage' => 'integer',
|
||||
'material_result' => 'integer',
|
||||
];
|
||||
}
|
||||
|
||||
@ -16,7 +16,7 @@ class PurchaseItem extends Model
|
||||
protected function casts(): array
|
||||
{
|
||||
return [
|
||||
'quantity' => 'decimal:4',
|
||||
'quantity' => 'integer',
|
||||
'unit_price' => 'integer',
|
||||
'subtotal' => 'integer',
|
||||
];
|
||||
|
||||
@ -21,7 +21,7 @@ protected function casts(): array
|
||||
{
|
||||
return [
|
||||
'price' => 'integer',
|
||||
'stock' => 'decimal:4',
|
||||
'stock' => 'integer',
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
@ -16,9 +16,9 @@ class StockMutation extends Model
|
||||
protected function casts(): array
|
||||
{
|
||||
return [
|
||||
'quantity' => 'decimal:4',
|
||||
'stock_before' => 'decimal:4',
|
||||
'stock_after' => 'decimal:4',
|
||||
'quantity' => 'integer',
|
||||
'stock_before' => 'integer',
|
||||
'stock_after' => 'integer',
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
@ -143,7 +143,7 @@ public function getForEdit(Purchase $purchase): array
|
||||
'existing_material_name' => $singleMaterial ? $materials->first()->name : null,
|
||||
'existing_quantities' => $singleMaterial
|
||||
? $items->mapWithKeys(fn (PurchaseItem $item) => [
|
||||
(int) $item->raw_material_price_id => (float) $item->quantity,
|
||||
(int) $item->raw_material_price_id => (int) $item->quantity,
|
||||
])->all()
|
||||
: [],
|
||||
];
|
||||
@ -201,7 +201,7 @@ private function createFromExisting(array $data): Purchase
|
||||
|
||||
foreach ($data['existing_items'] as $item) {
|
||||
RawMaterialPrice::whereKey($item['raw_material_price_id'])
|
||||
->increment('stock', (float) $item['quantity']);
|
||||
->increment('stock', (int) $item['quantity']);
|
||||
}
|
||||
|
||||
if (! empty($data['photo_key'])) {
|
||||
@ -325,7 +325,7 @@ public function update(Purchase $purchase, array $data): Purchase
|
||||
// get adjusted by the difference instead of being reset.
|
||||
$oldItems->each(function (PurchaseItem $item) {
|
||||
if ($item->rawMaterialPrice) {
|
||||
$item->rawMaterialPrice->decrement('stock', (float) $item->quantity);
|
||||
$item->rawMaterialPrice->decrement('stock', (int) $item->quantity);
|
||||
}
|
||||
});
|
||||
|
||||
@ -366,7 +366,7 @@ public function update(Purchase $purchase, array $data): Purchase
|
||||
|
||||
foreach ($data['existing_items'] as $item) {
|
||||
RawMaterialPrice::whereKey($item['raw_material_price_id'])
|
||||
->increment('stock', (float) $item['quantity']);
|
||||
->increment('stock', (int) $item['quantity']);
|
||||
}
|
||||
} else {
|
||||
// 2a. Always reuse the purchase's existing material in place;
|
||||
@ -402,7 +402,7 @@ public function update(Purchase $purchase, array $data): Purchase
|
||||
}
|
||||
|
||||
if ($price) {
|
||||
$price->increment('stock', (float) $v['stock']);
|
||||
$price->increment('stock', (int) $v['stock']);
|
||||
$price->update(['price' => $v['price']]);
|
||||
} else {
|
||||
$price = $rawMaterial->rawMaterialPrices()->create([
|
||||
@ -493,7 +493,7 @@ public function delete(Purchase $purchase): bool
|
||||
// Remove the stock the purchase added, keep the variants.
|
||||
$purchase->purchaseItems->each(function (PurchaseItem $item) {
|
||||
if ($item->rawMaterialPrice) {
|
||||
$item->rawMaterialPrice->decrement('stock', (float) $item->quantity);
|
||||
$item->rawMaterialPrice->decrement('stock', (int) $item->quantity);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@ -15,7 +15,7 @@ public function definition(): array
|
||||
'user_id' => User::factory(),
|
||||
'cutting_id' => Cutting::factory(),
|
||||
'raw_material_price_id' => RawMaterialPrice::factory(),
|
||||
'material_usage' => fake()->randomFloat(2, 0.1, 100),
|
||||
'material_usage' => fake()->numberBetween(1, 100),
|
||||
'material_result' => fake()->numberBetween(0, 100),
|
||||
];
|
||||
}
|
||||
|
||||
@ -11,7 +11,7 @@ class PurchaseItemFactory extends Factory
|
||||
{
|
||||
public function definition(): array
|
||||
{
|
||||
$quantity = fake()->randomFloat(4, 0.5, 100);
|
||||
$quantity = fake()->numberBetween(1, 100);
|
||||
$unitPrice = fake()->numberBetween(1000, 100000);
|
||||
|
||||
return [
|
||||
|
||||
@ -13,7 +13,7 @@ public function definition(): array
|
||||
'raw_material_id' => RawMaterial::factory(),
|
||||
'variant' => fake()->words(2, true),
|
||||
'price' => fake()->numberBetween(1000, 500000),
|
||||
'stock' => fake()->randomFloat(4, 0, 1000),
|
||||
'stock' => fake()->numberBetween(0, 1000),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
@ -9,8 +9,8 @@ class StockMutationFactory extends Factory
|
||||
{
|
||||
public function definition(): array
|
||||
{
|
||||
$stockBefore = fake()->randomFloat(4, 0, 1000);
|
||||
$quantity = fake()->randomFloat(4, -100, 100);
|
||||
$stockBefore = fake()->numberBetween(0, 1000);
|
||||
$quantity = fake()->numberBetween(-100, 100);
|
||||
|
||||
return [
|
||||
'stockable_type' => fake()->word(),
|
||||
|
||||
@ -15,7 +15,7 @@ public function up(): void
|
||||
|
||||
$table->string('variant', 200);
|
||||
$table->unsignedInteger('price');
|
||||
$table->decimal('stock', 18, 4)->default(0);
|
||||
$table->unsignedInteger('stock')->default(0);
|
||||
|
||||
$table->timestamp('created_at')->useCurrent();
|
||||
$table->timestamp('updated_at')->useCurrent()->useCurrentOnUpdate();
|
||||
|
||||
@ -15,7 +15,7 @@ public function up(): void
|
||||
$table->foreignId('user_id')->nullable()->constrained()->cascadeOnDelete();
|
||||
$table->foreignId('raw_material_price_id')->constrained()->cascadeOnDelete();
|
||||
|
||||
$table->decimal('quantity', 18, 4);
|
||||
$table->unsignedInteger('quantity');
|
||||
$table->unsignedBigInteger('unit_price');
|
||||
$table->unsignedBigInteger('subtotal');
|
||||
|
||||
|
||||
@ -16,7 +16,7 @@ public function up(): void
|
||||
$table->foreignId('raw_material_price_id')->constrained()->restrictOnDelete();
|
||||
$table->foreignId('combination_id')->nullable()->constrained('cutting_material_combinations')->cascadeOnDelete();
|
||||
|
||||
$table->decimal('material_usage', 18, 2);
|
||||
$table->integer('material_usage');
|
||||
$table->integer('material_result')->nullable();
|
||||
|
||||
$table->timestamp('created_at')->useCurrent();
|
||||
|
||||
@ -16,9 +16,9 @@ public function up(): void
|
||||
$table->morphs('stockable');
|
||||
$table->string('type');
|
||||
$table->nullableMorphs('source');
|
||||
$table->decimal('quantity', 18, 4);
|
||||
$table->decimal('stock_before', 18, 4);
|
||||
$table->decimal('stock_after', 18, 4);
|
||||
$table->integer('quantity');
|
||||
$table->integer('stock_before');
|
||||
$table->integer('stock_after');
|
||||
$table->string('stock_quality')->nullable();
|
||||
$table->string('description')->nullable();
|
||||
|
||||
|
||||
103
resources/js/components/number-input.tsx
Normal file
103
resources/js/components/number-input.tsx
Normal file
@ -0,0 +1,103 @@
|
||||
import { useCallback, useRef, useState } from 'react';
|
||||
import {
|
||||
InputGroup,
|
||||
InputGroupAddon,
|
||||
InputGroupInput,
|
||||
InputGroupText,
|
||||
} from '@/components/ui/input-group';
|
||||
|
||||
type NumberInputProps = {
|
||||
id?: string;
|
||||
name?: string;
|
||||
defaultValue?: number;
|
||||
value?: number;
|
||||
onValueChange?: (value: number) => void;
|
||||
placeholder?: string;
|
||||
disabled?: boolean;
|
||||
min?: number;
|
||||
max?: number;
|
||||
suffix?: string;
|
||||
className?: string;
|
||||
};
|
||||
|
||||
function formatDisplay(value: number): string {
|
||||
return value.toLocaleString('id-ID');
|
||||
}
|
||||
|
||||
function parseText(value: string): number {
|
||||
const cleaned = value.replace(/[^0-9]/g, '');
|
||||
|
||||
return cleaned === '' ? 0 : parseInt(cleaned, 10);
|
||||
}
|
||||
|
||||
export function NumberInput({
|
||||
id,
|
||||
name,
|
||||
defaultValue = 0,
|
||||
value,
|
||||
onValueChange,
|
||||
placeholder = '0',
|
||||
disabled = false,
|
||||
min,
|
||||
max,
|
||||
suffix,
|
||||
className,
|
||||
}: NumberInputProps) {
|
||||
const isControlled = value !== undefined;
|
||||
const [displayValue, setDisplayValue] = useState(
|
||||
formatDisplay(isControlled ? value : defaultValue),
|
||||
);
|
||||
const lastValidRef = useRef(isControlled ? value : defaultValue);
|
||||
const [prevValue, setPrevValue] = useState(value);
|
||||
|
||||
if (isControlled && value !== prevValue) {
|
||||
setPrevValue(value);
|
||||
setDisplayValue(formatDisplay(value));
|
||||
}
|
||||
|
||||
const handleChange = useCallback(
|
||||
(e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const raw = parseText(e.target.value);
|
||||
let clamped = raw;
|
||||
|
||||
if (min !== undefined && raw < min) {
|
||||
clamped = min;
|
||||
}
|
||||
|
||||
if (max !== undefined && raw > max) {
|
||||
clamped = max;
|
||||
}
|
||||
|
||||
lastValidRef.current = clamped;
|
||||
setDisplayValue(formatDisplay(clamped));
|
||||
onValueChange?.(clamped);
|
||||
},
|
||||
[min, max, onValueChange],
|
||||
);
|
||||
|
||||
const handleBlur = useCallback(() => {
|
||||
setDisplayValue(formatDisplay(lastValidRef.current));
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<InputGroup className={className}>
|
||||
<InputGroupInput
|
||||
id={id}
|
||||
name={name}
|
||||
type="text"
|
||||
inputMode="numeric"
|
||||
value={displayValue}
|
||||
onChange={handleChange}
|
||||
onBlur={handleBlur}
|
||||
placeholder={placeholder}
|
||||
disabled={disabled}
|
||||
autoComplete="off"
|
||||
/>
|
||||
{suffix && (
|
||||
<InputGroupAddon align="inline-end">
|
||||
<InputGroupText>{suffix}</InputGroupText>
|
||||
</InputGroupAddon>
|
||||
)}
|
||||
</InputGroup>
|
||||
);
|
||||
}
|
||||
@ -16,6 +16,7 @@ import { ConfirmDialog } from '@/components/confirm-dialog';
|
||||
import { FileUpload } from '@/components/file-upload';
|
||||
import { ImagePreviewModal } from '@/components/image-preview-modal';
|
||||
import InputError from '@/components/input-error';
|
||||
import { NumberInput } from '@/components/number-input';
|
||||
import { RupiahInput } from '@/components/rupiah-input';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
||||
@ -694,24 +695,18 @@ export default function PurchaseCreate({ data }: Props) {
|
||||
*
|
||||
</span>
|
||||
</Label>
|
||||
<Input
|
||||
type="number"
|
||||
<NumberInput
|
||||
min={0}
|
||||
step="0.0001"
|
||||
value={
|
||||
variant.stock
|
||||
}
|
||||
onChange={(
|
||||
e,
|
||||
onValueChange={(
|
||||
val,
|
||||
) =>
|
||||
updateVariant(
|
||||
variantIndex,
|
||||
'stock',
|
||||
Number(
|
||||
e
|
||||
.target
|
||||
.value,
|
||||
),
|
||||
val,
|
||||
)
|
||||
}
|
||||
/>
|
||||
@ -936,12 +931,8 @@ export default function PurchaseCreate({ data }: Props) {
|
||||
>
|
||||
<Minus className="h-4 w-4" />
|
||||
</Button>
|
||||
<Input
|
||||
type="number"
|
||||
min={
|
||||
0
|
||||
}
|
||||
step="0.0001"
|
||||
<NumberInput
|
||||
min={0}
|
||||
className="w-24 text-center"
|
||||
value={
|
||||
quantities[
|
||||
@ -950,16 +941,12 @@ export default function PurchaseCreate({ data }: Props) {
|
||||
] ??
|
||||
0
|
||||
}
|
||||
onChange={(
|
||||
e,
|
||||
onValueChange={(
|
||||
val,
|
||||
) =>
|
||||
updateQuantity(
|
||||
price.id,
|
||||
Number(
|
||||
e
|
||||
.target
|
||||
.value,
|
||||
),
|
||||
val,
|
||||
)
|
||||
}
|
||||
/>
|
||||
@ -1250,19 +1237,11 @@ export default function PurchaseCreate({ data }: Props) {
|
||||
>
|
||||
<Minus className="h-4 w-4" />
|
||||
</Button>
|
||||
<Input
|
||||
type="number"
|
||||
<NumberInput
|
||||
min={0}
|
||||
step="0.0001"
|
||||
className="w-20 text-center"
|
||||
value={item.quantity}
|
||||
onChange={(e) =>
|
||||
item.onSet(
|
||||
Number(
|
||||
e.target.value,
|
||||
),
|
||||
)
|
||||
}
|
||||
onValueChange={item.onSet}
|
||||
/>
|
||||
<Button
|
||||
type="button"
|
||||
|
||||
@ -16,6 +16,7 @@ import { ConfirmDialog } from '@/components/confirm-dialog';
|
||||
import { FileUpload } from '@/components/file-upload';
|
||||
import { ImagePreviewModal } from '@/components/image-preview-modal';
|
||||
import InputError from '@/components/input-error';
|
||||
import { NumberInput } from '@/components/number-input';
|
||||
import { RupiahInput } from '@/components/rupiah-input';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
||||
@ -592,24 +593,18 @@ export default function PurchaseEdit({ purchase, data }: Props) {
|
||||
*
|
||||
</span>
|
||||
</Label>
|
||||
<Input
|
||||
type="number"
|
||||
<NumberInput
|
||||
min={0}
|
||||
step="0.0001"
|
||||
value={
|
||||
variant.stock
|
||||
}
|
||||
onChange={(
|
||||
e,
|
||||
onValueChange={(
|
||||
val,
|
||||
) =>
|
||||
updateVariant(
|
||||
variantIndex,
|
||||
'stock',
|
||||
Number(
|
||||
e
|
||||
.target
|
||||
.value,
|
||||
),
|
||||
val,
|
||||
)
|
||||
}
|
||||
/>
|
||||
@ -834,12 +829,8 @@ export default function PurchaseEdit({ purchase, data }: Props) {
|
||||
>
|
||||
<Minus className="h-4 w-4" />
|
||||
</Button>
|
||||
<Input
|
||||
type="number"
|
||||
min={
|
||||
0
|
||||
}
|
||||
step="0.0001"
|
||||
<NumberInput
|
||||
min={0}
|
||||
className="w-24 text-center"
|
||||
value={
|
||||
quantities[
|
||||
@ -848,16 +839,12 @@ export default function PurchaseEdit({ purchase, data }: Props) {
|
||||
] ??
|
||||
0
|
||||
}
|
||||
onChange={(
|
||||
e,
|
||||
onValueChange={(
|
||||
val,
|
||||
) =>
|
||||
updateQuantity(
|
||||
price.id,
|
||||
Number(
|
||||
e
|
||||
.target
|
||||
.value,
|
||||
),
|
||||
val,
|
||||
)
|
||||
}
|
||||
/>
|
||||
@ -1148,19 +1135,11 @@ export default function PurchaseEdit({ purchase, data }: Props) {
|
||||
>
|
||||
<Minus className="h-4 w-4" />
|
||||
</Button>
|
||||
<Input
|
||||
type="number"
|
||||
<NumberInput
|
||||
min={0}
|
||||
step="0.0001"
|
||||
className="w-20 text-center"
|
||||
value={item.quantity}
|
||||
onChange={(e) =>
|
||||
item.onSet(
|
||||
Number(
|
||||
e.target.value,
|
||||
),
|
||||
)
|
||||
}
|
||||
onValueChange={item.onSet}
|
||||
/>
|
||||
<Button
|
||||
type="button"
|
||||
|
||||
@ -11,6 +11,7 @@ import { useCallback, useRef, useState } from 'react';
|
||||
import { ConfirmDialog } from '@/components/confirm-dialog';
|
||||
import { FileUploadMultiple } from '@/components/file-upload-multiple';
|
||||
import InputError from '@/components/input-error';
|
||||
import { NumberInput } from '@/components/number-input';
|
||||
import { RupiahInput } from '@/components/rupiah-input';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
||||
@ -656,21 +657,18 @@ export default function ProductCreate({ categories }: Props) {
|
||||
*
|
||||
</span>
|
||||
</Label>
|
||||
<Input
|
||||
type="number"
|
||||
<NumberInput
|
||||
min={0}
|
||||
value={
|
||||
variant.stock
|
||||
}
|
||||
onChange={(e) =>
|
||||
onValueChange={(
|
||||
val,
|
||||
) =>
|
||||
updateVariant(
|
||||
variantIndex,
|
||||
'stock',
|
||||
Number(
|
||||
e
|
||||
.target
|
||||
.value,
|
||||
),
|
||||
val,
|
||||
)
|
||||
}
|
||||
/>
|
||||
@ -689,21 +687,18 @@ export default function ProductCreate({ categories }: Props) {
|
||||
*
|
||||
</span>
|
||||
</Label>
|
||||
<Input
|
||||
type="number"
|
||||
<NumberInput
|
||||
min={0}
|
||||
value={
|
||||
variant.reject_stock
|
||||
}
|
||||
onChange={(e) =>
|
||||
onValueChange={(
|
||||
val,
|
||||
) =>
|
||||
updateVariant(
|
||||
variantIndex,
|
||||
'reject_stock',
|
||||
Number(
|
||||
e
|
||||
.target
|
||||
.value,
|
||||
),
|
||||
val,
|
||||
)
|
||||
}
|
||||
/>
|
||||
@ -722,21 +717,18 @@ export default function ProductCreate({ categories }: Props) {
|
||||
*
|
||||
</span>
|
||||
</Label>
|
||||
<Input
|
||||
type="number"
|
||||
<NumberInput
|
||||
min={0}
|
||||
value={
|
||||
variant.retail_stock
|
||||
}
|
||||
onChange={(e) =>
|
||||
onValueChange={(
|
||||
val,
|
||||
) =>
|
||||
updateVariant(
|
||||
variantIndex,
|
||||
'retail_stock',
|
||||
Number(
|
||||
e
|
||||
.target
|
||||
.value,
|
||||
),
|
||||
val,
|
||||
)
|
||||
}
|
||||
/>
|
||||
|
||||
@ -11,6 +11,7 @@ import { useCallback, useRef, useState } from 'react';
|
||||
import { ConfirmDialog } from '@/components/confirm-dialog';
|
||||
import { FileUploadMultiple } from '@/components/file-upload-multiple';
|
||||
import InputError from '@/components/input-error';
|
||||
import { NumberInput } from '@/components/number-input';
|
||||
import { RupiahInput } from '@/components/rupiah-input';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
||||
@ -706,21 +707,18 @@ export default function ProductEdit({ product, categories }: Props) {
|
||||
*
|
||||
</span>
|
||||
</Label>
|
||||
<Input
|
||||
type="number"
|
||||
<NumberInput
|
||||
min={0}
|
||||
value={
|
||||
variant.stock
|
||||
}
|
||||
onChange={(e) =>
|
||||
onValueChange={(
|
||||
val,
|
||||
) =>
|
||||
updateVariant(
|
||||
variantIndex,
|
||||
'stock',
|
||||
Number(
|
||||
e
|
||||
.target
|
||||
.value,
|
||||
),
|
||||
val,
|
||||
)
|
||||
}
|
||||
/>
|
||||
@ -739,21 +737,18 @@ export default function ProductEdit({ product, categories }: Props) {
|
||||
*
|
||||
</span>
|
||||
</Label>
|
||||
<Input
|
||||
type="number"
|
||||
<NumberInput
|
||||
min={0}
|
||||
value={
|
||||
variant.reject_stock
|
||||
}
|
||||
onChange={(e) =>
|
||||
onValueChange={(
|
||||
val,
|
||||
) =>
|
||||
updateVariant(
|
||||
variantIndex,
|
||||
'reject_stock',
|
||||
Number(
|
||||
e
|
||||
.target
|
||||
.value,
|
||||
),
|
||||
val,
|
||||
)
|
||||
}
|
||||
/>
|
||||
@ -772,21 +767,18 @@ export default function ProductEdit({ product, categories }: Props) {
|
||||
*
|
||||
</span>
|
||||
</Label>
|
||||
<Input
|
||||
type="number"
|
||||
<NumberInput
|
||||
min={0}
|
||||
value={
|
||||
variant.retail_stock
|
||||
}
|
||||
onChange={(e) =>
|
||||
onValueChange={(
|
||||
val,
|
||||
) =>
|
||||
updateVariant(
|
||||
variantIndex,
|
||||
'retail_stock',
|
||||
Number(
|
||||
e
|
||||
.target
|
||||
.value,
|
||||
),
|
||||
val,
|
||||
)
|
||||
}
|
||||
/>
|
||||
|
||||
@ -3,6 +3,7 @@ import { ArrowLeft } from 'lucide-react';
|
||||
import { useState } from 'react';
|
||||
import { FileUploadMultiple } from '@/components/file-upload-multiple';
|
||||
import InputError from '@/components/input-error';
|
||||
import { NumberInput } from '@/components/number-input';
|
||||
import { RupiahInput } from '@/components/rupiah-input';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
||||
@ -129,15 +130,10 @@ export default function ProductVariantEdit({ variant }: Props) {
|
||||
*
|
||||
</span>
|
||||
</Label>
|
||||
<Input
|
||||
type="number"
|
||||
<NumberInput
|
||||
min={0}
|
||||
value={stock}
|
||||
onChange={(e) =>
|
||||
setStock(
|
||||
Number(e.target.value),
|
||||
)
|
||||
}
|
||||
onValueChange={setStock}
|
||||
/>
|
||||
<InputError
|
||||
message={errors.stock}
|
||||
@ -150,15 +146,10 @@ export default function ProductVariantEdit({ variant }: Props) {
|
||||
*
|
||||
</span>
|
||||
</Label>
|
||||
<Input
|
||||
type="number"
|
||||
<NumberInput
|
||||
min={0}
|
||||
value={rejectStock}
|
||||
onChange={(e) =>
|
||||
setRejectStock(
|
||||
Number(e.target.value),
|
||||
)
|
||||
}
|
||||
onValueChange={setRejectStock}
|
||||
/>
|
||||
<InputError
|
||||
message={errors.reject_stock}
|
||||
@ -171,15 +162,10 @@ export default function ProductVariantEdit({ variant }: Props) {
|
||||
*
|
||||
</span>
|
||||
</Label>
|
||||
<Input
|
||||
type="number"
|
||||
<NumberInput
|
||||
min={0}
|
||||
value={retailStock}
|
||||
onChange={(e) =>
|
||||
setRetailStock(
|
||||
Number(e.target.value),
|
||||
)
|
||||
}
|
||||
onValueChange={setRetailStock}
|
||||
/>
|
||||
<InputError
|
||||
message={errors.retail_stock}
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
import { Form } from '@inertiajs/react';
|
||||
import InputError from '@/components/input-error';
|
||||
import { NumberInput } from '@/components/number-input';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import {
|
||||
Dialog,
|
||||
@ -8,7 +9,6 @@ import {
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from '@/components/ui/dialog';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { Label } from '@/components/ui/label';
|
||||
import { Textarea } from '@/components/ui/textarea';
|
||||
import { transferStock } from '@/routes/admin/master/products/variants';
|
||||
@ -77,11 +77,9 @@ export function TransferStockDialog({
|
||||
*
|
||||
</span>
|
||||
</Label>
|
||||
<Input
|
||||
<NumberInput
|
||||
id="quantity"
|
||||
name="quantity"
|
||||
type="number"
|
||||
min={1}
|
||||
max={variant.stock}
|
||||
placeholder="Masukkan jumlah transfer"
|
||||
/>
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
import { ConfirmDialog } from '@/components/confirm-dialog';
|
||||
import { FileUpload } from '@/components/file-upload';
|
||||
import InputError from '@/components/input-error';
|
||||
import { NumberInput } from '@/components/number-input';
|
||||
import { RupiahInput } from '@/components/rupiah-input';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
||||
@ -340,16 +341,14 @@ export default function RawMaterialCreate() {
|
||||
<Label>
|
||||
Stok <span className="text-destructive">*</span>
|
||||
</Label>
|
||||
<Input
|
||||
type="number"
|
||||
<NumberInput
|
||||
min={0}
|
||||
step="0.0001"
|
||||
value={variant.stock}
|
||||
onChange={(e) =>
|
||||
onValueChange={(val) =>
|
||||
updateVariant(
|
||||
variantIndex,
|
||||
'stock',
|
||||
Number(e.target.value),
|
||||
val,
|
||||
)
|
||||
}
|
||||
/>
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
import { ConfirmDialog } from '@/components/confirm-dialog';
|
||||
import { FileUpload } from '@/components/file-upload';
|
||||
import InputError from '@/components/input-error';
|
||||
import { NumberInput } from '@/components/number-input';
|
||||
import { RupiahInput } from '@/components/rupiah-input';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
||||
@ -356,16 +357,17 @@ export default function RawMaterialEdit({ rawMaterial }: Props) {
|
||||
<Label>
|
||||
Stok <span className="text-destructive">*</span>
|
||||
</Label>
|
||||
<Input
|
||||
type="number"
|
||||
<NumberInput
|
||||
min={0}
|
||||
step="0.0001"
|
||||
value={Number(variant.stock) || 0}
|
||||
onChange={(e) =>
|
||||
value={
|
||||
Number(variant.stock) ||
|
||||
0
|
||||
}
|
||||
onValueChange={(val) =>
|
||||
updateVariant(
|
||||
variantIndex,
|
||||
'stock',
|
||||
Number(e.target.value),
|
||||
val,
|
||||
)
|
||||
}
|
||||
/>
|
||||
|
||||
@ -3,6 +3,7 @@ import { ArrowLeft } from 'lucide-react';
|
||||
import { useState } from 'react';
|
||||
import { FileUpload } from '@/components/file-upload';
|
||||
import InputError from '@/components/input-error';
|
||||
import { NumberInput } from '@/components/number-input';
|
||||
import { RupiahInput } from '@/components/rupiah-input';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
||||
@ -96,12 +97,10 @@ export default function RawMaterialVariantEdit({ variant }: Props) {
|
||||
<Label>
|
||||
Stok <span className="text-destructive">*</span>
|
||||
</Label>
|
||||
<Input
|
||||
type="number"
|
||||
<NumberInput
|
||||
min={0}
|
||||
step="0.0001"
|
||||
value={stock}
|
||||
onChange={(e) => setStock(Number(e.target.value))}
|
||||
onValueChange={setStock}
|
||||
/>
|
||||
<InputError message={errors.stock} />
|
||||
</div>
|
||||
|
||||
@ -208,7 +208,7 @@ function makeValidPurchasePayload(array $overrides = []): array
|
||||
$response->assertOk();
|
||||
$response->assertInertia(fn (Assert $page) => $page
|
||||
->where('purchases.data.0.purchase_items.0.unit_price', 50000)
|
||||
->where('purchases.data.0.purchase_items.0.quantity', '10.0000')
|
||||
->where('purchases.data.0.purchase_items.0.quantity', 10)
|
||||
->where('purchases.data.0.purchase_items.0.subtotal', 500000)
|
||||
->where('purchases.data.0.purchase_items.0.raw_material_price.variant', 'Merah')
|
||||
->where('purchases.data.0.purchase_items.0.raw_material_price.raw_material.name', 'Bahan Baku Test')
|
||||
@ -375,7 +375,7 @@ function makeValidPurchasePayload(array $overrides = []): array
|
||||
|
||||
expect($item->purchase_id)->toBe($purchase->id);
|
||||
expect($item->raw_material_price_id)->not->toBeNull();
|
||||
expect($item->quantity)->toBe('10.0000');
|
||||
expect($item->quantity)->toBe(10);
|
||||
expect($item->unit_price)->toBe(50000);
|
||||
expect($item->subtotal)->toBe(500000);
|
||||
});
|
||||
@ -397,7 +397,7 @@ function makeValidPurchasePayload(array $overrides = []): array
|
||||
expect($price)->not->toBeNull();
|
||||
expect($price->variant)->toBe('Merah');
|
||||
expect($price->price)->toBe(50000);
|
||||
expect($price->stock)->toBe('10.0000');
|
||||
expect($price->stock)->toBe(10);
|
||||
});
|
||||
|
||||
test('purchase calculates multiple variant subtotals correctly', function () {
|
||||
|
||||
@ -271,18 +271,18 @@ function makeValidRawMaterialPayload(array $overrides = []): array
|
||||
$response->assertSessionHasNoErrors();
|
||||
});
|
||||
|
||||
test('raw material can be created with decimal stock', function () {
|
||||
test('raw material can be created with integer stock', function () {
|
||||
$user = User::factory()->create();
|
||||
$this->actingAs($user);
|
||||
|
||||
$response = $this->post(route('admin.master.raw-materials.store'), makeValidRawMaterialPayload([
|
||||
'variants' => [['variant' => 'Desimal', 'price' => 50000, 'stock' => 1.5, 'photo_key' => 'raw-material-variant/decimal.jpg']],
|
||||
'variants' => [['variant' => 'Bilangan Bulat', 'price' => 50000, 'stock' => 2, 'photo_key' => 'raw-material-variant/integer.jpg']],
|
||||
]));
|
||||
|
||||
$response->assertSessionHasNoErrors();
|
||||
|
||||
$price = RawMaterialPrice::where('variant', 'Desimal')->first();
|
||||
expect((float) $price->stock)->toBe(1.5);
|
||||
$price = RawMaterialPrice::where('variant', 'Bilangan Bulat')->first();
|
||||
expect($price->stock)->toBe(2);
|
||||
});
|
||||
|
||||
test('raw material can be created as inactive', function () {
|
||||
|
||||
Loading…
Reference in New Issue
Block a user