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.*.warehouse_stock' => ['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.*.product_variant_id' => 'varian produk',
|
||||
'results.*.cutting_result' => 'hasil',
|
||||
'results.*.warehouse_stock' => 'gudang',
|
||||
'results.*.warehouse_stock' => 'bagus',
|
||||
'results.*.cutting_reject' => 'reject',
|
||||
'sewing_cost' => 'jasa jahit',
|
||||
'other_cost' => 'biaya lainnya',
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
@ -31,6 +31,8 @@ protected function casts(): array
|
||||
return [
|
||||
'status' => CuttingStatus::class,
|
||||
'total_material_cost' => 'integer',
|
||||
'sewing_cost' => 'integer',
|
||||
'other_cost' => 'integer',
|
||||
'cost_per_unit' => 'integer',
|
||||
];
|
||||
}
|
||||
|
||||
@ -249,6 +249,8 @@ public function create(array $validated, User $user): Cutting
|
||||
$cutting = Cutting::create([
|
||||
'status' => CuttingStatus::IN_PROGRESS,
|
||||
'description' => $validated['description'] ?? null,
|
||||
'sewing_cost' => (int) ($validated['sewing_cost'] ?? 0),
|
||||
'other_cost' => (int) ($validated['other_cost'] ?? 0),
|
||||
'created_by_id' => $user->id,
|
||||
]);
|
||||
|
||||
@ -303,6 +305,8 @@ public function update(Cutting $cutting, array $validated): void
|
||||
$results = $this->buildResults($validated['results']);
|
||||
|
||||
$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) {
|
||||
$cutting->status = CuttingStatus::IN_PROGRESS;
|
||||
$cutting->rejection()?->delete();
|
||||
@ -677,12 +681,22 @@ private function appendCostPreview(Cutting $cutting): void
|
||||
}
|
||||
|
||||
$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);
|
||||
|
||||
$cutting->setAttribute('total_material_cost', $totalMaterialCost);
|
||||
$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_formatted', 'Rp '.number_format($costPerUnit, 0, ',', '.'));
|
||||
$cutting->setAttribute('total_result_pieces', (int) $cutting->results->sum('cutting_result'));
|
||||
}
|
||||
|
||||
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());
|
||||
}
|
||||
|
||||
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
|
||||
{
|
||||
$totalPieces = (int) $cutting->results->sum('cutting_result');
|
||||
@ -698,7 +719,7 @@ public function calculateCostPerUnit(Cutting $cutting): int
|
||||
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->unsignedBigInteger('total_material_cost')->nullable();
|
||||
$table->unsignedBigInteger('sewing_cost')->default(0);
|
||||
$table->unsignedBigInteger('other_cost')->default(0);
|
||||
$table->unsignedBigInteger('cost_per_unit')->nullable();
|
||||
|
||||
$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 { DecimalInput } from '@/components/form/decimal-input';
|
||||
import { NumberInput } from '@/components/form/number-input';
|
||||
import { RupiahInput } from '@/components/form/rupiah-input';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from '@/components/ui/dialog';
|
||||
import {
|
||||
Empty,
|
||||
EmptyDescription,
|
||||
@ -29,7 +37,7 @@ import { Textarea } from '@/components/ui/textarea';
|
||||
import { getFirstCoverImage } from '@/lib/catalog-cover';
|
||||
import { FIELD_LIMITS } from '@/lib/field-limits';
|
||||
import { formErrors } from '@/lib/form';
|
||||
import { formatRupiah } from '@/lib/rupiah';
|
||||
import { formatRupiah, parseRupiah } from '@/lib/rupiah';
|
||||
import type {
|
||||
CuttingMaterialCartItem,
|
||||
CuttingProductCatalogItem,
|
||||
@ -41,6 +49,33 @@ function usesLengthUnit(unit: string): boolean {
|
||||
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_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(() => {
|
||||
if (totalResultPieces.value <= 0) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
return Math.round(totalMaterialCost.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: '',
|
||||
return Math.round(totalProductionCost.value / totalResultPieces.value);
|
||||
});
|
||||
|
||||
function populateForm() {
|
||||
@ -131,6 +170,8 @@ function populateForm() {
|
||||
}
|
||||
|
||||
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) => ({
|
||||
...item,
|
||||
}));
|
||||
@ -145,6 +186,16 @@ watch(
|
||||
{ immediate: true },
|
||||
);
|
||||
|
||||
watch(
|
||||
[materialCart, resultCart],
|
||||
() => {
|
||||
if (materialCart.value.length > 0 && resultCart.value.length > 0) {
|
||||
costModalOpen.value = true;
|
||||
}
|
||||
},
|
||||
{ deep: true },
|
||||
);
|
||||
|
||||
type CatalogPrice = CuttingRawMaterialCatalogItem['prices'][number];
|
||||
|
||||
const filteredRawMaterials = computed(() => {
|
||||
@ -312,35 +363,11 @@ function syncResultTotals(item: CuttingResultCartItem) {
|
||||
item.cutting_reject = String(reject);
|
||||
}
|
||||
|
||||
function submit() {
|
||||
if (materialCart.value.length === 0) {
|
||||
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 = {
|
||||
function buildPayload() {
|
||||
return {
|
||||
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) => ({
|
||||
raw_material_price_id: item.raw_material_price_id,
|
||||
material_usage: item.material_usage,
|
||||
@ -353,9 +380,54 @@ function submit() {
|
||||
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') {
|
||||
form.transform(() => payload).put(props.submitUrl, {
|
||||
onSuccess: () => {
|
||||
costModalOpen.value = false;
|
||||
},
|
||||
onError: (errors) => {
|
||||
const message = Object.values(errors)[0];
|
||||
toast.error(
|
||||
@ -370,6 +442,9 @@ function submit() {
|
||||
}
|
||||
|
||||
form.transform(() => payload).post(props.submitUrl, {
|
||||
onSuccess: () => {
|
||||
costModalOpen.value = false;
|
||||
},
|
||||
onError: (errors) => {
|
||||
const message = Object.values(errors)[0];
|
||||
toast.error(
|
||||
@ -653,9 +728,18 @@ function submit() {
|
||||
|
||||
<Card class="h-fit xl:sticky xl:top-4">
|
||||
<CardHeader class="pb-3">
|
||||
<CardTitle class="flex items-center gap-2 text-base">
|
||||
<Scissors class="size-4" />
|
||||
Ringkasan Cutting
|
||||
<CardTitle class="flex items-center justify-between gap-2 text-base">
|
||||
<span class="flex items-center gap-2">
|
||||
<Scissors class="size-4" />
|
||||
Ringkasan Cutting
|
||||
</span>
|
||||
<Badge
|
||||
v-if="totalResultPieces > 0"
|
||||
variant="secondary"
|
||||
class="tabular-nums font-semibold"
|
||||
>
|
||||
Total: {{ totalResultPieces }} pcs
|
||||
</Badge>
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
@ -722,7 +806,7 @@ function submit() {
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-2 gap-2">
|
||||
<div class="grid grid-cols-3 gap-2">
|
||||
<Field>
|
||||
<FieldLabel class="text-xs">
|
||||
Pemakaian ({{ item.uses_length_unit ? 'cm' : item.unit_abbreviation }})
|
||||
@ -745,6 +829,16 @@ function submit() {
|
||||
class="h-8"
|
||||
/>
|
||||
</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>
|
||||
@ -753,7 +847,16 @@ function submit() {
|
||||
<Separator />
|
||||
|
||||
<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
|
||||
v-if="resultCart.length === 0"
|
||||
@ -840,29 +943,41 @@ function submit() {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="materialCart.length > 0"
|
||||
class="rounded-lg border bg-muted/40 p-3 text-sm"
|
||||
>
|
||||
<p class="font-medium">Estimasi Harga Modal</p>
|
||||
<p class="text-muted-foreground">
|
||||
Total bahan baku:
|
||||
{{ formatRupiah(totalMaterialCost) }}
|
||||
</p>
|
||||
<p
|
||||
v-if="totalResultPieces > 0"
|
||||
class="font-semibold tabular-nums"
|
||||
>
|
||||
{{ formatRupiah(estimatedCostPerUnit) }} / pcs
|
||||
</p>
|
||||
<p
|
||||
v-else
|
||||
class="text-xs text-muted-foreground"
|
||||
>
|
||||
Tambahkan hasil produk untuk estimasi per pcs.
|
||||
</p>
|
||||
<Separator />
|
||||
|
||||
<div class="grid gap-3 sm:grid-cols-2">
|
||||
<Field>
|
||||
<FieldLabel for="sewing_cost">Jasa Jahit</FieldLabel>
|
||||
<RupiahInput
|
||||
id="sewing_cost"
|
||||
v-model="form.sewing_cost"
|
||||
/>
|
||||
<FieldError
|
||||
:errors="formErrors(form, 'sewing_cost')"
|
||||
/>
|
||||
</Field>
|
||||
<Field>
|
||||
<FieldLabel for="other_cost">Biaya Lainnya</FieldLabel>
|
||||
<RupiahInput
|
||||
id="other_cost"
|
||||
v-model="form.other_cost"
|
||||
/>
|
||||
<FieldError
|
||||
:errors="formErrors(form, 'other_cost')"
|
||||
/>
|
||||
</Field>
|
||||
</div>
|
||||
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
class="w-full"
|
||||
:disabled="materialCart.length === 0"
|
||||
@click="costModalOpen = true"
|
||||
>
|
||||
Lihat Harga Modal
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
type="submit"
|
||||
class="w-full"
|
||||
@ -885,4 +1000,78 @@ function submit() {
|
||||
</CardContent>
|
||||
</Card>
|
||||
</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>
|
||||
|
||||
@ -479,13 +479,30 @@ function actionIcon(status: string) {
|
||||
|
||||
<div
|
||||
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="text-muted-foreground">
|
||||
Total bahan baku: {{ cutting.total_material_cost_formatted ?? '-' }}
|
||||
<p class="flex justify-between gap-2 text-muted-foreground">
|
||||
<span>Bahan baku</span>
|
||||
<span>{{ cutting.total_material_cost_formatted ?? '-' }}</span>
|
||||
</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
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@ -23,6 +23,8 @@ function usesLengthUnit(unit?: string): boolean {
|
||||
|
||||
const initialData = computed(() => ({
|
||||
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) => {
|
||||
const unit = item.raw_material_price?.raw_material?.unit ?? 'kilogram';
|
||||
const usesCm = usesLengthUnit(unit);
|
||||
|
||||
@ -49,6 +49,13 @@ export type CuttingListItem = {
|
||||
created_at_formatted: string;
|
||||
total_material_cost?: number;
|
||||
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_formatted?: string;
|
||||
created_by?: {
|
||||
@ -97,6 +104,8 @@ export type CuttingEditItem = {
|
||||
id: number;
|
||||
status: string;
|
||||
description: string | null;
|
||||
sewing_cost?: number;
|
||||
other_cost?: number;
|
||||
materials: Array<{
|
||||
raw_material_price_id: number;
|
||||
material_usage_input: string;
|
||||
|
||||
Loading…
Reference in New Issue
Block a user