From 15e2b7ec5b326089e1c81dcbc2d0dc9ddc6b939d Mon Sep 17 00:00:00 2001 From: Yoga Pangestu Date: Sat, 4 Jul 2026 23:14:37 +0700 Subject: [PATCH] feat: add material_result field to CuttingMaterial and CuttingMaterialCombination; update validation rules and service logic to handle new material result calculations; enhance frontend components for displaying material results in cutting management --- .../Manage/CuttingDraftCombinationRequest.php | 1 + .../Manage/CuttingDraftMaterialRequest.php | 1 + .../Requests/Admin/Manage/CuttingRequest.php | 6 +- app/Models/Cutting.php | 5 + app/Models/CuttingMaterial.php | 1 + app/Models/CuttingMaterialCombination.php | 7 + app/Services/Manage/CuttingService.php | 64 ++++- ...rial_result_to_cutting_materials_table.php | 28 +++ ..._to_integer_in_cutting_materials_table.php | 28 +++ ...to_cutting_material_combinations_table.php | 28 +++ .../js/components/data-table/DataTable.vue | 2 +- resources/js/composables/useCan.ts | 1 + resources/js/composables/useDestroy.ts | 2 + .../js/composables/usePaginationSummary.ts | 3 +- resources/js/constants/cutting-status.ts | 2 +- resources/js/constants/order-status.ts | 2 +- resources/js/layouts/SettingLayout.vue | 5 +- resources/js/pages/admin/Dashboard.vue | 4 + resources/js/pages/admin/TestChart.vue | 6 +- .../js/pages/admin/account/Appearance.vue | 2 +- .../finance/cash/table/data-table-actions.vue | 2 +- .../admin/finance/employee-advances/Index.vue | 8 +- .../js/pages/admin/finance/expenses/Index.vue | 6 +- .../expenses/table/data-table-actions.vue | 2 +- .../js/pages/admin/finance/payroll/Index.vue | 2 +- .../attendances/table/AttendanceCalendar.vue | 5 +- .../attendances/table/data-table-actions.vue | 2 +- .../js/pages/admin/hr/employees/Create.vue | 6 +- .../js/pages/admin/hr/employees/Edit.vue | 10 +- .../js/pages/admin/manage/cuttings/Create.vue | 4 +- .../js/pages/admin/manage/cuttings/Edit.vue | 26 +- .../js/pages/admin/manage/cuttings/Index.vue | 6 +- .../js/pages/admin/manage/cuttings/Share.vue | 28 ++- .../form/CuttingPosCombinationDialog.vue | 80 +++--- .../manage/cuttings/form/CuttingPosForm.vue | 34 ++- .../form/CuttingPosMaterialSummaryItems.vue | 139 ++++++----- .../form/CuttingPosResultCatalogPanel.vue | 4 +- .../cuttings/form/CuttingPosSummaryPanel.vue | 93 +++---- .../manage/cuttings/form/useCuttingPosCart.ts | 18 +- .../table/CuttingCompletedSection.vue | 9 +- .../cuttings/table/CuttingGroupedTable.vue | 230 ++++++------------ .../table/CuttingInProgressSection.vue | 7 + .../js/pages/admin/manage/orders/Create.vue | 6 +- .../js/pages/admin/manage/orders/Edit.vue | 6 +- .../js/pages/admin/manage/orders/Index.vue | 10 +- .../js/pages/admin/manage/orders/Show.vue | 2 +- .../orders/form/CustomerQuickCreateModal.vue | 6 +- .../manage/orders/form/useOrderPosCart.ts | 3 +- .../manage/orders/table/OrderGroupedTable.vue | 2 +- .../pages/admin/manage/purchases/Create.vue | 4 +- .../js/pages/admin/manage/purchases/Edit.vue | 6 +- .../js/pages/admin/manage/purchases/Index.vue | 6 +- .../form/PurchasePosCheckoutSection.vue | 5 +- .../form/PurchasePosMetadataFields.vue | 3 +- .../purchases/form/usePurchasePosCart.ts | 3 +- .../table/StockPendingApprovalSection.vue | 2 +- .../admin/manage/stok-opnames/Create.vue | 4 +- .../pages/admin/manage/stok-opnames/Edit.vue | 8 +- .../pages/admin/manage/stok-opnames/Index.vue | 21 +- .../stok-opnames/form/useStokOpnameForm.ts | 3 +- .../table/StokOpnameGroupedTable.vue | 4 +- .../stok-opnames/table/data-table-actions.vue | 2 +- .../pages/admin/master/categories/Index.vue | 6 +- .../categories/table/data-table-actions.vue | 2 +- .../js/pages/admin/master/customers/Index.vue | 6 +- .../customers/table/data-table-actions.vue | 2 +- .../js/pages/admin/master/products/Create.vue | 6 +- .../js/pages/admin/master/products/Edit.vue | 8 +- .../js/pages/admin/master/products/Index.vue | 6 +- .../master/products/form/ProductForm.vue | 8 +- .../admin/master/raw-materials/Create.vue | 4 +- .../pages/admin/master/raw-materials/Edit.vue | 8 +- .../admin/master/raw-materials/Index.vue | 6 +- .../form/RawMaterialInfoSection.vue | 5 +- .../form/RawMaterialSharedPriceSection.vue | 3 +- .../table/data-table-actions.vue | 2 +- .../js/pages/admin/master/suppliers/Index.vue | 6 +- .../suppliers/table/data-table-actions.vue | 2 +- .../admin/system/activity-logs/Index.vue | 2 +- .../js/pages/admin/system/roles/Create.vue | 4 +- .../js/pages/admin/system/roles/Edit.vue | 4 +- .../js/pages/admin/system/roles/Index.vue | 6 +- .../system/roles/table/data-table-actions.vue | 2 +- .../js/pages/admin/system/setting/Index.vue | 2 +- resources/js/types/cutting.ts | 8 +- resources/js/types/order.ts | 2 +- resources/js/types/purchase.ts | 2 +- tests/Feature/Admin/Manage/CuttingTest.php | 183 +++++++++++++- 88 files changed, 824 insertions(+), 496 deletions(-) create mode 100644 database/migrations/2026_07_04_215607_add_material_result_to_cutting_materials_table.php create mode 100644 database/migrations/2026_07_04_215831_change_material_result_to_integer_in_cutting_materials_table.php create mode 100644 database/migrations/2026_07_04_220536_add_material_result_to_cutting_material_combinations_table.php diff --git a/app/Http/Requests/Admin/Manage/CuttingDraftCombinationRequest.php b/app/Http/Requests/Admin/Manage/CuttingDraftCombinationRequest.php index 83fcdb1..4681968 100644 --- a/app/Http/Requests/Admin/Manage/CuttingDraftCombinationRequest.php +++ b/app/Http/Requests/Admin/Manage/CuttingDraftCombinationRequest.php @@ -26,6 +26,7 @@ public function rules(): array Rule::exists('raw_material_prices', 'id')->whereNull('deleted_at'), ], 'materials.*.material_usage' => ['required', 'numeric', 'decimal:0,4', 'gt:0'], + 'material_result' => ['nullable', 'integer', 'gte:0'], ]; } } diff --git a/app/Http/Requests/Admin/Manage/CuttingDraftMaterialRequest.php b/app/Http/Requests/Admin/Manage/CuttingDraftMaterialRequest.php index b295e52..be50f10 100644 --- a/app/Http/Requests/Admin/Manage/CuttingDraftMaterialRequest.php +++ b/app/Http/Requests/Admin/Manage/CuttingDraftMaterialRequest.php @@ -25,6 +25,7 @@ public function rules(): array Rule::exists('raw_material_prices', 'id')->whereNull('deleted_at'), ], 'material_usage' => ['required', 'numeric', 'decimal:0,4', 'gt:0'], + 'material_result' => ['nullable', 'integer', 'gte:0'], ]; } } diff --git a/app/Http/Requests/Admin/Manage/CuttingRequest.php b/app/Http/Requests/Admin/Manage/CuttingRequest.php index 72702cd..03f3604 100644 --- a/app/Http/Requests/Admin/Manage/CuttingRequest.php +++ b/app/Http/Requests/Admin/Manage/CuttingRequest.php @@ -42,7 +42,9 @@ public function rules(): array Rule::exists('raw_material_prices', 'id')->whereNull('deleted_at'), ]; $rules['materials.*.material_usage'] = ['required', 'numeric', 'decimal:0,4', 'gt:0']; - $rules['materials.*.combination_id'] = ['nullable', 'integer', 'exists:cutting_material_combinations,id']; + $rules['materials.*.material_result'] = ['nullable', 'integer', 'min:0']; + $rules['materials.*.combination_id'] = ['nullable', 'integer']; + $rules['materials.*.combination_material_result'] = ['nullable', 'integer', 'min:0']; $rules['results'] = ['required', 'array', 'min:1']; $rules['results.*.product_variant_id'] = [ @@ -73,6 +75,8 @@ public function attributes(): array 'materials' => 'Bahan Baku', 'materials.*.raw_material_price_id' => 'Bahan Baku', 'materials.*.material_usage' => 'Pemakaian', + 'materials.*.material_result' => 'Hasil', + 'materials.*.combination_material_result' => 'Hasil Kombinasi', 'results' => 'Hasil Produk', 'results.*.product_variant_id' => 'Varian Produk', 'results.*.cutting_result' => 'Hasil', diff --git a/app/Models/Cutting.php b/app/Models/Cutting.php index 7c4258d..89c628c 100644 --- a/app/Models/Cutting.php +++ b/app/Models/Cutting.php @@ -113,6 +113,11 @@ public function materials(): HasMany return $this->hasMany(CuttingMaterial::class); } + public function combinations(): HasMany + { + return $this->hasMany(CuttingMaterialCombination::class); + } + public function resultPrices(): HasMany { return $this->hasMany(CuttingResultPrice::class); diff --git a/app/Models/CuttingMaterial.php b/app/Models/CuttingMaterial.php index a9de9d5..d3f0275 100644 --- a/app/Models/CuttingMaterial.php +++ b/app/Models/CuttingMaterial.php @@ -27,6 +27,7 @@ protected function casts(): array { return [ 'material_usage' => 'decimal:4', + 'material_result' => 'integer', ]; } diff --git a/app/Models/CuttingMaterialCombination.php b/app/Models/CuttingMaterialCombination.php index 4b03cf3..201b7e0 100644 --- a/app/Models/CuttingMaterialCombination.php +++ b/app/Models/CuttingMaterialCombination.php @@ -15,6 +15,13 @@ class CuttingMaterialCombination extends Model { use HasFactory, InteractsWithActivityLog, SoftDeletes; + protected function casts(): array + { + return [ + 'material_result' => 'integer', + ]; + } + public function cutting(): BelongsTo { return $this->belongsTo(Cutting::class); diff --git a/app/Services/Manage/CuttingService.php b/app/Services/Manage/CuttingService.php index 04d8c5c..c9360d5 100644 --- a/app/Services/Manage/CuttingService.php +++ b/app/Services/Manage/CuttingService.php @@ -301,6 +301,10 @@ public function syncDraftMaterial(array $validated, User $user): array ]); } + $materialResult = array_key_exists('material_result', $validated) && $validated['material_result'] !== null + ? (int) $validated['material_result'] + : null; + $item = CuttingMaterial::query()->updateOrCreate( [ 'user_id' => $user->id, @@ -309,6 +313,7 @@ public function syncDraftMaterial(array $validated, User $user): array ], [ 'material_usage' => $materialUsage, + 'material_result' => $materialResult, ], ); @@ -409,9 +414,14 @@ public function removeDraftResult(User $user, ProductVariant $productVariant): v */ public function syncDraftCombination(array $validated, User $user): array { + $materialResult = array_key_exists('material_result', $validated) && $validated['material_result'] !== null + ? (int) $validated['material_result'] + : null; + $combination = CuttingMaterialCombination::create([ 'user_id' => $user->id, 'cutting_id' => null, + 'material_result' => $materialResult, ]); $items = []; @@ -558,7 +568,7 @@ public function update(Cutting $cutting, array $validated): void $this->runInTransaction( function () use ($cutting, $validated): void { - $cutting->load(['materials.rawMaterialPrice.rawMaterial', 'results']); + $cutting->load(['materials.rawMaterialPrice.rawMaterial', 'results', 'materials.combination']); if ($cutting->status === CuttingStatus::IN_PROGRESS) { $this->reverseTotalMaterialStock($cutting); @@ -568,6 +578,7 @@ function () use ($cutting, $validated): void { $cutting->materials()->delete(); $cutting->results()->delete(); + $cutting->combinations()->delete(); $materials = $this->buildMaterials($validated['materials']); $results = $this->buildResults($validated['results']); @@ -583,8 +594,41 @@ function () use ($cutting, $validated): void { $this->syncImages($cutting, $validated); + // Group materials by combination_id to create combinations + $combinationGroups = []; foreach ($materials as $materialData) { - $cutting->materials()->create($materialData); + $combinationId = $materialData['combination_id']; + if ($combinationId !== null) { + if (!isset($combinationGroups[$combinationId])) { + $combinationGroups[$combinationId] = [ + 'materials' => [], + 'material_result' => $materialData['combination_material_result'] ?? null, + ]; + } + $combinationGroups[$combinationId]['materials'][] = $materialData; + } + } + + // Create combinations and update combination_id for materials + $combinationIdMap = []; + foreach ($combinationGroups as $oldCombinationId => $group) { + $combination = $cutting->combinations()->create([ + 'material_result' => $group['material_result'], + ]); + $combinationIdMap[$oldCombinationId] = $combination->id; + } + + foreach ($materials as $materialData) { + $newCombinationId = $materialData['combination_id'] !== null + ? $combinationIdMap[$materialData['combination_id']] ?? null + : null; + + $cutting->materials()->create([ + 'raw_material_price_id' => $materialData['raw_material_price_id'], + 'material_usage' => $materialData['material_usage'], + 'material_result' => $materialData['material_result'], + 'combination_id' => $newCombinationId, + ]); } foreach ($results as $resultData) { @@ -756,10 +800,16 @@ private function buildMaterials(array $materials): array ]); } + $materialResult = array_key_exists('material_result', $itemData) && $itemData['material_result'] !== null + ? (int) $itemData['material_result'] + : null; + return [ 'raw_material_price_id' => $price->id, 'material_usage' => $materialUsage, + 'material_result' => $materialResult, 'combination_id' => $itemData['combination_id'] ?? null, + 'combination_material_result' => $itemData['combination_material_result'] ?? null, ]; }) ->all(); @@ -909,6 +959,12 @@ private function breakMaterialCircularReference(CuttingMaterial $material): void { $price = $material->rawMaterialPrice; + // Always set these attributes regardless of price + $material->setAttribute('material_result', $material->material_result); + $material->setAttribute('material_result_input', $material->material_result); + $material->setAttribute('combination_id', $material->combination_id); + $material->setAttribute('combination_material_result', $material->combination?->material_result); + if ($price) { $rawMaterial = $price->rawMaterial; @@ -929,7 +985,6 @@ private function breakMaterialCircularReference(CuttingMaterial $material): void $price->unsetRelation('rawMaterial'); } - $material->setAttribute('combination_id', $material->combination_id); $material->unsetRelation('rawMaterialPrice'); $material->unsetRelation('combination'); } @@ -963,6 +1018,7 @@ private function presentDraftMaterial(CuttingMaterial $item): array { $price = $item->rawMaterialPrice; $rawMaterial = $price?->rawMaterial; + $combination = $item->combination; return [ 'raw_material_price_id' => $item->raw_material_price_id, @@ -972,8 +1028,10 @@ private function presentDraftMaterial(CuttingMaterial $item): array 'unit_abbreviation' => $rawMaterial?->unit?->abbreviation() ?? '', 'stock_input' => $price?->stock_input ?? '', 'material_usage' => $this->formatQuantityInput((float) $item->material_usage), + 'material_result' => $item->material_result !== null ? (int) $item->material_result : null, 'images' => $price ? MediaPresenter::collection($price, 'images') : [], 'combination_id' => $item->combination_id, + 'combination_material_result' => $combination?->material_result, ]; } diff --git a/database/migrations/2026_07_04_215607_add_material_result_to_cutting_materials_table.php b/database/migrations/2026_07_04_215607_add_material_result_to_cutting_materials_table.php new file mode 100644 index 0000000..327dca5 --- /dev/null +++ b/database/migrations/2026_07_04_215607_add_material_result_to_cutting_materials_table.php @@ -0,0 +1,28 @@ +decimal('material_result', 18, 2)->nullable()->after('material_usage'); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table('cutting_materials', function (Blueprint $table) { + $table->dropColumn('material_result'); + }); + } +}; diff --git a/database/migrations/2026_07_04_215831_change_material_result_to_integer_in_cutting_materials_table.php b/database/migrations/2026_07_04_215831_change_material_result_to_integer_in_cutting_materials_table.php new file mode 100644 index 0000000..e1ec179 --- /dev/null +++ b/database/migrations/2026_07_04_215831_change_material_result_to_integer_in_cutting_materials_table.php @@ -0,0 +1,28 @@ +integer('material_result')->nullable()->change(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table('cutting_materials', function (Blueprint $table) { + $table->decimal('material_result', 18, 2)->nullable()->change(); + }); + } +}; diff --git a/database/migrations/2026_07_04_220536_add_material_result_to_cutting_material_combinations_table.php b/database/migrations/2026_07_04_220536_add_material_result_to_cutting_material_combinations_table.php new file mode 100644 index 0000000..e137c57 --- /dev/null +++ b/database/migrations/2026_07_04_220536_add_material_result_to_cutting_material_combinations_table.php @@ -0,0 +1,28 @@ +integer('material_result')->nullable()->after('cutting_id'); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table('cutting_material_combinations', function (Blueprint $table) { + $table->dropColumn('material_result'); + }); + } +}; diff --git a/resources/js/components/data-table/DataTable.vue b/resources/js/components/data-table/DataTable.vue index 17f98a5..c9021b0 100644 --- a/resources/js/components/data-table/DataTable.vue +++ b/resources/js/components/data-table/DataTable.vue @@ -4,7 +4,6 @@ import type { ColumnDef } from '@tanstack/vue-table'; import { FlexRender, getCoreRowModel, useVueTable } from '@tanstack/vue-table'; import { computed, provide } from 'vue'; import DataTableToolbar from '@/components/data-table/DataTableToolbar.vue'; -import { buildPaginationSummary } from '@/lib/grouped-table'; import { Button } from '@/components/ui/button'; import { Table, @@ -15,6 +14,7 @@ import { TableRow, } from '@/components/ui/table'; import TableEmpty from '@/components/ui/table/TableEmpty.vue'; +import { buildPaginationSummary } from '@/lib/grouped-table'; import type { DataTableFilterDef, DataTablePagination, diff --git a/resources/js/composables/useCan.ts b/resources/js/composables/useCan.ts index e42573f..6301213 100644 --- a/resources/js/composables/useCan.ts +++ b/resources/js/composables/useCan.ts @@ -11,6 +11,7 @@ export function useCan() { if (Array.isArray(permission)) { return permission.some((p) => permissions.value.includes(p)); } + return permissions.value.includes(permission); } diff --git a/resources/js/composables/useDestroy.ts b/resources/js/composables/useDestroy.ts index 494d0e8..6aefdbf 100644 --- a/resources/js/composables/useDestroy.ts +++ b/resources/js/composables/useDestroy.ts @@ -26,8 +26,10 @@ export function useDestroy({ url, preserveScroll = true, errorMessage, onSuccess onError: (errors) => { if (onError) { const result = onError(errors); + if (typeof result === 'string') { toast.error(result); + return; } } diff --git a/resources/js/composables/usePaginationSummary.ts b/resources/js/composables/usePaginationSummary.ts index 354555f..93f2944 100644 --- a/resources/js/composables/usePaginationSummary.ts +++ b/resources/js/composables/usePaginationSummary.ts @@ -1,4 +1,5 @@ -import { computed, type MaybeRefOrGetter, toValue } from 'vue'; +import { computed, toValue } from 'vue'; +import type {MaybeRefOrGetter} from 'vue'; import { buildPaginationSummary } from '@/lib/grouped-table'; import type { DataTablePagination } from '@/types/data-table'; diff --git a/resources/js/constants/cutting-status.ts b/resources/js/constants/cutting-status.ts index 890ffed..5f0320b 100644 --- a/resources/js/constants/cutting-status.ts +++ b/resources/js/constants/cutting-status.ts @@ -1,5 +1,5 @@ -import type { Component } from 'vue'; import { Check, RotateCcw, Scissors, X } from '@lucide/vue'; +import type { Component } from 'vue'; import type { BadgeVariant } from '@/lib/badge-variant'; export const CuttingStatus = { diff --git a/resources/js/constants/order-status.ts b/resources/js/constants/order-status.ts index 9f7d3a8..afcfc81 100644 --- a/resources/js/constants/order-status.ts +++ b/resources/js/constants/order-status.ts @@ -1,5 +1,5 @@ -import type { Component } from 'vue'; import { Check, Send, X } from '@lucide/vue'; +import type { Component } from 'vue'; import type { BadgeVariant } from '@/lib/badge-variant'; export const OrderStatus = { diff --git a/resources/js/layouts/SettingLayout.vue b/resources/js/layouts/SettingLayout.vue index a10bbfb..2faf278 100644 --- a/resources/js/layouts/SettingLayout.vue +++ b/resources/js/layouts/SettingLayout.vue @@ -1,8 +1,8 @@ diff --git a/resources/js/pages/admin/Dashboard.vue b/resources/js/pages/admin/Dashboard.vue index b8a26e3..4d09159 100644 --- a/resources/js/pages/admin/Dashboard.vue +++ b/resources/js/pages/admin/Dashboard.vue @@ -212,6 +212,7 @@ const donutSegmentSelector = Donut.selectors.segment; function channelTooltip(arc: any) { const d = arc.data as typeof props.orderStats.by_channel[number]; + return `

@@ -223,6 +224,7 @@ function channelTooltip(arc: any) { function paymentTooltip(arc: any) { const d = arc.data as typeof props.orderStats.by_payment_type[number]; + return `

@@ -235,6 +237,7 @@ function paymentTooltip(arc: any) { function marketingTooltip(arc: any) { const d = arc.data as typeof props.orderStats.by_marketing[number]; const idx = props.orderStats.by_marketing.indexOf(d); + return `

@@ -246,6 +249,7 @@ function marketingTooltip(arc: any) { function statusTooltip(arc: any) { const d = arc.data as typeof props.orderStats.by_status[number]; + return `

diff --git a/resources/js/pages/admin/TestChart.vue b/resources/js/pages/admin/TestChart.vue index a6eb699..b9aa160 100644 --- a/resources/js/pages/admin/TestChart.vue +++ b/resources/js/pages/admin/TestChart.vue @@ -1,7 +1,4 @@ diff --git a/resources/js/pages/admin/manage/cuttings/Index.vue b/resources/js/pages/admin/manage/cuttings/Index.vue index 25a0a09..7c1a44c 100644 --- a/resources/js/pages/admin/manage/cuttings/Index.vue +++ b/resources/js/pages/admin/manage/cuttings/Index.vue @@ -1,4 +1,6 @@