feat: add harga_modal price type and related logic for improved stock management and pricing accuracy
This commit is contained in:
parent
07e9a4ab18
commit
5e71277056
@ -15,6 +15,7 @@ enum PriceType: string
|
||||
case ECER = 'ecer';
|
||||
case TIKTOK = 'tiktok';
|
||||
case SHOPEE = 'shopee';
|
||||
case HARGA_MODAL = 'harga_modal';
|
||||
|
||||
public function label(): string
|
||||
{
|
||||
@ -26,6 +27,7 @@ public function label(): string
|
||||
self::ECER => 'Eceran',
|
||||
self::TIKTOK => 'TikTok',
|
||||
self::SHOPEE => 'Shopee',
|
||||
self::HARGA_MODAL => 'Harga Modal',
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@ -43,21 +43,4 @@ public function verify(StockVerifyRequest $request, Cutting $cutting): RedirectR
|
||||
|
||||
return redirect()->route('admin.manage.stocks.index');
|
||||
}
|
||||
|
||||
public function reject(Request $request, Cutting $cutting): RedirectResponse
|
||||
{
|
||||
$request->validate([
|
||||
'reason' => ['required', 'string', 'max:500'],
|
||||
]);
|
||||
|
||||
$this->stockService->reject(
|
||||
$cutting,
|
||||
$request->user(),
|
||||
$request->validated('reason'),
|
||||
);
|
||||
|
||||
$this->flashSuccess('Cutting berhasil ditolak dan dikembalikan ke proses.');
|
||||
|
||||
return redirect()->route('admin.manage.stocks.index');
|
||||
}
|
||||
}
|
||||
|
||||
@ -2,8 +2,10 @@
|
||||
|
||||
namespace App\Http\Middleware;
|
||||
|
||||
use App\Enums\CuttingStatus;
|
||||
use App\Enums\EmployeeAdvanceStatus;
|
||||
use App\Enums\LeaveRequestStatus;
|
||||
use App\Models\Cutting;
|
||||
use App\Models\EmployeeAdvance;
|
||||
use App\Models\LeaveRequest;
|
||||
use App\Services\System\Setting\SystemService;
|
||||
@ -70,6 +72,7 @@ public function share(Request $request): array
|
||||
'vapidPublicKey' => config('webpush.vapid.public_key'),
|
||||
'pendingLeaveRequests' => fn () => $this->pendingLeaveRequests($request),
|
||||
'pendingEmployeeAdvances' => fn () => $this->pendingEmployeeAdvances($request),
|
||||
'pendingCuttings' => fn () => $this->pendingCuttings($request),
|
||||
];
|
||||
}
|
||||
|
||||
@ -98,4 +101,17 @@ private function pendingEmployeeAdvances(Request $request): int
|
||||
->where('status', EmployeeAdvanceStatus::PENDING)
|
||||
->count();
|
||||
}
|
||||
|
||||
private function pendingCuttings(Request $request): int
|
||||
{
|
||||
$user = $request->user();
|
||||
|
||||
if ($user === null || ! $user->can('cuttings.view')) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
return Cutting::query()
|
||||
->where('status', CuttingStatus::COMPLETED)
|
||||
->count();
|
||||
}
|
||||
}
|
||||
|
||||
@ -32,7 +32,7 @@ public function rules(): array
|
||||
'result_prices.*.product_variant_id' => ['required_with:result_prices', 'integer', 'exists:product_variants,id'],
|
||||
'result_prices.*.prices' => ['required_with:result_prices', 'array', 'min:1'],
|
||||
'result_prices.*.prices.*.type' => ['required', Rule::enum(PriceType::class)],
|
||||
'result_prices.*.prices.*.price' => ['required', 'integer', 'gt:0'],
|
||||
'result_prices.*.prices.*.price' => ['required', 'integer', 'min:0'],
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
@ -5,6 +5,7 @@
|
||||
use App\Enums\CuttingStatus;
|
||||
use App\Models\Cutting;
|
||||
use App\Models\CuttingResultPrice;
|
||||
use App\Models\ProductPrice;
|
||||
use App\Models\ProductVariant;
|
||||
use App\Models\RawMaterialPrice;
|
||||
use App\Models\User;
|
||||
@ -101,6 +102,7 @@ public function verify(
|
||||
}
|
||||
|
||||
$this->applyProductStockOnVerify($cutting);
|
||||
|
||||
$this->storeResultPrices($cutting, $resultPrices ?? []);
|
||||
|
||||
if ($verificationNote !== null && trim($verificationNote) !== '') {
|
||||
@ -190,18 +192,49 @@ private function deductRemainingMaterialStock(Cutting $cutting): void
|
||||
*/
|
||||
private function storeResultPrices(Cutting $cutting, array $resultPrices): void
|
||||
{
|
||||
$costPerUnit = (int) ($cutting->cost_per_unit ?? 0);
|
||||
// Extract harga_modal from the first variant's prices and set as cost_per_unit
|
||||
$costPerUnit = 0;
|
||||
foreach ($resultPrices as $resultData) {
|
||||
foreach ($resultData['prices'] as $priceData) {
|
||||
if ($priceData['type'] === 'harga_modal' && (int) $priceData['price'] > 0) {
|
||||
$costPerUnit = (int) $priceData['price'];
|
||||
break 2;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ($costPerUnit > 0) {
|
||||
$cutting->cost_per_unit = $costPerUnit;
|
||||
} else {
|
||||
$costPerUnit = (int) ($cutting->cost_per_unit ?? 0);
|
||||
}
|
||||
|
||||
foreach ($resultPrices as $resultData) {
|
||||
foreach ($resultData['prices'] as $priceData) {
|
||||
if ((int) $priceData['price'] > 0) {
|
||||
CuttingResultPrice::query()->create([
|
||||
'cutting_id' => $cutting->id,
|
||||
'product_variant_id' => $resultData['product_variant_id'],
|
||||
'price_type' => $priceData['type'],
|
||||
'price' => (int) $priceData['price'],
|
||||
'cost_per_unit' => $costPerUnit,
|
||||
]);
|
||||
// Store/update to cutting_result_prices
|
||||
CuttingResultPrice::query()->updateOrCreate(
|
||||
[
|
||||
'cutting_id' => $cutting->id,
|
||||
'product_variant_id' => $resultData['product_variant_id'],
|
||||
'price_type' => $priceData['type'],
|
||||
],
|
||||
[
|
||||
'price' => (int) $priceData['price'],
|
||||
'cost_per_unit' => $costPerUnit,
|
||||
],
|
||||
);
|
||||
|
||||
// Store/update in product_prices
|
||||
ProductPrice::query()->updateOrCreate(
|
||||
[
|
||||
'variant_id' => $resultData['product_variant_id'],
|
||||
'type' => $priceData['type'],
|
||||
],
|
||||
[
|
||||
'price' => (int) $priceData['price'],
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -0,0 +1,23 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
DB::statement("ALTER TABLE cutting_result_prices MODIFY COLUMN price_type ENUM('distributor', 'agent', 'sub_agent', 'grosir', 'ecer', 'tiktok', 'shopee', 'harga_modal') NOT NULL");
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
DB::statement("ALTER TABLE cutting_result_prices MODIFY COLUMN price_type ENUM('distributor', 'agent', 'sub_agent', 'grosir', 'ecer', 'tiktok', 'shopee') NOT NULL");
|
||||
}
|
||||
};
|
||||
@ -0,0 +1,23 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
DB::statement("ALTER TABLE orders MODIFY COLUMN price_type ENUM('distributor', 'agent', 'sub_agent', 'grosir', 'ecer', 'tiktok', 'shopee', 'harga_modal') NOT NULL");
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
DB::statement("ALTER TABLE orders MODIFY COLUMN price_type ENUM('distributor', 'agent', 'sub_agent', 'grosir', 'ecer', 'tiktok', 'shopee') NOT NULL");
|
||||
}
|
||||
};
|
||||
@ -0,0 +1,23 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
DB::statement("ALTER TABLE product_prices MODIFY COLUMN type ENUM('distributor', 'agent', 'sub_agent', 'grosir', 'ecer', 'tiktok', 'shopee', 'harga_modal') NOT NULL");
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
DB::statement("ALTER TABLE product_prices MODIFY COLUMN type ENUM('distributor', 'agent', 'sub_agent', 'grosir', 'ecer', 'tiktok', 'shopee') NOT NULL");
|
||||
}
|
||||
};
|
||||
@ -24,7 +24,7 @@ interface MenuItem {
|
||||
href: string;
|
||||
icon: any;
|
||||
permission?: string;
|
||||
badgeKey?: 'pendingLeaveRequests' | 'pendingEmployeeAdvances';
|
||||
badgeKey?: 'pendingLeaveRequests' | 'pendingEmployeeAdvances' | 'pendingCuttings';
|
||||
}
|
||||
|
||||
interface MenuGroup {
|
||||
@ -54,7 +54,7 @@ const menuGroups: MenuGroup[] = [
|
||||
items: [
|
||||
{ title: 'Belanja', href: '/admin/manage/purchases', icon: ShoppingBag, permission: 'purchases.view' },
|
||||
{ title: 'Cutting', href: '/admin/manage/cuttings', icon: Scissors, permission: 'cuttings.view' },
|
||||
{ title: 'Stok', href: '/admin/manage/stocks', icon: Warehouse, permission: 'cuttings.view' },
|
||||
{ title: 'Stok', href: '/admin/manage/stocks', icon: Warehouse, permission: 'cuttings.view', badgeKey: 'pendingCuttings' },
|
||||
{ title: 'Pesanan', href: '/admin/manage/orders', icon: ShoppingCart, permission: 'orders.view' },
|
||||
],
|
||||
},
|
||||
|
||||
@ -18,9 +18,6 @@ defineProps<{
|
||||
<h2 class="text-2xl font-bold tracking-tight">
|
||||
Verifikasi Stok
|
||||
</h2>
|
||||
<p class="text-muted-foreground text-sm">
|
||||
Verifikasi hasil cutting untuk menambahkan stok produk ke toko.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@ -10,15 +10,6 @@ defineProps<{
|
||||
|
||||
<template>
|
||||
<div v-if="cuttings.length" class="space-y-4">
|
||||
<div class="flex items-center gap-2">
|
||||
<h3 class="text-lg font-semibold">
|
||||
Menunggu Verifikasi
|
||||
</h3>
|
||||
<span class="bg-primary text-primary-foreground inline-flex items-center rounded-full px-2.5 py-0.5 text-xs font-medium">
|
||||
{{ cuttings.length }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-1 gap-4 md:grid-cols-2 lg:grid-cols-3">
|
||||
<Card v-for="cutting in cuttings" :key="cutting.id"
|
||||
class="flex flex-col justify-between overflow-hidden border bg-card/50 py-0 gap-0">
|
||||
|
||||
@ -1,10 +1,8 @@
|
||||
<script setup lang="ts">
|
||||
import { router, useForm } from '@inertiajs/vue3';
|
||||
import { Check, X } from '@lucide/vue';
|
||||
import { Check, Copy } from '@lucide/vue';
|
||||
import { ref, watch } from 'vue';
|
||||
import { toast } from 'vue-sonner';
|
||||
import ConfirmDialog from '@/components/ConfirmDialog.vue';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import {
|
||||
Dialog,
|
||||
@ -16,12 +14,11 @@ import {
|
||||
import {
|
||||
Field,
|
||||
FieldError,
|
||||
FieldGroup,
|
||||
FieldLabel,
|
||||
FieldSet,
|
||||
} from '@/components/ui/field';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { Label } from '@/components/ui/label';
|
||||
import { Separator } from '@/components/ui/separator';
|
||||
import { Switch } from '@/components/ui/switch';
|
||||
import { Textarea } from '@/components/ui/textarea';
|
||||
import {
|
||||
@ -44,17 +41,19 @@ const props = defineProps<{
|
||||
|
||||
const { can } = useCan();
|
||||
|
||||
const rejectDialogOpen = ref(false);
|
||||
const verifyDialogOpen = ref(false);
|
||||
const allMatches = ref(true);
|
||||
const useSamePrice = ref(true);
|
||||
|
||||
function buildEmptyPrices(): Record<string, string> {
|
||||
return Object.fromEntries(PRICE_TYPES.map((type) => [type, '']));
|
||||
}
|
||||
|
||||
const rejectForm = useForm({
|
||||
reason: '',
|
||||
});
|
||||
interface VariantPriceRow {
|
||||
product_variant_id: number;
|
||||
name: string;
|
||||
prices: Record<string, string>;
|
||||
}
|
||||
|
||||
const verifyForm = useForm({
|
||||
verification_note: '',
|
||||
@ -66,17 +65,15 @@ const verifyForm = useForm({
|
||||
cutting_reject: res.cutting_reject,
|
||||
original_warehouse_stock: res.warehouse_stock,
|
||||
original_cutting_reject: res.cutting_reject,
|
||||
prices: buildEmptyPrices(),
|
||||
})),
|
||||
result_prices: [] as Array<{
|
||||
product_variant_id: number;
|
||||
prices: Array<{ type: string; price: number }>;
|
||||
}>,
|
||||
variant_prices: [] as VariantPriceRow[],
|
||||
shared_prices: buildEmptyPrices(),
|
||||
});
|
||||
|
||||
watch(verifyDialogOpen, (isOpen) => {
|
||||
if (isOpen) {
|
||||
allMatches.value = true;
|
||||
useSamePrice.value = true;
|
||||
verifyForm.verification_note = '';
|
||||
verifyForm.results = props.cutting.results.map(res => ({
|
||||
product_variant_id: res.product_variant?.id || 0,
|
||||
@ -86,9 +83,13 @@ watch(verifyDialogOpen, (isOpen) => {
|
||||
cutting_reject: res.cutting_reject,
|
||||
original_warehouse_stock: res.warehouse_stock,
|
||||
original_cutting_reject: res.cutting_reject,
|
||||
}));
|
||||
verifyForm.variant_prices = props.cutting.results.map(res => ({
|
||||
product_variant_id: res.product_variant?.id || 0,
|
||||
name: `${res.product_variant?.product?.name || ''} (${res.product_variant?.name || ''})`,
|
||||
prices: buildEmptyPrices(),
|
||||
}));
|
||||
verifyForm.result_prices = [];
|
||||
verifyForm.shared_prices = buildEmptyPrices();
|
||||
verifyForm.clearErrors();
|
||||
}
|
||||
});
|
||||
@ -117,24 +118,55 @@ function setAllMatches(value: boolean) {
|
||||
}
|
||||
}
|
||||
|
||||
function setResultPrice(resultIndex: number, type: string, value: string) {
|
||||
const result = verifyForm.results[resultIndex];
|
||||
if (!result) {
|
||||
function toggleUseSamePrice(checked: boolean) {
|
||||
useSamePrice.value = checked;
|
||||
|
||||
if (!checked || !verifyForm.variant_prices[0]) {
|
||||
return;
|
||||
}
|
||||
|
||||
result.prices = {
|
||||
...result.prices,
|
||||
[type]: value,
|
||||
};
|
||||
const source = verifyForm.variant_prices[0].prices;
|
||||
verifyForm.variant_prices = verifyForm.variant_prices.map((row) => ({
|
||||
...row,
|
||||
prices: { ...source },
|
||||
}));
|
||||
}
|
||||
|
||||
function setSharedPrice(type: string, value: string) {
|
||||
verifyForm.shared_prices[type] = value;
|
||||
verifyForm.variant_prices = verifyForm.variant_prices.map((row) => ({
|
||||
...row,
|
||||
prices: { ...row.prices, [type]: value },
|
||||
}));
|
||||
}
|
||||
|
||||
function setVariantPrice(index: number, type: string, value: string) {
|
||||
const row = verifyForm.variant_prices[index];
|
||||
if (!row) {
|
||||
return;
|
||||
}
|
||||
|
||||
row.prices[type] = value;
|
||||
}
|
||||
|
||||
function applyPriceToAll(index: number) {
|
||||
const source = verifyForm.variant_prices[index];
|
||||
if (!source) {
|
||||
return;
|
||||
}
|
||||
|
||||
verifyForm.variant_prices = verifyForm.variant_prices.map((row) => ({
|
||||
...row,
|
||||
prices: { ...source.prices },
|
||||
}));
|
||||
}
|
||||
|
||||
function buildResultPricesPayload() {
|
||||
return verifyForm.results.map((result) => ({
|
||||
product_variant_id: result.product_variant_id,
|
||||
return verifyForm.variant_prices.map((row) => ({
|
||||
product_variant_id: row.product_variant_id,
|
||||
prices: PRICE_TYPES.map((type) => ({
|
||||
type,
|
||||
price: Number.parseInt(parseRupiah(result.prices[type] ?? ''), 10) || 0,
|
||||
price: Number.parseInt(parseRupiah(row.prices[type] ?? ''), 10) || 0,
|
||||
})),
|
||||
}));
|
||||
}
|
||||
@ -166,120 +198,26 @@ function submitVerify() {
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
function submitReject() {
|
||||
rejectForm.post(`/admin/manage/stocks/${props.cutting.id}/reject`, {
|
||||
preserveScroll: true,
|
||||
onSuccess: () => {
|
||||
rejectDialogOpen.value = false;
|
||||
},
|
||||
onError: (errors) => {
|
||||
const message = Object.values(errors)[0];
|
||||
|
||||
toast.error(
|
||||
typeof message === 'string'
|
||||
? message
|
||||
: 'Gagal menolak cutting.',
|
||||
);
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
watch(rejectDialogOpen, (isOpen) => {
|
||||
if (!isOpen) {
|
||||
rejectForm.reset();
|
||||
rejectForm.clearErrors();
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex flex-wrap items-center justify-end gap-1">
|
||||
<Tooltip v-if="can('cuttings.verify')">
|
||||
<TooltipTrigger as-child>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
class="text-green-600 hover:text-green-700"
|
||||
@click="verifyDialogOpen = true"
|
||||
>
|
||||
<Check class="size-3.5" />
|
||||
<span class="sr-only">Verifikasi</span>
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>Verifikasi & Tambah Stok</TooltipContent>
|
||||
</Tooltip>
|
||||
|
||||
<Tooltip v-if="can('cuttings.reject')">
|
||||
<TooltipTrigger as-child>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
class="text-destructive hover:text-destructive"
|
||||
@click="rejectDialogOpen = true"
|
||||
>
|
||||
<X class="size-3.5" />
|
||||
<span class="sr-only">Tolak</span>
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>Tolak Verifikasi</TooltipContent>
|
||||
</Tooltip>
|
||||
</div>
|
||||
|
||||
<Dialog v-model:open="rejectDialogOpen">
|
||||
<DialogContent class="sm:max-w-md">
|
||||
<DialogHeader>
|
||||
<DialogTitle>Tolak Verifikasi Cutting</DialogTitle>
|
||||
</DialogHeader>
|
||||
|
||||
<form @submit.prevent="submitReject">
|
||||
<FieldGroup>
|
||||
<FieldSet class="grid gap-4">
|
||||
<Field>
|
||||
<FieldLabel for="stock-reject-reason" required
|
||||
>Alasan Penolakan</FieldLabel
|
||||
>
|
||||
<Textarea
|
||||
id="stock-reject-reason"
|
||||
v-model="rejectForm.reason"
|
||||
placeholder="Contoh: Jumlah barang yang diterima tidak sesuai"
|
||||
rows="3"
|
||||
autofocus
|
||||
/>
|
||||
<FieldError
|
||||
:errors="
|
||||
rejectForm.errors.reason
|
||||
? [rejectForm.errors.reason]
|
||||
: []
|
||||
"
|
||||
/>
|
||||
</Field>
|
||||
</FieldSet>
|
||||
</FieldGroup>
|
||||
|
||||
<DialogFooter class="mt-6">
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
:disabled="rejectForm.processing"
|
||||
@click="rejectDialogOpen = false"
|
||||
>
|
||||
Batal
|
||||
</Button>
|
||||
<Button
|
||||
type="submit"
|
||||
variant="destructive"
|
||||
:disabled="rejectForm.processing"
|
||||
>
|
||||
{{ rejectForm.processing ? 'Menyimpan...' : 'Tolak' }}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</form>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
<Tooltip v-if="can('cuttings.verify')">
|
||||
<TooltipTrigger as-child>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
class="text-green-600 hover:text-green-700"
|
||||
@click="verifyDialogOpen = true"
|
||||
>
|
||||
<Check class="size-3.5" />
|
||||
<span class="sr-only">Verifikasi</span>
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>Verifikasi & Tambah Stok</TooltipContent>
|
||||
</Tooltip>
|
||||
|
||||
<Dialog v-model:open="verifyDialogOpen">
|
||||
<DialogContent class="sm:max-w-2xl">
|
||||
<DialogContent class="sm:max-w-lg">
|
||||
<DialogHeader>
|
||||
<DialogTitle>Verifikasi Hasil Cutting</DialogTitle>
|
||||
</DialogHeader>
|
||||
@ -287,7 +225,7 @@ watch(rejectDialogOpen, (isOpen) => {
|
||||
<form @submit.prevent="submitVerify">
|
||||
<div class="space-y-4 max-h-[60vh] overflow-y-auto scrollbar-thin px-1 py-1">
|
||||
<p class="text-sm text-muted-foreground">
|
||||
Verifikasi jumlah produk yang diterima di toko dan tentukan harga jual per varian. Stok akan ditambahkan setelah verifikasi.
|
||||
Verifikasi jumlah produk yang diterima dan tentukan harga jual. Stok akan ditambahkan setelah verifikasi.
|
||||
</p>
|
||||
|
||||
<div class="flex items-center justify-between rounded-lg border p-3">
|
||||
@ -301,68 +239,101 @@ watch(rejectDialogOpen, (isOpen) => {
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="space-y-3">
|
||||
<div v-for="(result, index) in verifyForm.results" :key="result.product_variant_id" class="p-3 border rounded-lg space-y-3">
|
||||
<div class="font-medium text-sm">
|
||||
{{ result.name }}
|
||||
<div class="space-y-2">
|
||||
<div v-for="(result, index) in verifyForm.results" :key="result.product_variant_id" class="flex items-center gap-3 rounded-lg border p-3">
|
||||
<div class="flex-1 min-w-0">
|
||||
<p class="font-medium text-sm truncate">{{ result.name }}</p>
|
||||
<p class="text-xs text-muted-foreground">
|
||||
{{ result.cutting_result }} pcs
|
||||
<span class="text-[10px]">({{ result.original_warehouse_stock }} bagus, {{ result.original_cutting_reject }} reject)</span>
|
||||
</p>
|
||||
</div>
|
||||
<div class="grid grid-cols-3 gap-2 items-center text-xs">
|
||||
<div>
|
||||
<span class="text-muted-foreground block">Hasil Potong:</span>
|
||||
<span class="font-semibold">{{ result.cutting_result }} pcs</span>
|
||||
</div>
|
||||
<div>
|
||||
<span class="text-muted-foreground block mb-0.5">Data cutting:</span>
|
||||
<span class="font-medium tabular-nums">
|
||||
{{ result.original_warehouse_stock }} bagus ·
|
||||
{{ result.original_cutting_reject }} reject
|
||||
</span>
|
||||
</div>
|
||||
<div>
|
||||
<span class="text-muted-foreground block">Stok Reject:</span>
|
||||
<Badge variant="secondary" class="font-semibold">
|
||||
{{ result.cutting_reject }} pcs
|
||||
</Badge>
|
||||
</div>
|
||||
</div>
|
||||
<div class="grid grid-cols-2 gap-2 items-end text-xs">
|
||||
<div>
|
||||
<label :for="`stock-good-${index}`" class="text-muted-foreground block mb-0.5">Stok Bagus (diterima):</label>
|
||||
<div class="flex items-center gap-2 shrink-0">
|
||||
<div class="text-center">
|
||||
<label :for="`stock-good-${index}`" class="text-[10px] text-muted-foreground block">Bagus</label>
|
||||
<Input
|
||||
:id="`stock-good-${index}`"
|
||||
type="number"
|
||||
v-model.number="result.warehouse_stock"
|
||||
min="0"
|
||||
:max="result.cutting_result"
|
||||
class="h-8 w-full px-2 text-xs"
|
||||
class="h-7 w-16 px-1.5 text-xs text-center"
|
||||
:disabled="allMatches"
|
||||
@input="result.cutting_reject = result.cutting_result - result.warehouse_stock"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label :for="`stock-reject-${index}`" class="text-muted-foreground block mb-0.5">Stok Reject (diterima):</label>
|
||||
<div class="text-center">
|
||||
<label :for="`stock-reject-${index}`" class="text-[10px] text-muted-foreground block">Reject</label>
|
||||
<Input
|
||||
:id="`stock-reject-${index}`"
|
||||
type="number"
|
||||
v-model.number="result.cutting_reject"
|
||||
min="0"
|
||||
:max="result.cutting_result"
|
||||
class="h-8 w-full px-2 text-xs"
|
||||
class="h-7 w-16 px-1.5 text-xs text-center"
|
||||
:disabled="allMatches"
|
||||
@input="result.warehouse_stock = result.cutting_result - result.cutting_reject"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Separator />
|
||||
|
||||
<div v-if="verifyForm.variant_prices.length > 1" class="flex items-center justify-between rounded-lg border p-3">
|
||||
<Label for="use-same-price" class="text-sm font-medium">
|
||||
Gunakan harga yang sama untuk semua varian
|
||||
</Label>
|
||||
<Switch
|
||||
id="use-same-price"
|
||||
:model-value="useSamePrice"
|
||||
@update:model-value="toggleUseSamePrice"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div v-if="useSamePrice" class="space-y-2">
|
||||
<p class="text-sm font-medium">Harga</p>
|
||||
<div class="grid gap-2 sm:grid-cols-2">
|
||||
<Field v-for="type in PRICE_TYPES" :key="`shared-${type}`">
|
||||
<FieldLabel class="text-xs" :for="`shared-price-${type}`">
|
||||
{{ PRICE_TYPE_LABELS[type] }}
|
||||
</FieldLabel>
|
||||
<RupiahInput
|
||||
:id="`shared-price-${type}`"
|
||||
:model-value="verifyForm.shared_prices[type]"
|
||||
@update:model-value="setSharedPrice(type, $event)"
|
||||
/>
|
||||
</Field>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-else class="space-y-3">
|
||||
<p class="text-sm font-medium">Harga Per Varian</p>
|
||||
<div v-for="(row, index) in verifyForm.variant_prices" :key="row.product_variant_id" class="space-y-2 rounded-lg border p-3">
|
||||
<div class="flex items-center justify-between">
|
||||
<p class="text-sm font-medium truncate">{{ row.name }}</p>
|
||||
<Button
|
||||
v-if="verifyForm.variant_prices.length > 1"
|
||||
type="button"
|
||||
variant="outline"
|
||||
size="sm"
|
||||
class="shrink-0"
|
||||
@click="applyPriceToAll(index)"
|
||||
>
|
||||
<Copy class="size-3" />
|
||||
Terapkan ke Semua
|
||||
</Button>
|
||||
</div>
|
||||
<div class="grid gap-2 sm:grid-cols-2">
|
||||
<Field v-for="type in PRICE_TYPES" :key="`${result.product_variant_id}-${type}`">
|
||||
<FieldLabel class="text-xs" :for="`stock-price-${index}-${type}`">
|
||||
<Field v-for="type in PRICE_TYPES" :key="`${row.product_variant_id}-${type}`">
|
||||
<FieldLabel class="text-xs" :for="`variant-price-${index}-${type}`">
|
||||
{{ PRICE_TYPE_LABELS[type] }}
|
||||
</FieldLabel>
|
||||
<RupiahInput
|
||||
:id="`stock-price-${index}-${type}`"
|
||||
:model-value="result.prices[type]"
|
||||
@update:model-value="setResultPrice(index, type, $event)"
|
||||
:id="`variant-price-${index}-${type}`"
|
||||
:model-value="row.prices[type]"
|
||||
@update:model-value="setVariantPrice(index, type, $event)"
|
||||
/>
|
||||
</Field>
|
||||
</div>
|
||||
|
||||
@ -82,6 +82,7 @@ export type PaginatedProducts = {
|
||||
};
|
||||
|
||||
export const PRICE_TYPES = [
|
||||
'harga_modal',
|
||||
'distributor',
|
||||
'agent',
|
||||
'sub_agent',
|
||||
@ -94,6 +95,7 @@ export const PRICE_TYPES = [
|
||||
export type PriceType = (typeof PRICE_TYPES)[number];
|
||||
|
||||
export const PRICE_TYPE_LABELS: Record<PriceType, string> = {
|
||||
harga_modal: 'Harga Modal',
|
||||
distributor: 'Distributor',
|
||||
agent: 'Agen',
|
||||
sub_agent: 'Sub Agen',
|
||||
|
||||
@ -305,10 +305,6 @@
|
||||
Route::post('{cutting}/verify', [StockController::class, 'verify'])
|
||||
->middleware('permission:'.Permission::CUTTINGS_VERIFY->value)
|
||||
->name('verify');
|
||||
|
||||
Route::post('{cutting}/reject', [StockController::class, 'reject'])
|
||||
->middleware('permission:'.Permission::CUTTINGS_REJECT->value)
|
||||
->name('reject');
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
Loading…
Reference in New Issue
Block a user