diff --git a/app/Http/Requests/Admin/Manage/StokOpnameAutoSaveRequest.php b/app/Http/Requests/Admin/Manage/StokOpnameAutoSaveRequest.php index 91927fb..dec0f09 100644 --- a/app/Http/Requests/Admin/Manage/StokOpnameAutoSaveRequest.php +++ b/app/Http/Requests/Admin/Manage/StokOpnameAutoSaveRequest.php @@ -25,6 +25,7 @@ public function rules(): array 'notes' => ['nullable', 'string', 'max:1000'], 'items' => ['nullable', 'array'], 'items.*.product_variant_id' => ['required', 'integer', 'exists:product_variants,id'], + 'items.*.stock_quality' => ['required', \Illuminate\Validation\Rule::enum(\App\Enums\ProductStockQuality::class)], 'items.*.physical_stock' => ['nullable', 'integer', 'min:0'], 'items.*.notes' => ['nullable', 'string', 'max:500'], ]; diff --git a/app/Http/Requests/Admin/Manage/StokOpnameRequest.php b/app/Http/Requests/Admin/Manage/StokOpnameRequest.php index 01b6cfa..b3cef96 100644 --- a/app/Http/Requests/Admin/Manage/StokOpnameRequest.php +++ b/app/Http/Requests/Admin/Manage/StokOpnameRequest.php @@ -23,6 +23,7 @@ public function rules(): array 'notes' => ['nullable', 'string', 'max:1000'], 'items' => ['required', 'array', 'min:1'], 'items.*.product_variant_id' => ['required', 'integer', 'exists:product_variants,id'], + 'items.*.stock_quality' => ['required', \Illuminate\Validation\Rule::enum(\App\Enums\ProductStockQuality::class)], 'items.*.physical_stock' => ['required', 'integer', 'min:0'], 'items.*.notes' => ['nullable', 'string', 'max:500'], ]; diff --git a/app/Models/StokOpnameItem.php b/app/Models/StokOpnameItem.php index 6011946..6cedea3 100644 --- a/app/Models/StokOpnameItem.php +++ b/app/Models/StokOpnameItem.php @@ -2,6 +2,7 @@ namespace App\Models; +use App\Enums\ProductStockQuality; use App\Models\Concerns\InteractsWithActivityLog; use Illuminate\Database\Eloquent\Attributes\Guarded; use Illuminate\Database\Eloquent\Factories\HasFactory; @@ -21,9 +22,23 @@ protected function casts(): array 'difference' => 'integer', 'physical_stock' => 'integer', 'system_stock' => 'integer', + 'stock_quality' => ProductStockQuality::class, ]; } + protected $appends = ['product_name', 'variant_name']; + + public function getProductNameAttribute(): string + { + return $this->productVariant->product->name; + } + + public function getVariantNameAttribute(): string + { + $qualityLabel = $this->stock_quality ? ' (' . $this->stock_quality->label() . ')' : ''; + return $this->productVariant->name . $qualityLabel; + } + // 3. Relation public function productVariant(): BelongsTo { diff --git a/app/Services/Manage/StokOpnameService.php b/app/Services/Manage/StokOpnameService.php index 48a5ce7..fa4213b 100644 --- a/app/Services/Manage/StokOpnameService.php +++ b/app/Services/Manage/StokOpnameService.php @@ -54,7 +54,7 @@ public function catalogItems(): array ->active() ->with([ 'variants' => fn ($query) => $query - ->select('id', 'product_id', 'name', 'stock') + ->select('id', 'product_id', 'name', 'stock', 'retail_stock', 'reject_stock') ->orderBy('created_at'), ]) ->orderBy('name') @@ -67,6 +67,10 @@ public function catalogItems(): array 'name' => $variant->name, 'stock' => $variant->stock, 'stock_formatted' => $variant->stock_formatted, + 'retail_stock' => $variant->retail_stock, + 'retail_stock_formatted' => $variant->retail_stock_formatted, + 'reject_stock' => $variant->reject_stock, + 'reject_stock_formatted' => $variant->reject_stock_formatted, ]), ]) ->toArray(); @@ -90,6 +94,7 @@ public function findForEdit(StokOpname $stokOpname): array 'product_variant_id' => $item->product_variant_id, 'variant_name' => $item->productVariant->name, 'product_name' => $item->productVariant->product->name, + 'stock_quality' => $item->stock_quality->value, 'system_stock' => $item->system_stock, 'physical_stock' => $item->physical_stock, 'difference' => $item->difference, @@ -235,8 +240,13 @@ public function verify(StokOpname $stokOpname, User $user, ?string $verification DB::transaction(function () use ($stokOpname, $user, $verificationNotes): void { foreach ($stokOpname->items as $item) { if ($item->difference !== 0) { + $column = match ($item->stock_quality) { + \App\Enums\ProductStockQuality::GOOD => 'stock', + \App\Enums\ProductStockQuality::RETAIL => 'retail_stock', + \App\Enums\ProductStockQuality::REJECT => 'reject_stock', + }; $item->productVariant()->update([ - 'stock' => $item->physical_stock, + $column => $item->physical_stock, ]); } } @@ -346,10 +356,23 @@ private function syncItems(StokOpname $stokOpname, array $items): void $stokOpname->items()->delete(); foreach ($items as $item) { - $systemStock = ProductVariant::find($item['product_variant_id'])?->stock ?? 0; + $variant = ProductVariant::find($item['product_variant_id']); + if (!$variant) { + continue; + } + + $quality = \App\Enums\ProductStockQuality::from($item['stock_quality']); + $column = match ($quality) { + \App\Enums\ProductStockQuality::GOOD => 'stock', + \App\Enums\ProductStockQuality::RETAIL => 'retail_stock', + \App\Enums\ProductStockQuality::REJECT => 'reject_stock', + }; + + $systemStock = $variant->$column ?? 0; $stokOpname->items()->create([ 'product_variant_id' => $item['product_variant_id'], + 'stock_quality' => $quality->value, 'system_stock' => $systemStock, 'physical_stock' => $item['physical_stock'], 'difference' => $item['physical_stock'] - $systemStock, diff --git a/app/Services/System/AnalysisService.php b/app/Services/System/AnalysisService.php index e442184..b3add16 100644 --- a/app/Services/System/AnalysisService.php +++ b/app/Services/System/AnalysisService.php @@ -325,39 +325,39 @@ public function getMonthlyRevenue(?Carbon $startDate = null, ?Carbon $endDate = $deduction = $discount + $fees; $monthItems = $monthlyItemsData->get($key, collect()); - $gudangItem = $monthItems->first(fn ($item) => ($item->stock_quality instanceof ProductStockQuality ? $item->stock_quality->value : $item->stock_quality) === 'good' + $warehouseItem = $monthItems->first(fn ($item) => ($item->stock_quality instanceof ProductStockQuality ? $item->stock_quality->value : $item->stock_quality) === 'good' ); - $ecerItem = $monthItems->first(fn ($item) => ($item->stock_quality instanceof ProductStockQuality ? $item->stock_quality->value : $item->stock_quality) === 'retail' + $retailItem = $monthItems->first(fn ($item) => ($item->stock_quality instanceof ProductStockQuality ? $item->stock_quality->value : $item->stock_quality) === 'retail' ); $rejectItem = $monthItems->first(fn ($item) => ($item->stock_quality instanceof ProductStockQuality ? $item->stock_quality->value : $item->stock_quality) === 'reject' ); - $gudangSubtotal = (int) ($gudangItem?->subtotal ?? 0); - $gudangHpp = (int) ($gudangItem?->hpp ?? 0); - $ecerSubtotal = (int) ($ecerItem?->subtotal ?? 0); - $ecerHpp = (int) ($ecerItem?->hpp ?? 0); + $warehouseSubtotal = (int) ($warehouseItem?->subtotal ?? 0); + $warehouseHpp = (int) ($warehouseItem?->hpp ?? 0); + $retailSubtotal = (int) ($retailItem?->subtotal ?? 0); + $retailHpp = (int) ($retailItem?->hpp ?? 0); $rejectSubtotal = (int) ($rejectItem?->subtotal ?? 0); $rejectHpp = (int) ($rejectItem?->hpp ?? 0); - $totalItemsSubtotal = $gudangSubtotal + $ecerSubtotal + $rejectSubtotal; - $hpp = $gudangHpp + $ecerHpp + $rejectHpp; + $totalItemsSubtotal = $warehouseSubtotal + $retailSubtotal + $rejectSubtotal; + $hpp = $warehouseHpp + $retailHpp + $rejectHpp; - $gudangDeduction = 0; - $ecerDeduction = 0; + $warehouseDeduction = 0; + $retailDeduction = 0; if ($totalItemsSubtotal > 0) { - $gudangDeduction = ($gudangSubtotal / $totalItemsSubtotal) * $deduction; - $ecerDeduction = ($ecerSubtotal / $totalItemsSubtotal) * $deduction; + $warehouseDeduction = ($warehouseSubtotal / $totalItemsSubtotal) * $deduction; + $retailDeduction = ($retailSubtotal / $totalItemsSubtotal) * $deduction; } - $netGudang = $gudangSubtotal - $gudangDeduction - $gudangHpp; - $netEcer = $ecerSubtotal - $ecerDeduction - $ecerHpp; + $netWarehouse = $warehouseSubtotal - $warehouseDeduction - $warehouseHpp; + $netRetail = $retailSubtotal - $retailDeduction - $retailHpp; $result[] = [ 'month' => $monthLabel, 'total' => (int) ($revenue->total_revenue ?? 0), 'net' => (int) ($revenue->total_revenue ?? 0) - $deduction - $hpp, - 'net_gudang' => (int) round($netGudang), - 'net_ecer' => (int) round($netEcer), + 'net_warehouse' => (int) round($netWarehouse), + 'net_retail' => (int) round($netRetail), 'deduction' => $deduction, ]; diff --git a/database/migrations/2026_07_05_124618_add_stock_quality_to_stok_opname_items_table.php b/database/migrations/2026_07_05_124618_add_stock_quality_to_stok_opname_items_table.php new file mode 100644 index 0000000..c2b5f6c --- /dev/null +++ b/database/migrations/2026_07_05_124618_add_stock_quality_to_stok_opname_items_table.php @@ -0,0 +1,45 @@ +dropForeign(['stok_opname_id']); + $table->dropForeign(['product_variant_id']); + $table->dropUnique('stok_opname_items_stok_opname_id_product_variant_id_unique'); + + $table->enum('stock_quality', ProductStockQuality::values())->default(ProductStockQuality::GOOD->value)->after('product_variant_id'); + $table->unique(['stok_opname_id', 'product_variant_id', 'stock_quality'], 'stok_opname_items_opname_variant_quality_unique'); + + $table->foreign('stok_opname_id')->references('id')->on('stok_opnames')->cascadeOnDelete(); + $table->foreign('product_variant_id')->references('id')->on('product_variants')->cascadeOnDelete(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table('stok_opname_items', function (Blueprint $table) { + $table->dropForeign(['stok_opname_id']); + $table->dropForeign(['product_variant_id']); + $table->dropUnique('stok_opname_items_opname_variant_quality_unique'); + + $table->dropColumn('stock_quality'); + $table->unique(['stok_opname_id', 'product_variant_id']); + + $table->foreign('stok_opname_id')->references('id')->on('stok_opnames')->cascadeOnDelete(); + $table->foreign('product_variant_id')->references('id')->on('product_variants')->cascadeOnDelete(); + }); + } +}; diff --git a/resources/js/pages/admin/Analysis.vue b/resources/js/pages/admin/Analysis.vue index cb24846..e335f68 100644 --- a/resources/js/pages/admin/Analysis.vue +++ b/resources/js/pages/admin/Analysis.vue @@ -97,8 +97,8 @@ const props = defineProps<{ month: string; total: number; net: number; - net_gudang: number; - net_ecer: number; + net_warehouse: number; + net_retail: number; deduction: number; }>; expenseSummary: { @@ -200,8 +200,8 @@ type MonthlyRevenueData = { month: string; total: number; net: number; - net_gudang: number; - net_ecer: number; + net_warehouse: number; + net_retail: number; deduction: number; }; @@ -214,11 +214,11 @@ const revenueChartConfig = { label: 'Bersih', color: '#22c55e', }, - net_gudang: { + net_warehouse: { label: 'Total Gudang', color: '#10b981', }, - net_ecer: { + net_retail: { label: 'Total Ecer', color: '#06b6d4', }, @@ -229,16 +229,16 @@ const revenueChartConfig = { } satisfies ChartConfig; const revenueTotals = computed(() => { - const net_gudang = props.monthlyRevenue.reduce((sum, item) => sum + (item.net_gudang ?? 0), 0); - const net_ecer = props.monthlyRevenue.reduce((sum, item) => sum + (item.net_ecer ?? 0), 0); + const net_warehouse = props.monthlyRevenue.reduce((sum, item) => sum + (item.net_warehouse ?? 0), 0); + const net_retail = props.monthlyRevenue.reduce((sum, item) => sum + (item.net_retail ?? 0), 0); return { total: props.revenueSummary.total_revenue, net: props.revenueSummary.total_revenue - props.revenueSummary.total_deduction - props.profitMetrics.hpp, - net_gudang, - net_ecer, + net_warehouse, + net_retail, deduction: props.revenueSummary.total_deduction, }; }); @@ -248,7 +248,7 @@ const visibleRevenueCharts = computed(() => { const isCashier = hasRole('cashier'); if (isOwner) { - return ['total', 'net', 'net_gudang', 'net_ecer', 'deduction'] as const; + return ['total', 'net', 'net_warehouse', 'net_retail', 'deduction'] as const; } if (isCashier) { @@ -416,14 +416,14 @@ function revenueTooltip(d: any) { ` : ''} ${isOwner ? `
- - ${revenueChartConfig.net_gudang.label} - Rp${formatRupiah(item.net_gudang)} + + ${revenueChartConfig.net_warehouse.label} + Rp${formatRupiah(item.net_warehouse)}
- - ${revenueChartConfig.net_ecer.label} - Rp${formatRupiah(item.net_ecer)} + + ${revenueChartConfig.net_retail.label} + Rp${formatRupiah(item.net_retail)}
` : ''}diff --git a/resources/js/pages/admin/manage/stok-opnames/form/StokOpnameProductTable.vue b/resources/js/pages/admin/manage/stok-opnames/form/StokOpnameProductTable.vue index 8794b6f..090f924 100644 --- a/resources/js/pages/admin/manage/stok-opnames/form/StokOpnameProductTable.vue +++ b/resources/js/pages/admin/manage/stok-opnames/form/StokOpnameProductTable.vue @@ -2,6 +2,7 @@ import { NumberInput } from '@/components/form/number-input'; import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; import { Input } from '@/components/ui/input'; +import { Badge } from '@/components/ui/badge'; import { groupedTableRowNumber } from '@/lib/grouped-table'; import { stokOpnameDifference, @@ -39,56 +40,74 @@ const emit = defineEmits<{
| No. | -Varian | -Stok Sistem | -Stok Fisik | -Selisih | +||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|
| No. | +Varian | +Kualitas | +Stok Sistem | +Stok Fisik | +Selisih | Catatan | ||||||
| - {{ groupedTableRowNumber(group.startIndex + 1, rIdx) }} - | -{{ row.variant_name }} | -- {{ row.system_stock_formatted ?? row.system_stock }} - | -
- |
-
-
+ |
- {{ stokOpnameDifferenceText(stokOpnameDifference(row.physical_stock, row.system_stock)) }}
-
- |
-
-
- |
- |
+ + {{ vGroup.variant_name }} + | +
+ |
+ + {{ qRow.system_stock_formatted ?? qRow.system_stock }} + | +
+ |
+ + + {{ stokOpnameDifferenceText(stokOpnameDifference(qRow.row_ref.physical_stock, qRow.system_stock)) }} + + | ++ + | +
| + {{ vIdx + 1 }} + | ++ {{ vGroup.product_name }} + | ++ {{ vGroup.variant_name }} + | + +
+ |
+ + {{ qRow.system_stock }} + | ++ {{ qRow.physical_stock }} + | ++ + {{ stokOpnameDifferenceText(qRow.difference) }} + + | ++ {{ qRow.notes || '-' }} + | +