feat: add sewing and other cost fields to cutting management, including validation, calculations, and UI updates for total production cost
This commit is contained in:
parent
2db6dfece9
commit
db3f6887ea
@ -45,6 +45,9 @@ public function rules(): array
|
|||||||
'results.*.cutting_result' => ['required', 'integer', 'min:1'],
|
'results.*.cutting_result' => ['required', 'integer', 'min:1'],
|
||||||
'results.*.warehouse_stock' => ['required', 'integer', 'min:0'],
|
'results.*.warehouse_stock' => ['required', 'integer', 'min:0'],
|
||||||
'results.*.cutting_reject' => ['required', 'integer', 'min:0'],
|
'results.*.cutting_reject' => ['required', 'integer', 'min:0'],
|
||||||
|
|
||||||
|
'sewing_cost' => ['nullable', 'integer', 'min:0'],
|
||||||
|
'other_cost' => ['nullable', 'integer', 'min:0'],
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -62,8 +65,10 @@ public function attributes(): array
|
|||||||
'results' => 'hasil produk',
|
'results' => 'hasil produk',
|
||||||
'results.*.product_variant_id' => 'varian produk',
|
'results.*.product_variant_id' => 'varian produk',
|
||||||
'results.*.cutting_result' => 'hasil',
|
'results.*.cutting_result' => 'hasil',
|
||||||
'results.*.warehouse_stock' => 'gudang',
|
'results.*.warehouse_stock' => 'bagus',
|
||||||
'results.*.cutting_reject' => 'reject',
|
'results.*.cutting_reject' => 'reject',
|
||||||
|
'sewing_cost' => 'jasa jahit',
|
||||||
|
'other_cost' => 'biaya lainnya',
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -31,6 +31,8 @@ protected function casts(): array
|
|||||||
return [
|
return [
|
||||||
'status' => CuttingStatus::class,
|
'status' => CuttingStatus::class,
|
||||||
'total_material_cost' => 'integer',
|
'total_material_cost' => 'integer',
|
||||||
|
'sewing_cost' => 'integer',
|
||||||
|
'other_cost' => 'integer',
|
||||||
'cost_per_unit' => 'integer',
|
'cost_per_unit' => 'integer',
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|||||||
@ -249,6 +249,8 @@ public function create(array $validated, User $user): Cutting
|
|||||||
$cutting = Cutting::create([
|
$cutting = Cutting::create([
|
||||||
'status' => CuttingStatus::IN_PROGRESS,
|
'status' => CuttingStatus::IN_PROGRESS,
|
||||||
'description' => $validated['description'] ?? null,
|
'description' => $validated['description'] ?? null,
|
||||||
|
'sewing_cost' => (int) ($validated['sewing_cost'] ?? 0),
|
||||||
|
'other_cost' => (int) ($validated['other_cost'] ?? 0),
|
||||||
'created_by_id' => $user->id,
|
'created_by_id' => $user->id,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
@ -303,6 +305,8 @@ public function update(Cutting $cutting, array $validated): void
|
|||||||
$results = $this->buildResults($validated['results']);
|
$results = $this->buildResults($validated['results']);
|
||||||
|
|
||||||
$cutting->description = $validated['description'] ?? null;
|
$cutting->description = $validated['description'] ?? null;
|
||||||
|
$cutting->sewing_cost = (int) ($validated['sewing_cost'] ?? 0);
|
||||||
|
$cutting->other_cost = (int) ($validated['other_cost'] ?? 0);
|
||||||
if ($cutting->status === CuttingStatus::REJECTED) {
|
if ($cutting->status === CuttingStatus::REJECTED) {
|
||||||
$cutting->status = CuttingStatus::IN_PROGRESS;
|
$cutting->status = CuttingStatus::IN_PROGRESS;
|
||||||
$cutting->rejection()?->delete();
|
$cutting->rejection()?->delete();
|
||||||
@ -677,12 +681,22 @@ private function appendCostPreview(Cutting $cutting): void
|
|||||||
}
|
}
|
||||||
|
|
||||||
$totalMaterialCost = $cutting->total_material_cost ?? $this->calculateTotalMaterialCost($cutting);
|
$totalMaterialCost = $cutting->total_material_cost ?? $this->calculateTotalMaterialCost($cutting);
|
||||||
|
$sewingCost = (int) ($cutting->sewing_cost ?? 0);
|
||||||
|
$otherCost = (int) ($cutting->other_cost ?? 0);
|
||||||
|
$totalProductionCost = $totalMaterialCost + $sewingCost + $otherCost;
|
||||||
$costPerUnit = $cutting->cost_per_unit ?? $this->calculateCostPerUnit($cutting);
|
$costPerUnit = $cutting->cost_per_unit ?? $this->calculateCostPerUnit($cutting);
|
||||||
|
|
||||||
$cutting->setAttribute('total_material_cost', $totalMaterialCost);
|
$cutting->setAttribute('total_material_cost', $totalMaterialCost);
|
||||||
$cutting->setAttribute('total_material_cost_formatted', 'Rp '.number_format($totalMaterialCost, 0, ',', '.'));
|
$cutting->setAttribute('total_material_cost_formatted', 'Rp '.number_format($totalMaterialCost, 0, ',', '.'));
|
||||||
|
$cutting->setAttribute('sewing_cost', $sewingCost);
|
||||||
|
$cutting->setAttribute('sewing_cost_formatted', 'Rp '.number_format($sewingCost, 0, ',', '.'));
|
||||||
|
$cutting->setAttribute('other_cost', $otherCost);
|
||||||
|
$cutting->setAttribute('other_cost_formatted', 'Rp '.number_format($otherCost, 0, ',', '.'));
|
||||||
|
$cutting->setAttribute('total_production_cost', $totalProductionCost);
|
||||||
|
$cutting->setAttribute('total_production_cost_formatted', 'Rp '.number_format($totalProductionCost, 0, ',', '.'));
|
||||||
$cutting->setAttribute('estimated_cost_per_unit', $costPerUnit);
|
$cutting->setAttribute('estimated_cost_per_unit', $costPerUnit);
|
||||||
$cutting->setAttribute('estimated_cost_per_unit_formatted', 'Rp '.number_format($costPerUnit, 0, ',', '.'));
|
$cutting->setAttribute('estimated_cost_per_unit_formatted', 'Rp '.number_format($costPerUnit, 0, ',', '.'));
|
||||||
|
$cutting->setAttribute('total_result_pieces', (int) $cutting->results->sum('cutting_result'));
|
||||||
}
|
}
|
||||||
|
|
||||||
public function calculateTotalMaterialCost(Cutting $cutting): int
|
public function calculateTotalMaterialCost(Cutting $cutting): int
|
||||||
@ -690,6 +704,13 @@ public function calculateTotalMaterialCost(Cutting $cutting): int
|
|||||||
return (int) $cutting->materials->sum(fn (CuttingMaterial $material) => $material->materialCost());
|
return (int) $cutting->materials->sum(fn (CuttingMaterial $material) => $material->materialCost());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function calculateTotalProductionCost(Cutting $cutting): int
|
||||||
|
{
|
||||||
|
return $this->calculateTotalMaterialCost($cutting)
|
||||||
|
+ (int) ($cutting->sewing_cost ?? 0)
|
||||||
|
+ (int) ($cutting->other_cost ?? 0);
|
||||||
|
}
|
||||||
|
|
||||||
public function calculateCostPerUnit(Cutting $cutting): int
|
public function calculateCostPerUnit(Cutting $cutting): int
|
||||||
{
|
{
|
||||||
$totalPieces = (int) $cutting->results->sum('cutting_result');
|
$totalPieces = (int) $cutting->results->sum('cutting_result');
|
||||||
@ -698,7 +719,7 @@ public function calculateCostPerUnit(Cutting $cutting): int
|
|||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
return (int) round($this->calculateTotalMaterialCost($cutting) / $totalPieces);
|
return (int) round($this->calculateTotalProductionCost($cutting) / $totalPieces);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@ -17,6 +17,8 @@ public function up(): void
|
|||||||
$table->string('description', 100)->nullable();
|
$table->string('description', 100)->nullable();
|
||||||
|
|
||||||
$table->unsignedBigInteger('total_material_cost')->nullable();
|
$table->unsignedBigInteger('total_material_cost')->nullable();
|
||||||
|
$table->unsignedBigInteger('sewing_cost')->default(0);
|
||||||
|
$table->unsignedBigInteger('other_cost')->default(0);
|
||||||
$table->unsignedBigInteger('cost_per_unit')->nullable();
|
$table->unsignedBigInteger('cost_per_unit')->nullable();
|
||||||
|
|
||||||
$table->foreignId('created_by_id')->constrained('users')->restrictOnDelete();
|
$table->foreignId('created_by_id')->constrained('users')->restrictOnDelete();
|
||||||
|
|||||||
@ -7,9 +7,17 @@ import PosCatalogCard from '@/components/admin/manage/PosCatalogCard.vue';
|
|||||||
import PosCatalogVariantThumb from '@/components/admin/manage/PosCatalogVariantThumb.vue';
|
import PosCatalogVariantThumb from '@/components/admin/manage/PosCatalogVariantThumb.vue';
|
||||||
import { DecimalInput } from '@/components/form/decimal-input';
|
import { DecimalInput } from '@/components/form/decimal-input';
|
||||||
import { NumberInput } from '@/components/form/number-input';
|
import { NumberInput } from '@/components/form/number-input';
|
||||||
|
import { RupiahInput } from '@/components/form/rupiah-input';
|
||||||
import { Badge } from '@/components/ui/badge';
|
import { Badge } from '@/components/ui/badge';
|
||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
||||||
|
import {
|
||||||
|
Dialog,
|
||||||
|
DialogContent,
|
||||||
|
DialogFooter,
|
||||||
|
DialogHeader,
|
||||||
|
DialogTitle,
|
||||||
|
} from '@/components/ui/dialog';
|
||||||
import {
|
import {
|
||||||
Empty,
|
Empty,
|
||||||
EmptyDescription,
|
EmptyDescription,
|
||||||
@ -29,7 +37,7 @@ import { Textarea } from '@/components/ui/textarea';
|
|||||||
import { getFirstCoverImage } from '@/lib/catalog-cover';
|
import { getFirstCoverImage } from '@/lib/catalog-cover';
|
||||||
import { FIELD_LIMITS } from '@/lib/field-limits';
|
import { FIELD_LIMITS } from '@/lib/field-limits';
|
||||||
import { formErrors } from '@/lib/form';
|
import { formErrors } from '@/lib/form';
|
||||||
import { formatRupiah } from '@/lib/rupiah';
|
import { formatRupiah, parseRupiah } from '@/lib/rupiah';
|
||||||
import type {
|
import type {
|
||||||
CuttingMaterialCartItem,
|
CuttingMaterialCartItem,
|
||||||
CuttingProductCatalogItem,
|
CuttingProductCatalogItem,
|
||||||
@ -41,6 +49,33 @@ function usesLengthUnit(unit: string): boolean {
|
|||||||
return unit === 'yard' || unit === 'meter';
|
return unit === 'yard' || unit === 'meter';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
rawMaterialCatalog: CuttingRawMaterialCatalogItem[];
|
||||||
|
productCatalog: CuttingProductCatalogItem[];
|
||||||
|
initialData?: {
|
||||||
|
description: string;
|
||||||
|
sewing_cost?: string;
|
||||||
|
other_cost?: string;
|
||||||
|
materials: CuttingMaterialCartItem[];
|
||||||
|
results: CuttingResultCartItem[];
|
||||||
|
};
|
||||||
|
submitUrl: string;
|
||||||
|
method: 'post' | 'put';
|
||||||
|
submitLabel: string;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const materialSearch = ref('');
|
||||||
|
const productSearch = ref('');
|
||||||
|
const materialCart = ref<CuttingMaterialCartItem[]>([]);
|
||||||
|
const resultCart = ref<CuttingResultCartItem[]>([]);
|
||||||
|
const costModalOpen = ref(false);
|
||||||
|
|
||||||
|
const form = useForm({
|
||||||
|
description: '',
|
||||||
|
sewing_cost: '0',
|
||||||
|
other_cost: '0',
|
||||||
|
});
|
||||||
|
|
||||||
const CM_PER_YARD = 91.44;
|
const CM_PER_YARD = 91.44;
|
||||||
const CM_PER_METER = 100;
|
const CM_PER_METER = 100;
|
||||||
|
|
||||||
@ -95,34 +130,38 @@ const totalResultPieces = computed(() =>
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
function materialLineCost(item: CuttingMaterialCartItem): number {
|
||||||
|
const catalogPrice = findCatalogPrice(item.raw_material_price_id);
|
||||||
|
|
||||||
|
if (!catalogPrice) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
const usage = usageInNativeUnit(
|
||||||
|
Number(item.material_usage) || 0,
|
||||||
|
catalogPrice.unit,
|
||||||
|
);
|
||||||
|
|
||||||
|
return Math.round(usage * catalogPrice.price);
|
||||||
|
}
|
||||||
|
|
||||||
|
const sewingCostAmount = computed(
|
||||||
|
() => Number.parseInt(parseRupiah(form.sewing_cost), 10) || 0,
|
||||||
|
);
|
||||||
|
const otherCostAmount = computed(
|
||||||
|
() => Number.parseInt(parseRupiah(form.other_cost), 10) || 0,
|
||||||
|
);
|
||||||
|
|
||||||
|
const totalProductionCost = computed(
|
||||||
|
() => totalMaterialCost.value + sewingCostAmount.value + otherCostAmount.value,
|
||||||
|
);
|
||||||
|
|
||||||
const estimatedCostPerUnit = computed(() => {
|
const estimatedCostPerUnit = computed(() => {
|
||||||
if (totalResultPieces.value <= 0) {
|
if (totalResultPieces.value <= 0) {
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
return Math.round(totalMaterialCost.value / totalResultPieces.value);
|
return Math.round(totalProductionCost.value / totalResultPieces.value);
|
||||||
});
|
|
||||||
|
|
||||||
const props = defineProps<{
|
|
||||||
rawMaterialCatalog: CuttingRawMaterialCatalogItem[];
|
|
||||||
productCatalog: CuttingProductCatalogItem[];
|
|
||||||
initialData?: {
|
|
||||||
description: string;
|
|
||||||
materials: CuttingMaterialCartItem[];
|
|
||||||
results: CuttingResultCartItem[];
|
|
||||||
};
|
|
||||||
submitUrl: string;
|
|
||||||
method: 'post' | 'put';
|
|
||||||
submitLabel: string;
|
|
||||||
}>();
|
|
||||||
|
|
||||||
const materialSearch = ref('');
|
|
||||||
const productSearch = ref('');
|
|
||||||
const materialCart = ref<CuttingMaterialCartItem[]>([]);
|
|
||||||
const resultCart = ref<CuttingResultCartItem[]>([]);
|
|
||||||
|
|
||||||
const form = useForm({
|
|
||||||
description: '',
|
|
||||||
});
|
});
|
||||||
|
|
||||||
function populateForm() {
|
function populateForm() {
|
||||||
@ -131,6 +170,8 @@ function populateForm() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
form.description = props.initialData.description;
|
form.description = props.initialData.description;
|
||||||
|
form.sewing_cost = props.initialData.sewing_cost ?? '0';
|
||||||
|
form.other_cost = props.initialData.other_cost ?? '0';
|
||||||
materialCart.value = props.initialData.materials.map((item) => ({
|
materialCart.value = props.initialData.materials.map((item) => ({
|
||||||
...item,
|
...item,
|
||||||
}));
|
}));
|
||||||
@ -145,6 +186,16 @@ watch(
|
|||||||
{ immediate: true },
|
{ immediate: true },
|
||||||
);
|
);
|
||||||
|
|
||||||
|
watch(
|
||||||
|
[materialCart, resultCart],
|
||||||
|
() => {
|
||||||
|
if (materialCart.value.length > 0 && resultCart.value.length > 0) {
|
||||||
|
costModalOpen.value = true;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ deep: true },
|
||||||
|
);
|
||||||
|
|
||||||
type CatalogPrice = CuttingRawMaterialCatalogItem['prices'][number];
|
type CatalogPrice = CuttingRawMaterialCatalogItem['prices'][number];
|
||||||
|
|
||||||
const filteredRawMaterials = computed(() => {
|
const filteredRawMaterials = computed(() => {
|
||||||
@ -312,35 +363,11 @@ function syncResultTotals(item: CuttingResultCartItem) {
|
|||||||
item.cutting_reject = String(reject);
|
item.cutting_reject = String(reject);
|
||||||
}
|
}
|
||||||
|
|
||||||
function submit() {
|
function buildPayload() {
|
||||||
if (materialCart.value.length === 0) {
|
return {
|
||||||
toast.error('Tambahkan minimal satu bahan baku.');
|
|
||||||
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (resultCart.value.length === 0) {
|
|
||||||
toast.error('Tambahkan minimal satu hasil produk.');
|
|
||||||
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const item of resultCart.value) {
|
|
||||||
const total = Number(item.cutting_result) || 0;
|
|
||||||
const warehouse = Number(item.warehouse_stock) || 0;
|
|
||||||
const reject = Number(item.cutting_reject) || 0;
|
|
||||||
|
|
||||||
if (warehouse + reject !== total) {
|
|
||||||
toast.error(
|
|
||||||
`Hasil cutting ${item.product_name} - ${item.variant_name} harus sama dengan stok bagus + reject.`,
|
|
||||||
);
|
|
||||||
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const payload = {
|
|
||||||
description: form.description,
|
description: form.description,
|
||||||
|
sewing_cost: Number.parseInt(parseRupiah(form.sewing_cost), 10) || 0,
|
||||||
|
other_cost: Number.parseInt(parseRupiah(form.other_cost), 10) || 0,
|
||||||
materials: materialCart.value.map((item) => ({
|
materials: materialCart.value.map((item) => ({
|
||||||
raw_material_price_id: item.raw_material_price_id,
|
raw_material_price_id: item.raw_material_price_id,
|
||||||
material_usage: item.material_usage,
|
material_usage: item.material_usage,
|
||||||
@ -353,9 +380,54 @@ function submit() {
|
|||||||
cutting_reject: item.cutting_reject,
|
cutting_reject: item.cutting_reject,
|
||||||
})),
|
})),
|
||||||
};
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function validateBeforeSave(): boolean {
|
||||||
|
if (materialCart.value.length === 0) {
|
||||||
|
toast.error('Tambahkan minimal satu bahan baku.');
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (resultCart.value.length === 0) {
|
||||||
|
toast.error('Tambahkan minimal satu hasil produk.');
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const item of resultCart.value) {
|
||||||
|
const total = Number(item.cutting_result) || 0;
|
||||||
|
const warehouse = Number(item.warehouse_stock) || 0;
|
||||||
|
const reject = Number(item.cutting_reject) || 0;
|
||||||
|
|
||||||
|
if (warehouse + reject !== total) {
|
||||||
|
toast.error(
|
||||||
|
`Hasil cutting ${item.product_name} - ${item.variant_name} harus sama dengan stok bagus + reject.`,
|
||||||
|
);
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
function submit() {
|
||||||
|
if (!validateBeforeSave()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
costModalOpen.value = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
function confirmSubmit() {
|
||||||
|
const payload = buildPayload();
|
||||||
|
|
||||||
if (props.method === 'put') {
|
if (props.method === 'put') {
|
||||||
form.transform(() => payload).put(props.submitUrl, {
|
form.transform(() => payload).put(props.submitUrl, {
|
||||||
|
onSuccess: () => {
|
||||||
|
costModalOpen.value = false;
|
||||||
|
},
|
||||||
onError: (errors) => {
|
onError: (errors) => {
|
||||||
const message = Object.values(errors)[0];
|
const message = Object.values(errors)[0];
|
||||||
toast.error(
|
toast.error(
|
||||||
@ -370,6 +442,9 @@ function submit() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
form.transform(() => payload).post(props.submitUrl, {
|
form.transform(() => payload).post(props.submitUrl, {
|
||||||
|
onSuccess: () => {
|
||||||
|
costModalOpen.value = false;
|
||||||
|
},
|
||||||
onError: (errors) => {
|
onError: (errors) => {
|
||||||
const message = Object.values(errors)[0];
|
const message = Object.values(errors)[0];
|
||||||
toast.error(
|
toast.error(
|
||||||
@ -653,9 +728,18 @@ function submit() {
|
|||||||
|
|
||||||
<Card class="h-fit xl:sticky xl:top-4">
|
<Card class="h-fit xl:sticky xl:top-4">
|
||||||
<CardHeader class="pb-3">
|
<CardHeader class="pb-3">
|
||||||
<CardTitle class="flex items-center gap-2 text-base">
|
<CardTitle class="flex items-center justify-between gap-2 text-base">
|
||||||
<Scissors class="size-4" />
|
<span class="flex items-center gap-2">
|
||||||
Ringkasan Cutting
|
<Scissors class="size-4" />
|
||||||
|
Ringkasan Cutting
|
||||||
|
</span>
|
||||||
|
<Badge
|
||||||
|
v-if="totalResultPieces > 0"
|
||||||
|
variant="secondary"
|
||||||
|
class="tabular-nums font-semibold"
|
||||||
|
>
|
||||||
|
Total: {{ totalResultPieces }} pcs
|
||||||
|
</Badge>
|
||||||
</CardTitle>
|
</CardTitle>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent>
|
<CardContent>
|
||||||
@ -722,7 +806,7 @@ function submit() {
|
|||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="grid grid-cols-2 gap-2">
|
<div class="grid grid-cols-3 gap-2">
|
||||||
<Field>
|
<Field>
|
||||||
<FieldLabel class="text-xs">
|
<FieldLabel class="text-xs">
|
||||||
Pemakaian ({{ item.uses_length_unit ? 'cm' : item.unit_abbreviation }})
|
Pemakaian ({{ item.uses_length_unit ? 'cm' : item.unit_abbreviation }})
|
||||||
@ -745,6 +829,16 @@ function submit() {
|
|||||||
class="h-8"
|
class="h-8"
|
||||||
/>
|
/>
|
||||||
</Field>
|
</Field>
|
||||||
|
<Field>
|
||||||
|
<FieldLabel class="text-xs">
|
||||||
|
Harga Modal
|
||||||
|
</FieldLabel>
|
||||||
|
<div
|
||||||
|
class="flex h-8 items-center rounded-md border bg-muted/40 px-2 text-xs font-medium tabular-nums"
|
||||||
|
>
|
||||||
|
{{ formatRupiah(materialLineCost(item)) }}
|
||||||
|
</div>
|
||||||
|
</Field>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -753,7 +847,16 @@ function submit() {
|
|||||||
<Separator />
|
<Separator />
|
||||||
|
|
||||||
<div class="space-y-2">
|
<div class="space-y-2">
|
||||||
<p class="text-sm font-medium">Hasil Produk</p>
|
<div class="flex items-center justify-between gap-2">
|
||||||
|
<p class="text-sm font-medium">Hasil Produk</p>
|
||||||
|
<Badge
|
||||||
|
v-if="totalResultPieces > 0"
|
||||||
|
variant="outline"
|
||||||
|
class="tabular-nums text-xs"
|
||||||
|
>
|
||||||
|
{{ totalResultPieces }} pcs
|
||||||
|
</Badge>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
v-if="resultCart.length === 0"
|
v-if="resultCart.length === 0"
|
||||||
@ -840,29 +943,41 @@ function submit() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div
|
<Separator />
|
||||||
v-if="materialCart.length > 0"
|
|
||||||
class="rounded-lg border bg-muted/40 p-3 text-sm"
|
<div class="grid gap-3 sm:grid-cols-2">
|
||||||
>
|
<Field>
|
||||||
<p class="font-medium">Estimasi Harga Modal</p>
|
<FieldLabel for="sewing_cost">Jasa Jahit</FieldLabel>
|
||||||
<p class="text-muted-foreground">
|
<RupiahInput
|
||||||
Total bahan baku:
|
id="sewing_cost"
|
||||||
{{ formatRupiah(totalMaterialCost) }}
|
v-model="form.sewing_cost"
|
||||||
</p>
|
/>
|
||||||
<p
|
<FieldError
|
||||||
v-if="totalResultPieces > 0"
|
:errors="formErrors(form, 'sewing_cost')"
|
||||||
class="font-semibold tabular-nums"
|
/>
|
||||||
>
|
</Field>
|
||||||
{{ formatRupiah(estimatedCostPerUnit) }} / pcs
|
<Field>
|
||||||
</p>
|
<FieldLabel for="other_cost">Biaya Lainnya</FieldLabel>
|
||||||
<p
|
<RupiahInput
|
||||||
v-else
|
id="other_cost"
|
||||||
class="text-xs text-muted-foreground"
|
v-model="form.other_cost"
|
||||||
>
|
/>
|
||||||
Tambahkan hasil produk untuk estimasi per pcs.
|
<FieldError
|
||||||
</p>
|
:errors="formErrors(form, 'other_cost')"
|
||||||
|
/>
|
||||||
|
</Field>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant="outline"
|
||||||
|
class="w-full"
|
||||||
|
:disabled="materialCart.length === 0"
|
||||||
|
@click="costModalOpen = true"
|
||||||
|
>
|
||||||
|
Lihat Harga Modal
|
||||||
|
</Button>
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
type="submit"
|
type="submit"
|
||||||
class="w-full"
|
class="w-full"
|
||||||
@ -885,4 +1000,78 @@ function submit() {
|
|||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<Dialog v-model:open="costModalOpen">
|
||||||
|
<DialogContent class="sm:max-w-md">
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle>Ringkasan Harga Modal</DialogTitle>
|
||||||
|
</DialogHeader>
|
||||||
|
|
||||||
|
<div class="space-y-3 text-sm">
|
||||||
|
<div class="flex items-center justify-between rounded-lg border bg-muted/40 px-3 py-2">
|
||||||
|
<span class="text-muted-foreground">Total Hasil Cutting</span>
|
||||||
|
<span class="font-semibold tabular-nums">{{ totalResultPieces }} pcs</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="space-y-2 rounded-lg border p-3">
|
||||||
|
<div class="flex items-center justify-between gap-3">
|
||||||
|
<span class="text-muted-foreground">Bahan Baku</span>
|
||||||
|
<span class="font-medium tabular-nums">{{ formatRupiah(totalMaterialCost) }}</span>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
v-for="item in materialCart"
|
||||||
|
:key="`cost-${item.raw_material_price_id}`"
|
||||||
|
class="flex items-center justify-between gap-3 text-xs"
|
||||||
|
>
|
||||||
|
<span class="truncate text-muted-foreground">
|
||||||
|
{{ item.raw_material_name }} ({{ item.variant }})
|
||||||
|
</span>
|
||||||
|
<span class="shrink-0 tabular-nums">
|
||||||
|
{{ formatRupiah(materialLineCost(item)) }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center justify-between gap-3 border-t pt-2">
|
||||||
|
<span class="text-muted-foreground">Jasa Jahit</span>
|
||||||
|
<span class="font-medium tabular-nums">{{ formatRupiah(sewingCostAmount) }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center justify-between gap-3">
|
||||||
|
<span class="text-muted-foreground">Biaya Lainnya</span>
|
||||||
|
<span class="font-medium tabular-nums">{{ formatRupiah(otherCostAmount) }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center justify-between gap-3 border-t pt-2 font-semibold">
|
||||||
|
<span>Total Harga Modal</span>
|
||||||
|
<span class="tabular-nums">{{ formatRupiah(totalProductionCost) }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
v-if="totalResultPieces > 0"
|
||||||
|
class="flex items-center justify-between rounded-lg border border-primary/30 bg-primary/5 px-3 py-2 font-semibold"
|
||||||
|
>
|
||||||
|
<span>Harga Modal / pcs</span>
|
||||||
|
<span class="tabular-nums">{{ formatRupiah(estimatedCostPerUnit) }}</span>
|
||||||
|
</div>
|
||||||
|
<p v-else class="text-xs text-muted-foreground">
|
||||||
|
Tambahkan hasil produk untuk menghitung harga modal per pcs.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<DialogFooter class="gap-2 sm:justify-end">
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant="outline"
|
||||||
|
@click="costModalOpen = false"
|
||||||
|
>
|
||||||
|
Tutup
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
:disabled="form.processing"
|
||||||
|
@click="confirmSubmit"
|
||||||
|
>
|
||||||
|
{{ form.processing ? 'Menyimpan...' : submitLabel }}
|
||||||
|
</Button>
|
||||||
|
</DialogFooter>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@ -479,13 +479,30 @@ function actionIcon(status: string) {
|
|||||||
|
|
||||||
<div
|
<div
|
||||||
v-if="cutting.estimated_cost_per_unit_formatted"
|
v-if="cutting.estimated_cost_per_unit_formatted"
|
||||||
class="rounded-lg border bg-muted/40 p-3 text-sm"
|
class="rounded-lg border bg-muted/40 p-3 text-sm space-y-1"
|
||||||
>
|
>
|
||||||
<p class="font-medium">Harga Modal Batch</p>
|
<p class="font-medium">Harga Modal Batch</p>
|
||||||
<p class="text-muted-foreground">
|
<p class="flex justify-between gap-2 text-muted-foreground">
|
||||||
Total bahan baku: {{ cutting.total_material_cost_formatted ?? '-' }}
|
<span>Bahan baku</span>
|
||||||
|
<span>{{ cutting.total_material_cost_formatted ?? '-' }}</span>
|
||||||
</p>
|
</p>
|
||||||
<p class="font-semibold tabular-nums">
|
<p class="flex justify-between gap-2 text-muted-foreground">
|
||||||
|
<span>Jasa jahit</span>
|
||||||
|
<span>{{ cutting.sewing_cost_formatted ?? '-' }}</span>
|
||||||
|
</p>
|
||||||
|
<p class="flex justify-between gap-2 text-muted-foreground">
|
||||||
|
<span>Biaya lainnya</span>
|
||||||
|
<span>{{ cutting.other_cost_formatted ?? '-' }}</span>
|
||||||
|
</p>
|
||||||
|
<p class="flex justify-between gap-2 font-medium">
|
||||||
|
<span>Total</span>
|
||||||
|
<span>{{ cutting.total_production_cost_formatted ?? '-' }}</span>
|
||||||
|
</p>
|
||||||
|
<p class="flex justify-between gap-2 text-muted-foreground">
|
||||||
|
<span>Total hasil</span>
|
||||||
|
<span>{{ cutting.total_result_pieces ?? 0 }} pcs</span>
|
||||||
|
</p>
|
||||||
|
<p class="font-semibold tabular-nums pt-1 border-t">
|
||||||
{{ cutting.estimated_cost_per_unit_formatted }} / pcs
|
{{ cutting.estimated_cost_per_unit_formatted }} / pcs
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -23,6 +23,8 @@ function usesLengthUnit(unit?: string): boolean {
|
|||||||
|
|
||||||
const initialData = computed(() => ({
|
const initialData = computed(() => ({
|
||||||
description: props.cutting.description ?? '',
|
description: props.cutting.description ?? '',
|
||||||
|
sewing_cost: String(props.cutting.sewing_cost ?? 0),
|
||||||
|
other_cost: String(props.cutting.other_cost ?? 0),
|
||||||
materials: props.cutting.materials.map((item) => {
|
materials: props.cutting.materials.map((item) => {
|
||||||
const unit = item.raw_material_price?.raw_material?.unit ?? 'kilogram';
|
const unit = item.raw_material_price?.raw_material?.unit ?? 'kilogram';
|
||||||
const usesCm = usesLengthUnit(unit);
|
const usesCm = usesLengthUnit(unit);
|
||||||
|
|||||||
@ -49,6 +49,13 @@ export type CuttingListItem = {
|
|||||||
created_at_formatted: string;
|
created_at_formatted: string;
|
||||||
total_material_cost?: number;
|
total_material_cost?: number;
|
||||||
total_material_cost_formatted?: string;
|
total_material_cost_formatted?: string;
|
||||||
|
sewing_cost?: number;
|
||||||
|
sewing_cost_formatted?: string;
|
||||||
|
other_cost?: number;
|
||||||
|
other_cost_formatted?: string;
|
||||||
|
total_production_cost?: number;
|
||||||
|
total_production_cost_formatted?: string;
|
||||||
|
total_result_pieces?: number;
|
||||||
estimated_cost_per_unit?: number;
|
estimated_cost_per_unit?: number;
|
||||||
estimated_cost_per_unit_formatted?: string;
|
estimated_cost_per_unit_formatted?: string;
|
||||||
created_by?: {
|
created_by?: {
|
||||||
@ -97,6 +104,8 @@ export type CuttingEditItem = {
|
|||||||
id: number;
|
id: number;
|
||||||
status: string;
|
status: string;
|
||||||
description: string | null;
|
description: string | null;
|
||||||
|
sewing_cost?: number;
|
||||||
|
other_cost?: number;
|
||||||
materials: Array<{
|
materials: Array<{
|
||||||
raw_material_price_id: number;
|
raw_material_price_id: number;
|
||||||
material_usage_input: string;
|
material_usage_input: string;
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user