From 38b4a31c6b54ba6a3ff5d1f37bc90cf9ccf9568c Mon Sep 17 00:00:00 2001 From: Yoga Pangestu Date: Sun, 2 Aug 2026 14:59:13 +0700 Subject: [PATCH] refactor: update stock and quantity fields to use integer type across models, requests, factories, and migrations; introduce NumberInput component for consistent numeric input handling --- .../Requests/Admin/Manage/PurchaseRequest.php | 4 +- .../Master/RawMaterial/RawMaterialRequest.php | 2 +- .../RawMaterial/RawMaterialVariantRequest.php | 2 +- app/Models/CuttingMaterial.php | 2 +- app/Models/PurchaseItem.php | 2 +- app/Models/RawMaterialPrice.php | 2 +- app/Models/StockMutation.php | 6 +- app/Services/Admin/Manage/PurchaseService.php | 12 +- database/factories/CuttingMaterialFactory.php | 2 +- database/factories/PurchaseItemFactory.php | 2 +- .../factories/RawMaterialPriceFactory.php | 2 +- database/factories/StockMutationFactory.php | 4 +- ...10002_create_raw_material_prices_table.php | 2 +- ..._11_120002_create_purchase_items_table.php | 2 +- ..._110002_create_cutting_materials_table.php | 2 +- ...27_000001_create_stock_mutations_table.php | 6 +- resources/js/components/number-input.tsx | 103 ++++++++++++++++++ .../js/pages/admin/manage/purchase/create.tsx | 45 ++------ .../js/pages/admin/manage/purchase/edit.tsx | 45 ++------ .../js/pages/admin/master/product/create.tsx | 40 +++---- .../js/pages/admin/master/product/edit.tsx | 40 +++---- .../admin/master/product/variant/edit.tsx | 28 ++--- .../product/variant/transfer-stock-dialog.tsx | 6 +- .../admin/master/raw-material/create.tsx | 9 +- .../pages/admin/master/raw-material/edit.tsx | 14 ++- .../master/raw-material/variant/edit.tsx | 7 +- tests/Feature/Admin/Manage/PurchaseTest.php | 6 +- .../Feature/Admin/Master/RawMaterialTest.php | 8 +- 28 files changed, 217 insertions(+), 188 deletions(-) create mode 100644 resources/js/components/number-input.tsx diff --git a/app/Http/Requests/Admin/Manage/PurchaseRequest.php b/app/Http/Requests/Admin/Manage/PurchaseRequest.php index 7417590..fdf1546 100644 --- a/app/Http/Requests/Admin/Manage/PurchaseRequest.php +++ b/app/Http/Requests/Admin/Manage/PurchaseRequest.php @@ -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'], diff --git a/app/Http/Requests/Admin/Master/RawMaterial/RawMaterialRequest.php b/app/Http/Requests/Admin/Master/RawMaterial/RawMaterialRequest.php index b9059b1..bc7a794 100644 --- a/app/Http/Requests/Admin/Master/RawMaterial/RawMaterialRequest.php +++ b/app/Http/Requests/Admin/Master/RawMaterial/RawMaterialRequest.php @@ -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'], ]; } diff --git a/app/Http/Requests/Admin/Master/RawMaterial/RawMaterialVariantRequest.php b/app/Http/Requests/Admin/Master/RawMaterial/RawMaterialVariantRequest.php index 16a872d..c7dab29 100644 --- a/app/Http/Requests/Admin/Master/RawMaterial/RawMaterialVariantRequest.php +++ b/app/Http/Requests/Admin/Master/RawMaterial/RawMaterialVariantRequest.php @@ -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'], ]; } diff --git a/app/Models/CuttingMaterial.php b/app/Models/CuttingMaterial.php index 3f6961f..fde35c4 100644 --- a/app/Models/CuttingMaterial.php +++ b/app/Models/CuttingMaterial.php @@ -16,7 +16,7 @@ class CuttingMaterial extends Model protected function casts(): array { return [ - 'material_usage' => 'decimal:2', + 'material_usage' => 'integer', 'material_result' => 'integer', ]; } diff --git a/app/Models/PurchaseItem.php b/app/Models/PurchaseItem.php index f7e6f04..20d2276 100644 --- a/app/Models/PurchaseItem.php +++ b/app/Models/PurchaseItem.php @@ -16,7 +16,7 @@ class PurchaseItem extends Model protected function casts(): array { return [ - 'quantity' => 'decimal:4', + 'quantity' => 'integer', 'unit_price' => 'integer', 'subtotal' => 'integer', ]; diff --git a/app/Models/RawMaterialPrice.php b/app/Models/RawMaterialPrice.php index 894ca11..1c0261a 100644 --- a/app/Models/RawMaterialPrice.php +++ b/app/Models/RawMaterialPrice.php @@ -21,7 +21,7 @@ protected function casts(): array { return [ 'price' => 'integer', - 'stock' => 'decimal:4', + 'stock' => 'integer', ]; } diff --git a/app/Models/StockMutation.php b/app/Models/StockMutation.php index eea15aa..7711bf3 100644 --- a/app/Models/StockMutation.php +++ b/app/Models/StockMutation.php @@ -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', ]; } diff --git a/app/Services/Admin/Manage/PurchaseService.php b/app/Services/Admin/Manage/PurchaseService.php index f2df4ef..85a1b68 100644 --- a/app/Services/Admin/Manage/PurchaseService.php +++ b/app/Services/Admin/Manage/PurchaseService.php @@ -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); } }); diff --git a/database/factories/CuttingMaterialFactory.php b/database/factories/CuttingMaterialFactory.php index 2627569..b32146f 100644 --- a/database/factories/CuttingMaterialFactory.php +++ b/database/factories/CuttingMaterialFactory.php @@ -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), ]; } diff --git a/database/factories/PurchaseItemFactory.php b/database/factories/PurchaseItemFactory.php index 03c24d8..320efc0 100644 --- a/database/factories/PurchaseItemFactory.php +++ b/database/factories/PurchaseItemFactory.php @@ -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 [ diff --git a/database/factories/RawMaterialPriceFactory.php b/database/factories/RawMaterialPriceFactory.php index 8d31171..1fc6cfd 100644 --- a/database/factories/RawMaterialPriceFactory.php +++ b/database/factories/RawMaterialPriceFactory.php @@ -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), ]; } } diff --git a/database/factories/StockMutationFactory.php b/database/factories/StockMutationFactory.php index 455dc08..9ce95a4 100644 --- a/database/factories/StockMutationFactory.php +++ b/database/factories/StockMutationFactory.php @@ -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(), diff --git a/database/migrations/2026_06_10_110002_create_raw_material_prices_table.php b/database/migrations/2026_06_10_110002_create_raw_material_prices_table.php index af6e358..9709b33 100644 --- a/database/migrations/2026_06_10_110002_create_raw_material_prices_table.php +++ b/database/migrations/2026_06_10_110002_create_raw_material_prices_table.php @@ -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(); diff --git a/database/migrations/2026_06_11_120002_create_purchase_items_table.php b/database/migrations/2026_06_11_120002_create_purchase_items_table.php index 6282625..61b9d45 100644 --- a/database/migrations/2026_06_11_120002_create_purchase_items_table.php +++ b/database/migrations/2026_06_11_120002_create_purchase_items_table.php @@ -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'); diff --git a/database/migrations/2026_06_12_110002_create_cutting_materials_table.php b/database/migrations/2026_06_12_110002_create_cutting_materials_table.php index 5143e6b..f541fa7 100644 --- a/database/migrations/2026_06_12_110002_create_cutting_materials_table.php +++ b/database/migrations/2026_06_12_110002_create_cutting_materials_table.php @@ -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(); diff --git a/database/migrations/2026_07_27_000001_create_stock_mutations_table.php b/database/migrations/2026_07_27_000001_create_stock_mutations_table.php index 4101a6d..53e7d39 100644 --- a/database/migrations/2026_07_27_000001_create_stock_mutations_table.php +++ b/database/migrations/2026_07_27_000001_create_stock_mutations_table.php @@ -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(); diff --git a/resources/js/components/number-input.tsx b/resources/js/components/number-input.tsx new file mode 100644 index 0000000..0811dbd --- /dev/null +++ b/resources/js/components/number-input.tsx @@ -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) => { + 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 ( + + + {suffix && ( + + {suffix} + + )} + + ); +} diff --git a/resources/js/pages/admin/manage/purchase/create.tsx b/resources/js/pages/admin/manage/purchase/create.tsx index 1847aab..cb7ffe1 100644 --- a/resources/js/pages/admin/manage/purchase/create.tsx +++ b/resources/js/pages/admin/manage/purchase/create.tsx @@ -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) { * - updateVariant( variantIndex, 'stock', - Number( - e - .target - .value, - ), + val, ) } /> @@ -936,12 +931,8 @@ export default function PurchaseCreate({ data }: Props) { > - updateQuantity( price.id, - Number( - e - .target - .value, - ), + val, ) } /> @@ -1250,19 +1237,11 @@ export default function PurchaseCreate({ data }: Props) { > - - item.onSet( - Number( - e.target.value, - ), - ) - } + onValueChange={item.onSet} /> - updateQuantity( price.id, - Number( - e - .target - .value, - ), + val, ) } /> @@ -1148,19 +1135,11 @@ export default function PurchaseEdit({ purchase, data }: Props) { > - - item.onSet( - Number( - e.target.value, - ), - ) - } + onValueChange={item.onSet} />