From dd3a2aa1eb545512bdd7a71cd50adfa49baa1462 Mon Sep 17 00:00:00 2001 From: Yoga Pangestu Date: Sat, 15 Aug 2026 20:03:00 +0700 Subject: [PATCH] feat: update material usage and stock fields to use decimal type for improved precision --- .ai/REFERENCE.md | 8 +++--- .../Requests/Admin/Manage/CuttingRequest.php | 2 +- .../Master/RawMaterial/RawMaterialRequest.php | 2 +- .../RawMaterial/RawMaterialVariantRequest.php | 2 +- app/Models/CuttingMaterial.php | 4 +-- app/Models/RawMaterialPrice.php | 4 +-- app/Services/Admin/Manage/CuttingService.php | 8 +++--- .../factories/RawMaterialPriceFactory.php | 2 +- ...ck_column_in_raw_material_prices_table.php | 28 +++++++++++++++++++ ...sage_column_in_cutting_materials_table.php | 28 +++++++++++++++++++ .../js/pages/admin/manage/cutting/create.tsx | 26 +++++++++-------- .../js/pages/admin/manage/cutting/edit.tsx | 26 +++++++++-------- .../admin/master/raw-material/create.tsx | 21 +++++++------- .../pages/admin/master/raw-material/edit.tsx | 25 ++++++++--------- .../master/raw-material/variant/edit.tsx | 11 ++++---- 15 files changed, 129 insertions(+), 68 deletions(-) create mode 100644 database/migrations/2026_08_15_192608_change_stock_column_in_raw_material_prices_table.php create mode 100644 database/migrations/2026_08_15_194830_change_material_usage_column_in_cutting_materials_table.php diff --git a/.ai/REFERENCE.md b/.ai/REFERENCE.md index a897258..764882b 100644 --- a/.ai/REFERENCE.md +++ b/.ai/REFERENCE.md @@ -81,8 +81,8 @@ ### `raw_materials` → RawMaterial - Relations: rawMaterialPrices(HasMany→RawMaterialPrice) ### `raw_material_prices` → RawMaterialPrice -`id` `raw_material_id`(FK→raw_materials) `variant`(200) `price`(uint) `stock`(uint,default:0) `created_at` `updated_at` `deleted_at` -- Casts: price(int), stock(int) +`id` `raw_material_id`(FK→raw_materials) `variant`(200) `price`(uint) `stock`(decimal(10,2),default:0) `created_at` `updated_at` `deleted_at` +- Casts: price(int), stock(decimal:2) - GlobalScope: orderBy(variant) - Relations: rawMaterial(BelongsTo→RawMaterial,withTrashed), cuttingMaterials(HasMany→CuttingMaterial), purchaseItems(HasMany→PurchaseItem) - Accessor: photo_url → first media presigned S3 URL @@ -199,8 +199,8 @@ ### `cutting_material_combinations` → CuttingMaterialCombination - Relations: cutting(BelongsTo→Cutting), cuttingMaterials(HasMany→CuttingMaterial,combination_id), user(BelongsTo→User) ### `cutting_materials` → CuttingMaterial -`id` `user_id`(FK→users,null) `cutting_id`(FK→cuttings,null) `raw_material_price_id`(FK→raw_material_prices) `combination_id`(FK→cutting_material_combinations,null) `material_usage`(int) `material_result`(int,null) `created_at` `updated_at` `deleted_at` -- Casts: material_usage(int), material_result(int) +`id` `user_id`(FK→users,null) `cutting_id`(FK→cuttings,null) `raw_material_price_id`(FK→raw_material_prices) `combination_id`(FK→cutting_material_combinations,null) `material_usage`(decimal(10,2)) `material_result`(int,null) `created_at` `updated_at` `deleted_at` +- Casts: material_usage(decimal:2), material_result(int) - Accessor: formatted_material_result → 'X.XXX', formatted_material_usage → 'X.XXX' - Relations: cutting(BelongsTo→Cutting), rawMaterialPrice(BelongsTo→RawMaterialPrice), combination(BelongsTo→CuttingMaterialCombination), user(BelongsTo→User) diff --git a/app/Http/Requests/Admin/Manage/CuttingRequest.php b/app/Http/Requests/Admin/Manage/CuttingRequest.php index 5d2dd64..e61db0b 100644 --- a/app/Http/Requests/Admin/Manage/CuttingRequest.php +++ b/app/Http/Requests/Admin/Manage/CuttingRequest.php @@ -23,7 +23,7 @@ public function rules(): array 'cutting_result' => ['required', 'integer', 'min:1'], 'materials' => ['required', 'array', 'min:1'], 'materials.*.raw_material_price_id' => ['required', 'integer', Rule::exists('raw_material_prices', 'id')], - 'materials.*.material_usage' => ['required', 'integer', 'min:1'], + 'materials.*.material_usage' => ['required', 'numeric', 'min:0.01', 'decimal:0,2'], 'materials.*.material_result' => ['required', 'integer'], 'materials.*.combination_index' => ['nullable', 'integer'], 'combinations' => ['nullable', 'array'], diff --git a/app/Http/Requests/Admin/Master/RawMaterial/RawMaterialRequest.php b/app/Http/Requests/Admin/Master/RawMaterial/RawMaterialRequest.php index 9b60a84..1f76159 100644 --- a/app/Http/Requests/Admin/Master/RawMaterial/RawMaterialRequest.php +++ b/app/Http/Requests/Admin/Master/RawMaterial/RawMaterialRequest.php @@ -32,7 +32,7 @@ public function rules(): array 'variants.*.id' => ['nullable', 'integer'], 'variants.*.variant' => ['required', 'string', 'max:200'], 'variants.*.price' => ['required', 'integer', 'min:0'], - 'variants.*.stock' => ['required', 'integer', 'min:0'], + 'variants.*.stock' => ['required', 'numeric', 'min:0', 'decimal:0,2'], '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 c247381..2b000af 100644 --- a/app/Http/Requests/Admin/Master/RawMaterial/RawMaterialVariantRequest.php +++ b/app/Http/Requests/Admin/Master/RawMaterial/RawMaterialVariantRequest.php @@ -25,7 +25,7 @@ public function rules(): array return [ 'variant' => ['required', 'string', 'max:200'], 'price' => ['required', 'integer', 'min:0'], - 'stock' => ['required', 'integer', 'min:0'], + 'stock' => ['required', 'numeric', 'min:0', 'decimal:0,2'], 'photo_key' => ['required', 'string', 'max:500'], ]; } diff --git a/app/Models/CuttingMaterial.php b/app/Models/CuttingMaterial.php index 69ad875..afc647e 100644 --- a/app/Models/CuttingMaterial.php +++ b/app/Models/CuttingMaterial.php @@ -19,7 +19,7 @@ class CuttingMaterial extends Model protected function casts(): array { return [ - 'material_usage' => 'integer', + 'material_usage' => 'decimal:2', 'material_result' => 'integer', ]; } @@ -34,7 +34,7 @@ protected function formattedMaterialResult(): Attribute protected function formattedMaterialUsage(): Attribute { return Attribute::make( - get: fn () => number_format($this->material_usage, 0, ',', '.'), + get: fn () => number_format((float) $this->material_usage, 2, ',', '.'), ); } diff --git a/app/Models/RawMaterialPrice.php b/app/Models/RawMaterialPrice.php index e573ded..d1c17c1 100644 --- a/app/Models/RawMaterialPrice.php +++ b/app/Models/RawMaterialPrice.php @@ -29,7 +29,7 @@ protected function casts(): array { return [ 'price' => 'integer', - 'stock' => 'integer', + 'stock' => 'decimal:2', ]; } @@ -43,7 +43,7 @@ protected function formattedPrice(): Attribute protected function formattedStock(): Attribute { return Attribute::make( - get: fn () => number_format($this->stock, 0, ',', '.'), + get: fn () => number_format((float) $this->stock, 2, ',', '.'), ); } diff --git a/app/Services/Admin/Manage/CuttingService.php b/app/Services/Admin/Manage/CuttingService.php index 63d9d26..0df9cdf 100644 --- a/app/Services/Admin/Manage/CuttingService.php +++ b/app/Services/Admin/Manage/CuttingService.php @@ -259,7 +259,7 @@ public function store(array $data): Cutting $pricesMap = RawMaterialPrice::whereIn('id', $priceIds)->get()->keyBy('id'); foreach ($data['materials'] as $materialData) { - $usage = (int) ($materialData['material_usage'] ?? 0); + $usage = (float) ($materialData['material_usage'] ?? 0); if ($usage <= 0) { continue; } @@ -320,7 +320,7 @@ public function store(array $data): Cutting 'updated_at' => $now, ]); - $usage = (int) ($materialData['material_usage'] ?? 0); + $usage = (float) ($materialData['material_usage'] ?? 0); if ($price && $usage > 0) { $stockDecrementMap[$materialData['raw_material_price_id']] = ($stockDecrementMap[$materialData['raw_material_price_id']] ?? 0) + $usage; } @@ -393,7 +393,7 @@ public function update(Cutting $cutting, array $data): Cutting $pricesMap = RawMaterialPrice::whereIn('id', $priceIds)->get()->keyBy('id'); foreach ($data['materials'] as $materialData) { - $usage = (int) ($materialData['material_usage'] ?? 0); + $usage = (float) ($materialData['material_usage'] ?? 0); if ($usage <= 0) { continue; } @@ -452,7 +452,7 @@ public function update(Cutting $cutting, array $data): Cutting 'updated_at' => $now, ]); - $usage = (int) ($materialData['material_usage'] ?? 0); + $usage = (float) ($materialData['material_usage'] ?? 0); if ($price && $usage > 0) { $stockDecrementMap[$materialData['raw_material_price_id']] = ($stockDecrementMap[$materialData['raw_material_price_id']] ?? 0) + $usage; } diff --git a/database/factories/RawMaterialPriceFactory.php b/database/factories/RawMaterialPriceFactory.php index 1fc6cfd..50702d0 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()->numberBetween(0, 1000), + 'stock' => round(fake()->randomFloat(2, 0, 1000), 2), ]; } } diff --git a/database/migrations/2026_08_15_192608_change_stock_column_in_raw_material_prices_table.php b/database/migrations/2026_08_15_192608_change_stock_column_in_raw_material_prices_table.php new file mode 100644 index 0000000..13ca0da --- /dev/null +++ b/database/migrations/2026_08_15_192608_change_stock_column_in_raw_material_prices_table.php @@ -0,0 +1,28 @@ +decimal('stock', 10, 2)->default(0)->change(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table('raw_material_prices', function (Blueprint $table) { + $table->unsignedInteger('stock')->default(0)->change(); + }); + } +}; diff --git a/database/migrations/2026_08_15_194830_change_material_usage_column_in_cutting_materials_table.php b/database/migrations/2026_08_15_194830_change_material_usage_column_in_cutting_materials_table.php new file mode 100644 index 0000000..58bb3bc --- /dev/null +++ b/database/migrations/2026_08_15_194830_change_material_usage_column_in_cutting_materials_table.php @@ -0,0 +1,28 @@ +decimal('material_usage', 10, 2)->change(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table('cutting_materials', function (Blueprint $table) { + $table->integer('material_usage')->change(); + }); + } +}; diff --git a/resources/js/pages/admin/manage/cutting/create.tsx b/resources/js/pages/admin/manage/cutting/create.tsx index be86706..c9f9853 100644 --- a/resources/js/pages/admin/manage/cutting/create.tsx +++ b/resources/js/pages/admin/manage/cutting/create.tsx @@ -27,7 +27,7 @@ import type { CuttingCreateData } from './columns'; type MaterialState = { raw_material_price_id: number; - material_usage: number; + material_usage: string; material_result: number; combination_id: number | null; variant: string; @@ -54,7 +54,7 @@ export default function CuttingCreate({ rawMaterials }: Props) { if (draft?.materials && draft.materials.length > 0) { return draft.materials.map((m) => ({ raw_material_price_id: m.raw_material_price_id, - material_usage: m.material_usage, + material_usage: String(m.material_usage), material_result: m.material_result ?? 0, combination_id: m.combination_index ?? null, variant: m.variant, @@ -185,7 +185,7 @@ export default function CuttingCreate({ rawMaterials }: Props) { ...prev, { raw_material_price_id: price.id, - material_usage: 0, + material_usage: '0', material_result: 0, combination_id: null, variant: price.variant, @@ -235,7 +235,7 @@ export default function CuttingCreate({ rawMaterials }: Props) { return { raw_material_price_id: priceId, - material_usage: 0, + material_usage: '0', material_result: 0, combination_id: comboIndex, variant: foundPrice?.variant ?? '', @@ -284,7 +284,7 @@ export default function CuttingCreate({ rawMaterials }: Props) { return materials.reduce((sum, m) => { const price = priceMap.get(m.raw_material_price_id); - return sum + (price ? price.price * m.material_usage : 0); + return sum + (price ? price.price * (parseFloat(String(m.material_usage)) || 0) : 0); }, 0); }, [materials, priceMap]); @@ -304,7 +304,7 @@ export default function CuttingCreate({ rawMaterials }: Props) { cutting_result: cuttingResult, materials: materialsRef.current.map((m) => ({ raw_material_price_id: m.raw_material_price_id, - material_usage: m.material_usage, + material_usage: parseFloat(String(m.material_usage)) || 0, material_result: m.material_result, combination_index: m.combination_id, })), @@ -646,11 +646,13 @@ export default function CuttingCreate({ rawMaterials }: Props) {
Pemakaian * - updateMaterial(index, 'material_usage', val)} - /> + updateMaterial(index, 'material_usage', e.target.value)} + />
{group.comboIndex === null && ( @@ -675,7 +677,7 @@ export default function CuttingCreate({ rawMaterials }: Props) {
Total Pemakaian - {formatQuantity(materials.reduce((sum, m) => sum + m.material_usage, 0))} + {formatQuantity(materials.reduce((sum, m) => sum + (parseFloat(String(m.material_usage)) || 0), 0))}
diff --git a/resources/js/pages/admin/manage/cutting/edit.tsx b/resources/js/pages/admin/manage/cutting/edit.tsx index c79f81b..1f8315d 100644 --- a/resources/js/pages/admin/manage/cutting/edit.tsx +++ b/resources/js/pages/admin/manage/cutting/edit.tsx @@ -26,7 +26,7 @@ import type { CuttingCreateData, CuttingForEdit } from './columns'; type MaterialState = { id?: number; raw_material_price_id: number; - material_usage: number; + material_usage: string; material_result: number; combination_id: number | null; variant: string; @@ -63,7 +63,7 @@ export default function CuttingEdit({ cutting, rawMaterials }: Props) { return { id: m.id, raw_material_price_id: m.raw_material_price_id, - material_usage: m.material_usage, + material_usage: String(m.material_usage), material_result: m.material_result ?? 0, combination_id: m.combination_id !== null @@ -168,7 +168,7 @@ export default function CuttingEdit({ cutting, rawMaterials }: Props) { ...prev, { raw_material_price_id: price.id, - material_usage: 0, + material_usage: '0', material_result: 0, combination_id: null, variant: price.variant, @@ -218,7 +218,7 @@ export default function CuttingEdit({ cutting, rawMaterials }: Props) { return { raw_material_price_id: priceId, - material_usage: 0, + material_usage: '0', material_result: 0, combination_id: comboIndex, variant: foundPrice?.variant ?? '', @@ -262,7 +262,7 @@ export default function CuttingEdit({ cutting, rawMaterials }: Props) { return materials.reduce((sum, m) => { const price = priceMap.get(m.raw_material_price_id); - return sum + (price ? price.price * m.material_usage : 0); + return sum + (price ? price.price * (parseFloat(String(m.material_usage)) || 0) : 0); }, 0); }, [materials, priceMap]); @@ -282,7 +282,7 @@ export default function CuttingEdit({ cutting, rawMaterials }: Props) { cutting_result: cuttingResult, materials: materialsRef.current.map((m) => ({ raw_material_price_id: m.raw_material_price_id, - material_usage: m.material_usage, + material_usage: parseFloat(String(m.material_usage)) || 0, material_result: m.material_result, combination_index: m.combination_id, })), @@ -624,11 +624,13 @@ export default function CuttingEdit({ cutting, rawMaterials }: Props) {
Pemakaian * - updateMaterial(index, 'material_usage', val)} - /> + updateMaterial(index, 'material_usage', e.target.value)} + />
{group.comboIndex === null && ( @@ -653,7 +655,7 @@ export default function CuttingEdit({ cutting, rawMaterials }: Props) {
Total Pemakaian - {formatQuantity(materials.reduce((sum, m) => sum + m.material_usage, 0))} + {formatQuantity(materials.reduce((sum, m) => sum + (parseFloat(String(m.material_usage)) || 0), 0))}
diff --git a/resources/js/pages/admin/master/raw-material/create.tsx b/resources/js/pages/admin/master/raw-material/create.tsx index 711f5a4..ac0ba1a 100644 --- a/resources/js/pages/admin/master/raw-material/create.tsx +++ b/resources/js/pages/admin/master/raw-material/create.tsx @@ -12,7 +12,6 @@ import { toast } from 'sonner'; import { ConfirmDialog } from '@/components/dialogs'; import { FileUpload } from '@/components/inputs'; import { InputError } from '@/components/ui'; -import { NumberInput } from '@/components/inputs'; import { RupiahInput } from '@/components/inputs'; import { Button } from '@/components/ui/button'; import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; @@ -31,7 +30,7 @@ import { type VariantState = { variant: string; price: number; - stock: number; + stock: string; photo: string | null; photoUrl: string | null; uploading: boolean; @@ -50,7 +49,7 @@ export default function RawMaterialCreate() { return draft.variants.map((v) => ({ variant: v.variant, price: v.price, - stock: v.stock, + stock: String(v.stock), photo: v.photo ?? null, photoUrl: v.photo ? getTemporaryUrl(v.photo) : null, uploading: false, @@ -61,7 +60,7 @@ export default function RawMaterialCreate() { { variant: '', price: 0, - stock: 0, + stock: '0', photo: null, photoUrl: null, uploading: false, @@ -92,7 +91,7 @@ export default function RawMaterialCreate() { { variant: '', price: 0, - stock: 0, + stock: '0', photo: null, photoUrl: null, uploading: false, @@ -178,7 +177,7 @@ export default function RawMaterialCreate() { variants: variantsRef.current.map((v) => ({ variant: v.variant, price: Number(v.price), - stock: Number(v.stock), + stock: parseFloat(String(v.stock)) || 0, photo_key: v.photo, })), }; @@ -422,17 +421,19 @@ export default function RawMaterialCreate() { * - updateVariant( variantIndex, 'stock', - val, + e.target.value, ) } /> diff --git a/resources/js/pages/admin/master/raw-material/edit.tsx b/resources/js/pages/admin/master/raw-material/edit.tsx index 9b59718..1caca19 100644 --- a/resources/js/pages/admin/master/raw-material/edit.tsx +++ b/resources/js/pages/admin/master/raw-material/edit.tsx @@ -12,7 +12,6 @@ import { toast } from 'sonner'; import { ConfirmDialog } from '@/components/dialogs'; import { FileUpload } from '@/components/inputs'; import { InputError } from '@/components/ui'; -import { NumberInput } from '@/components/inputs'; import { RupiahInput } from '@/components/inputs'; import { Button } from '@/components/ui/button'; import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; @@ -37,7 +36,7 @@ type VariantState = { id: number | null; variant: string; price: number; - stock: number; + stock: string; photo: string | null; photoUrl: string | null; uploading: boolean; @@ -58,7 +57,7 @@ export default function RawMaterialEdit({ rawMaterial }: Props) { id: v.id, variant: v.variant, price: v.price, - stock: v.stock, + stock: String(v.stock), photo: v.photo_key, photoUrl: v.photo_url, uploading: false, @@ -73,7 +72,7 @@ export default function RawMaterialEdit({ rawMaterial }: Props) { id: null, variant: '', price: 0, - stock: 0, + stock: '0', photo: null, photoUrl: null, uploading: false, @@ -106,7 +105,7 @@ export default function RawMaterialEdit({ rawMaterial }: Props) { id: null, variant: '', price: 0, - stock: 0, + stock: '0', photo: null, photoUrl: null, uploading: false, @@ -193,7 +192,7 @@ export default function RawMaterialEdit({ rawMaterial }: Props) { id: v.id, variant: v.variant, price: Number(v.price), - stock: Number(v.stock), + stock: parseFloat(String(v.stock)) || 0, photo_key: v.photo, })), }; @@ -438,19 +437,19 @@ export default function RawMaterialEdit({ rawMaterial }: Props) { * - updateVariant( variantIndex, 'stock', - val, + e.target.value, ) } /> diff --git a/resources/js/pages/admin/master/raw-material/variant/edit.tsx b/resources/js/pages/admin/master/raw-material/variant/edit.tsx index addb2a1..f5283cd 100644 --- a/resources/js/pages/admin/master/raw-material/variant/edit.tsx +++ b/resources/js/pages/admin/master/raw-material/variant/edit.tsx @@ -4,7 +4,6 @@ import { useState } from 'react'; import { toast } from 'sonner'; import { FileUpload } from '@/components/inputs'; import { InputError } from '@/components/ui'; -import { NumberInput } from '@/components/inputs'; import { RupiahInput } from '@/components/inputs'; import { Button } from '@/components/ui/button'; import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; @@ -28,7 +27,7 @@ type Props = { export default function RawMaterialVariantEdit({ variant }: Props) { const [variantName, setVariantName] = useState(variant.variant); const [price, setPrice] = useState(variant.price); - const [stock, setStock] = useState(Number(variant.stock) || 0); + const [stock, setStock] = useState(String(variant.stock) || '0'); const [photo, setPhoto] = useState(variant.photo_key); const [photoUrl, setPhotoUrl] = useState(variant.photo_url); const [uploading, setUploading] = useState(false); @@ -37,7 +36,7 @@ export default function RawMaterialVariantEdit({ variant }: Props) { return { variant: variantName, price: Number(price), - stock: Number(stock), + stock: parseFloat(String(stock)) || 0, photo_key: photo, }; } @@ -101,9 +100,11 @@ export default function RawMaterialVariantEdit({ variant }: Props) { - setStock(e.target.value)} />