feat: add material_result field to CuttingMaterial and CuttingMaterialCombination; update validation rules and service logic to handle new material result calculations; enhance frontend components for displaying material results in cutting management
This commit is contained in:
parent
a3ff10d6e1
commit
15e2b7ec5b
@ -26,6 +26,7 @@ public function rules(): array
|
|||||||
Rule::exists('raw_material_prices', 'id')->whereNull('deleted_at'),
|
Rule::exists('raw_material_prices', 'id')->whereNull('deleted_at'),
|
||||||
],
|
],
|
||||||
'materials.*.material_usage' => ['required', 'numeric', 'decimal:0,4', 'gt:0'],
|
'materials.*.material_usage' => ['required', 'numeric', 'decimal:0,4', 'gt:0'],
|
||||||
|
'material_result' => ['nullable', 'integer', 'gte:0'],
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -25,6 +25,7 @@ public function rules(): array
|
|||||||
Rule::exists('raw_material_prices', 'id')->whereNull('deleted_at'),
|
Rule::exists('raw_material_prices', 'id')->whereNull('deleted_at'),
|
||||||
],
|
],
|
||||||
'material_usage' => ['required', 'numeric', 'decimal:0,4', 'gt:0'],
|
'material_usage' => ['required', 'numeric', 'decimal:0,4', 'gt:0'],
|
||||||
|
'material_result' => ['nullable', 'integer', 'gte:0'],
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -42,7 +42,9 @@ public function rules(): array
|
|||||||
Rule::exists('raw_material_prices', 'id')->whereNull('deleted_at'),
|
Rule::exists('raw_material_prices', 'id')->whereNull('deleted_at'),
|
||||||
];
|
];
|
||||||
$rules['materials.*.material_usage'] = ['required', 'numeric', 'decimal:0,4', 'gt:0'];
|
$rules['materials.*.material_usage'] = ['required', 'numeric', 'decimal:0,4', 'gt:0'];
|
||||||
$rules['materials.*.combination_id'] = ['nullable', 'integer', 'exists:cutting_material_combinations,id'];
|
$rules['materials.*.material_result'] = ['nullable', 'integer', 'min:0'];
|
||||||
|
$rules['materials.*.combination_id'] = ['nullable', 'integer'];
|
||||||
|
$rules['materials.*.combination_material_result'] = ['nullable', 'integer', 'min:0'];
|
||||||
|
|
||||||
$rules['results'] = ['required', 'array', 'min:1'];
|
$rules['results'] = ['required', 'array', 'min:1'];
|
||||||
$rules['results.*.product_variant_id'] = [
|
$rules['results.*.product_variant_id'] = [
|
||||||
@ -73,6 +75,8 @@ public function attributes(): array
|
|||||||
'materials' => 'Bahan Baku',
|
'materials' => 'Bahan Baku',
|
||||||
'materials.*.raw_material_price_id' => 'Bahan Baku',
|
'materials.*.raw_material_price_id' => 'Bahan Baku',
|
||||||
'materials.*.material_usage' => 'Pemakaian',
|
'materials.*.material_usage' => 'Pemakaian',
|
||||||
|
'materials.*.material_result' => 'Hasil',
|
||||||
|
'materials.*.combination_material_result' => 'Hasil Kombinasi',
|
||||||
'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',
|
||||||
|
|||||||
@ -113,6 +113,11 @@ public function materials(): HasMany
|
|||||||
return $this->hasMany(CuttingMaterial::class);
|
return $this->hasMany(CuttingMaterial::class);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function combinations(): HasMany
|
||||||
|
{
|
||||||
|
return $this->hasMany(CuttingMaterialCombination::class);
|
||||||
|
}
|
||||||
|
|
||||||
public function resultPrices(): HasMany
|
public function resultPrices(): HasMany
|
||||||
{
|
{
|
||||||
return $this->hasMany(CuttingResultPrice::class);
|
return $this->hasMany(CuttingResultPrice::class);
|
||||||
|
|||||||
@ -27,6 +27,7 @@ protected function casts(): array
|
|||||||
{
|
{
|
||||||
return [
|
return [
|
||||||
'material_usage' => 'decimal:4',
|
'material_usage' => 'decimal:4',
|
||||||
|
'material_result' => 'integer',
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -15,6 +15,13 @@ class CuttingMaterialCombination extends Model
|
|||||||
{
|
{
|
||||||
use HasFactory, InteractsWithActivityLog, SoftDeletes;
|
use HasFactory, InteractsWithActivityLog, SoftDeletes;
|
||||||
|
|
||||||
|
protected function casts(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'material_result' => 'integer',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
public function cutting(): BelongsTo
|
public function cutting(): BelongsTo
|
||||||
{
|
{
|
||||||
return $this->belongsTo(Cutting::class);
|
return $this->belongsTo(Cutting::class);
|
||||||
|
|||||||
@ -301,6 +301,10 @@ public function syncDraftMaterial(array $validated, User $user): array
|
|||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$materialResult = array_key_exists('material_result', $validated) && $validated['material_result'] !== null
|
||||||
|
? (int) $validated['material_result']
|
||||||
|
: null;
|
||||||
|
|
||||||
$item = CuttingMaterial::query()->updateOrCreate(
|
$item = CuttingMaterial::query()->updateOrCreate(
|
||||||
[
|
[
|
||||||
'user_id' => $user->id,
|
'user_id' => $user->id,
|
||||||
@ -309,6 +313,7 @@ public function syncDraftMaterial(array $validated, User $user): array
|
|||||||
],
|
],
|
||||||
[
|
[
|
||||||
'material_usage' => $materialUsage,
|
'material_usage' => $materialUsage,
|
||||||
|
'material_result' => $materialResult,
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -409,9 +414,14 @@ public function removeDraftResult(User $user, ProductVariant $productVariant): v
|
|||||||
*/
|
*/
|
||||||
public function syncDraftCombination(array $validated, User $user): array
|
public function syncDraftCombination(array $validated, User $user): array
|
||||||
{
|
{
|
||||||
|
$materialResult = array_key_exists('material_result', $validated) && $validated['material_result'] !== null
|
||||||
|
? (int) $validated['material_result']
|
||||||
|
: null;
|
||||||
|
|
||||||
$combination = CuttingMaterialCombination::create([
|
$combination = CuttingMaterialCombination::create([
|
||||||
'user_id' => $user->id,
|
'user_id' => $user->id,
|
||||||
'cutting_id' => null,
|
'cutting_id' => null,
|
||||||
|
'material_result' => $materialResult,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$items = [];
|
$items = [];
|
||||||
@ -558,7 +568,7 @@ public function update(Cutting $cutting, array $validated): void
|
|||||||
|
|
||||||
$this->runInTransaction(
|
$this->runInTransaction(
|
||||||
function () use ($cutting, $validated): void {
|
function () use ($cutting, $validated): void {
|
||||||
$cutting->load(['materials.rawMaterialPrice.rawMaterial', 'results']);
|
$cutting->load(['materials.rawMaterialPrice.rawMaterial', 'results', 'materials.combination']);
|
||||||
|
|
||||||
if ($cutting->status === CuttingStatus::IN_PROGRESS) {
|
if ($cutting->status === CuttingStatus::IN_PROGRESS) {
|
||||||
$this->reverseTotalMaterialStock($cutting);
|
$this->reverseTotalMaterialStock($cutting);
|
||||||
@ -568,6 +578,7 @@ function () use ($cutting, $validated): void {
|
|||||||
|
|
||||||
$cutting->materials()->delete();
|
$cutting->materials()->delete();
|
||||||
$cutting->results()->delete();
|
$cutting->results()->delete();
|
||||||
|
$cutting->combinations()->delete();
|
||||||
|
|
||||||
$materials = $this->buildMaterials($validated['materials']);
|
$materials = $this->buildMaterials($validated['materials']);
|
||||||
$results = $this->buildResults($validated['results']);
|
$results = $this->buildResults($validated['results']);
|
||||||
@ -583,8 +594,41 @@ function () use ($cutting, $validated): void {
|
|||||||
|
|
||||||
$this->syncImages($cutting, $validated);
|
$this->syncImages($cutting, $validated);
|
||||||
|
|
||||||
|
// Group materials by combination_id to create combinations
|
||||||
|
$combinationGroups = [];
|
||||||
foreach ($materials as $materialData) {
|
foreach ($materials as $materialData) {
|
||||||
$cutting->materials()->create($materialData);
|
$combinationId = $materialData['combination_id'];
|
||||||
|
if ($combinationId !== null) {
|
||||||
|
if (!isset($combinationGroups[$combinationId])) {
|
||||||
|
$combinationGroups[$combinationId] = [
|
||||||
|
'materials' => [],
|
||||||
|
'material_result' => $materialData['combination_material_result'] ?? null,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
$combinationGroups[$combinationId]['materials'][] = $materialData;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create combinations and update combination_id for materials
|
||||||
|
$combinationIdMap = [];
|
||||||
|
foreach ($combinationGroups as $oldCombinationId => $group) {
|
||||||
|
$combination = $cutting->combinations()->create([
|
||||||
|
'material_result' => $group['material_result'],
|
||||||
|
]);
|
||||||
|
$combinationIdMap[$oldCombinationId] = $combination->id;
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ($materials as $materialData) {
|
||||||
|
$newCombinationId = $materialData['combination_id'] !== null
|
||||||
|
? $combinationIdMap[$materialData['combination_id']] ?? null
|
||||||
|
: null;
|
||||||
|
|
||||||
|
$cutting->materials()->create([
|
||||||
|
'raw_material_price_id' => $materialData['raw_material_price_id'],
|
||||||
|
'material_usage' => $materialData['material_usage'],
|
||||||
|
'material_result' => $materialData['material_result'],
|
||||||
|
'combination_id' => $newCombinationId,
|
||||||
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
foreach ($results as $resultData) {
|
foreach ($results as $resultData) {
|
||||||
@ -756,10 +800,16 @@ private function buildMaterials(array $materials): array
|
|||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$materialResult = array_key_exists('material_result', $itemData) && $itemData['material_result'] !== null
|
||||||
|
? (int) $itemData['material_result']
|
||||||
|
: null;
|
||||||
|
|
||||||
return [
|
return [
|
||||||
'raw_material_price_id' => $price->id,
|
'raw_material_price_id' => $price->id,
|
||||||
'material_usage' => $materialUsage,
|
'material_usage' => $materialUsage,
|
||||||
|
'material_result' => $materialResult,
|
||||||
'combination_id' => $itemData['combination_id'] ?? null,
|
'combination_id' => $itemData['combination_id'] ?? null,
|
||||||
|
'combination_material_result' => $itemData['combination_material_result'] ?? null,
|
||||||
];
|
];
|
||||||
})
|
})
|
||||||
->all();
|
->all();
|
||||||
@ -909,6 +959,12 @@ private function breakMaterialCircularReference(CuttingMaterial $material): void
|
|||||||
{
|
{
|
||||||
$price = $material->rawMaterialPrice;
|
$price = $material->rawMaterialPrice;
|
||||||
|
|
||||||
|
// Always set these attributes regardless of price
|
||||||
|
$material->setAttribute('material_result', $material->material_result);
|
||||||
|
$material->setAttribute('material_result_input', $material->material_result);
|
||||||
|
$material->setAttribute('combination_id', $material->combination_id);
|
||||||
|
$material->setAttribute('combination_material_result', $material->combination?->material_result);
|
||||||
|
|
||||||
if ($price) {
|
if ($price) {
|
||||||
$rawMaterial = $price->rawMaterial;
|
$rawMaterial = $price->rawMaterial;
|
||||||
|
|
||||||
@ -929,7 +985,6 @@ private function breakMaterialCircularReference(CuttingMaterial $material): void
|
|||||||
$price->unsetRelation('rawMaterial');
|
$price->unsetRelation('rawMaterial');
|
||||||
}
|
}
|
||||||
|
|
||||||
$material->setAttribute('combination_id', $material->combination_id);
|
|
||||||
$material->unsetRelation('rawMaterialPrice');
|
$material->unsetRelation('rawMaterialPrice');
|
||||||
$material->unsetRelation('combination');
|
$material->unsetRelation('combination');
|
||||||
}
|
}
|
||||||
@ -963,6 +1018,7 @@ private function presentDraftMaterial(CuttingMaterial $item): array
|
|||||||
{
|
{
|
||||||
$price = $item->rawMaterialPrice;
|
$price = $item->rawMaterialPrice;
|
||||||
$rawMaterial = $price?->rawMaterial;
|
$rawMaterial = $price?->rawMaterial;
|
||||||
|
$combination = $item->combination;
|
||||||
|
|
||||||
return [
|
return [
|
||||||
'raw_material_price_id' => $item->raw_material_price_id,
|
'raw_material_price_id' => $item->raw_material_price_id,
|
||||||
@ -972,8 +1028,10 @@ private function presentDraftMaterial(CuttingMaterial $item): array
|
|||||||
'unit_abbreviation' => $rawMaterial?->unit?->abbreviation() ?? '',
|
'unit_abbreviation' => $rawMaterial?->unit?->abbreviation() ?? '',
|
||||||
'stock_input' => $price?->stock_input ?? '',
|
'stock_input' => $price?->stock_input ?? '',
|
||||||
'material_usage' => $this->formatQuantityInput((float) $item->material_usage),
|
'material_usage' => $this->formatQuantityInput((float) $item->material_usage),
|
||||||
|
'material_result' => $item->material_result !== null ? (int) $item->material_result : null,
|
||||||
'images' => $price ? MediaPresenter::collection($price, 'images') : [],
|
'images' => $price ? MediaPresenter::collection($price, 'images') : [],
|
||||||
'combination_id' => $item->combination_id,
|
'combination_id' => $item->combination_id,
|
||||||
|
'combination_material_result' => $combination?->material_result,
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -0,0 +1,28 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
|
||||||
|
return new class extends Migration
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Run the migrations.
|
||||||
|
*/
|
||||||
|
public function up(): void
|
||||||
|
{
|
||||||
|
Schema::table('cutting_materials', function (Blueprint $table) {
|
||||||
|
$table->decimal('material_result', 18, 2)->nullable()->after('material_usage');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reverse the migrations.
|
||||||
|
*/
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
Schema::table('cutting_materials', function (Blueprint $table) {
|
||||||
|
$table->dropColumn('material_result');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
@ -0,0 +1,28 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
|
||||||
|
return new class extends Migration
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Run the migrations.
|
||||||
|
*/
|
||||||
|
public function up(): void
|
||||||
|
{
|
||||||
|
Schema::table('cutting_materials', function (Blueprint $table) {
|
||||||
|
$table->integer('material_result')->nullable()->change();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reverse the migrations.
|
||||||
|
*/
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
Schema::table('cutting_materials', function (Blueprint $table) {
|
||||||
|
$table->decimal('material_result', 18, 2)->nullable()->change();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
@ -0,0 +1,28 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
|
||||||
|
return new class extends Migration
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Run the migrations.
|
||||||
|
*/
|
||||||
|
public function up(): void
|
||||||
|
{
|
||||||
|
Schema::table('cutting_material_combinations', function (Blueprint $table) {
|
||||||
|
$table->integer('material_result')->nullable()->after('cutting_id');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reverse the migrations.
|
||||||
|
*/
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
Schema::table('cutting_material_combinations', function (Blueprint $table) {
|
||||||
|
$table->dropColumn('material_result');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
@ -4,7 +4,6 @@ import type { ColumnDef } from '@tanstack/vue-table';
|
|||||||
import { FlexRender, getCoreRowModel, useVueTable } from '@tanstack/vue-table';
|
import { FlexRender, getCoreRowModel, useVueTable } from '@tanstack/vue-table';
|
||||||
import { computed, provide } from 'vue';
|
import { computed, provide } from 'vue';
|
||||||
import DataTableToolbar from '@/components/data-table/DataTableToolbar.vue';
|
import DataTableToolbar from '@/components/data-table/DataTableToolbar.vue';
|
||||||
import { buildPaginationSummary } from '@/lib/grouped-table';
|
|
||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
import {
|
import {
|
||||||
Table,
|
Table,
|
||||||
@ -15,6 +14,7 @@ import {
|
|||||||
TableRow,
|
TableRow,
|
||||||
} from '@/components/ui/table';
|
} from '@/components/ui/table';
|
||||||
import TableEmpty from '@/components/ui/table/TableEmpty.vue';
|
import TableEmpty from '@/components/ui/table/TableEmpty.vue';
|
||||||
|
import { buildPaginationSummary } from '@/lib/grouped-table';
|
||||||
import type {
|
import type {
|
||||||
DataTableFilterDef,
|
DataTableFilterDef,
|
||||||
DataTablePagination,
|
DataTablePagination,
|
||||||
|
|||||||
@ -11,6 +11,7 @@ export function useCan() {
|
|||||||
if (Array.isArray(permission)) {
|
if (Array.isArray(permission)) {
|
||||||
return permission.some((p) => permissions.value.includes(p));
|
return permission.some((p) => permissions.value.includes(p));
|
||||||
}
|
}
|
||||||
|
|
||||||
return permissions.value.includes(permission);
|
return permissions.value.includes(permission);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -26,8 +26,10 @@ export function useDestroy({ url, preserveScroll = true, errorMessage, onSuccess
|
|||||||
onError: (errors) => {
|
onError: (errors) => {
|
||||||
if (onError) {
|
if (onError) {
|
||||||
const result = onError(errors);
|
const result = onError(errors);
|
||||||
|
|
||||||
if (typeof result === 'string') {
|
if (typeof result === 'string') {
|
||||||
toast.error(result);
|
toast.error(result);
|
||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,4 +1,5 @@
|
|||||||
import { computed, type MaybeRefOrGetter, toValue } from 'vue';
|
import { computed, toValue } from 'vue';
|
||||||
|
import type {MaybeRefOrGetter} from 'vue';
|
||||||
import { buildPaginationSummary } from '@/lib/grouped-table';
|
import { buildPaginationSummary } from '@/lib/grouped-table';
|
||||||
import type { DataTablePagination } from '@/types/data-table';
|
import type { DataTablePagination } from '@/types/data-table';
|
||||||
|
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
import type { Component } from 'vue';
|
|
||||||
import { Check, RotateCcw, Scissors, X } from '@lucide/vue';
|
import { Check, RotateCcw, Scissors, X } from '@lucide/vue';
|
||||||
|
import type { Component } from 'vue';
|
||||||
import type { BadgeVariant } from '@/lib/badge-variant';
|
import type { BadgeVariant } from '@/lib/badge-variant';
|
||||||
|
|
||||||
export const CuttingStatus = {
|
export const CuttingStatus = {
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
import type { Component } from 'vue';
|
|
||||||
import { Check, Send, X } from '@lucide/vue';
|
import { Check, Send, X } from '@lucide/vue';
|
||||||
|
import type { Component } from 'vue';
|
||||||
import type { BadgeVariant } from '@/lib/badge-variant';
|
import type { BadgeVariant } from '@/lib/badge-variant';
|
||||||
|
|
||||||
export const OrderStatus = {
|
export const OrderStatus = {
|
||||||
|
|||||||
@ -1,8 +1,8 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed } from 'vue';
|
|
||||||
import { Home, Settings2, Share2, ShoppingBag, Users } from '@lucide/vue';
|
import { Home, Settings2, Share2, ShoppingBag, Users } from '@lucide/vue';
|
||||||
import { SettingSection as SettingSectionConst } from '@/constants/setting-section';
|
import { computed } from 'vue';
|
||||||
import { useCan } from '@/composables/useCan';
|
import { useCan } from '@/composables/useCan';
|
||||||
|
import { SettingSection as SettingSectionConst } from '@/constants/setting-section';
|
||||||
import AdminLayout from '@/layouts/AdminLayout.vue';
|
import AdminLayout from '@/layouts/AdminLayout.vue';
|
||||||
import { cn } from '@/lib/utils';
|
import { cn } from '@/lib/utils';
|
||||||
import type { SettingSection } from '@/types/setting';
|
import type { SettingSection } from '@/types/setting';
|
||||||
@ -23,6 +23,7 @@ const filteredNavItems = computed(() => {
|
|||||||
if (hasRole('admin-toko')) {
|
if (hasRole('admin-toko')) {
|
||||||
return navItems.filter((item) => item.key === SettingSectionConst.MARKETPLACE);
|
return navItems.filter((item) => item.key === SettingSectionConst.MARKETPLACE);
|
||||||
}
|
}
|
||||||
|
|
||||||
return navItems;
|
return navItems;
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@ -212,6 +212,7 @@ const donutSegmentSelector = Donut.selectors.segment;
|
|||||||
|
|
||||||
function channelTooltip(arc: any) {
|
function channelTooltip(arc: any) {
|
||||||
const d = arc.data as typeof props.orderStats.by_channel[number];
|
const d = arc.data as typeof props.orderStats.by_channel[number];
|
||||||
|
|
||||||
return `<div class="rounded-lg border bg-background px-3 py-1.5 shadow-xl" style="line-height:1.2">
|
return `<div class="rounded-lg border bg-background px-3 py-1.5 shadow-xl" style="line-height:1.2">
|
||||||
<p style="margin:0;display:flex;align-items:center;gap:0.5rem">
|
<p style="margin:0;display:flex;align-items:center;gap:0.5rem">
|
||||||
<span class="size-2.5 rounded-full" style="background-color: ${channelChartConfig.value[d.channel]?.color ?? channelColors[0]}"></span>
|
<span class="size-2.5 rounded-full" style="background-color: ${channelChartConfig.value[d.channel]?.color ?? channelColors[0]}"></span>
|
||||||
@ -223,6 +224,7 @@ function channelTooltip(arc: any) {
|
|||||||
|
|
||||||
function paymentTooltip(arc: any) {
|
function paymentTooltip(arc: any) {
|
||||||
const d = arc.data as typeof props.orderStats.by_payment_type[number];
|
const d = arc.data as typeof props.orderStats.by_payment_type[number];
|
||||||
|
|
||||||
return `<div class="rounded-lg border bg-background px-3 py-1.5 shadow-xl" style="line-height:1.2">
|
return `<div class="rounded-lg border bg-background px-3 py-1.5 shadow-xl" style="line-height:1.2">
|
||||||
<p style="margin:0;display:flex;align-items:center;gap:0.5rem">
|
<p style="margin:0;display:flex;align-items:center;gap:0.5rem">
|
||||||
<span class="size-2.5 rounded-full" style="background-color: ${paymentChartConfig.value[d.payment_type]?.color ?? paymentColors[0]}"></span>
|
<span class="size-2.5 rounded-full" style="background-color: ${paymentChartConfig.value[d.payment_type]?.color ?? paymentColors[0]}"></span>
|
||||||
@ -235,6 +237,7 @@ function paymentTooltip(arc: any) {
|
|||||||
function marketingTooltip(arc: any) {
|
function marketingTooltip(arc: any) {
|
||||||
const d = arc.data as typeof props.orderStats.by_marketing[number];
|
const d = arc.data as typeof props.orderStats.by_marketing[number];
|
||||||
const idx = props.orderStats.by_marketing.indexOf(d);
|
const idx = props.orderStats.by_marketing.indexOf(d);
|
||||||
|
|
||||||
return `<div class="rounded-lg border bg-background px-3 py-1.5 shadow-xl" style="line-height:1.2">
|
return `<div class="rounded-lg border bg-background px-3 py-1.5 shadow-xl" style="line-height:1.2">
|
||||||
<p style="margin:0;display:flex;align-items:center;gap:0.5rem">
|
<p style="margin:0;display:flex;align-items:center;gap:0.5rem">
|
||||||
<span class="size-2.5 rounded-full" style="background-color: ${marketingColors[idx % marketingColors.length]}"></span>
|
<span class="size-2.5 rounded-full" style="background-color: ${marketingColors[idx % marketingColors.length]}"></span>
|
||||||
@ -246,6 +249,7 @@ function marketingTooltip(arc: any) {
|
|||||||
|
|
||||||
function statusTooltip(arc: any) {
|
function statusTooltip(arc: any) {
|
||||||
const d = arc.data as typeof props.orderStats.by_status[number];
|
const d = arc.data as typeof props.orderStats.by_status[number];
|
||||||
|
|
||||||
return `<div class="rounded-lg border bg-background px-3 py-1.5 shadow-xl" style="line-height:1.2">
|
return `<div class="rounded-lg border bg-background px-3 py-1.5 shadow-xl" style="line-height:1.2">
|
||||||
<p style="margin:0;display:flex;align-items:center;gap:0.5rem">
|
<p style="margin:0;display:flex;align-items:center;gap:0.5rem">
|
||||||
<span class="size-2.5 rounded-full" style="background-color: ${statusChartConfig.value[d.status]?.color ?? statusColors[0]}"></span>
|
<span class="size-2.5 rounded-full" style="background-color: ${statusChartConfig.value[d.status]?.color ?? statusColors[0]}"></span>
|
||||||
|
|||||||
@ -1,7 +1,4 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import type {
|
|
||||||
ChartConfig,
|
|
||||||
} from "@/components/ui/chart"
|
|
||||||
import { TrendingUp } from "@lucide/vue"
|
import { TrendingUp } from "@lucide/vue"
|
||||||
|
|
||||||
import { CurveType } from "@unovis/ts"
|
import { CurveType } from "@unovis/ts"
|
||||||
@ -14,6 +11,9 @@ import {
|
|||||||
CardHeader,
|
CardHeader,
|
||||||
CardTitle,
|
CardTitle,
|
||||||
} from "@/components/ui/card"
|
} from "@/components/ui/card"
|
||||||
|
import type {
|
||||||
|
ChartConfig,
|
||||||
|
} from "@/components/ui/chart"
|
||||||
import {
|
import {
|
||||||
ChartContainer,
|
ChartContainer,
|
||||||
ChartCrosshair,
|
ChartCrosshair,
|
||||||
|
|||||||
@ -17,9 +17,9 @@ import { AppearanceMode as AppearanceModeConst } from '@/constants/appearance-mo
|
|||||||
import AccountLayout from '@/layouts/AccountLayout.vue';
|
import AccountLayout from '@/layouts/AccountLayout.vue';
|
||||||
import { formErrors } from '@/lib/form';
|
import { formErrors } from '@/lib/form';
|
||||||
import { cn } from '@/lib/utils';
|
import { cn } from '@/lib/utils';
|
||||||
|
import { update } from '@/routes/admin/account/appearance';
|
||||||
import type { AppearanceFormData, AppearanceMode } from '@/types/account';
|
import type { AppearanceFormData, AppearanceMode } from '@/types/account';
|
||||||
|
|
||||||
import { update } from '@/routes/admin/account/appearance';
|
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
appearance: AppearanceMode;
|
appearance: AppearanceMode;
|
||||||
|
|||||||
@ -2,8 +2,8 @@
|
|||||||
import { computed } from 'vue';
|
import { computed } from 'vue';
|
||||||
import { RowDeleteAction, RowEditAction } from '@/components/button';
|
import { RowDeleteAction, RowEditAction } from '@/components/button';
|
||||||
import { useCan } from '@/composables/useCan';
|
import { useCan } from '@/composables/useCan';
|
||||||
import type { CashTransactionListItem } from '@/types/cash';
|
|
||||||
import { destroy } from '@/routes/admin/finance/cash/transactions';
|
import { destroy } from '@/routes/admin/finance/cash/transactions';
|
||||||
|
import type { CashTransactionListItem } from '@/types/cash';
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
transaction: CashTransactionListItem;
|
transaction: CashTransactionListItem;
|
||||||
|
|||||||
@ -1,4 +1,7 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
import { Head } from '@inertiajs/vue3';
|
||||||
|
import { HandCoins } from '@lucide/vue';
|
||||||
|
import { computed, ref, watch } from 'vue';
|
||||||
import CreateButton from '@/components/button/CreateButton.vue';
|
import CreateButton from '@/components/button/CreateButton.vue';
|
||||||
import { DataTable } from '@/components/data-table';
|
import { DataTable } from '@/components/data-table';
|
||||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
||||||
@ -9,19 +12,16 @@ import {
|
|||||||
} from '@/composables/useDataTableQuery';
|
} from '@/composables/useDataTableQuery';
|
||||||
import { EmployeeAdvanceStatus } from '@/constants/employee-advance-status';
|
import { EmployeeAdvanceStatus } from '@/constants/employee-advance-status';
|
||||||
import AdminLayout from '@/layouts/AdminLayout.vue';
|
import AdminLayout from '@/layouts/AdminLayout.vue';
|
||||||
|
import { index } from '@/routes/admin/finance/employee_advances';
|
||||||
import type { DataTableFilterDef, DataTableSort } from '@/types/data-table';
|
import type { DataTableFilterDef, DataTableSort } from '@/types/data-table';
|
||||||
import type {
|
import type {
|
||||||
EmployeeAdvanceListItem,
|
EmployeeAdvanceListItem,
|
||||||
EmployeeAdvancePageProps,
|
EmployeeAdvancePageProps,
|
||||||
} from '@/types/employee-advance';
|
} from '@/types/employee-advance';
|
||||||
import { Head } from '@inertiajs/vue3';
|
|
||||||
import { HandCoins } from '@lucide/vue';
|
|
||||||
import { computed, ref, watch } from 'vue';
|
|
||||||
import EmployeeAdvanceFormModal from './form/EmployeeAdvanceFormModal.vue';
|
import EmployeeAdvanceFormModal from './form/EmployeeAdvanceFormModal.vue';
|
||||||
import RejectEmployeeAdvanceModal from './form/RejectEmployeeAdvanceModal.vue';
|
import RejectEmployeeAdvanceModal from './form/RejectEmployeeAdvanceModal.vue';
|
||||||
import { createColumns } from './table/columns';
|
import { createColumns } from './table/columns';
|
||||||
|
|
||||||
import { index } from '@/routes/admin/finance/employee_advances';
|
|
||||||
|
|
||||||
const props = defineProps<EmployeeAdvancePageProps>();
|
const props = defineProps<EmployeeAdvancePageProps>();
|
||||||
|
|
||||||
|
|||||||
@ -1,4 +1,6 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
import { Head } from '@inertiajs/vue3';
|
||||||
|
import { computed, ref, watch } from 'vue';
|
||||||
import CreateButton from '@/components/button/CreateButton.vue';
|
import CreateButton from '@/components/button/CreateButton.vue';
|
||||||
import { DataTable } from '@/components/data-table';
|
import { DataTable } from '@/components/data-table';
|
||||||
import { Card, CardContent } from '@/components/ui/card';
|
import { Card, CardContent } from '@/components/ui/card';
|
||||||
@ -8,14 +10,12 @@ import {
|
|||||||
useDataTableQuerySync,
|
useDataTableQuerySync,
|
||||||
} from '@/composables/useDataTableQuery';
|
} from '@/composables/useDataTableQuery';
|
||||||
import AdminLayout from '@/layouts/AdminLayout.vue';
|
import AdminLayout from '@/layouts/AdminLayout.vue';
|
||||||
|
import { index } from '@/routes/admin/finance/expenses';
|
||||||
import type { DataTableSort } from '@/types/data-table';
|
import type { DataTableSort } from '@/types/data-table';
|
||||||
import type { ExpenseListItem, PaginatedExpenses } from '@/types/expense';
|
import type { ExpenseListItem, PaginatedExpenses } from '@/types/expense';
|
||||||
import { Head } from '@inertiajs/vue3';
|
|
||||||
import { computed, ref, watch } from 'vue';
|
|
||||||
import ExpenseFormModal from './form/ExpenseFormModal.vue';
|
import ExpenseFormModal from './form/ExpenseFormModal.vue';
|
||||||
import { createColumns } from './table/columns';
|
import { createColumns } from './table/columns';
|
||||||
|
|
||||||
import { index } from '@/routes/admin/finance/expenses';
|
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
expenses: PaginatedExpenses;
|
expenses: PaginatedExpenses;
|
||||||
|
|||||||
@ -1,8 +1,8 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { RowDeleteAction, RowEditAction } from '@/components/button';
|
import { RowDeleteAction, RowEditAction } from '@/components/button';
|
||||||
import { useCan } from '@/composables/useCan';
|
import { useCan } from '@/composables/useCan';
|
||||||
import type { ExpenseListItem } from '@/types/expense';
|
|
||||||
import { destroy } from '@/routes/admin/finance/expenses';
|
import { destroy } from '@/routes/admin/finance/expenses';
|
||||||
|
import type { ExpenseListItem } from '@/types/expense';
|
||||||
|
|
||||||
defineProps<{
|
defineProps<{
|
||||||
expense: ExpenseListItem;
|
expense: ExpenseListItem;
|
||||||
|
|||||||
@ -22,12 +22,12 @@ import {
|
|||||||
useDataTableQuerySync,
|
useDataTableQuerySync,
|
||||||
} from '@/composables/useDataTableQuery';
|
} from '@/composables/useDataTableQuery';
|
||||||
import AdminLayout from '@/layouts/AdminLayout.vue';
|
import AdminLayout from '@/layouts/AdminLayout.vue';
|
||||||
|
import { index } from '@/routes/admin/finance/payroll';
|
||||||
import type { DataTableSort } from '@/types/data-table';
|
import type { DataTableSort } from '@/types/data-table';
|
||||||
import type { PayrollListItem, PayrollPageProps } from '@/types/payroll';
|
import type { PayrollListItem, PayrollPageProps } from '@/types/payroll';
|
||||||
import PayrollAdjustmentModal from './form/PayrollAdjustmentModal.vue';
|
import PayrollAdjustmentModal from './form/PayrollAdjustmentModal.vue';
|
||||||
import { createColumns } from './table/columns';
|
import { createColumns } from './table/columns';
|
||||||
|
|
||||||
import { index } from '@/routes/admin/finance/payroll';
|
|
||||||
|
|
||||||
const props = defineProps<PayrollPageProps>();
|
const props = defineProps<PayrollPageProps>();
|
||||||
|
|
||||||
|
|||||||
@ -7,13 +7,13 @@ import FullCalendar from '@fullcalendar/vue3';
|
|||||||
import { router } from '@inertiajs/vue3';
|
import { router } from '@inertiajs/vue3';
|
||||||
import { ChevronLeft, ChevronRight } from '@lucide/vue';
|
import { ChevronLeft, ChevronRight } from '@lucide/vue';
|
||||||
import { computed, ref, watch } from 'vue';
|
import { computed, ref, watch } from 'vue';
|
||||||
import AttendanceDetailDialog from './AttendanceDetailDialog.vue';
|
|
||||||
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 { DatePicker } from '@/components/ui/date-picker';
|
import { DatePicker } from '@/components/ui/date-picker';
|
||||||
import { cn } from '@/lib/utils';
|
import { cn } from '@/lib/utils';
|
||||||
import type { AttendanceListItem, CalendarRange } from '@/types/attendance';
|
|
||||||
import { index } from '@/routes/admin/hr/attendances';
|
import { index } from '@/routes/admin/hr/attendances';
|
||||||
|
import type { AttendanceListItem, CalendarRange } from '@/types/attendance';
|
||||||
|
import AttendanceDetailDialog from './AttendanceDetailDialog.vue';
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
attendances: AttendanceListItem[];
|
attendances: AttendanceListItem[];
|
||||||
@ -147,6 +147,7 @@ function scrollToToday() {
|
|||||||
requestAnimationFrame(() => {
|
requestAnimationFrame(() => {
|
||||||
const container = calendarRef.value?.$el?.closest('.overflow-x-auto');
|
const container = calendarRef.value?.$el?.closest('.overflow-x-auto');
|
||||||
const todayEl = container?.querySelector('.fc-day-today');
|
const todayEl = container?.querySelector('.fc-day-today');
|
||||||
|
|
||||||
if (!container || !todayEl) {
|
if (!container || !todayEl) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,8 +1,8 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { RowDeleteAction } from '@/components/button';
|
import { RowDeleteAction } from '@/components/button';
|
||||||
import { useCan } from '@/composables/useCan';
|
import { useCan } from '@/composables/useCan';
|
||||||
import type { AttendanceListItem } from '@/types/attendance';
|
|
||||||
import { destroy } from '@/routes/admin/hr/attendances';
|
import { destroy } from '@/routes/admin/hr/attendances';
|
||||||
|
import type { AttendanceListItem } from '@/types/attendance';
|
||||||
|
|
||||||
defineProps<{
|
defineProps<{
|
||||||
attendance: AttendanceListItem;
|
attendance: AttendanceListItem;
|
||||||
|
|||||||
@ -1,10 +1,10 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
import { Head } from '@inertiajs/vue3';
|
||||||
import BackButton from '@/components/button/BackButton.vue';
|
import BackButton from '@/components/button/BackButton.vue';
|
||||||
import AdminLayout from '@/layouts/AdminLayout.vue';
|
import AdminLayout from '@/layouts/AdminLayout.vue';
|
||||||
import type { EnumOption } from '@/types/employee';
|
|
||||||
import { Head } from '@inertiajs/vue3';
|
|
||||||
import EmployeeForm from './form/EmployeeForm.vue';
|
|
||||||
import { index, store } from '@/routes/admin/hr/employees';
|
import { index, store } from '@/routes/admin/hr/employees';
|
||||||
|
import type { EnumOption } from '@/types/employee';
|
||||||
|
import EmployeeForm from './form/EmployeeForm.vue';
|
||||||
|
|
||||||
defineProps<{
|
defineProps<{
|
||||||
genders: EnumOption[];
|
genders: EnumOption[];
|
||||||
|
|||||||
@ -1,12 +1,12 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import BackButton from '@/components/button/BackButton.vue';
|
|
||||||
import AdminLayout from '@/layouts/AdminLayout.vue';
|
|
||||||
import type { EmployeeListItem, EnumOption } from '@/types/employee';
|
|
||||||
import type { MediaItem } from '@/types/media';
|
|
||||||
import { Head } from '@inertiajs/vue3';
|
import { Head } from '@inertiajs/vue3';
|
||||||
import { computed } from 'vue';
|
import { computed } from 'vue';
|
||||||
import EmployeeForm from './form/EmployeeForm.vue';
|
import BackButton from '@/components/button/BackButton.vue';
|
||||||
|
import AdminLayout from '@/layouts/AdminLayout.vue';
|
||||||
import { index, update } from '@/routes/admin/hr/employees';
|
import { index, update } from '@/routes/admin/hr/employees';
|
||||||
|
import type { EmployeeListItem, EnumOption } from '@/types/employee';
|
||||||
|
import type { MediaItem } from '@/types/media';
|
||||||
|
import EmployeeForm from './form/EmployeeForm.vue';
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
employee: EmployeeListItem;
|
employee: EmployeeListItem;
|
||||||
|
|||||||
@ -1,6 +1,8 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
import { Head } from '@inertiajs/vue3';
|
||||||
import BackButton from '@/components/button/BackButton.vue';
|
import BackButton from '@/components/button/BackButton.vue';
|
||||||
import AdminLayout from '@/layouts/AdminLayout.vue';
|
import AdminLayout from '@/layouts/AdminLayout.vue';
|
||||||
|
import { index, store } from '@/routes/admin/manage/cuttings';
|
||||||
import type {
|
import type {
|
||||||
CuttingMaterialCartItem,
|
CuttingMaterialCartItem,
|
||||||
CuttingProductCatalogItem,
|
CuttingProductCatalogItem,
|
||||||
@ -9,9 +11,7 @@ import type {
|
|||||||
} from '@/types/cutting';
|
} from '@/types/cutting';
|
||||||
import type { CategoryOption } from '@/types/product';
|
import type { CategoryOption } from '@/types/product';
|
||||||
import type { EnumOption } from '@/types/raw-material';
|
import type { EnumOption } from '@/types/raw-material';
|
||||||
import { Head } from '@inertiajs/vue3';
|
|
||||||
import CuttingPosForm from './form/CuttingPosForm.vue';
|
import CuttingPosForm from './form/CuttingPosForm.vue';
|
||||||
import { index, store } from '@/routes/admin/manage/cuttings';
|
|
||||||
|
|
||||||
defineProps<{
|
defineProps<{
|
||||||
rawMaterialCatalog: CuttingRawMaterialCatalogItem[];
|
rawMaterialCatalog: CuttingRawMaterialCatalogItem[];
|
||||||
|
|||||||
@ -1,6 +1,9 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
import { Head } from '@inertiajs/vue3';
|
||||||
|
import { computed } from 'vue';
|
||||||
import BackButton from '@/components/button/BackButton.vue';
|
import BackButton from '@/components/button/BackButton.vue';
|
||||||
import AdminLayout from '@/layouts/AdminLayout.vue';
|
import AdminLayout from '@/layouts/AdminLayout.vue';
|
||||||
|
import { index, update } from '@/routes/admin/manage/cuttings';
|
||||||
import type {
|
import type {
|
||||||
CuttingEditItem,
|
CuttingEditItem,
|
||||||
CuttingProductCatalogItem,
|
CuttingProductCatalogItem,
|
||||||
@ -8,10 +11,7 @@ import type {
|
|||||||
} from '@/types/cutting';
|
} from '@/types/cutting';
|
||||||
import type { CategoryOption } from '@/types/product';
|
import type { CategoryOption } from '@/types/product';
|
||||||
import type { EnumOption } from '@/types/raw-material';
|
import type { EnumOption } from '@/types/raw-material';
|
||||||
import { Head } from '@inertiajs/vue3';
|
|
||||||
import { computed } from 'vue';
|
|
||||||
import CuttingPosForm from './form/CuttingPosForm.vue';
|
import CuttingPosForm from './form/CuttingPosForm.vue';
|
||||||
import { index, update } from '@/routes/admin/manage/cuttings';
|
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
cutting: CuttingEditItem;
|
cutting: CuttingEditItem;
|
||||||
@ -34,8 +34,10 @@ const initialData = computed(() => ({
|
|||||||
unit_abbreviation: item.unit_abbreviation ?? '',
|
unit_abbreviation: item.unit_abbreviation ?? '',
|
||||||
stock_input: item.stock_input ?? '',
|
stock_input: item.stock_input ?? '',
|
||||||
material_usage: item.material_usage_input,
|
material_usage: item.material_usage_input,
|
||||||
|
material_result: item.material_result_input !== null && item.material_result_input !== undefined ? String(item.material_result_input) : null,
|
||||||
images: item.images ?? [],
|
images: item.images ?? [],
|
||||||
combination_id: item.combination_id ?? null,
|
combination_id: item.combination_id ?? null,
|
||||||
|
combination_material_result: item.combination_material_result ?? null,
|
||||||
})),
|
})),
|
||||||
results: props.cutting.results.map((item) => ({
|
results: props.cutting.results.map((item) => ({
|
||||||
product_variant_id: item.product_variant_id,
|
product_variant_id: item.product_variant_id,
|
||||||
@ -51,12 +53,11 @@ const initialData = computed(() => ({
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
|
|
||||||
<Head title="Ubah Cutting" />
|
<Head title="Ubah Cutting" />
|
||||||
|
|
||||||
<AdminLayout>
|
<AdminLayout>
|
||||||
<div
|
<div class="flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between">
|
||||||
class="flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between"
|
|
||||||
>
|
|
||||||
<div class="space-y-1">
|
<div class="space-y-1">
|
||||||
<h2 class="text-2xl font-bold tracking-tight">Ubah Cutting</h2>
|
<h2 class="text-2xl font-bold tracking-tight">Ubah Cutting</h2>
|
||||||
</div>
|
</div>
|
||||||
@ -64,15 +65,8 @@ const initialData = computed(() => ({
|
|||||||
<BackButton :href="index.url()" />
|
<BackButton :href="index.url()" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<CuttingPosForm
|
<CuttingPosForm :raw-material-catalog="rawMaterialCatalog" :product-catalog="productCatalog"
|
||||||
:raw-material-catalog="rawMaterialCatalog"
|
:categories="categories" :units="units" :initial-data="initialData"
|
||||||
:product-catalog="productCatalog"
|
:submit-url="update.url(props.cutting.id)" method="put" submit-label="Perbarui" />
|
||||||
:categories="categories"
|
|
||||||
:units="units"
|
|
||||||
:initial-data="initialData"
|
|
||||||
:submit-url="update.url(props.cutting.id)"
|
|
||||||
method="put"
|
|
||||||
submit-label="Perbarui"
|
|
||||||
/>
|
|
||||||
</AdminLayout>
|
</AdminLayout>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@ -1,4 +1,6 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
import { Head } from '@inertiajs/vue3';
|
||||||
|
import { computed, ref, watch } from 'vue';
|
||||||
import CreateButton from '@/components/button/CreateButton.vue';
|
import CreateButton from '@/components/button/CreateButton.vue';
|
||||||
import { Card, CardContent } from '@/components/ui/card';
|
import { Card, CardContent } from '@/components/ui/card';
|
||||||
import { useCan } from '@/composables/useCan';
|
import { useCan } from '@/composables/useCan';
|
||||||
@ -7,12 +9,10 @@ import {
|
|||||||
useDataTableQuerySync,
|
useDataTableQuerySync,
|
||||||
} from '@/composables/useDataTableQuery';
|
} from '@/composables/useDataTableQuery';
|
||||||
import AdminLayout from '@/layouts/AdminLayout.vue';
|
import AdminLayout from '@/layouts/AdminLayout.vue';
|
||||||
|
import { index, create } from '@/routes/admin/manage/cuttings';
|
||||||
import type { CuttingListItem, PaginatedCuttings } from '@/types/cutting';
|
import type { CuttingListItem, PaginatedCuttings } from '@/types/cutting';
|
||||||
import { Head } from '@inertiajs/vue3';
|
|
||||||
import { computed, ref, watch } from 'vue';
|
|
||||||
import CuttingGroupedTable from './table/CuttingGroupedTable.vue';
|
import CuttingGroupedTable from './table/CuttingGroupedTable.vue';
|
||||||
import CuttingInProgressSection from './table/CuttingInProgressSection.vue';
|
import CuttingInProgressSection from './table/CuttingInProgressSection.vue';
|
||||||
import { index, create } from '@/routes/admin/manage/cuttings';
|
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
cuttings: PaginatedCuttings;
|
cuttings: PaginatedCuttings;
|
||||||
|
|||||||
@ -1,5 +1,6 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed } from 'vue';
|
import { computed } from 'vue';
|
||||||
|
import MediaThumbnailCell from '@/components/media/MediaThumbnailCell.vue';
|
||||||
import { Badge } from '@/components/ui/badge';
|
import { Badge } from '@/components/ui/badge';
|
||||||
import {
|
import {
|
||||||
Table,
|
Table,
|
||||||
@ -10,7 +11,6 @@ import {
|
|||||||
TableRow,
|
TableRow,
|
||||||
} from '@/components/ui/table';
|
} from '@/components/ui/table';
|
||||||
import { cuttingStatusBadgeVariant } from '@/constants/cutting-status';
|
import { cuttingStatusBadgeVariant } from '@/constants/cutting-status';
|
||||||
import MediaThumbnailCell from '@/components/media/MediaThumbnailCell.vue';
|
|
||||||
import type { CuttingListItem } from '@/types/cutting';
|
import type { CuttingListItem } from '@/types/cutting';
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
@ -41,6 +41,7 @@ const groupedMaterials = computed<GroupedMaterials[]>(() => {
|
|||||||
if (!combinationGroups[item.combination_id]) {
|
if (!combinationGroups[item.combination_id]) {
|
||||||
combinationGroups[item.combination_id] = [];
|
combinationGroups[item.combination_id] = [];
|
||||||
}
|
}
|
||||||
|
|
||||||
combinationGroups[item.combination_id].push(item);
|
combinationGroups[item.combination_id].push(item);
|
||||||
} else {
|
} else {
|
||||||
nonCombinationItems.push(item);
|
nonCombinationItems.push(item);
|
||||||
@ -195,6 +196,9 @@ const groupedResults = computed<GroupedResults[]>(() => {
|
|||||||
<TableHead class="text-right"
|
<TableHead class="text-right"
|
||||||
>Pemakaian</TableHead
|
>Pemakaian</TableHead
|
||||||
>
|
>
|
||||||
|
<TableHead class="text-right"
|
||||||
|
>Hasil</TableHead
|
||||||
|
>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
</TableHeader>
|
</TableHeader>
|
||||||
<TableBody>
|
<TableBody>
|
||||||
@ -206,7 +210,7 @@ const groupedResults = computed<GroupedResults[]>(() => {
|
|||||||
class="bg-muted/20 hover:bg-muted/20"
|
class="bg-muted/20 hover:bg-muted/20"
|
||||||
>
|
>
|
||||||
<TableCell
|
<TableCell
|
||||||
colspan="3"
|
colspan="4"
|
||||||
class="font-semibold"
|
class="font-semibold"
|
||||||
>
|
>
|
||||||
<template v-if="group.isCombination">
|
<template v-if="group.isCombination">
|
||||||
@ -217,6 +221,13 @@ const groupedResults = computed<GroupedResults[]>(() => {
|
|||||||
>
|
>
|
||||||
{{ group.items.length }} bahan
|
{{ group.items.length }} bahan
|
||||||
</Badge>
|
</Badge>
|
||||||
|
<Badge
|
||||||
|
v-if="group.items[0]?.combination_material_result !== null && group.items[0]?.combination_material_result !== undefined"
|
||||||
|
variant="secondary"
|
||||||
|
class="ml-2 font-normal"
|
||||||
|
>
|
||||||
|
Hasil: {{ group.items[0].combination_material_result }} pcs
|
||||||
|
</Badge>
|
||||||
</template>
|
</template>
|
||||||
<template v-else>
|
<template v-else>
|
||||||
{{ group.name }}
|
{{ group.name }}
|
||||||
@ -250,6 +261,19 @@ const groupedResults = computed<GroupedResults[]>(() => {
|
|||||||
material.material_usage_formatted
|
material.material_usage_formatted
|
||||||
}}
|
}}
|
||||||
</TableCell>
|
</TableCell>
|
||||||
|
<TableCell
|
||||||
|
class="text-right tabular-nums"
|
||||||
|
>
|
||||||
|
<template v-if="group.isCombination && material.combination_material_result !== null && material.combination_material_result !== undefined">
|
||||||
|
{{ material.combination_material_result }} pcs
|
||||||
|
</template>
|
||||||
|
<template v-else-if="!group.isCombination && material.material_result !== null && material.material_result !== undefined">
|
||||||
|
{{ material.material_result }} pcs
|
||||||
|
</template>
|
||||||
|
<template v-else>
|
||||||
|
-
|
||||||
|
</template>
|
||||||
|
</TableCell>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
</template>
|
</template>
|
||||||
</TableBody>
|
</TableBody>
|
||||||
|
|||||||
@ -3,9 +3,10 @@ import { Layers, Plus, Search, X } from '@lucide/vue';
|
|||||||
import { computed, ref, watch } from 'vue';
|
import { computed, ref, watch } from 'vue';
|
||||||
import { toast } from 'vue-sonner';
|
import { toast } from 'vue-sonner';
|
||||||
import PosCatalogVariantThumb from '@/components/catalog/PosCatalogVariantThumb.vue';
|
import PosCatalogVariantThumb from '@/components/catalog/PosCatalogVariantThumb.vue';
|
||||||
|
import { DecimalInput } from '@/components/form/decimal-input';
|
||||||
|
import { NumberInput } from '@/components/form/number-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 { DecimalInput } from '@/components/form/decimal-input';
|
|
||||||
import {
|
import {
|
||||||
Dialog,
|
Dialog,
|
||||||
DialogContent,
|
DialogContent,
|
||||||
@ -13,6 +14,7 @@ import {
|
|||||||
DialogTitle,
|
DialogTitle,
|
||||||
} from '@/components/ui/dialog';
|
} from '@/components/ui/dialog';
|
||||||
import { Input } from '@/components/ui/input';
|
import { Input } from '@/components/ui/input';
|
||||||
|
import { Label } from '@/components/ui/label';
|
||||||
import { apiFetch } from '@/lib/api';
|
import { apiFetch } from '@/lib/api';
|
||||||
import draft_combinations from '@/routes/admin/manage/cuttings/draft_combinations';
|
import draft_combinations from '@/routes/admin/manage/cuttings/draft_combinations';
|
||||||
import type { CuttingMaterialCartItem, CuttingRawMaterialCatalogItem } from '@/types/cutting';
|
import type { CuttingMaterialCartItem, CuttingRawMaterialCatalogItem } from '@/types/cutting';
|
||||||
@ -48,6 +50,7 @@ const emit = defineEmits<{
|
|||||||
|
|
||||||
const search = ref('');
|
const search = ref('');
|
||||||
const selectedMaterials = ref<SelectedMaterial[]>([]);
|
const selectedMaterials = ref<SelectedMaterial[]>([]);
|
||||||
|
const combinationResult = ref<string>('');
|
||||||
const loading = ref(false);
|
const loading = ref(false);
|
||||||
|
|
||||||
const filteredRawMaterials = computed(() => {
|
const filteredRawMaterials = computed(() => {
|
||||||
@ -101,6 +104,7 @@ function removeSelected(priceId: number) {
|
|||||||
function resetForm() {
|
function resetForm() {
|
||||||
search.value = '';
|
search.value = '';
|
||||||
selectedMaterials.value = [];
|
selectedMaterials.value = [];
|
||||||
|
combinationResult.value = '';
|
||||||
}
|
}
|
||||||
|
|
||||||
function initFromVariant() {
|
function initFromVariant() {
|
||||||
@ -123,6 +127,7 @@ function initFromVariant() {
|
|||||||
async function submit() {
|
async function submit() {
|
||||||
if (selectedMaterials.value.length < 2) {
|
if (selectedMaterials.value.length < 2) {
|
||||||
toast.error('Pilih minimal 2 bahan baku untuk dikombinasikan.');
|
toast.error('Pilih minimal 2 bahan baku untuk dikombinasikan.');
|
||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -136,6 +141,7 @@ async function submit() {
|
|||||||
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,
|
||||||
})),
|
})),
|
||||||
|
material_result: combinationResult.value ? Number(combinationResult.value) : null,
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -173,12 +179,9 @@ watch(open, (value) => {
|
|||||||
<div v-if="selectedMaterials.length > 0" class="rounded-lg border p-3 space-y-2">
|
<div v-if="selectedMaterials.length > 0" class="rounded-lg border p-3 space-y-2">
|
||||||
<p class="text-sm font-medium">Bahan Baku Terpilih ({{ selectedMaterials.length }})</p>
|
<p class="text-sm font-medium">Bahan Baku Terpilih ({{ selectedMaterials.length }})</p>
|
||||||
<div class="space-y-2">
|
<div class="space-y-2">
|
||||||
<div
|
<div v-for="item in selectedMaterials" :key="item.raw_material_price_id"
|
||||||
v-for="item in selectedMaterials"
|
|
||||||
:key="item.raw_material_price_id"
|
|
||||||
class="flex items-center gap-2 rounded-md border p-2"
|
class="flex items-center gap-2 rounded-md border p-2"
|
||||||
:class="item.is_initial ? 'border-primary bg-primary/5' : ''"
|
:class="item.is_initial ? 'border-primary bg-primary/5' : ''">
|
||||||
>
|
|
||||||
<PosCatalogVariantThumb :items="item.images" />
|
<PosCatalogVariantThumb :items="item.images" />
|
||||||
<div class="min-w-0 flex-1">
|
<div class="min-w-0 flex-1">
|
||||||
<p class="truncate text-sm font-medium">
|
<p class="truncate text-sm font-medium">
|
||||||
@ -190,20 +193,11 @@ watch(open, (value) => {
|
|||||||
</div>
|
</div>
|
||||||
<div class="flex items-center gap-1" @click.stop>
|
<div class="flex items-center gap-1" @click.stop>
|
||||||
<Label class="text-xs whitespace-nowrap">Pemakaian:</Label>
|
<Label class="text-xs whitespace-nowrap">Pemakaian:</Label>
|
||||||
<DecimalInput
|
<DecimalInput v-model="item.material_usage" class="h-7 w-20" />
|
||||||
v-model="item.material_usage"
|
|
||||||
class="h-7 w-20"
|
|
||||||
/>
|
|
||||||
<span class="text-xs text-muted-foreground">{{ item.unit_abbreviation }}</span>
|
<span class="text-xs text-muted-foreground">{{ item.unit_abbreviation }}</span>
|
||||||
</div>
|
</div>
|
||||||
<Button
|
<Button v-if="!item.is_initial" type="button" variant="ghost" size="icon-sm"
|
||||||
v-if="!item.is_initial"
|
class="text-destructive" @click="removeSelected(item.raw_material_price_id)">
|
||||||
type="button"
|
|
||||||
variant="ghost"
|
|
||||||
size="icon-sm"
|
|
||||||
class="text-destructive"
|
|
||||||
@click="removeSelected(item.raw_material_price_id)"
|
|
||||||
>
|
|
||||||
<X class="size-3.5" />
|
<X class="size-3.5" />
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
@ -212,19 +206,18 @@ watch(open, (value) => {
|
|||||||
|
|
||||||
<div class="relative">
|
<div class="relative">
|
||||||
<Search class="absolute top-1/2 left-3 size-4 -translate-y-1/2 text-muted-foreground" />
|
<Search class="absolute top-1/2 left-3 size-4 -translate-y-1/2 text-muted-foreground" />
|
||||||
<Input v-model="search" placeholder="Cari bahan baku lain untuk dikombinasikan..." class="pl-9" />
|
<Input v-model="search" placeholder="Cari bahan baku lain untuk dikombinasikan..."
|
||||||
|
class="pl-9" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-if="filteredRawMaterials.length === 0" class="py-8 text-center text-sm text-muted-foreground">
|
<div v-if="filteredRawMaterials.length === 0"
|
||||||
|
class="py-8 text-center text-sm text-muted-foreground">
|
||||||
Tidak ada bahan baku ditemukan.
|
Tidak ada bahan baku ditemukan.
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-else class="space-y-3">
|
<div v-else class="space-y-3">
|
||||||
<div
|
<div v-for="rawMaterial in filteredRawMaterials" :key="rawMaterial.id"
|
||||||
v-for="rawMaterial in filteredRawMaterials"
|
class="rounded-lg border p-3">
|
||||||
:key="rawMaterial.id"
|
|
||||||
class="rounded-lg border p-3"
|
|
||||||
>
|
|
||||||
<div class="mb-2 flex items-center gap-2">
|
<div class="mb-2 flex items-center gap-2">
|
||||||
<p class="text-sm font-medium">{{ rawMaterial.name }}</p>
|
<p class="text-sm font-medium">{{ rawMaterial.name }}</p>
|
||||||
<Badge variant="outline" class="text-xs">
|
<Badge variant="outline" class="text-xs">
|
||||||
@ -233,15 +226,11 @@ watch(open, (value) => {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="space-y-1">
|
<div class="space-y-1">
|
||||||
<div
|
<div v-for="price in rawMaterial.prices" :key="price.id"
|
||||||
v-for="price in rawMaterial.prices"
|
|
||||||
:key="price.id"
|
|
||||||
class="flex items-center gap-2.5 rounded-md px-2 py-1.5 transition-all duration-200"
|
class="flex items-center gap-2.5 rounded-md px-2 py-1.5 transition-all duration-200"
|
||||||
:class="[
|
:class="[
|
||||||
isSelected(price.id) ? 'border-2 border-primary bg-primary/5' : 'cursor-pointer hover:bg-muted/30',
|
isSelected(price.id) ? 'border-2 border-primary bg-primary/5' : 'cursor-pointer hover:bg-muted/30',
|
||||||
]"
|
]" @click="!isSelected(price.id) && toggleVariant(rawMaterial, price)">
|
||||||
@click="!isSelected(price.id) && toggleVariant(rawMaterial, price)"
|
|
||||||
>
|
|
||||||
<PosCatalogVariantThumb :items="price.images" />
|
<PosCatalogVariantThumb :items="price.images" />
|
||||||
<div class="min-w-0 flex-1">
|
<div class="min-w-0 flex-1">
|
||||||
<p class="truncate text-sm">{{ price.variant }}</p>
|
<p class="truncate text-sm">{{ price.variant }}</p>
|
||||||
@ -249,14 +238,8 @@ watch(open, (value) => {
|
|||||||
Stok: {{ price.stock_formatted }}
|
Stok: {{ price.stock_formatted }}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<Button
|
<Button v-if="!isSelected(price.id)" type="button" variant="outline" size="icon-sm"
|
||||||
v-if="!isSelected(price.id)"
|
class="shrink-0" @click.stop="toggleVariant(rawMaterial, price)">
|
||||||
type="button"
|
|
||||||
variant="outline"
|
|
||||||
size="icon-sm"
|
|
||||||
class="shrink-0"
|
|
||||||
@click.stop="toggleVariant(rawMaterial, price)"
|
|
||||||
>
|
|
||||||
<Plus class="size-3.5" />
|
<Plus class="size-3.5" />
|
||||||
</Button>
|
</Button>
|
||||||
<Badge v-else variant="secondary" class="text-xs">
|
<Badge v-else variant="secondary" class="text-xs">
|
||||||
@ -270,18 +253,21 @@ watch(open, (value) => {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex items-center justify-between border-t pt-4">
|
<div class="flex items-center justify-between border-t pt-4">
|
||||||
<p class="text-sm text-muted-foreground">
|
<div class="flex items-center gap-3">
|
||||||
{{ selectedMaterials.length }} bahan dipilih
|
<div class="flex items-center gap-2">
|
||||||
</p>
|
<Label class="text-sm whitespace-nowrap">Hasil:</Label>
|
||||||
|
<NumberInput v-model="combinationResult" class="h-8 w-20" placeholder="0" />
|
||||||
|
<span class="text-xs text-muted-foreground">pcs</span>
|
||||||
|
</div>
|
||||||
|
<p class="text-sm text-muted-foreground">
|
||||||
|
{{ selectedMaterials.length }} bahan dipilih
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
<div class="flex gap-2">
|
<div class="flex gap-2">
|
||||||
<Button type="button" variant="outline" :disabled="loading" @click="open = false">
|
<Button type="button" variant="outline" :disabled="loading" @click="open = false">
|
||||||
Batal
|
Batal
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button type="button" :disabled="loading || selectedMaterials.length < 2" @click="submit">
|
||||||
type="button"
|
|
||||||
:disabled="loading || selectedMaterials.length < 2"
|
|
||||||
@click="submit"
|
|
||||||
>
|
|
||||||
<Layers class="size-4 mr-1" />
|
<Layers class="size-4 mr-1" />
|
||||||
{{ loading ? 'Menyimpan...' : 'Simpan Kombinasi' }}
|
{{ loading ? 'Menyimpan...' : 'Simpan Kombinasi' }}
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
@ -99,8 +99,8 @@ const {
|
|||||||
decreaseResultQty,
|
decreaseResultQty,
|
||||||
syncMaterialField,
|
syncMaterialField,
|
||||||
syncResultField,
|
syncResultField,
|
||||||
syncDraftCombination,
|
|
||||||
removeCombination,
|
removeCombination,
|
||||||
|
syncCombinationResult,
|
||||||
} = useCuttingPosCart({
|
} = useCuttingPosCart({
|
||||||
rawMaterialCatalog: rawMaterialCatalogState,
|
rawMaterialCatalog: rawMaterialCatalogState,
|
||||||
productCatalog: productCatalogState,
|
productCatalog: productCatalogState,
|
||||||
@ -144,6 +144,14 @@ function buildFormData(): FormData {
|
|||||||
appendRootPhotosToFormData(formData, imageState.value);
|
appendRootPhotosToFormData(formData, imageState.value);
|
||||||
|
|
||||||
if (props.method === 'put') {
|
if (props.method === 'put') {
|
||||||
|
// Build combination results map
|
||||||
|
const combinationResults: Record<number, number | null> = {};
|
||||||
|
materialCart.value.forEach(item => {
|
||||||
|
if (item.combination_id && item.combination_material_result !== undefined) {
|
||||||
|
combinationResults[item.combination_id] = item.combination_material_result;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
materialCart.value.forEach((item, index) => {
|
materialCart.value.forEach((item, index) => {
|
||||||
formData.append(
|
formData.append(
|
||||||
`materials[${index}][raw_material_price_id]`,
|
`materials[${index}][raw_material_price_id]`,
|
||||||
@ -153,11 +161,29 @@ function buildFormData(): FormData {
|
|||||||
`materials[${index}][material_usage]`,
|
`materials[${index}][material_usage]`,
|
||||||
item.material_usage,
|
item.material_usage,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
if (item.material_result !== undefined && item.material_result !== null) {
|
||||||
|
formData.append(
|
||||||
|
`materials[${index}][material_result]`,
|
||||||
|
item.material_result,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
if (item.combination_id) {
|
if (item.combination_id) {
|
||||||
formData.append(
|
formData.append(
|
||||||
`materials[${index}][combination_id]`,
|
`materials[${index}][combination_id]`,
|
||||||
String(item.combination_id),
|
String(item.combination_id),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Add combination_material_result
|
||||||
|
const combinationResult = combinationResults[item.combination_id];
|
||||||
|
|
||||||
|
if (combinationResult !== undefined && combinationResult !== null) {
|
||||||
|
formData.append(
|
||||||
|
`materials[${index}][combination_material_result]`,
|
||||||
|
String(combinationResult),
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
resultCart.value.forEach((item, index) => {
|
resultCart.value.forEach((item, index) => {
|
||||||
@ -251,8 +277,8 @@ function onProductCreated(product: CuttingProductCatalogItem) {
|
|||||||
<div class="space-y-4">
|
<div class="space-y-4">
|
||||||
<CuttingPosMaterialCatalogPanel v-model:material-search="materialSearch"
|
<CuttingPosMaterialCatalogPanel v-model:material-search="materialSearch"
|
||||||
:filtered-raw-materials="filteredRawMaterials" :raw-material-catalog="rawMaterialCatalogState"
|
:filtered-raw-materials="filteredRawMaterials" :raw-material-catalog="rawMaterialCatalogState"
|
||||||
:material-cart="materialCart" :get-material-cart-item="getMaterialCartItem"
|
:material-cart="materialCart" :get-material-cart-item="getMaterialCartItem" :units="units"
|
||||||
:units="units" @add-material="addMaterial" @decrease-material-qty="decreaseMaterialQty"
|
@add-material="addMaterial" @decrease-material-qty="decreaseMaterialQty"
|
||||||
@raw-material-created="onRawMaterialCreated" @combination-created="onCombinationCreated" />
|
@raw-material-created="onRawMaterialCreated" @combination-created="onCombinationCreated" />
|
||||||
|
|
||||||
<CuttingPosResultCatalogPanel v-model:product-search="productSearch" :filtered-products="filteredProducts"
|
<CuttingPosResultCatalogPanel v-model:product-search="productSearch" :filtered-products="filteredProducts"
|
||||||
@ -266,7 +292,7 @@ function onProductCreated(product: CuttingProductCatalogItem) {
|
|||||||
@open-detail="cartDetailOpen = true" @remove-material="removeMaterial"
|
@open-detail="cartDetailOpen = true" @remove-material="removeMaterial"
|
||||||
@sync-material-field="syncMaterialField" @remove-result="removeResult"
|
@sync-material-field="syncMaterialField" @remove-result="removeResult"
|
||||||
@sync-result-totals="syncResultTotals" @sync-result-field="syncResultField"
|
@sync-result-totals="syncResultTotals" @sync-result-field="syncResultField"
|
||||||
@remove-combination="removeCombination" />
|
@remove-combination="removeCombination" @sync-combination-result="syncCombinationResult" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<CuttingPosCartDetailDialog v-model:open="cartDetailOpen" :material-cart="materialCart" :result-cart="resultCart"
|
<CuttingPosCartDetailDialog v-model:open="cartDetailOpen" :material-cart="materialCart" :result-cart="resultCart"
|
||||||
|
|||||||
@ -2,6 +2,7 @@
|
|||||||
import { Layers, Trash2 } from '@lucide/vue';
|
import { Layers, Trash2 } from '@lucide/vue';
|
||||||
import { computed } from 'vue';
|
import { computed } from 'vue';
|
||||||
import { DecimalInput } from '@/components/form/decimal-input';
|
import { DecimalInput } from '@/components/form/decimal-input';
|
||||||
|
import { NumberInput } from '@/components/form/number-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 {
|
import {
|
||||||
@ -22,6 +23,7 @@ const emit = defineEmits<{
|
|||||||
remove: [index: number];
|
remove: [index: number];
|
||||||
'sync-field': [index: number];
|
'sync-field': [index: number];
|
||||||
'remove-combination': [combinationId: number];
|
'remove-combination': [combinationId: number];
|
||||||
|
'sync-combination-result': [combinationId: number, result: number | null];
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
type MaterialGroup = {
|
type MaterialGroup = {
|
||||||
@ -39,6 +41,7 @@ const materialGroups = computed<MaterialGroup[]>(() => {
|
|||||||
if (!combinationMap.has(item.combination_id)) {
|
if (!combinationMap.has(item.combination_id)) {
|
||||||
combinationMap.set(item.combination_id, []);
|
combinationMap.set(item.combination_id, []);
|
||||||
}
|
}
|
||||||
|
|
||||||
combinationMap.get(item.combination_id)!.push({ item, index });
|
combinationMap.get(item.combination_id)!.push({ item, index });
|
||||||
} else {
|
} else {
|
||||||
singleItems.push({ item, index });
|
singleItems.push({ item, index });
|
||||||
@ -64,25 +67,40 @@ const materialGroups = computed<MaterialGroup[]>(() => {
|
|||||||
|
|
||||||
return groups;
|
return groups;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
function getCombinationResult(combinationId: number | undefined): number | null {
|
||||||
|
if (!combinationId) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const item = props.materialCart.find(i => i.combination_id === combinationId);
|
||||||
|
|
||||||
|
return item?.combination_material_result ?? null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function setCombinationResult(combinationId: number | undefined, value: string) {
|
||||||
|
if (!combinationId) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = value ? Number(value) : null;
|
||||||
|
emit('sync-combination-result', combinationId, result);
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="space-y-2">
|
<div class="space-y-2">
|
||||||
<p class="text-sm font-medium">Bahan Baku</p>
|
<p class="text-sm font-medium">Bahan Baku</p>
|
||||||
|
|
||||||
<div
|
<div v-if="materialCart.length === 0"
|
||||||
v-if="materialCart.length === 0"
|
class="rounded-lg border border-dashed px-4 py-6 text-center text-sm text-muted-foreground">
|
||||||
class="rounded-lg border border-dashed px-4 py-6 text-center text-sm text-muted-foreground"
|
|
||||||
>
|
|
||||||
Belum ada bahan baku dipilih.
|
Belum ada bahan baku dipilih.
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-else class="scrollbar-thin max-h-80 space-y-3 overflow-y-auto overscroll-y-contain">
|
<div v-else class="scrollbar-thin max-h-80 space-y-3 overflow-y-auto overscroll-y-contain">
|
||||||
<template v-for="group in materialGroups" :key="group.combinationId ?? `single-${group.items[0]?.index}`">
|
<template v-for="group in materialGroups" :key="group.combinationId ?? `single-${group.items[0]?.index}`">
|
||||||
<div
|
<div v-if="group.type === 'combination'"
|
||||||
v-if="group.type === 'combination'"
|
class="rounded-lg border-2 border-dashed border-primary/30 p-3">
|
||||||
class="rounded-lg border-2 border-dashed border-primary/30 p-3"
|
|
||||||
>
|
|
||||||
<div class="mb-2 flex items-center justify-between">
|
<div class="mb-2 flex items-center justify-between">
|
||||||
<div class="flex items-center gap-2">
|
<div class="flex items-center gap-2">
|
||||||
<Layers class="size-4 text-primary" />
|
<Layers class="size-4 text-primary" />
|
||||||
@ -91,23 +109,27 @@ const materialGroups = computed<MaterialGroup[]>(() => {
|
|||||||
{{ group.items.length }} bahan
|
{{ group.items.length }} bahan
|
||||||
</Badge>
|
</Badge>
|
||||||
</div>
|
</div>
|
||||||
<Button
|
<div class="flex items-center gap-1.5">
|
||||||
type="button"
|
<div class="flex items-center gap-1" @click.stop>
|
||||||
variant="ghost"
|
<NumberInput
|
||||||
size="icon"
|
:model-value="getCombinationResult(group.combinationId)"
|
||||||
class="size-7 shrink-0 text-destructive hover:text-destructive"
|
class="h-7 w-16"
|
||||||
@click="group.combinationId && emit('remove-combination', group.combinationId)"
|
placeholder="Hasil"
|
||||||
>
|
@update:model-value="setCombinationResult(group.combinationId, $event)"
|
||||||
<Trash2 class="size-3.5" />
|
/>
|
||||||
</Button>
|
<span class="text-[10px] text-muted-foreground">pcs</span>
|
||||||
|
</div>
|
||||||
|
<Button type="button" variant="ghost" size="icon"
|
||||||
|
class="size-7 shrink-0 text-destructive hover:text-destructive"
|
||||||
|
@click="group.combinationId && emit('remove-combination', group.combinationId)">
|
||||||
|
<Trash2 class="size-3.5" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="space-y-2">
|
<div class="space-y-2">
|
||||||
<div
|
<div v-for="{ item, index } in group.items" :key="item.raw_material_price_id"
|
||||||
v-for="{ item, index } in group.items"
|
class="rounded-md border bg-background p-2.5">
|
||||||
:key="item.raw_material_price_id"
|
|
||||||
class="rounded-md border bg-background p-2.5"
|
|
||||||
>
|
|
||||||
<div class="mb-1.5 flex items-start justify-between gap-2">
|
<div class="mb-1.5 flex items-start justify-between gap-2">
|
||||||
<div class="min-w-0">
|
<div class="min-w-0">
|
||||||
<p class="truncate text-sm font-medium">
|
<p class="truncate text-sm font-medium">
|
||||||
@ -117,39 +139,30 @@ const materialGroups = computed<MaterialGroup[]>(() => {
|
|||||||
{{ item.variant }} · Stok {{ item.stock_input }} {{ item.unit_abbreviation }}
|
{{ item.variant }} · Stok {{ item.stock_input }} {{ item.unit_abbreviation }}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<Button
|
<Button type="button" variant="ghost" size="icon"
|
||||||
type="button"
|
|
||||||
variant="ghost"
|
|
||||||
size="icon"
|
|
||||||
class="size-6 shrink-0 text-destructive hover:text-destructive"
|
class="size-6 shrink-0 text-destructive hover:text-destructive"
|
||||||
@click="emit('remove', index)"
|
@click="emit('remove', index)">
|
||||||
>
|
|
||||||
<Trash2 class="size-3" />
|
<Trash2 class="size-3" />
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Field :data-invalid="formErrors(form, `materials.${index}.material_usage`).length > 0 ? 'true' : undefined">
|
<Field
|
||||||
|
:data-invalid="formErrors(form, `materials.${index}.material_usage`).length > 0 ? 'true' : undefined">
|
||||||
<FieldLabel class="text-xs">
|
<FieldLabel class="text-xs">
|
||||||
Pemakaian ({{ item.unit_abbreviation }})
|
Pemakaian ({{ item.unit_abbreviation }})
|
||||||
</FieldLabel>
|
</FieldLabel>
|
||||||
<DecimalInput
|
<DecimalInput v-model="item.material_usage" class="h-7"
|
||||||
v-model="item.material_usage"
|
|
||||||
class="h-7"
|
|
||||||
:aria-invalid="formErrors(form, `materials.${index}.material_usage`).length > 0"
|
:aria-invalid="formErrors(form, `materials.${index}.material_usage`).length > 0"
|
||||||
@change="emit('sync-field', index)"
|
@change="emit('sync-field', index)" />
|
||||||
/>
|
<FieldError :errors="formErrors(form, `materials.${index}.material_usage`)"
|
||||||
<FieldError :errors="formErrors(form, `materials.${index}.material_usage`)" class="text-[10px] mt-0.5 leading-tight" />
|
class="text-[10px] mt-0.5 leading-tight" />
|
||||||
</Field>
|
</Field>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div
|
<div v-else v-for="{ item, index } in group.items" :key="item.raw_material_price_id"
|
||||||
v-else
|
class="rounded-lg border p-3">
|
||||||
v-for="{ item, index } in group.items"
|
|
||||||
:key="item.raw_material_price_id"
|
|
||||||
class="rounded-lg border p-3"
|
|
||||||
>
|
|
||||||
<div class="mb-2 flex items-start justify-between gap-2">
|
<div class="mb-2 flex items-start justify-between gap-2">
|
||||||
<div class="min-w-0">
|
<div class="min-w-0">
|
||||||
<p class="truncate text-sm font-medium">
|
<p class="truncate text-sm font-medium">
|
||||||
@ -159,29 +172,37 @@ const materialGroups = computed<MaterialGroup[]>(() => {
|
|||||||
{{ item.variant }} · Stok {{ item.stock_input }} {{ item.unit_abbreviation }}
|
{{ item.variant }} · Stok {{ item.stock_input }} {{ item.unit_abbreviation }}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<Button
|
<Button type="button" variant="ghost" size="icon"
|
||||||
type="button"
|
|
||||||
variant="ghost"
|
|
||||||
size="icon"
|
|
||||||
class="size-7 shrink-0 text-destructive hover:text-destructive"
|
class="size-7 shrink-0 text-destructive hover:text-destructive"
|
||||||
@click="emit('remove', index)"
|
@click="emit('remove', index)">
|
||||||
>
|
|
||||||
<Trash2 class="size-3.5" />
|
<Trash2 class="size-3.5" />
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Field :data-invalid="formErrors(form, `materials.${index}.material_usage`).length > 0 ? 'true' : undefined">
|
<div class="grid grid-cols-2 gap-2">
|
||||||
<FieldLabel class="text-xs">
|
<Field
|
||||||
Pemakaian ({{ item.unit_abbreviation }})
|
:data-invalid="formErrors(form, `materials.${index}.material_usage`).length > 0 ? 'true' : undefined">
|
||||||
</FieldLabel>
|
<FieldLabel class="text-xs">
|
||||||
<DecimalInput
|
Pemakaian ({{ item.unit_abbreviation }})
|
||||||
v-model="item.material_usage"
|
</FieldLabel>
|
||||||
class="h-8"
|
<DecimalInput v-model="item.material_usage" class="h-8"
|
||||||
:aria-invalid="formErrors(form, `materials.${index}.material_usage`).length > 0"
|
:aria-invalid="formErrors(form, `materials.${index}.material_usage`).length > 0"
|
||||||
@change="emit('sync-field', index)"
|
@change="emit('sync-field', index)" />
|
||||||
/>
|
<FieldError :errors="formErrors(form, `materials.${index}.material_usage`)"
|
||||||
<FieldError :errors="formErrors(form, `materials.${index}.material_usage`)" class="text-[10px] mt-0.5 leading-tight" />
|
class="text-[10px] mt-0.5 leading-tight" />
|
||||||
</Field>
|
</Field>
|
||||||
|
<Field
|
||||||
|
:data-invalid="formErrors(form, `materials.${index}.material_result`).length > 0 ? 'true' : undefined">
|
||||||
|
<FieldLabel class="text-xs">
|
||||||
|
Hasil (pcs)
|
||||||
|
</FieldLabel>
|
||||||
|
<NumberInput v-model="item.material_result" class="h-8"
|
||||||
|
:aria-invalid="formErrors(form, `materials.${index}.material_result`).length > 0"
|
||||||
|
@change="emit('sync-field', index)" />
|
||||||
|
<FieldError :errors="formErrors(form, `materials.${index}.material_result`)"
|
||||||
|
class="text-[10px] mt-0.5 leading-tight" />
|
||||||
|
</Field>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref } from 'vue';
|
|
||||||
import { Check, Minus, Plus, Search } from '@lucide/vue';
|
import { Check, Minus, Plus, Search } from '@lucide/vue';
|
||||||
|
import { ref } from 'vue';
|
||||||
import PosCatalogCard from '@/components/catalog/PosCatalogCard.vue';
|
import PosCatalogCard from '@/components/catalog/PosCatalogCard.vue';
|
||||||
import PosCatalogVariantThumb from '@/components/catalog/PosCatalogVariantThumb.vue';
|
import PosCatalogVariantThumb from '@/components/catalog/PosCatalogVariantThumb.vue';
|
||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
@ -15,8 +15,8 @@ import { Input } from '@/components/ui/input';
|
|||||||
import { getFirstCoverImage } from '@/lib/catalog-cover';
|
import { getFirstCoverImage } from '@/lib/catalog-cover';
|
||||||
import type { CuttingProductCatalogItem, CuttingResultCartItem } from '@/types/cutting';
|
import type { CuttingProductCatalogItem, CuttingResultCartItem } from '@/types/cutting';
|
||||||
import type { CategoryOption } from '@/types/product';
|
import type { CategoryOption } from '@/types/product';
|
||||||
import type { CuttingCatalogVariant } from './useCuttingPosCart';
|
|
||||||
import QuickCreateProductModal from './QuickCreateProductModal.vue';
|
import QuickCreateProductModal from './QuickCreateProductModal.vue';
|
||||||
|
import type { CuttingCatalogVariant } from './useCuttingPosCart';
|
||||||
|
|
||||||
defineProps<{
|
defineProps<{
|
||||||
filteredProducts: CuttingProductCatalogItem[];
|
filteredProducts: CuttingProductCatalogItem[];
|
||||||
|
|||||||
@ -1,5 +1,6 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { Save, Scissors } from '@lucide/vue';
|
import { Save, Scissors } from '@lucide/vue';
|
||||||
|
import MediaDropzone from '@/components/media/MediaDropzone.vue';
|
||||||
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';
|
||||||
@ -13,15 +14,15 @@ import {
|
|||||||
import { Separator } from '@/components/ui/separator';
|
import { Separator } from '@/components/ui/separator';
|
||||||
import { Textarea } from '@/components/ui/textarea';
|
import { Textarea } from '@/components/ui/textarea';
|
||||||
import { FIELD_LIMITS } from '@/lib/field-limits';
|
import { FIELD_LIMITS } from '@/lib/field-limits';
|
||||||
import { formErrors, type FormWithErrors } from '@/lib/form';
|
import { formErrors } from '@/lib/form';
|
||||||
|
import type { FormWithErrors } from '@/lib/form';
|
||||||
import type {
|
import type {
|
||||||
CuttingMaterialCartItem,
|
CuttingMaterialCartItem,
|
||||||
CuttingResultCartItem,
|
CuttingResultCartItem,
|
||||||
} from '@/types/cutting';
|
} from '@/types/cutting';
|
||||||
|
import type { MediaUploadState } from '@/types/media';
|
||||||
import CuttingPosMaterialSummaryItems from './CuttingPosMaterialSummaryItems.vue';
|
import CuttingPosMaterialSummaryItems from './CuttingPosMaterialSummaryItems.vue';
|
||||||
import CuttingPosResultSummaryItems from './CuttingPosResultSummaryItems.vue';
|
import CuttingPosResultSummaryItems from './CuttingPosResultSummaryItems.vue';
|
||||||
import MediaDropzone from '@/components/media/MediaDropzone.vue';
|
|
||||||
import type { MediaUploadState } from '@/types/media';
|
|
||||||
|
|
||||||
defineProps<{
|
defineProps<{
|
||||||
form: FormWithErrors & { description: string; processing?: boolean };
|
form: FormWithErrors & { description: string; processing?: boolean };
|
||||||
@ -42,6 +43,7 @@ const emit = defineEmits<{
|
|||||||
'sync-result-totals': [item: CuttingResultCartItem];
|
'sync-result-totals': [item: CuttingResultCartItem];
|
||||||
'sync-result-field': [index: number];
|
'sync-result-field': [index: number];
|
||||||
'remove-combination': [combinationId: number];
|
'remove-combination': [combinationId: number];
|
||||||
|
'sync-combination-result': [combinationId: number, result: number | null];
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
const imageState = defineModel<MediaUploadState>('imageState', {
|
const imageState = defineModel<MediaUploadState>('imageState', {
|
||||||
@ -52,27 +54,19 @@ const imageState = defineModel<MediaUploadState>('imageState', {
|
|||||||
<template>
|
<template>
|
||||||
<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
|
<CardTitle class="flex items-center justify-between gap-2 text-base">
|
||||||
class="flex items-center justify-between gap-2 text-base"
|
|
||||||
>
|
|
||||||
<span class="flex items-center gap-2">
|
<span class="flex items-center gap-2">
|
||||||
<Scissors class="size-4" />
|
<Scissors class="size-4" />
|
||||||
Ringkasan Cutting
|
Ringkasan Cutting
|
||||||
</span>
|
</span>
|
||||||
<span class="flex items-center gap-2">
|
<span class="flex items-center gap-2">
|
||||||
<button
|
<button v-if="materialCart.length > 0 || resultCart.length > 0" type="button"
|
||||||
v-if="materialCart.length > 0 || resultCart.length > 0"
|
|
||||||
type="button"
|
|
||||||
class="text-xs font-normal text-primary underline underline-offset-2 hover:text-primary/80"
|
class="text-xs font-normal text-primary underline underline-offset-2 hover:text-primary/80"
|
||||||
@click="emit('open-detail')"
|
@click="emit('open-detail')">
|
||||||
>
|
|
||||||
Lihat Detail
|
Lihat Detail
|
||||||
</button>
|
</button>
|
||||||
<Badge
|
<Badge v-if="!isCreateMode && totalResultPieces > 0" variant="secondary"
|
||||||
v-if="!isCreateMode && totalResultPieces > 0"
|
class="font-semibold tabular-nums">
|
||||||
variant="secondary"
|
|
||||||
class="font-semibold tabular-nums"
|
|
||||||
>
|
|
||||||
Total: {{ totalResultPieces }} pcs
|
Total: {{ totalResultPieces }} pcs
|
||||||
</Badge>
|
</Badge>
|
||||||
</span>
|
</span>
|
||||||
@ -83,66 +77,39 @@ const imageState = defineModel<MediaUploadState>('imageState', {
|
|||||||
<FieldGroup>
|
<FieldGroup>
|
||||||
<FieldSet class="grid gap-4">
|
<FieldSet class="grid gap-4">
|
||||||
<Field>
|
<Field>
|
||||||
<FieldLabel for="description"
|
<FieldLabel for="description">Keterangan</FieldLabel>
|
||||||
>Keterangan</FieldLabel
|
<Textarea id="description" v-model="form.description" placeholder="Masukkan keterangan"
|
||||||
>
|
rows="2" :maxlength="FIELD_LIMITS.description" />
|
||||||
<Textarea
|
<FieldError :errors="formErrors(form, 'description')" />
|
||||||
id="description"
|
|
||||||
v-model="form.description"
|
|
||||||
placeholder="Masukkan keterangan"
|
|
||||||
rows="2"
|
|
||||||
:maxlength="FIELD_LIMITS.description"
|
|
||||||
/>
|
|
||||||
<FieldError
|
|
||||||
:errors="formErrors(form, 'description')"
|
|
||||||
/>
|
|
||||||
</Field>
|
</Field>
|
||||||
|
|
||||||
<MediaDropzone
|
<MediaDropzone id="cutting-images" v-model="imageState" label="Foto Cutting" :max-files="10"
|
||||||
id="cutting-images"
|
:errors="formErrors(form, 'images')" />
|
||||||
v-model="imageState"
|
|
||||||
label="Foto Cutting"
|
|
||||||
:max-files="10"
|
|
||||||
:errors="formErrors(form, 'images')"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<CuttingPosMaterialSummaryItems
|
<CuttingPosMaterialSummaryItems :form="form" :material-cart="materialCart"
|
||||||
:form="form"
|
@remove="emit('remove-material', $event)" @sync-field="emit('sync-material-field', $event)"
|
||||||
:material-cart="materialCart"
|
|
||||||
@remove="emit('remove-material', $event)"
|
|
||||||
@sync-field="emit('sync-material-field', $event)"
|
|
||||||
@remove-combination="emit('remove-combination', $event)"
|
@remove-combination="emit('remove-combination', $event)"
|
||||||
/>
|
@sync-combination-result="(combinationId, result) => emit('sync-combination-result', combinationId, result)" />
|
||||||
|
|
||||||
<Separator />
|
<Separator />
|
||||||
|
|
||||||
<CuttingPosResultSummaryItems
|
<CuttingPosResultSummaryItems :form="form" :result-cart="resultCart"
|
||||||
:form="form"
|
:total-result-pieces="totalResultPieces" :is-create-mode="isCreateMode"
|
||||||
:result-cart="resultCart"
|
@remove="emit('remove-result', $event)" @sync-totals="emit('sync-result-totals', $event)"
|
||||||
:total-result-pieces="totalResultPieces"
|
@sync-field="emit('sync-result-field', $event)" />
|
||||||
:is-create-mode="isCreateMode"
|
|
||||||
@remove="emit('remove-result', $event)"
|
|
||||||
@sync-totals="emit('sync-result-totals', $event)"
|
|
||||||
@sync-field="emit('sync-result-field', $event)"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<Button
|
<Button type="submit" class="w-full" :disabled="form.processing ||
|
||||||
type="submit"
|
isUploading ||
|
||||||
class="w-full"
|
materialCart.length === 0 ||
|
||||||
:disabled="
|
resultCart.length === 0
|
||||||
form.processing ||
|
">
|
||||||
isUploading ||
|
|
||||||
materialCart.length === 0 ||
|
|
||||||
resultCart.length === 0
|
|
||||||
"
|
|
||||||
>
|
|
||||||
<Save class="size-4" />
|
<Save class="size-4" />
|
||||||
{{
|
{{
|
||||||
isUploading
|
isUploading
|
||||||
? 'Mengunggah...'
|
? 'Mengunggah...'
|
||||||
: form.processing
|
: form.processing
|
||||||
? 'Menyimpan...'
|
? 'Menyimpan...'
|
||||||
: submitLabel
|
: submitLabel
|
||||||
}}
|
}}
|
||||||
</Button>
|
</Button>
|
||||||
</FieldSet>
|
</FieldSet>
|
||||||
|
|||||||
@ -152,24 +152,27 @@ export function useCuttingPosCart(options: {
|
|||||||
rawMaterial: CuttingRawMaterialCatalogItem,
|
rawMaterial: CuttingRawMaterialCatalogItem,
|
||||||
price: CuttingCatalogPrice,
|
price: CuttingCatalogPrice,
|
||||||
materialUsage: string,
|
materialUsage: string,
|
||||||
|
materialResult?: string | null,
|
||||||
) {
|
) {
|
||||||
const { item } = await apiFetch<{ item: CuttingMaterialCartItem }>(draft_materials.store.url(), {
|
const { item } = await apiFetch<{ item: CuttingMaterialCartItem }>(draft_materials.store.url(), {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
raw_material_price_id: price.id,
|
raw_material_price_id: price.id,
|
||||||
material_usage: materialUsage,
|
material_usage: materialUsage,
|
||||||
|
material_result: materialResult ?? null,
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
|
|
||||||
upsertMaterialCartItem(item);
|
upsertMaterialCartItem(item);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function syncDraftMaterialById(priceId: number, materialUsage: string) {
|
async function syncDraftMaterialById(priceId: number, materialUsage: string, materialResult?: string | null) {
|
||||||
const { item } = await apiFetch<{ item: CuttingMaterialCartItem }>(draft_materials.store.url(), {
|
const { item } = await apiFetch<{ item: CuttingMaterialCartItem }>(draft_materials.store.url(), {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
raw_material_price_id: priceId,
|
raw_material_price_id: priceId,
|
||||||
material_usage: materialUsage,
|
material_usage: materialUsage,
|
||||||
|
material_result: materialResult ?? null,
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -248,6 +251,15 @@ export function useCuttingPosCart(options: {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function syncCombinationResult(combinationId: number, result: number | null) {
|
||||||
|
// Update local state for all items in this combination
|
||||||
|
materialCart.value.forEach(item => {
|
||||||
|
if (item.combination_id === combinationId) {
|
||||||
|
item.combination_material_result = result;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
function getMaterialCartItem(priceId: number): CuttingMaterialCartItem | undefined {
|
function getMaterialCartItem(priceId: number): CuttingMaterialCartItem | undefined {
|
||||||
return materialCart.value.find((item) => item.raw_material_price_id === priceId);
|
return materialCart.value.find((item) => item.raw_material_price_id === priceId);
|
||||||
}
|
}
|
||||||
@ -346,6 +358,7 @@ export function useCuttingPosCart(options: {
|
|||||||
unit_abbreviation: rawMaterial.unit_abbreviation,
|
unit_abbreviation: rawMaterial.unit_abbreviation,
|
||||||
stock_input: price.stock_input,
|
stock_input: price.stock_input,
|
||||||
material_usage: defaultUsage,
|
material_usage: defaultUsage,
|
||||||
|
material_result: null,
|
||||||
images: price.images ?? [],
|
images: price.images ?? [],
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -462,7 +475,7 @@ export function useCuttingPosCart(options: {
|
|||||||
const item = materialCart.value[index];
|
const item = materialCart.value[index];
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await syncDraftMaterialById(item.raw_material_price_id, item.material_usage);
|
await syncDraftMaterialById(item.raw_material_price_id, item.material_usage, item.material_result);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
toast.error(error instanceof Error ? error.message : 'Gagal memperbarui item.');
|
toast.error(error instanceof Error ? error.message : 'Gagal memperbarui item.');
|
||||||
}
|
}
|
||||||
@ -513,5 +526,6 @@ export function useCuttingPosCart(options: {
|
|||||||
syncResultField,
|
syncResultField,
|
||||||
syncDraftCombination,
|
syncDraftCombination,
|
||||||
removeCombination,
|
removeCombination,
|
||||||
|
syncCombinationResult,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,7 +1,6 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { Check } from '@lucide/vue';
|
import { Check } from '@lucide/vue';
|
||||||
import { computed } from 'vue';
|
import { computed } from 'vue';
|
||||||
import DataTableActions from './data-table-actions.vue';
|
|
||||||
import { Badge } from '@/components/ui/badge';
|
import { Badge } from '@/components/ui/badge';
|
||||||
import { Card, CardContent } from '@/components/ui/card';
|
import { Card, CardContent } from '@/components/ui/card';
|
||||||
import {
|
import {
|
||||||
@ -11,6 +10,7 @@ import {
|
|||||||
EmptyTitle,
|
EmptyTitle,
|
||||||
} from '@/components/ui/empty';
|
} from '@/components/ui/empty';
|
||||||
import type { CuttingListItem } from '@/types/cutting';
|
import type { CuttingListItem } from '@/types/cutting';
|
||||||
|
import DataTableActions from './data-table-actions.vue';
|
||||||
|
|
||||||
function formatMaterialUsage(mat: any): string {
|
function formatMaterialUsage(mat: any): string {
|
||||||
return String(parseFloat(Number(mat.material_usage ?? 0).toFixed(2)));
|
return String(parseFloat(Number(mat.material_usage ?? 0).toFixed(2)));
|
||||||
@ -57,6 +57,7 @@ function getMaterialGroups(materials: any[]): MaterialGroup[] {
|
|||||||
if (!combinationMap[mat.combination_id]) {
|
if (!combinationMap[mat.combination_id]) {
|
||||||
combinationMap[mat.combination_id] = [];
|
combinationMap[mat.combination_id] = [];
|
||||||
}
|
}
|
||||||
|
|
||||||
combinationMap[mat.combination_id].push(mat);
|
combinationMap[mat.combination_id].push(mat);
|
||||||
} else {
|
} else {
|
||||||
singleItems.push(mat);
|
singleItems.push(mat);
|
||||||
@ -130,6 +131,9 @@ const totalCompleted = computed(() => props.cuttings.length);
|
|||||||
<div v-for="(group, gi) in getMaterialGroups(cutting.materials)" :key="gi" class="mt-1">
|
<div v-for="(group, gi) in getMaterialGroups(cutting.materials)" :key="gi" class="mt-1">
|
||||||
<div v-if="group.type === 'combination'" class="mb-1">
|
<div v-if="group.type === 'combination'" class="mb-1">
|
||||||
<span class="text-primary font-medium text-[11px]">{{ group.name }} ({{ group.items.length }} bahan)</span>
|
<span class="text-primary font-medium text-[11px]">{{ group.name }} ({{ group.items.length }} bahan)</span>
|
||||||
|
<span v-if="group.items[0]?.combination_material_result !== null && group.items[0]?.combination_material_result !== undefined" class="text-primary font-medium text-[11px] ml-1">
|
||||||
|
- Hasil: {{ group.items[0].combination_material_result }} pcs
|
||||||
|
</span>
|
||||||
<ul class="list-disc pl-4 space-y-0.5 text-muted-foreground">
|
<ul class="list-disc pl-4 space-y-0.5 text-muted-foreground">
|
||||||
<li v-for="mat in group.items" :key="mat.id">
|
<li v-for="mat in group.items" :key="mat.id">
|
||||||
{{ mat.raw_material_name }} ({{ mat.variant }}) - {{ formatMaterialUsage(mat) }} {{ getMaterialUnit(mat) }}
|
{{ mat.raw_material_name }} ({{ mat.variant }}) - {{ formatMaterialUsage(mat) }} {{ getMaterialUnit(mat) }}
|
||||||
@ -139,6 +143,9 @@ const totalCompleted = computed(() => props.cuttings.length);
|
|||||||
<ul v-else class="list-disc pl-4 space-y-0.5 text-muted-foreground">
|
<ul v-else class="list-disc pl-4 space-y-0.5 text-muted-foreground">
|
||||||
<li v-for="mat in group.items" :key="mat.id">
|
<li v-for="mat in group.items" :key="mat.id">
|
||||||
{{ mat.raw_material_name }} ({{ mat.variant }}) - {{ formatMaterialUsage(mat) }} {{ getMaterialUnit(mat) }}
|
{{ mat.raw_material_name }} ({{ mat.variant }}) - {{ formatMaterialUsage(mat) }} {{ getMaterialUnit(mat) }}
|
||||||
|
<template v-if="mat.material_result !== null && mat.material_result !== undefined">
|
||||||
|
→ {{ mat.material_result }} pcs
|
||||||
|
</template>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -1,9 +1,9 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed } from 'vue';
|
import { computed } from 'vue';
|
||||||
import DataTableActions from './data-table-actions.vue';
|
|
||||||
import { DataTableEmpty } from '@/components/data-table';
|
import { DataTableEmpty } from '@/components/data-table';
|
||||||
import DataTableToolbar from '@/components/data-table/DataTableToolbar.vue';
|
import DataTableToolbar from '@/components/data-table/DataTableToolbar.vue';
|
||||||
import GroupedTableFooter from '@/components/data-table/GroupedTableFooter.vue';
|
import GroupedTableFooter from '@/components/data-table/GroupedTableFooter.vue';
|
||||||
|
import MediaThumbnailCell from '@/components/media/MediaThumbnailCell.vue';
|
||||||
import { Badge } from '@/components/ui/badge';
|
import { Badge } from '@/components/ui/badge';
|
||||||
import {
|
import {
|
||||||
Table,
|
Table,
|
||||||
@ -15,13 +15,13 @@ import {
|
|||||||
} from '@/components/ui/table';
|
} from '@/components/ui/table';
|
||||||
import { usePaginationSummary } from '@/composables/usePaginationSummary';
|
import { usePaginationSummary } from '@/composables/usePaginationSummary';
|
||||||
import { cuttingStatusBadgeVariant } from '@/constants/cutting-status';
|
import { cuttingStatusBadgeVariant } from '@/constants/cutting-status';
|
||||||
import MediaThumbnailCell from '@/components/media/MediaThumbnailCell.vue';
|
|
||||||
import { groupedTableRowNumber } from '@/lib/grouped-table';
|
import { groupedTableRowNumber } from '@/lib/grouped-table';
|
||||||
import type { CuttingListItem } from '@/types/cutting';
|
import type { CuttingListItem } from '@/types/cutting';
|
||||||
import type {
|
import type {
|
||||||
DataTablePagination,
|
DataTablePagination,
|
||||||
DataTablePaginationLink,
|
DataTablePaginationLink,
|
||||||
} from '@/types/data-table';
|
} from '@/types/data-table';
|
||||||
|
import DataTableActions from './data-table-actions.vue';
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
cuttings: CuttingListItem[];
|
cuttings: CuttingListItem[];
|
||||||
@ -71,6 +71,7 @@ function getGroupedMaterials(materials: any[]): GroupedCuttingMaterials[] {
|
|||||||
if (!combinationGroups[item.combination_id]) {
|
if (!combinationGroups[item.combination_id]) {
|
||||||
combinationGroups[item.combination_id] = [];
|
combinationGroups[item.combination_id] = [];
|
||||||
}
|
}
|
||||||
|
|
||||||
combinationGroups[item.combination_id].push(item);
|
combinationGroups[item.combination_id].push(item);
|
||||||
} else {
|
} else {
|
||||||
nonCombinationItems.push(item);
|
nonCombinationItems.push(item);
|
||||||
@ -145,24 +146,14 @@ function getGroupedResults(results: any[]): GroupedCuttingResults[] {
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="space-y-4">
|
<div class="space-y-4">
|
||||||
<DataTableToolbar
|
<DataTableToolbar v-model:search="search" @filters-reset="emit('filters-reset')" />
|
||||||
v-model:search="search"
|
|
||||||
@filters-reset="emit('filters-reset')"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<div v-if="cuttings.length" class="space-y-4">
|
<div v-if="cuttings.length" class="space-y-4">
|
||||||
<div
|
<div v-for="(cutting, index) in cuttings" :key="cutting.id" class="overflow-hidden rounded-md border">
|
||||||
v-for="(cutting, index) in cuttings"
|
|
||||||
:key="cutting.id"
|
|
||||||
class="overflow-hidden rounded-md border"
|
|
||||||
>
|
|
||||||
<div
|
<div
|
||||||
class="flex flex-col gap-3 border-b bg-muted/30 px-4 py-3 sm:flex-row sm:items-start sm:justify-between"
|
class="flex flex-col gap-3 border-b bg-muted/30 px-4 py-3 sm:flex-row sm:items-start sm:justify-between">
|
||||||
>
|
|
||||||
<div class="flex min-w-0 items-start gap-3">
|
<div class="flex min-w-0 items-start gap-3">
|
||||||
<span
|
<span class="w-8 shrink-0 pt-0.5 text-center text-sm text-muted-foreground tabular-nums">
|
||||||
class="w-8 shrink-0 pt-0.5 text-center text-sm text-muted-foreground tabular-nums"
|
|
||||||
>
|
|
||||||
{{ rowNumber(index) }}
|
{{ rowNumber(index) }}
|
||||||
</span>
|
</span>
|
||||||
<div class="min-w-0 space-y-2">
|
<div class="min-w-0 space-y-2">
|
||||||
@ -170,19 +161,14 @@ function getGroupedResults(results: any[]): GroupedCuttingResults[] {
|
|||||||
<h3 class="leading-tight font-medium">
|
<h3 class="leading-tight font-medium">
|
||||||
Cutting #{{ cutting.id }}
|
Cutting #{{ cutting.id }}
|
||||||
</h3>
|
</h3>
|
||||||
<Badge
|
<Badge :variant="cuttingStatusBadgeVariant(
|
||||||
:variant="
|
cutting.status,
|
||||||
cuttingStatusBadgeVariant(
|
)
|
||||||
cutting.status,
|
">
|
||||||
)
|
|
||||||
"
|
|
||||||
>
|
|
||||||
{{ cutting.status_label }}
|
{{ cutting.status_label }}
|
||||||
</Badge>
|
</Badge>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div class="space-y-1 text-sm text-muted-foreground">
|
||||||
class="space-y-1 text-sm text-muted-foreground"
|
|
||||||
>
|
|
||||||
<p>{{ cutting.created_at_formatted }}</p>
|
<p>{{ cutting.created_at_formatted }}</p>
|
||||||
<p>
|
<p>
|
||||||
Oleh
|
Oleh
|
||||||
@ -193,72 +179,48 @@ function getGroupedResults(results: any[]): GroupedCuttingResults[] {
|
|||||||
}}
|
}}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<p
|
<p v-if="cutting.description" class="text-sm text-muted-foreground">
|
||||||
v-if="cutting.description"
|
|
||||||
class="text-sm text-muted-foreground"
|
|
||||||
>
|
|
||||||
{{ cutting.description }}
|
{{ cutting.description }}
|
||||||
</p>
|
</p>
|
||||||
<div
|
<div v-if="
|
||||||
v-if="
|
cutting.images && cutting.images.length > 0
|
||||||
cutting.images && cutting.images.length > 0
|
" class="flex flex-wrap gap-2 pt-1">
|
||||||
"
|
<MediaThumbnailCell :items="cutting.images" :max-visible="10" />
|
||||||
class="flex flex-wrap gap-2 pt-1"
|
|
||||||
>
|
|
||||||
<MediaThumbnailCell
|
|
||||||
:items="cutting.images"
|
|
||||||
:max-visible="10"
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="flex flex-wrap gap-x-4 gap-y-1 text-sm">
|
<div class="flex flex-wrap gap-x-4 gap-y-1 text-sm">
|
||||||
<span
|
<span>Total Hasil Cutting
|
||||||
>Total Hasil Cutting
|
<strong class="text-primary">{{
|
||||||
<strong class="text-primary"
|
cutting.total_result_pieces ?? 0
|
||||||
>{{
|
}}
|
||||||
cutting.total_result_pieces ?? 0
|
pcs</strong></span>
|
||||||
}}
|
<span v-if="
|
||||||
pcs</strong
|
cutting.total_material_usage_summary_formatted
|
||||||
></span
|
">Total Pemakaian Bahan
|
||||||
>
|
|
||||||
<span
|
|
||||||
v-if="
|
|
||||||
cutting.total_material_usage_summary_formatted
|
|
||||||
"
|
|
||||||
>Total Pemakaian Bahan
|
|
||||||
<strong class="text-primary">{{
|
<strong class="text-primary">{{
|
||||||
cutting.total_material_usage_summary_formatted
|
cutting.total_material_usage_summary_formatted
|
||||||
}}</strong></span
|
}}</strong></span>
|
||||||
>
|
<span>Biaya Bahan
|
||||||
<span
|
|
||||||
>Biaya Bahan
|
|
||||||
<strong class="text-primary">{{
|
<strong class="text-primary">{{
|
||||||
cutting.total_material_cost_formatted ??
|
cutting.total_material_cost_formatted ??
|
||||||
'Rp 0'
|
'Rp 0'
|
||||||
}}</strong></span
|
}}</strong></span>
|
||||||
>
|
<span>Biaya per Produk
|
||||||
<span
|
|
||||||
>Biaya per Produk
|
|
||||||
<strong class="text-primary">{{
|
<strong class="text-primary">{{
|
||||||
cutting.material_cost_per_product_formatted ??
|
cutting.material_cost_per_product_formatted ??
|
||||||
'Rp 0'
|
'Rp 0'
|
||||||
}}</strong></span
|
}}</strong></span>
|
||||||
>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div
|
<div class="flex shrink-0 items-center justify-end gap-2 sm:pt-0.5">
|
||||||
class="flex shrink-0 items-center justify-end gap-2 sm:pt-0.5"
|
|
||||||
>
|
|
||||||
<DataTableActions :cutting="cutting" />
|
<DataTableActions :cutting="cutting" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="space-y-6 p-4">
|
<div class="space-y-6 p-4">
|
||||||
<div class="space-y-2">
|
<div class="space-y-2">
|
||||||
<h4
|
<h4 class="text-sm font-semibold tracking-tight text-foreground">
|
||||||
class="text-sm font-semibold tracking-tight text-foreground"
|
|
||||||
>
|
|
||||||
Bahan Baku
|
Bahan Baku
|
||||||
</h4>
|
</h4>
|
||||||
<div class="rounded-md border">
|
<div class="rounded-md border">
|
||||||
@ -268,75 +230,62 @@ function getGroupedResults(results: any[]): GroupedCuttingResults[] {
|
|||||||
<TableHead>Varian</TableHead>
|
<TableHead>Varian</TableHead>
|
||||||
<TableHead>Foto</TableHead>
|
<TableHead>Foto</TableHead>
|
||||||
<TableHead>Pemakaian</TableHead>
|
<TableHead>Pemakaian</TableHead>
|
||||||
|
<TableHead>Hasil</TableHead>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
</TableHeader>
|
</TableHeader>
|
||||||
<TableBody>
|
<TableBody>
|
||||||
<TableRow
|
<TableRow v-if="!cutting.materials.length" :key="`${cutting.id}-material-empty`">
|
||||||
v-if="!cutting.materials.length"
|
<TableCell colspan="4" class="text-muted-foreground">
|
||||||
:key="`${cutting.id}-material-empty`"
|
|
||||||
>
|
|
||||||
<TableCell
|
|
||||||
colspan="3"
|
|
||||||
class="text-muted-foreground"
|
|
||||||
>
|
|
||||||
Belum ada bahan baku
|
Belum ada bahan baku
|
||||||
</TableCell>
|
</TableCell>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
<template
|
<template v-else v-for="group in getGroupedMaterials(
|
||||||
v-else
|
cutting.materials,
|
||||||
v-for="group in getGroupedMaterials(
|
)" :key="group.rawMaterialId">
|
||||||
cutting.materials,
|
<TableRow class="bg-muted/20 hover:bg-muted/20">
|
||||||
)"
|
<TableCell colspan="4" class="font-semibold text-foreground">
|
||||||
:key="group.rawMaterialId"
|
|
||||||
>
|
|
||||||
<TableRow
|
|
||||||
class="bg-muted/20 hover:bg-muted/20"
|
|
||||||
>
|
|
||||||
<TableCell
|
|
||||||
colspan="3"
|
|
||||||
class="font-semibold text-foreground"
|
|
||||||
>
|
|
||||||
<template v-if="group.isCombination">
|
<template v-if="group.isCombination">
|
||||||
<span class="text-primary">{{ group.rawMaterialName }}</span>
|
<span class="text-primary">{{ group.rawMaterialName }}</span>
|
||||||
<Badge
|
<Badge variant="default" class="ml-2 font-normal">
|
||||||
variant="default"
|
|
||||||
class="ml-2 font-normal"
|
|
||||||
>
|
|
||||||
{{ group.items.length }} bahan
|
{{ group.items.length }} bahan
|
||||||
</Badge>
|
</Badge>
|
||||||
|
<Badge
|
||||||
|
v-if="group.items[0]?.combination_material_result !== null && group.items[0]?.combination_material_result !== undefined"
|
||||||
|
variant="secondary" class="ml-2 font-normal">
|
||||||
|
Hasil: {{ group.items[0].combination_material_result }} pcs
|
||||||
|
</Badge>
|
||||||
</template>
|
</template>
|
||||||
<template v-else>
|
<template v-else>
|
||||||
{{ group.rawMaterialName }}
|
{{ group.rawMaterialName }}
|
||||||
<Badge
|
<Badge v-if="group.unitLabel" variant="secondary"
|
||||||
v-if="group.unitLabel"
|
class="ml-2 font-normal">
|
||||||
variant="secondary"
|
|
||||||
class="ml-2 font-normal"
|
|
||||||
>
|
|
||||||
{{ group.unitLabel }}
|
{{ group.unitLabel }}
|
||||||
</Badge>
|
</Badge>
|
||||||
</template>
|
</template>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
<TableRow
|
<TableRow v-for="material in group.items" :key="material.id">
|
||||||
v-for="material in group.items"
|
|
||||||
:key="material.id"
|
|
||||||
>
|
|
||||||
<TableCell class="pl-6 font-medium">
|
<TableCell class="pl-6 font-medium">
|
||||||
{{ material.variant }}
|
{{ material.variant }}
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell>
|
<TableCell>
|
||||||
<MediaThumbnailCell
|
<MediaThumbnailCell :items="material.images ?? []
|
||||||
:items="
|
" :max-visible="1" />
|
||||||
material.images ?? []
|
|
||||||
"
|
|
||||||
:max-visible="1"
|
|
||||||
/>
|
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell class="tabular-nums">
|
<TableCell class="tabular-nums">
|
||||||
{{
|
{{
|
||||||
material.material_usage_formatted
|
material.material_usage_formatted
|
||||||
}}
|
}}
|
||||||
</TableCell>
|
</TableCell>
|
||||||
|
<TableCell class="tabular-nums">
|
||||||
|
<template
|
||||||
|
v-if="!group.isCombination && material.material_result !== null && material.material_result !== undefined">
|
||||||
|
{{ material.material_result }} pcs
|
||||||
|
</template>
|
||||||
|
<template v-else>
|
||||||
|
-
|
||||||
|
</template>
|
||||||
|
</TableCell>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
</template>
|
</template>
|
||||||
</TableBody>
|
</TableBody>
|
||||||
@ -345,9 +294,7 @@ function getGroupedResults(results: any[]): GroupedCuttingResults[] {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="space-y-2">
|
<div class="space-y-2">
|
||||||
<h4
|
<h4 class="text-sm font-semibold tracking-tight text-foreground">
|
||||||
class="text-sm font-semibold tracking-tight text-foreground"
|
|
||||||
>
|
|
||||||
Hasil Produk
|
Hasil Produk
|
||||||
</h4>
|
</h4>
|
||||||
<div class="rounded-md border">
|
<div class="rounded-md border">
|
||||||
@ -362,51 +309,29 @@ function getGroupedResults(results: any[]): GroupedCuttingResults[] {
|
|||||||
</TableRow>
|
</TableRow>
|
||||||
</TableHeader>
|
</TableHeader>
|
||||||
<TableBody>
|
<TableBody>
|
||||||
<TableRow
|
<TableRow v-if="!cutting.results.length" :key="`${cutting.id}-result-empty`">
|
||||||
v-if="!cutting.results.length"
|
<TableCell colspan="5" class="text-muted-foreground">
|
||||||
:key="`${cutting.id}-result-empty`"
|
|
||||||
>
|
|
||||||
<TableCell
|
|
||||||
colspan="5"
|
|
||||||
class="text-muted-foreground"
|
|
||||||
>
|
|
||||||
Belum ada hasil produk
|
Belum ada hasil produk
|
||||||
</TableCell>
|
</TableCell>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
<template
|
<template v-else v-for="group in getGroupedResults(
|
||||||
v-else
|
cutting.results,
|
||||||
v-for="group in getGroupedResults(
|
)" :key="group.productId">
|
||||||
cutting.results,
|
<TableRow class="bg-muted/20 hover:bg-muted/20">
|
||||||
)"
|
<TableCell colspan="5" class="font-semibold text-foreground">
|
||||||
:key="group.productId"
|
|
||||||
>
|
|
||||||
<TableRow
|
|
||||||
class="bg-muted/20 hover:bg-muted/20"
|
|
||||||
>
|
|
||||||
<TableCell
|
|
||||||
colspan="5"
|
|
||||||
class="font-semibold text-foreground"
|
|
||||||
>
|
|
||||||
{{ group.productName }}
|
{{ group.productName }}
|
||||||
</TableCell>
|
</TableCell>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
<TableRow
|
<TableRow v-for="result in group.items" :key="result.id">
|
||||||
v-for="result in group.items"
|
|
||||||
:key="result.id"
|
|
||||||
>
|
|
||||||
<TableCell class="pl-6 font-medium">
|
<TableCell class="pl-6 font-medium">
|
||||||
{{
|
{{
|
||||||
result.product_variant?.name
|
result.product_variant?.name
|
||||||
}}
|
}}
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell>
|
<TableCell>
|
||||||
<MediaThumbnailCell
|
<MediaThumbnailCell :items="result.product_variant
|
||||||
:items="
|
?.images ?? []
|
||||||
result.product_variant
|
" :max-visible="1" />
|
||||||
?.images ?? []
|
|
||||||
"
|
|
||||||
:max-visible="1"
|
|
||||||
/>
|
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell class="tabular-nums">
|
<TableCell class="tabular-nums">
|
||||||
{{ result.cutting_result }} pcs
|
{{ result.cutting_result }} pcs
|
||||||
@ -430,15 +355,8 @@ function getGroupedResults(results: any[]): GroupedCuttingResults[] {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<DataTableEmpty
|
<DataTableEmpty v-else description="Silakan lakukan pencarian untuk menemukan data yang Anda cari." />
|
||||||
v-else
|
|
||||||
description="Silakan lakukan pencarian untuk menemukan data yang Anda cari."
|
|
||||||
/>
|
|
||||||
|
|
||||||
<GroupedTableFooter
|
<GroupedTableFooter :summary="paginationSummary" :pagination="pagination" :pagination-links="paginationLinks" />
|
||||||
:summary="paginationSummary"
|
|
||||||
:pagination="pagination"
|
|
||||||
:pagination-links="paginationLinks"
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@ -48,6 +48,7 @@ function getMaterialGroups(materials: any[]): MaterialGroup[] {
|
|||||||
if (!combinationMap[mat.combination_id]) {
|
if (!combinationMap[mat.combination_id]) {
|
||||||
combinationMap[mat.combination_id] = [];
|
combinationMap[mat.combination_id] = [];
|
||||||
}
|
}
|
||||||
|
|
||||||
combinationMap[mat.combination_id].push(mat);
|
combinationMap[mat.combination_id].push(mat);
|
||||||
} else {
|
} else {
|
||||||
singleItems.push(mat);
|
singleItems.push(mat);
|
||||||
@ -102,6 +103,9 @@ defineProps<{
|
|||||||
<div v-for="(group, gi) in getMaterialGroups(cutting.materials)" :key="gi" class="mt-1">
|
<div v-for="(group, gi) in getMaterialGroups(cutting.materials)" :key="gi" class="mt-1">
|
||||||
<div v-if="group.type === 'combination'" class="mb-1">
|
<div v-if="group.type === 'combination'" class="mb-1">
|
||||||
<span class="text-primary font-medium text-[11px]">{{ group.name }} ({{ group.items.length }} bahan)</span>
|
<span class="text-primary font-medium text-[11px]">{{ group.name }} ({{ group.items.length }} bahan)</span>
|
||||||
|
<span v-if="group.items[0]?.combination_material_result !== null && group.items[0]?.combination_material_result !== undefined" class="text-primary font-medium text-[11px] ml-1">
|
||||||
|
- Hasil: {{ group.items[0].combination_material_result }} pcs
|
||||||
|
</span>
|
||||||
<ul class="list-disc pl-4 space-y-0.5 text-muted-foreground">
|
<ul class="list-disc pl-4 space-y-0.5 text-muted-foreground">
|
||||||
<li v-for="mat in group.items" :key="mat.id">
|
<li v-for="mat in group.items" :key="mat.id">
|
||||||
{{ mat.raw_material_name }} ({{ mat.variant }}) - {{ formatMaterialUsage(mat) }} {{ getMaterialUnit(mat) }}
|
{{ mat.raw_material_name }} ({{ mat.variant }}) - {{ formatMaterialUsage(mat) }} {{ getMaterialUnit(mat) }}
|
||||||
@ -111,6 +115,9 @@ defineProps<{
|
|||||||
<ul v-else class="list-disc pl-4 space-y-0.5 text-muted-foreground">
|
<ul v-else class="list-disc pl-4 space-y-0.5 text-muted-foreground">
|
||||||
<li v-for="mat in group.items" :key="mat.id">
|
<li v-for="mat in group.items" :key="mat.id">
|
||||||
{{ mat.raw_material_name }} ({{ mat.variant }}) - {{ formatMaterialUsage(mat) }} {{ getMaterialUnit(mat) }}
|
{{ mat.raw_material_name }} ({{ mat.variant }}) - {{ formatMaterialUsage(mat) }} {{ getMaterialUnit(mat) }}
|
||||||
|
<template v-if="mat.material_result !== null && mat.material_result !== undefined">
|
||||||
|
→ {{ mat.material_result }} pcs
|
||||||
|
</template>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -1,16 +1,16 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
import { Head } from '@inertiajs/vue3';
|
||||||
|
import { ref } from 'vue';
|
||||||
import BackButton from '@/components/button/BackButton.vue';
|
import BackButton from '@/components/button/BackButton.vue';
|
||||||
import AdminLayout from '@/layouts/AdminLayout.vue';
|
import AdminLayout from '@/layouts/AdminLayout.vue';
|
||||||
|
import { index, store } from '@/routes/admin/manage/orders';
|
||||||
import type {
|
import type {
|
||||||
EnumOption,
|
EnumOption,
|
||||||
OrderCartItem,
|
OrderCartItem,
|
||||||
OrderCatalogItem,
|
OrderCatalogItem,
|
||||||
SelectOption,
|
SelectOption,
|
||||||
} from '@/types/order';
|
} from '@/types/order';
|
||||||
import { Head } from '@inertiajs/vue3';
|
|
||||||
import { ref } from 'vue';
|
|
||||||
import OrderPosForm from './form/OrderPosForm.vue';
|
import OrderPosForm from './form/OrderPosForm.vue';
|
||||||
import { index, store } from '@/routes/admin/manage/orders';
|
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
customers: SelectOption[];
|
customers: SelectOption[];
|
||||||
|
|||||||
@ -1,16 +1,16 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
import { Head } from '@inertiajs/vue3';
|
||||||
|
import { computed, ref } from 'vue';
|
||||||
import BackButton from '@/components/button/BackButton.vue';
|
import BackButton from '@/components/button/BackButton.vue';
|
||||||
import AdminLayout from '@/layouts/AdminLayout.vue';
|
import AdminLayout from '@/layouts/AdminLayout.vue';
|
||||||
|
import { index, update } from '@/routes/admin/manage/orders';
|
||||||
import type {
|
import type {
|
||||||
EnumOption,
|
EnumOption,
|
||||||
OrderCatalogItem,
|
OrderCatalogItem,
|
||||||
OrderEditItem,
|
OrderEditItem,
|
||||||
SelectOption,
|
SelectOption,
|
||||||
} from '@/types/order';
|
} from '@/types/order';
|
||||||
import { Head } from '@inertiajs/vue3';
|
|
||||||
import { computed, ref } from 'vue';
|
|
||||||
import OrderPosForm from './form/OrderPosForm.vue';
|
import OrderPosForm from './form/OrderPosForm.vue';
|
||||||
import { index, update } from '@/routes/admin/manage/orders';
|
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
order: OrderEditItem;
|
order: OrderEditItem;
|
||||||
|
|||||||
@ -1,5 +1,9 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
import { Head, usePage } from '@inertiajs/vue3';
|
||||||
|
import { computed, onMounted, ref, watch } from 'vue';
|
||||||
|
import { toast } from 'vue-sonner';
|
||||||
import CreateButton from '@/components/button/CreateButton.vue';
|
import CreateButton from '@/components/button/CreateButton.vue';
|
||||||
|
import ThermalPrinterConnectButton from '@/components/order/ThermalPrinterConnectButton.vue';
|
||||||
import { Card, CardContent } from '@/components/ui/card';
|
import { Card, CardContent } from '@/components/ui/card';
|
||||||
import { useCan } from '@/composables/useCan';
|
import { useCan } from '@/composables/useCan';
|
||||||
import {
|
import {
|
||||||
@ -12,12 +16,8 @@ import {
|
|||||||
} from '@/composables/useThermalPrinter';
|
} from '@/composables/useThermalPrinter';
|
||||||
import AdminLayout from '@/layouts/AdminLayout.vue';
|
import AdminLayout from '@/layouts/AdminLayout.vue';
|
||||||
import type { PaperSize } from '@/lib/thermal-printer/types';
|
import type { PaperSize } from '@/lib/thermal-printer/types';
|
||||||
import type { PaginatedOrders } from '@/types/order';
|
|
||||||
import { Head, usePage } from '@inertiajs/vue3';
|
|
||||||
import { computed, onMounted, ref, watch } from 'vue';
|
|
||||||
import { toast } from 'vue-sonner';
|
|
||||||
import { index, create } from '@/routes/admin/manage/orders';
|
import { index, create } from '@/routes/admin/manage/orders';
|
||||||
import ThermalPrinterConnectButton from '@/components/order/ThermalPrinterConnectButton.vue';
|
import type { PaginatedOrders } from '@/types/order';
|
||||||
import OrderGroupedTable from './table/OrderGroupedTable.vue';
|
import OrderGroupedTable from './table/OrderGroupedTable.vue';
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
|
|||||||
@ -11,8 +11,8 @@ import {
|
|||||||
} from '@lucide/vue';
|
} from '@lucide/vue';
|
||||||
import { computed, ref } from 'vue';
|
import { computed, ref } from 'vue';
|
||||||
import { toast } from 'vue-sonner';
|
import { toast } from 'vue-sonner';
|
||||||
import BackButton from '@/components/button/BackButton.vue';
|
|
||||||
import { RowDeleteAction, RowDetailAction, RowEditAction, RowStatusAction } from '@/components/button';
|
import { RowDeleteAction, RowDetailAction, RowEditAction, RowStatusAction } from '@/components/button';
|
||||||
|
import BackButton from '@/components/button/BackButton.vue';
|
||||||
import ConfirmDialog from '@/components/ConfirmDialog.vue';
|
import ConfirmDialog from '@/components/ConfirmDialog.vue';
|
||||||
import { Badge } from '@/components/ui/badge';
|
import { Badge } from '@/components/ui/badge';
|
||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
|
|||||||
@ -1,4 +1,7 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
import { Save } from '@lucide/vue';
|
||||||
|
import { reactive, ref } from 'vue';
|
||||||
|
import { toast } from 'vue-sonner';
|
||||||
import { PhoneNumberInput } from '@/components/form/phone-number-input';
|
import { PhoneNumberInput } from '@/components/form/phone-number-input';
|
||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
import {
|
import {
|
||||||
@ -19,9 +22,6 @@ import { Input } from '@/components/ui/input';
|
|||||||
import { Textarea } from '@/components/ui/textarea';
|
import { Textarea } from '@/components/ui/textarea';
|
||||||
import { apiFetch } from '@/lib/api';
|
import { apiFetch } from '@/lib/api';
|
||||||
import { FIELD_LIMITS } from '@/lib/field-limits';
|
import { FIELD_LIMITS } from '@/lib/field-limits';
|
||||||
import { Save } from '@lucide/vue';
|
|
||||||
import { reactive, ref } from 'vue';
|
|
||||||
import { toast } from 'vue-sonner';
|
|
||||||
import { store } from '@/routes/api/master/customers';
|
import { store } from '@/routes/api/master/customers';
|
||||||
|
|
||||||
const open = defineModel<boolean>('open', { default: false });
|
const open = defineModel<boolean>('open', { default: false });
|
||||||
|
|||||||
@ -1,4 +1,5 @@
|
|||||||
import { computed, ref, watch, type MaybeRefOrGetter, toValue } from 'vue';
|
import { computed, ref, watch, toValue } from 'vue';
|
||||||
|
import type {MaybeRefOrGetter} from 'vue';
|
||||||
import { toast } from 'vue-sonner';
|
import { toast } from 'vue-sonner';
|
||||||
import { OrderChannel } from '@/constants/order-channel';
|
import { OrderChannel } from '@/constants/order-channel';
|
||||||
import { StockQuality } from '@/constants/stock-quality';
|
import { StockQuality } from '@/constants/stock-quality';
|
||||||
|
|||||||
@ -1,6 +1,5 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed } from 'vue';
|
import { computed } from 'vue';
|
||||||
import DataTableActions from './data-table-actions.vue';
|
|
||||||
import DataTableToolbar from '@/components/data-table/DataTableToolbar.vue';
|
import DataTableToolbar from '@/components/data-table/DataTableToolbar.vue';
|
||||||
import GroupedTableFooter from '@/components/data-table/GroupedTableFooter.vue';
|
import GroupedTableFooter from '@/components/data-table/GroupedTableFooter.vue';
|
||||||
import { Badge } from '@/components/ui/badge';
|
import { Badge } from '@/components/ui/badge';
|
||||||
@ -26,6 +25,7 @@ import type {
|
|||||||
DataTablePaginationLink,
|
DataTablePaginationLink,
|
||||||
} from '@/types/data-table';
|
} from '@/types/data-table';
|
||||||
import type { OrderListItem } from '@/types/order';
|
import type { OrderListItem } from '@/types/order';
|
||||||
|
import DataTableActions from './data-table-actions.vue';
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
orders: OrderListItem[];
|
orders: OrderListItem[];
|
||||||
|
|||||||
@ -1,14 +1,14 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
import { Head } from '@inertiajs/vue3';
|
||||||
import BackButton from '@/components/button/BackButton.vue';
|
import BackButton from '@/components/button/BackButton.vue';
|
||||||
import AdminLayout from '@/layouts/AdminLayout.vue';
|
import AdminLayout from '@/layouts/AdminLayout.vue';
|
||||||
|
import { index, store } from '@/routes/admin/manage/purchases';
|
||||||
import type {
|
import type {
|
||||||
PurchaseCartItem,
|
PurchaseCartItem,
|
||||||
PurchaseCatalogItem,
|
PurchaseCatalogItem,
|
||||||
SelectOption,
|
SelectOption,
|
||||||
} from '@/types/purchase';
|
} from '@/types/purchase';
|
||||||
import { Head } from '@inertiajs/vue3';
|
|
||||||
import PurchasePosForm from './form/PurchasePosForm.vue';
|
import PurchasePosForm from './form/PurchasePosForm.vue';
|
||||||
import { index, store } from '@/routes/admin/manage/purchases';
|
|
||||||
|
|
||||||
defineProps<{
|
defineProps<{
|
||||||
suppliers: SelectOption[];
|
suppliers: SelectOption[];
|
||||||
|
|||||||
@ -1,15 +1,15 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
import { Head } from '@inertiajs/vue3';
|
||||||
|
import { computed } from 'vue';
|
||||||
import BackButton from '@/components/button/BackButton.vue';
|
import BackButton from '@/components/button/BackButton.vue';
|
||||||
import AdminLayout from '@/layouts/AdminLayout.vue';
|
import AdminLayout from '@/layouts/AdminLayout.vue';
|
||||||
|
import { index, update } from '@/routes/admin/manage/purchases';
|
||||||
import type {
|
import type {
|
||||||
PurchaseCatalogItem,
|
PurchaseCatalogItem,
|
||||||
PurchaseEditItem,
|
PurchaseEditItem,
|
||||||
SelectOption,
|
SelectOption,
|
||||||
} from '@/types/purchase';
|
} from '@/types/purchase';
|
||||||
import { Head } from '@inertiajs/vue3';
|
|
||||||
import { computed } from 'vue';
|
|
||||||
import PurchasePosForm from './form/PurchasePosForm.vue';
|
import PurchasePosForm from './form/PurchasePosForm.vue';
|
||||||
import { index, update } from '@/routes/admin/manage/purchases';
|
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
purchase: PurchaseEditItem;
|
purchase: PurchaseEditItem;
|
||||||
|
|||||||
@ -1,4 +1,6 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
import { Head } from '@inertiajs/vue3';
|
||||||
|
import { computed, ref, watch } from 'vue';
|
||||||
import CreateButton from '@/components/button/CreateButton.vue';
|
import CreateButton from '@/components/button/CreateButton.vue';
|
||||||
import { Card, CardContent } from '@/components/ui/card';
|
import { Card, CardContent } from '@/components/ui/card';
|
||||||
import { useCan } from '@/composables/useCan';
|
import { useCan } from '@/composables/useCan';
|
||||||
@ -7,10 +9,8 @@ import {
|
|||||||
useDataTableQuerySync,
|
useDataTableQuerySync,
|
||||||
} from '@/composables/useDataTableQuery';
|
} from '@/composables/useDataTableQuery';
|
||||||
import AdminLayout from '@/layouts/AdminLayout.vue';
|
import AdminLayout from '@/layouts/AdminLayout.vue';
|
||||||
import type { PaginatedPurchases } from '@/types/purchase';
|
|
||||||
import { Head } from '@inertiajs/vue3';
|
|
||||||
import { computed, ref, watch } from 'vue';
|
|
||||||
import { index, create } from '@/routes/admin/manage/purchases';
|
import { index, create } from '@/routes/admin/manage/purchases';
|
||||||
|
import type { PaginatedPurchases } from '@/types/purchase';
|
||||||
import PurchaseGroupedTable from './table/PurchaseGroupedTable.vue';
|
import PurchaseGroupedTable from './table/PurchaseGroupedTable.vue';
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed } from 'vue';
|
|
||||||
import { Save } from '@lucide/vue';
|
import { Save } from '@lucide/vue';
|
||||||
|
import { computed } from 'vue';
|
||||||
import { RupiahInput } from '@/components/form/rupiah-input';
|
import { RupiahInput } from '@/components/form/rupiah-input';
|
||||||
import MediaDropzone from '@/components/media/MediaDropzone.vue';
|
import MediaDropzone from '@/components/media/MediaDropzone.vue';
|
||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
@ -12,7 +12,8 @@ import {
|
|||||||
import { Separator } from '@/components/ui/separator';
|
import { Separator } from '@/components/ui/separator';
|
||||||
import { Textarea } from '@/components/ui/textarea';
|
import { Textarea } from '@/components/ui/textarea';
|
||||||
import { FIELD_LIMITS } from '@/lib/field-limits';
|
import { FIELD_LIMITS } from '@/lib/field-limits';
|
||||||
import { formErrors, type FormWithErrors } from '@/lib/form';
|
import { formErrors } from '@/lib/form';
|
||||||
|
import type {FormWithErrors} from '@/lib/form';
|
||||||
import { formatRupiah } from '@/lib/rupiah';
|
import { formatRupiah } from '@/lib/rupiah';
|
||||||
import type { MediaUploadState } from '@/types/media';
|
import type { MediaUploadState } from '@/types/media';
|
||||||
|
|
||||||
|
|||||||
@ -12,7 +12,8 @@ import {
|
|||||||
SelectTrigger,
|
SelectTrigger,
|
||||||
SelectValue,
|
SelectValue,
|
||||||
} from '@/components/ui/select';
|
} from '@/components/ui/select';
|
||||||
import { formErrors, type FormWithErrors } from '@/lib/form';
|
import { formErrors } from '@/lib/form';
|
||||||
|
import type {FormWithErrors} from '@/lib/form';
|
||||||
import type { SelectOption } from '@/types/purchase';
|
import type { SelectOption } from '@/types/purchase';
|
||||||
|
|
||||||
defineProps<{
|
defineProps<{
|
||||||
|
|||||||
@ -1,4 +1,5 @@
|
|||||||
import { computed, ref, type MaybeRefOrGetter, toValue } from 'vue';
|
import { computed, ref, toValue } from 'vue';
|
||||||
|
import type {MaybeRefOrGetter} from 'vue';
|
||||||
import { toast } from 'vue-sonner';
|
import { toast } from 'vue-sonner';
|
||||||
import { apiFetch } from '@/lib/api';
|
import { apiFetch } from '@/lib/api';
|
||||||
import { store as syncDraftRoute, destroy as destroyDraftRoute } from '@/routes/admin/manage/purchases/draft_items';
|
import { store as syncDraftRoute, destroy as destroyDraftRoute } from '@/routes/admin/manage/purchases/draft_items';
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import OwnerVerificationRowActions from '@/components/owner-verification/OwnerVerificationRowActions.vue';
|
|
||||||
import MediaThumbnailCell from '@/components/media/MediaThumbnailCell.vue';
|
import MediaThumbnailCell from '@/components/media/MediaThumbnailCell.vue';
|
||||||
|
import OwnerVerificationRowActions from '@/components/owner-verification/OwnerVerificationRowActions.vue';
|
||||||
import { Badge } from '@/components/ui/badge';
|
import { Badge } from '@/components/ui/badge';
|
||||||
import {
|
import {
|
||||||
Table,
|
Table,
|
||||||
|
|||||||
@ -1,12 +1,12 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import BackButton from '@/components/button/BackButton.vue';
|
|
||||||
import { Head, router } from '@inertiajs/vue3';
|
import { Head, router } from '@inertiajs/vue3';
|
||||||
import { Loader2, Send } from '@lucide/vue';
|
import { Loader2, Send } from '@lucide/vue';
|
||||||
import { ref } from 'vue';
|
import { ref } from 'vue';
|
||||||
|
import BackButton from '@/components/button/BackButton.vue';
|
||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
import AdminLayout from '@/layouts/AdminLayout.vue';
|
import AdminLayout from '@/layouts/AdminLayout.vue';
|
||||||
import type { CatalogProduct } from '@/types/stok-opname';
|
|
||||||
import { index, submit } from '@/routes/admin/manage/stok-opnames';
|
import { index, submit } from '@/routes/admin/manage/stok-opnames';
|
||||||
|
import type { CatalogProduct } from '@/types/stok-opname';
|
||||||
import StokOpnameAutoSaveStatus from './form/StokOpnameAutoSaveStatus.vue';
|
import StokOpnameAutoSaveStatus from './form/StokOpnameAutoSaveStatus.vue';
|
||||||
import StokOpnameInfoSection from './form/StokOpnameInfoSection.vue';
|
import StokOpnameInfoSection from './form/StokOpnameInfoSection.vue';
|
||||||
import StokOpnameProductTable from './form/StokOpnameProductTable.vue';
|
import StokOpnameProductTable from './form/StokOpnameProductTable.vue';
|
||||||
|
|||||||
@ -1,14 +1,14 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import BackButton from '@/components/button/BackButton.vue';
|
|
||||||
import { Badge } from '@/components/ui/badge';
|
|
||||||
import { Head, router } from '@inertiajs/vue3';
|
import { Head, router } from '@inertiajs/vue3';
|
||||||
import { Loader2, Send } from '@lucide/vue';
|
import { Loader2, Send } from '@lucide/vue';
|
||||||
import { ref } from 'vue';
|
import { ref } from 'vue';
|
||||||
|
import BackButton from '@/components/button/BackButton.vue';
|
||||||
|
import { Badge } from '@/components/ui/badge';
|
||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
import AdminLayout from '@/layouts/AdminLayout.vue';
|
|
||||||
import { stokOpnameStatusBadgeVariant } from '@/constants/stok-opname-status';
|
import { stokOpnameStatusBadgeVariant } from '@/constants/stok-opname-status';
|
||||||
import type { CatalogProduct, StokOpnameDetail } from '@/types/stok-opname';
|
import AdminLayout from '@/layouts/AdminLayout.vue';
|
||||||
import { index, submit } from '@/routes/admin/manage/stok-opnames';
|
import { index, submit } from '@/routes/admin/manage/stok-opnames';
|
||||||
|
import type { CatalogProduct, StokOpnameDetail } from '@/types/stok-opname';
|
||||||
import StokOpnameAutoSaveStatus from './form/StokOpnameAutoSaveStatus.vue';
|
import StokOpnameAutoSaveStatus from './form/StokOpnameAutoSaveStatus.vue';
|
||||||
import StokOpnameInfoSection from './form/StokOpnameInfoSection.vue';
|
import StokOpnameInfoSection from './form/StokOpnameInfoSection.vue';
|
||||||
import StokOpnameProductTable from './form/StokOpnameProductTable.vue';
|
import StokOpnameProductTable from './form/StokOpnameProductTable.vue';
|
||||||
|
|||||||
@ -1,4 +1,6 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
import { Head, router } from '@inertiajs/vue3';
|
||||||
|
import { ref, watch } from 'vue';
|
||||||
import CreateButton from '@/components/button/CreateButton.vue';
|
import CreateButton from '@/components/button/CreateButton.vue';
|
||||||
import ConfirmDialog from '@/components/ConfirmDialog.vue';
|
import ConfirmDialog from '@/components/ConfirmDialog.vue';
|
||||||
import { Card, CardContent } from '@/components/ui/card';
|
import { Card, CardContent } from '@/components/ui/card';
|
||||||
@ -8,10 +10,8 @@ import {
|
|||||||
useDataTableQuerySync,
|
useDataTableQuerySync,
|
||||||
} from '@/composables/useDataTableQuery';
|
} from '@/composables/useDataTableQuery';
|
||||||
import AdminLayout from '@/layouts/AdminLayout.vue';
|
import AdminLayout from '@/layouts/AdminLayout.vue';
|
||||||
import type { PaginatedStokOpnames, StokOpnameListItem } from '@/types/stok-opname';
|
|
||||||
import { Head, router } from '@inertiajs/vue3';
|
|
||||||
import { ref, watch } from 'vue';
|
|
||||||
import { index, create, edit, submit, verify, reject } from '@/routes/admin/manage/stok-opnames';
|
import { index, create, edit, submit, verify, reject } from '@/routes/admin/manage/stok-opnames';
|
||||||
|
import type { PaginatedStokOpnames, StokOpnameListItem } from '@/types/stok-opname';
|
||||||
import StokOpnameGroupedTable from './table/StokOpnameGroupedTable.vue';
|
import StokOpnameGroupedTable from './table/StokOpnameGroupedTable.vue';
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
@ -68,19 +68,28 @@ function openRejectDialog(item: StokOpnameListItem) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function confirmSubmit() {
|
function confirmSubmit() {
|
||||||
if (!selectedStokOpname.value) return;
|
if (!selectedStokOpname.value) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
router.post(submit.url(selectedStokOpname.value.id));
|
router.post(submit.url(selectedStokOpname.value.id));
|
||||||
submitDialogOpen.value = false;
|
submitDialogOpen.value = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
function confirmVerify() {
|
function confirmVerify() {
|
||||||
if (!selectedStokOpname.value) return;
|
if (!selectedStokOpname.value) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
router.post(verify.url(selectedStokOpname.value.id));
|
router.post(verify.url(selectedStokOpname.value.id));
|
||||||
verifyDialogOpen.value = false;
|
verifyDialogOpen.value = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
function confirmReject() {
|
function confirmReject() {
|
||||||
if (!selectedStokOpname.value || !rejectReason.value.trim()) return;
|
if (!selectedStokOpname.value || !rejectReason.value.trim()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
router.post(reject.url(selectedStokOpname.value.id), {
|
router.post(reject.url(selectedStokOpname.value.id), {
|
||||||
reason: rejectReason.value,
|
reason: rejectReason.value,
|
||||||
});
|
});
|
||||||
|
|||||||
@ -1,5 +1,6 @@
|
|||||||
import { useDebounceFn } from '@vueuse/core';
|
import { useDebounceFn } from '@vueuse/core';
|
||||||
import { computed, ref, watch, type MaybeRefOrGetter, toValue } from 'vue';
|
import { computed, ref, watch, toValue } from 'vue';
|
||||||
|
import type {MaybeRefOrGetter} from 'vue';
|
||||||
import { apiFetch } from '@/lib/api';
|
import { apiFetch } from '@/lib/api';
|
||||||
import { auto_save } from '@/routes/admin/manage/stok-opnames';
|
import { auto_save } from '@/routes/admin/manage/stok-opnames';
|
||||||
import type {
|
import type {
|
||||||
|
|||||||
@ -14,14 +14,14 @@ import {
|
|||||||
} from '@/components/ui/table';
|
} from '@/components/ui/table';
|
||||||
import { useCan } from '@/composables/useCan';
|
import { useCan } from '@/composables/useCan';
|
||||||
import { usePaginationSummary } from '@/composables/usePaginationSummary';
|
import { usePaginationSummary } from '@/composables/usePaginationSummary';
|
||||||
import { cn } from '@/lib/utils';
|
import { stokOpnameStatusBadgeVariant } from '@/constants/stok-opname-status';
|
||||||
import { groupedTableRowNumber } from '@/lib/grouped-table';
|
import { groupedTableRowNumber } from '@/lib/grouped-table';
|
||||||
import {
|
import {
|
||||||
stokOpnameDifference,
|
stokOpnameDifference,
|
||||||
stokOpnameDifferenceClass,
|
stokOpnameDifferenceClass,
|
||||||
stokOpnameDifferenceText,
|
stokOpnameDifferenceText,
|
||||||
} from '@/lib/stok-opname-display';
|
} from '@/lib/stok-opname-display';
|
||||||
import { stokOpnameStatusBadgeVariant } from '@/constants/stok-opname-status';
|
import { cn } from '@/lib/utils';
|
||||||
import type {
|
import type {
|
||||||
DataTablePagination,
|
DataTablePagination,
|
||||||
DataTablePaginationLink,
|
DataTablePaginationLink,
|
||||||
|
|||||||
@ -1,8 +1,8 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { RowDeleteAction, RowEditAction, RowSubmitAction, RowApproveAction, RowRejectAction } from '@/components/button';
|
import { RowDeleteAction, RowEditAction, RowSubmitAction, RowApproveAction, RowRejectAction } from '@/components/button';
|
||||||
import { useCan } from '@/composables/useCan';
|
import { useCan } from '@/composables/useCan';
|
||||||
import type { StokOpnameListItem } from '@/types/stok-opname';
|
|
||||||
import { destroy } from '@/routes/admin/manage/stok-opnames';
|
import { destroy } from '@/routes/admin/manage/stok-opnames';
|
||||||
|
import type { StokOpnameListItem } from '@/types/stok-opname';
|
||||||
|
|
||||||
defineProps<{
|
defineProps<{
|
||||||
stokOpname: StokOpnameListItem;
|
stokOpname: StokOpnameListItem;
|
||||||
|
|||||||
@ -1,4 +1,6 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
import { Head } from '@inertiajs/vue3';
|
||||||
|
import { computed, ref, watch } from 'vue';
|
||||||
import CreateButton from '@/components/button/CreateButton.vue';
|
import CreateButton from '@/components/button/CreateButton.vue';
|
||||||
import { DataTable } from '@/components/data-table';
|
import { DataTable } from '@/components/data-table';
|
||||||
import { Card, CardContent } from '@/components/ui/card';
|
import { Card, CardContent } from '@/components/ui/card';
|
||||||
@ -8,13 +10,11 @@ import {
|
|||||||
useDataTableQuerySync,
|
useDataTableQuerySync,
|
||||||
} from '@/composables/useDataTableQuery';
|
} from '@/composables/useDataTableQuery';
|
||||||
import AdminLayout from '@/layouts/AdminLayout.vue';
|
import AdminLayout from '@/layouts/AdminLayout.vue';
|
||||||
|
import { index } from '@/routes/admin/master/categories';
|
||||||
import type { CategoryListItem, PaginatedCategories } from '@/types/category';
|
import type { CategoryListItem, PaginatedCategories } from '@/types/category';
|
||||||
import type { DataTableSort } from '@/types/data-table';
|
import type { DataTableSort } from '@/types/data-table';
|
||||||
import { Head } from '@inertiajs/vue3';
|
|
||||||
import { computed, ref, watch } from 'vue';
|
|
||||||
import CategoryFormModal from './form/CategoryFormModal.vue';
|
import CategoryFormModal from './form/CategoryFormModal.vue';
|
||||||
import { createColumns } from './table/columns';
|
import { createColumns } from './table/columns';
|
||||||
import { index } from '@/routes/admin/master/categories';
|
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
categories: PaginatedCategories;
|
categories: PaginatedCategories;
|
||||||
|
|||||||
@ -1,8 +1,8 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { RowDeleteAction, RowEditAction } from '@/components/button';
|
import { RowDeleteAction, RowEditAction } from '@/components/button';
|
||||||
import { useCan } from '@/composables/useCan';
|
import { useCan } from '@/composables/useCan';
|
||||||
import type { CategoryListItem } from '@/types/category';
|
|
||||||
import { destroy } from '@/routes/admin/master/categories';
|
import { destroy } from '@/routes/admin/master/categories';
|
||||||
|
import type { CategoryListItem } from '@/types/category';
|
||||||
|
|
||||||
defineProps<{
|
defineProps<{
|
||||||
category: CategoryListItem;
|
category: CategoryListItem;
|
||||||
|
|||||||
@ -1,4 +1,6 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
import { Head } from '@inertiajs/vue3';
|
||||||
|
import { computed, ref, watch } from 'vue';
|
||||||
import CreateButton from '@/components/button/CreateButton.vue';
|
import CreateButton from '@/components/button/CreateButton.vue';
|
||||||
import { DataTable } from '@/components/data-table';
|
import { DataTable } from '@/components/data-table';
|
||||||
import { Card, CardContent } from '@/components/ui/card';
|
import { Card, CardContent } from '@/components/ui/card';
|
||||||
@ -8,13 +10,11 @@ import {
|
|||||||
useDataTableQuerySync,
|
useDataTableQuerySync,
|
||||||
} from '@/composables/useDataTableQuery';
|
} from '@/composables/useDataTableQuery';
|
||||||
import AdminLayout from '@/layouts/AdminLayout.vue';
|
import AdminLayout from '@/layouts/AdminLayout.vue';
|
||||||
|
import { index } from '@/routes/admin/master/customers';
|
||||||
import type { CustomerListItem, PaginatedCustomers } from '@/types/customer';
|
import type { CustomerListItem, PaginatedCustomers } from '@/types/customer';
|
||||||
import type { DataTableSort } from '@/types/data-table';
|
import type { DataTableSort } from '@/types/data-table';
|
||||||
import { Head } from '@inertiajs/vue3';
|
|
||||||
import { computed, ref, watch } from 'vue';
|
|
||||||
import CustomerFormModal from './form/CustomerFormModal.vue';
|
import CustomerFormModal from './form/CustomerFormModal.vue';
|
||||||
import { createColumns } from './table/columns';
|
import { createColumns } from './table/columns';
|
||||||
import { index } from '@/routes/admin/master/customers';
|
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
customers: PaginatedCustomers;
|
customers: PaginatedCustomers;
|
||||||
|
|||||||
@ -1,8 +1,8 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { RowDeleteAction, RowEditAction } from '@/components/button';
|
import { RowDeleteAction, RowEditAction } from '@/components/button';
|
||||||
import { useCan } from '@/composables/useCan';
|
import { useCan } from '@/composables/useCan';
|
||||||
import type { CustomerListItem } from '@/types/customer';
|
|
||||||
import { destroy } from '@/routes/admin/master/customers';
|
import { destroy } from '@/routes/admin/master/customers';
|
||||||
|
import type { CustomerListItem } from '@/types/customer';
|
||||||
|
|
||||||
defineProps<{
|
defineProps<{
|
||||||
customer: CustomerListItem;
|
customer: CustomerListItem;
|
||||||
|
|||||||
@ -1,10 +1,10 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
import { Head } from '@inertiajs/vue3';
|
||||||
import BackButton from '@/components/button/BackButton.vue';
|
import BackButton from '@/components/button/BackButton.vue';
|
||||||
import AdminLayout from '@/layouts/AdminLayout.vue';
|
import AdminLayout from '@/layouts/AdminLayout.vue';
|
||||||
import type { CategoryOption } from '@/types/product';
|
|
||||||
import { Head } from '@inertiajs/vue3';
|
|
||||||
import ProductForm from './form/ProductForm.vue';
|
|
||||||
import { index, store } from '@/routes/admin/master/products';
|
import { index, store } from '@/routes/admin/master/products';
|
||||||
|
import type { CategoryOption } from '@/types/product';
|
||||||
|
import ProductForm from './form/ProductForm.vue';
|
||||||
|
|
||||||
defineProps<{
|
defineProps<{
|
||||||
categories: CategoryOption[];
|
categories: CategoryOption[];
|
||||||
|
|||||||
@ -1,11 +1,11 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import BackButton from '@/components/button/BackButton.vue';
|
|
||||||
import AdminLayout from '@/layouts/AdminLayout.vue';
|
|
||||||
import type { CategoryOption, ProductListItem } from '@/types/product';
|
|
||||||
import { Head } from '@inertiajs/vue3';
|
import { Head } from '@inertiajs/vue3';
|
||||||
import { computed } from 'vue';
|
import { computed } from 'vue';
|
||||||
import ProductForm from './form/ProductForm.vue';
|
import BackButton from '@/components/button/BackButton.vue';
|
||||||
|
import AdminLayout from '@/layouts/AdminLayout.vue';
|
||||||
import { index, update } from '@/routes/admin/master/products';
|
import { index, update } from '@/routes/admin/master/products';
|
||||||
|
import type { CategoryOption, ProductListItem } from '@/types/product';
|
||||||
|
import ProductForm from './form/ProductForm.vue';
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
product: ProductListItem & { description?: string | null };
|
product: ProductListItem & { description?: string | null };
|
||||||
|
|||||||
@ -1,4 +1,6 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
import { Head } from '@inertiajs/vue3';
|
||||||
|
import { computed, ref, watch } from 'vue';
|
||||||
import CreateButton from '@/components/button/CreateButton.vue';
|
import CreateButton from '@/components/button/CreateButton.vue';
|
||||||
import { Card, CardContent } from '@/components/ui/card';
|
import { Card, CardContent } from '@/components/ui/card';
|
||||||
import { useCan } from '@/composables/useCan';
|
import { useCan } from '@/composables/useCan';
|
||||||
@ -9,11 +11,9 @@ import {
|
|||||||
import { ActiveStatus } from '@/constants/active-status';
|
import { ActiveStatus } from '@/constants/active-status';
|
||||||
import { StockStatus } from '@/constants/stock-status';
|
import { StockStatus } from '@/constants/stock-status';
|
||||||
import AdminLayout from '@/layouts/AdminLayout.vue';
|
import AdminLayout from '@/layouts/AdminLayout.vue';
|
||||||
|
import { index, create } from '@/routes/admin/master/products';
|
||||||
import type { DataTableFilterDef } from '@/types/data-table';
|
import type { DataTableFilterDef } from '@/types/data-table';
|
||||||
import type { CategoryOption, PaginatedProducts } from '@/types/product';
|
import type { CategoryOption, PaginatedProducts } from '@/types/product';
|
||||||
import { Head } from '@inertiajs/vue3';
|
|
||||||
import { computed, ref, watch } from 'vue';
|
|
||||||
import { index, create } from '@/routes/admin/master/products';
|
|
||||||
import ProductGroupedTable from './table/ProductGroupedTable.vue';
|
import ProductGroupedTable from './table/ProductGroupedTable.vue';
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
|
|||||||
@ -100,6 +100,7 @@ const {
|
|||||||
prices[price.type] = String(price.price);
|
prices[price.type] = String(price.price);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
client_id: createClientId(),
|
client_id: createClientId(),
|
||||||
id: variant.id,
|
id: variant.id,
|
||||||
@ -120,7 +121,10 @@ function copyPrices(variantPrices: Record<string, string>) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function pastePrices(clientId: string) {
|
function pastePrices(clientId: string) {
|
||||||
if (!copiedPrices.value) return;
|
if (!copiedPrices.value) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
setVariantField(clientId, 'prices', { ...copiedPrices.value });
|
setVariantField(clientId, 'prices', { ...copiedPrices.value });
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -164,11 +168,13 @@ function buildFormData(): FormData {
|
|||||||
formData.append(`variants[${index}][name]`, variant.name.trim());
|
formData.append(`variants[${index}][name]`, variant.name.trim());
|
||||||
formData.append(`variants[${index}][stock]`, String(Number.parseInt(String(variant.stock), 10) || 0));
|
formData.append(`variants[${index}][stock]`, String(Number.parseInt(String(variant.stock), 10) || 0));
|
||||||
formData.append(`variants[${index}][retail_stock]`, String(Number.parseInt(String(variant.retail_stock), 10) || 0));
|
formData.append(`variants[${index}][retail_stock]`, String(Number.parseInt(String(variant.retail_stock), 10) || 0));
|
||||||
|
|
||||||
if (variant.prices && showPrices) {
|
if (variant.prices && showPrices) {
|
||||||
Object.entries(variant.prices as Record<string, string>).forEach(([type, value]) => {
|
Object.entries(variant.prices as Record<string, string>).forEach(([type, value]) => {
|
||||||
formData.append(`variants[${index}][prices][${type}]`, String(Number.parseInt(value, 10) || 0));
|
formData.append(`variants[${index}][prices][${type}]`, String(Number.parseInt(value, 10) || 0));
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
appendMediaToFormData(formData, `variants[${index}]`, variant.media);
|
appendMediaToFormData(formData, `variants[${index}]`, variant.media);
|
||||||
}, props.method);
|
}, props.method);
|
||||||
|
|
||||||
|
|||||||
@ -1,10 +1,10 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { Head, Link } from '@inertiajs/vue3';
|
import { Head, Link } from '@inertiajs/vue3';
|
||||||
|
import BackButton from '@/components/button/BackButton.vue';
|
||||||
import AdminLayout from '@/layouts/AdminLayout.vue';
|
import AdminLayout from '@/layouts/AdminLayout.vue';
|
||||||
|
import { index, store } from '@/routes/admin/master/raw_materials';
|
||||||
import type { EnumOption } from '@/types/raw-material';
|
import type { EnumOption } from '@/types/raw-material';
|
||||||
import RawMaterialForm from './form/RawMaterialForm.vue';
|
import RawMaterialForm from './form/RawMaterialForm.vue';
|
||||||
import BackButton from '@/components/button/BackButton.vue';
|
|
||||||
import { index, store } from '@/routes/admin/master/raw_materials';
|
|
||||||
|
|
||||||
defineProps<{
|
defineProps<{
|
||||||
units: EnumOption[];
|
units: EnumOption[];
|
||||||
|
|||||||
@ -1,11 +1,11 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import BackButton from '@/components/button/BackButton.vue';
|
|
||||||
import AdminLayout from '@/layouts/AdminLayout.vue';
|
|
||||||
import type { EnumOption, RawMaterialListItem } from '@/types/raw-material';
|
|
||||||
import { Head } from '@inertiajs/vue3';
|
import { Head } from '@inertiajs/vue3';
|
||||||
import { computed } from 'vue';
|
import { computed } from 'vue';
|
||||||
import RawMaterialForm from './form/RawMaterialForm.vue';
|
import BackButton from '@/components/button/BackButton.vue';
|
||||||
|
import AdminLayout from '@/layouts/AdminLayout.vue';
|
||||||
import { index, update } from '@/routes/admin/master/raw_materials';
|
import { index, update } from '@/routes/admin/master/raw_materials';
|
||||||
|
import type { EnumOption, RawMaterialListItem } from '@/types/raw-material';
|
||||||
|
import RawMaterialForm from './form/RawMaterialForm.vue';
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
rawMaterial: RawMaterialListItem;
|
rawMaterial: RawMaterialListItem;
|
||||||
|
|||||||
@ -1,4 +1,6 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
import { Head } from '@inertiajs/vue3';
|
||||||
|
import { computed, ref, watch } from 'vue';
|
||||||
import CreateButton from '@/components/button/CreateButton.vue';
|
import CreateButton from '@/components/button/CreateButton.vue';
|
||||||
import { Card, CardContent } from '@/components/ui/card';
|
import { Card, CardContent } from '@/components/ui/card';
|
||||||
import { useCan } from '@/composables/useCan';
|
import { useCan } from '@/composables/useCan';
|
||||||
@ -9,11 +11,9 @@ import {
|
|||||||
import { ActiveStatus } from '@/constants/active-status';
|
import { ActiveStatus } from '@/constants/active-status';
|
||||||
import { StockStatus } from '@/constants/stock-status';
|
import { StockStatus } from '@/constants/stock-status';
|
||||||
import AdminLayout from '@/layouts/AdminLayout.vue';
|
import AdminLayout from '@/layouts/AdminLayout.vue';
|
||||||
|
import { index, create } from '@/routes/admin/master/raw_materials';
|
||||||
import type { DataTableFilterDef } from '@/types/data-table';
|
import type { DataTableFilterDef } from '@/types/data-table';
|
||||||
import type { PaginatedRawMaterials } from '@/types/raw-material';
|
import type { PaginatedRawMaterials } from '@/types/raw-material';
|
||||||
import { Head } from '@inertiajs/vue3';
|
|
||||||
import { computed, ref, watch } from 'vue';
|
|
||||||
import { index, create } from '@/routes/admin/master/raw_materials';
|
|
||||||
import RawMaterialGroupedTable from './table/RawMaterialGroupedTable.vue';
|
import RawMaterialGroupedTable from './table/RawMaterialGroupedTable.vue';
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
|
|||||||
@ -1,4 +1,5 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
||||||
import {
|
import {
|
||||||
Field,
|
Field,
|
||||||
FieldError,
|
FieldError,
|
||||||
@ -14,9 +15,9 @@ import {
|
|||||||
SelectTrigger,
|
SelectTrigger,
|
||||||
SelectValue,
|
SelectValue,
|
||||||
} from '@/components/ui/select';
|
} from '@/components/ui/select';
|
||||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
|
||||||
import { FIELD_LIMITS } from '@/lib/field-limits';
|
import { FIELD_LIMITS } from '@/lib/field-limits';
|
||||||
import { formErrors, type FormWithErrors } from '@/lib/form';
|
import { formErrors } from '@/lib/form';
|
||||||
|
import type {FormWithErrors} from '@/lib/form';
|
||||||
import type { EnumOption } from '@/types/raw-material';
|
import type { EnumOption } from '@/types/raw-material';
|
||||||
|
|
||||||
defineProps<{
|
defineProps<{
|
||||||
|
|||||||
@ -7,7 +7,8 @@ import {
|
|||||||
FieldGroup,
|
FieldGroup,
|
||||||
FieldLabel,
|
FieldLabel,
|
||||||
} from '@/components/ui/field';
|
} from '@/components/ui/field';
|
||||||
import { formErrors, type FormWithErrors } from '@/lib/form';
|
import { formErrors } from '@/lib/form';
|
||||||
|
import type {FormWithErrors} from '@/lib/form';
|
||||||
import type { RawMaterialPriceFormItem } from '@/types/raw-material';
|
import type { RawMaterialPriceFormItem } from '@/types/raw-material';
|
||||||
|
|
||||||
defineProps<{
|
defineProps<{
|
||||||
|
|||||||
@ -2,8 +2,8 @@
|
|||||||
import { RowDeleteAction, RowEditAction } from '@/components/button';
|
import { RowDeleteAction, RowEditAction } from '@/components/button';
|
||||||
import OwnerVerificationRowActions from '@/components/owner-verification/OwnerVerificationRowActions.vue';
|
import OwnerVerificationRowActions from '@/components/owner-verification/OwnerVerificationRowActions.vue';
|
||||||
import { useCan } from '@/composables/useCan';
|
import { useCan } from '@/composables/useCan';
|
||||||
import type { RawMaterialListItem } from '@/types/raw-material';
|
|
||||||
import { edit, destroy } from '@/routes/admin/master/raw_materials';
|
import { edit, destroy } from '@/routes/admin/master/raw_materials';
|
||||||
|
import type { RawMaterialListItem } from '@/types/raw-material';
|
||||||
|
|
||||||
defineProps<{
|
defineProps<{
|
||||||
material: RawMaterialListItem;
|
material: RawMaterialListItem;
|
||||||
|
|||||||
@ -1,4 +1,6 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
import { Head } from '@inertiajs/vue3';
|
||||||
|
import { computed, ref, watch } from 'vue';
|
||||||
import CreateButton from '@/components/button/CreateButton.vue';
|
import CreateButton from '@/components/button/CreateButton.vue';
|
||||||
import { DataTable } from '@/components/data-table';
|
import { DataTable } from '@/components/data-table';
|
||||||
import { Card, CardContent } from '@/components/ui/card';
|
import { Card, CardContent } from '@/components/ui/card';
|
||||||
@ -8,13 +10,11 @@ import {
|
|||||||
useDataTableQuerySync,
|
useDataTableQuerySync,
|
||||||
} from '@/composables/useDataTableQuery';
|
} from '@/composables/useDataTableQuery';
|
||||||
import AdminLayout from '@/layouts/AdminLayout.vue';
|
import AdminLayout from '@/layouts/AdminLayout.vue';
|
||||||
|
import { index } from '@/routes/admin/master/suppliers';
|
||||||
import type { DataTableSort } from '@/types/data-table';
|
import type { DataTableSort } from '@/types/data-table';
|
||||||
import type { PaginatedSuppliers, SupplierListItem } from '@/types/supplier';
|
import type { PaginatedSuppliers, SupplierListItem } from '@/types/supplier';
|
||||||
import { Head } from '@inertiajs/vue3';
|
|
||||||
import { computed, ref, watch } from 'vue';
|
|
||||||
import SupplierFormModal from './form/SupplierFormModal.vue';
|
import SupplierFormModal from './form/SupplierFormModal.vue';
|
||||||
import { createColumns } from './table/columns';
|
import { createColumns } from './table/columns';
|
||||||
import { index } from '@/routes/admin/master/suppliers';
|
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
suppliers: PaginatedSuppliers;
|
suppliers: PaginatedSuppliers;
|
||||||
|
|||||||
@ -1,8 +1,8 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { RowDeleteAction, RowEditAction } from '@/components/button';
|
import { RowDeleteAction, RowEditAction } from '@/components/button';
|
||||||
import { useCan } from '@/composables/useCan';
|
import { useCan } from '@/composables/useCan';
|
||||||
import type { SupplierListItem } from '@/types/supplier';
|
|
||||||
import { destroy } from '@/routes/admin/master/suppliers';
|
import { destroy } from '@/routes/admin/master/suppliers';
|
||||||
|
import type { SupplierListItem } from '@/types/supplier';
|
||||||
|
|
||||||
defineProps<{
|
defineProps<{
|
||||||
supplier: SupplierListItem;
|
supplier: SupplierListItem;
|
||||||
|
|||||||
@ -5,11 +5,11 @@ import { DataTable } from '@/components/data-table';
|
|||||||
import { Card, CardContent } from '@/components/ui/card';
|
import { Card, CardContent } from '@/components/ui/card';
|
||||||
import { useDataTableQuery, useDataTableQuerySync } from '@/composables/useDataTableQuery';
|
import { useDataTableQuery, useDataTableQuerySync } from '@/composables/useDataTableQuery';
|
||||||
import AdminLayout from '@/layouts/AdminLayout.vue';
|
import AdminLayout from '@/layouts/AdminLayout.vue';
|
||||||
|
import { index } from '@/routes/admin/system/activity_logs';
|
||||||
import type { ActivityLogListItem, PaginatedActivityLogs, SelectOption } from '@/types/activity-log';
|
import type { ActivityLogListItem, PaginatedActivityLogs, SelectOption } from '@/types/activity-log';
|
||||||
import type { DataTableSort } from '@/types/data-table';
|
import type { DataTableSort } from '@/types/data-table';
|
||||||
import ActivityLogDetailModal from './table/ActivityLogDetailModal.vue';
|
import ActivityLogDetailModal from './table/ActivityLogDetailModal.vue';
|
||||||
import { createColumns } from './table/columns';
|
import { createColumns } from './table/columns';
|
||||||
import { index } from '@/routes/admin/system/activity_logs';
|
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
activityLogs: PaginatedActivityLogs;
|
activityLogs: PaginatedActivityLogs;
|
||||||
|
|||||||
@ -1,9 +1,9 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { Head, Link } from '@inertiajs/vue3';
|
import { Head, Link } from '@inertiajs/vue3';
|
||||||
import AdminLayout from '@/layouts/AdminLayout.vue';
|
|
||||||
import RoleForm from './form/RoleForm.vue';
|
|
||||||
import BackButton from '@/components/button/BackButton.vue';
|
import BackButton from '@/components/button/BackButton.vue';
|
||||||
|
import AdminLayout from '@/layouts/AdminLayout.vue';
|
||||||
import { index, store } from '@/routes/admin/system/roles';
|
import { index, store } from '@/routes/admin/system/roles';
|
||||||
|
import RoleForm from './form/RoleForm.vue';
|
||||||
|
|
||||||
interface PermissionOption {
|
interface PermissionOption {
|
||||||
value: string;
|
value: string;
|
||||||
|
|||||||
@ -1,9 +1,9 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
import { Head } from '@inertiajs/vue3';
|
||||||
import BackButton from '@/components/button/BackButton.vue';
|
import BackButton from '@/components/button/BackButton.vue';
|
||||||
import AdminLayout from '@/layouts/AdminLayout.vue';
|
import AdminLayout from '@/layouts/AdminLayout.vue';
|
||||||
import { Head } from '@inertiajs/vue3';
|
|
||||||
import RoleForm from './form/RoleForm.vue';
|
|
||||||
import { index, update } from '@/routes/admin/system/roles';
|
import { index, update } from '@/routes/admin/system/roles';
|
||||||
|
import RoleForm from './form/RoleForm.vue';
|
||||||
|
|
||||||
interface PermissionOption {
|
interface PermissionOption {
|
||||||
value: string;
|
value: string;
|
||||||
|
|||||||
@ -1,4 +1,6 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
import { Head } from '@inertiajs/vue3';
|
||||||
|
import { computed, ref, watch } from 'vue';
|
||||||
import CreateButton from '@/components/button/CreateButton.vue';
|
import CreateButton from '@/components/button/CreateButton.vue';
|
||||||
import { DataTable } from '@/components/data-table';
|
import { DataTable } from '@/components/data-table';
|
||||||
import { Card, CardContent } from '@/components/ui/card';
|
import { Card, CardContent } from '@/components/ui/card';
|
||||||
@ -8,10 +10,8 @@ import {
|
|||||||
useDataTableQuerySync,
|
useDataTableQuerySync,
|
||||||
} from '@/composables/useDataTableQuery';
|
} from '@/composables/useDataTableQuery';
|
||||||
import AdminLayout from '@/layouts/AdminLayout.vue';
|
import AdminLayout from '@/layouts/AdminLayout.vue';
|
||||||
import type { DataTableSort } from '@/types/data-table';
|
|
||||||
import { Head } from '@inertiajs/vue3';
|
|
||||||
import { computed, ref, watch } from 'vue';
|
|
||||||
import { index, create } from '@/routes/admin/system/roles';
|
import { index, create } from '@/routes/admin/system/roles';
|
||||||
|
import type { DataTableSort } from '@/types/data-table';
|
||||||
import type { RoleListItem } from './table/columns';
|
import type { RoleListItem } from './table/columns';
|
||||||
import { createColumns } from './table/columns';
|
import { createColumns } from './table/columns';
|
||||||
|
|
||||||
|
|||||||
@ -2,8 +2,8 @@
|
|||||||
import { computed } from 'vue';
|
import { computed } from 'vue';
|
||||||
import { RowDeleteAction, RowEditAction } from '@/components/button';
|
import { RowDeleteAction, RowEditAction } from '@/components/button';
|
||||||
import { useCan } from '@/composables/useCan';
|
import { useCan } from '@/composables/useCan';
|
||||||
import type { RoleListItem } from './columns';
|
|
||||||
import { edit, destroy } from '@/routes/admin/system/roles';
|
import { edit, destroy } from '@/routes/admin/system/roles';
|
||||||
|
import type { RoleListItem } from './columns';
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
role: RoleListItem;
|
role: RoleListItem;
|
||||||
|
|||||||
@ -1,8 +1,8 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { Head } from '@inertiajs/vue3';
|
import { Head } from '@inertiajs/vue3';
|
||||||
import { ref } from 'vue';
|
import { ref } from 'vue';
|
||||||
import { SettingSection as SettingSectionConst } from '@/constants/setting-section';
|
|
||||||
import { useCan } from '@/composables/useCan';
|
import { useCan } from '@/composables/useCan';
|
||||||
|
import { SettingSection as SettingSectionConst } from '@/constants/setting-section';
|
||||||
import SettingLayout from '@/layouts/SettingLayout.vue';
|
import SettingLayout from '@/layouts/SettingLayout.vue';
|
||||||
import type {
|
import type {
|
||||||
HomepageSettingsData,
|
HomepageSettingsData,
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
import type { MediaItem } from '@/types/media';
|
|
||||||
import type { Paginated } from '@/types/common';
|
import type { Paginated } from '@/types/common';
|
||||||
|
import type { MediaItem } from '@/types/media';
|
||||||
import type { ProductListItem } from '@/types/product';
|
import type { ProductListItem } from '@/types/product';
|
||||||
import type { RawMaterialListItem } from '@/types/raw-material';
|
import type { RawMaterialListItem } from '@/types/raw-material';
|
||||||
|
|
||||||
@ -14,6 +14,8 @@ export type CuttingStatusAction = {
|
|||||||
export type CuttingMaterialListItem = {
|
export type CuttingMaterialListItem = {
|
||||||
id: number;
|
id: number;
|
||||||
material_usage_formatted: string;
|
material_usage_formatted: string;
|
||||||
|
material_result?: number | null;
|
||||||
|
combination_material_result?: number | null;
|
||||||
variant?: string;
|
variant?: string;
|
||||||
raw_material_id?: number;
|
raw_material_id?: number;
|
||||||
raw_material_name?: string;
|
raw_material_name?: string;
|
||||||
@ -118,8 +120,10 @@ export type CuttingMaterialCartItem = {
|
|||||||
unit_abbreviation: string;
|
unit_abbreviation: string;
|
||||||
stock_input: string;
|
stock_input: string;
|
||||||
material_usage: string;
|
material_usage: string;
|
||||||
|
material_result?: string | null;
|
||||||
images?: MediaItem[];
|
images?: MediaItem[];
|
||||||
combination_id?: number | null;
|
combination_id?: number | null;
|
||||||
|
combination_material_result?: number | null;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type CuttingResultCartItem = {
|
export type CuttingResultCartItem = {
|
||||||
@ -143,6 +147,8 @@ export type CuttingEditItem = {
|
|||||||
materials: Array<{
|
materials: Array<{
|
||||||
raw_material_price_id: number;
|
raw_material_price_id: number;
|
||||||
material_usage_input: string;
|
material_usage_input: string;
|
||||||
|
material_result_input?: number | null;
|
||||||
|
combination_material_result?: number | null;
|
||||||
variant?: string;
|
variant?: string;
|
||||||
stock_input?: string;
|
stock_input?: string;
|
||||||
images?: MediaItem[];
|
images?: MediaItem[];
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
import type { MediaItem } from '@/types/media';
|
|
||||||
import type { EnumOption, Paginated, SelectOption } from '@/types/common';
|
import type { EnumOption, Paginated, SelectOption } from '@/types/common';
|
||||||
|
import type { MediaItem } from '@/types/media';
|
||||||
import type { ProductListItem } from '@/types/product';
|
import type { ProductListItem } from '@/types/product';
|
||||||
|
|
||||||
export type { EnumOption, SelectOption } from '@/types/common';
|
export type { EnumOption, SelectOption } from '@/types/common';
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
|
import type { Paginated } from '@/types/common';
|
||||||
import type { MediaItem } from '@/types/media';
|
import type { MediaItem } from '@/types/media';
|
||||||
import type { Paginated, SelectOption } from '@/types/common';
|
|
||||||
import type { RawMaterialListItem } from '@/types/raw-material';
|
import type { RawMaterialListItem } from '@/types/raw-material';
|
||||||
|
|
||||||
export type { SelectOption } from '@/types/common';
|
export type { SelectOption } from '@/types/common';
|
||||||
|
|||||||
@ -192,6 +192,61 @@ function setupDraftItems(User $user): array
|
|||||||
expect($cutting->results)->toHaveCount(1);
|
expect($cutting->results)->toHaveCount(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('draft material can have material_result', function () {
|
||||||
|
$user = createCuttingUserWithPermission(PermissionEnum::CUTTINGS_VIEW, PermissionEnum::CUTTINGS_CREATE);
|
||||||
|
|
||||||
|
$draft = setupDraftItems($user);
|
||||||
|
|
||||||
|
$this->actingAs($user)
|
||||||
|
->postJson(route('admin.manage.cuttings.draft_materials.store'), [
|
||||||
|
'raw_material_price_id' => $draft['price']->id,
|
||||||
|
'material_usage' => 5,
|
||||||
|
'material_result' => 10,
|
||||||
|
])
|
||||||
|
->assertOk()
|
||||||
|
->assertJsonPath('item.material_result', 10);
|
||||||
|
|
||||||
|
$this->assertDatabaseHas('cutting_materials', [
|
||||||
|
'raw_material_price_id' => $draft['price']->id,
|
||||||
|
'material_result' => 10,
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('material_result is preserved when creating cutting from drafts', function () {
|
||||||
|
$user = createCuttingUserWithPermission(PermissionEnum::CUTTINGS_VIEW, PermissionEnum::CUTTINGS_CREATE);
|
||||||
|
|
||||||
|
$draft = setupDraftItems($user);
|
||||||
|
|
||||||
|
// Create draft material with material_result
|
||||||
|
$this->actingAs($user)
|
||||||
|
->postJson(route('admin.manage.cuttings.draft_materials.store'), [
|
||||||
|
'raw_material_price_id' => $draft['price']->id,
|
||||||
|
'material_usage' => 5,
|
||||||
|
'material_result' => 10,
|
||||||
|
])
|
||||||
|
->assertOk();
|
||||||
|
|
||||||
|
// Create draft result
|
||||||
|
$this->actingAs($user)
|
||||||
|
->postJson(route('admin.manage.cuttings.draft_results.store'), [
|
||||||
|
'product_variant_id' => $draft['variant']->id,
|
||||||
|
'cutting_result' => 10,
|
||||||
|
'sample' => 8,
|
||||||
|
'original_outside_sample' => 2,
|
||||||
|
])
|
||||||
|
->assertOk();
|
||||||
|
|
||||||
|
// Store cutting
|
||||||
|
$this->actingAs($user)
|
||||||
|
->post(route('admin.manage.cuttings.store'), [
|
||||||
|
'description' => 'Cutting with result',
|
||||||
|
])
|
||||||
|
->assertRedirect(route('admin.manage.cuttings.index'));
|
||||||
|
|
||||||
|
$cutting = Cutting::where('description', 'Cutting with result')->first();
|
||||||
|
expect($cutting->materials->first()->material_result)->toBe(10);
|
||||||
|
});
|
||||||
|
|
||||||
test('guest cannot create a cutting', function () {
|
test('guest cannot create a cutting', function () {
|
||||||
$this->post(route('admin.manage.cuttings.store'), [
|
$this->post(route('admin.manage.cuttings.store'), [
|
||||||
'description' => 'Test',
|
'description' => 'Test',
|
||||||
@ -328,6 +383,40 @@ function setupDraftItems(User $user): array
|
|||||||
expect($cutting->fresh()->description)->toBe('Updated description');
|
expect($cutting->fresh()->description)->toBe('Updated description');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('update can save material_result for single materials', function () {
|
||||||
|
$user = createCuttingUserWithPermission(PermissionEnum::CUTTINGS_VIEW, PermissionEnum::CUTTINGS_UPDATE);
|
||||||
|
|
||||||
|
$cutting = createCuttingWithMaterialsAndResults($user);
|
||||||
|
|
||||||
|
$rawMaterial = RawMaterial::factory()->create();
|
||||||
|
$price = RawMaterialPrice::factory()->create(['raw_material_id' => $rawMaterial->id]);
|
||||||
|
|
||||||
|
$product = Product::factory()->create();
|
||||||
|
$variant = ProductVariant::factory()->create(['product_id' => $product->id]);
|
||||||
|
|
||||||
|
$this->actingAs($user)
|
||||||
|
->put(route('admin.manage.cuttings.update', $cutting), [
|
||||||
|
'description' => 'With material result',
|
||||||
|
'sewing_cost' => 0,
|
||||||
|
'other_cost' => 0,
|
||||||
|
'materials' => [
|
||||||
|
['raw_material_price_id' => $price->id, 'material_usage' => 10, 'material_result' => 25],
|
||||||
|
],
|
||||||
|
'results' => [
|
||||||
|
[
|
||||||
|
'product_variant_id' => $variant->id,
|
||||||
|
'cutting_result' => 20,
|
||||||
|
'sample' => 15,
|
||||||
|
'original_outside_sample' => 5,
|
||||||
|
],
|
||||||
|
],
|
||||||
|
])
|
||||||
|
->assertRedirect(route('admin.manage.cuttings.index'));
|
||||||
|
|
||||||
|
$cutting->refresh();
|
||||||
|
expect($cutting->materials->first()->material_result)->toBe(25);
|
||||||
|
});
|
||||||
|
|
||||||
test('guest cannot update a cutting', function () {
|
test('guest cannot update a cutting', function () {
|
||||||
$cutting = createCuttingWithMaterialsAndResults();
|
$cutting = createCuttingWithMaterialsAndResults();
|
||||||
|
|
||||||
@ -518,6 +607,27 @@ function setupDraftItems(User $user): array
|
|||||||
$this->assertDatabaseCount('cutting_materials', 2);
|
$this->assertDatabaseCount('cutting_materials', 2);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('draft combination can have material_result', function () {
|
||||||
|
$user = createCuttingUserWithPermission(PermissionEnum::CUTTINGS_VIEW, PermissionEnum::CUTTINGS_CREATE);
|
||||||
|
|
||||||
|
$rawMaterial = RawMaterial::factory()->create();
|
||||||
|
$price1 = RawMaterialPrice::factory()->create(['raw_material_id' => $rawMaterial->id]);
|
||||||
|
$price2 = RawMaterialPrice::factory()->create(['raw_material_id' => $rawMaterial->id]);
|
||||||
|
|
||||||
|
$this->actingAs($user)
|
||||||
|
->postJson(route('admin.manage.cuttings.draft_combinations.store'), [
|
||||||
|
'materials' => [
|
||||||
|
['raw_material_price_id' => $price1->id, 'material_usage' => 3],
|
||||||
|
['raw_material_price_id' => $price2->id, 'material_usage' => 2],
|
||||||
|
],
|
||||||
|
'material_result' => 15,
|
||||||
|
])
|
||||||
|
->assertOk();
|
||||||
|
|
||||||
|
$combination = CuttingMaterialCombination::first();
|
||||||
|
expect($combination->material_result)->toBe(15);
|
||||||
|
});
|
||||||
|
|
||||||
test('draft combination requires at least 2 materials', function () {
|
test('draft combination requires at least 2 materials', function () {
|
||||||
$user = createCuttingUserWithPermission(PermissionEnum::CUTTINGS_VIEW, PermissionEnum::CUTTINGS_CREATE);
|
$user = createCuttingUserWithPermission(PermissionEnum::CUTTINGS_VIEW, PermissionEnum::CUTTINGS_CREATE);
|
||||||
|
|
||||||
@ -628,7 +738,7 @@ function setupDraftItems(User $user): array
|
|||||||
// ─── Update with Combination ────────────────────────────────
|
// ─── Update with Combination ────────────────────────────────
|
||||||
|
|
||||||
describe('Cutting Update with Combination', function () {
|
describe('Cutting Update with Combination', function () {
|
||||||
test('cutting can be updated preserving combination_id', function () {
|
test('cutting can be updated with combination materials', function () {
|
||||||
$user = createCuttingUserWithPermission(PermissionEnum::CUTTINGS_VIEW, PermissionEnum::CUTTINGS_UPDATE);
|
$user = createCuttingUserWithPermission(PermissionEnum::CUTTINGS_VIEW, PermissionEnum::CUTTINGS_UPDATE);
|
||||||
|
|
||||||
$cutting = createCuttingWithMaterialsAndResults($user);
|
$cutting = createCuttingWithMaterialsAndResults($user);
|
||||||
@ -637,11 +747,6 @@ function setupDraftItems(User $user): array
|
|||||||
$price1 = RawMaterialPrice::factory()->create(['raw_material_id' => $rawMaterial->id]);
|
$price1 = RawMaterialPrice::factory()->create(['raw_material_id' => $rawMaterial->id]);
|
||||||
$price2 = RawMaterialPrice::factory()->create(['raw_material_id' => $rawMaterial->id]);
|
$price2 = RawMaterialPrice::factory()->create(['raw_material_id' => $rawMaterial->id]);
|
||||||
|
|
||||||
$combination = CuttingMaterialCombination::factory()->create([
|
|
||||||
'cutting_id' => $cutting->id,
|
|
||||||
'user_id' => null,
|
|
||||||
]);
|
|
||||||
|
|
||||||
$product = Product::factory()->create();
|
$product = Product::factory()->create();
|
||||||
$variant = ProductVariant::factory()->create(['product_id' => $product->id]);
|
$variant = ProductVariant::factory()->create(['product_id' => $product->id]);
|
||||||
|
|
||||||
@ -651,8 +756,8 @@ function setupDraftItems(User $user): array
|
|||||||
'sewing_cost' => 0,
|
'sewing_cost' => 0,
|
||||||
'other_cost' => 0,
|
'other_cost' => 0,
|
||||||
'materials' => [
|
'materials' => [
|
||||||
['raw_material_price_id' => $price1->id, 'material_usage' => 3, 'combination_id' => $combination->id],
|
['raw_material_price_id' => $price1->id, 'material_usage' => 3, 'combination_id' => 999],
|
||||||
['raw_material_price_id' => $price2->id, 'material_usage' => 2, 'combination_id' => $combination->id],
|
['raw_material_price_id' => $price2->id, 'material_usage' => 2, 'combination_id' => 999],
|
||||||
],
|
],
|
||||||
'results' => [
|
'results' => [
|
||||||
[
|
[
|
||||||
@ -668,7 +773,47 @@ function setupDraftItems(User $user): array
|
|||||||
$cutting->refresh();
|
$cutting->refresh();
|
||||||
expect($cutting->description)->toBe('Updated with combination');
|
expect($cutting->description)->toBe('Updated with combination');
|
||||||
expect($cutting->materials)->toHaveCount(2);
|
expect($cutting->materials)->toHaveCount(2);
|
||||||
expect($cutting->materials->first()->combination_id)->toBe($combination->id);
|
// Both materials should have the same combination_id
|
||||||
|
expect($cutting->materials->first()->combination_id)->not->toBeNull();
|
||||||
|
expect($cutting->materials->first()->combination_id)->toBe($cutting->materials->last()->combination_id);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('update can save combination_material_result', function () {
|
||||||
|
$user = createCuttingUserWithPermission(PermissionEnum::CUTTINGS_VIEW, PermissionEnum::CUTTINGS_UPDATE);
|
||||||
|
|
||||||
|
$cutting = createCuttingWithMaterialsAndResults($user);
|
||||||
|
|
||||||
|
$rawMaterial = RawMaterial::factory()->create();
|
||||||
|
$price1 = RawMaterialPrice::factory()->create(['raw_material_id' => $rawMaterial->id]);
|
||||||
|
$price2 = RawMaterialPrice::factory()->create(['raw_material_id' => $rawMaterial->id]);
|
||||||
|
|
||||||
|
$product = Product::factory()->create();
|
||||||
|
$variant = ProductVariant::factory()->create(['product_id' => $product->id]);
|
||||||
|
|
||||||
|
$this->actingAs($user)
|
||||||
|
->put(route('admin.manage.cuttings.update', $cutting), [
|
||||||
|
'description' => 'Combination with result',
|
||||||
|
'sewing_cost' => 0,
|
||||||
|
'other_cost' => 0,
|
||||||
|
'materials' => [
|
||||||
|
['raw_material_price_id' => $price1->id, 'material_usage' => 3, 'combination_id' => 999, 'combination_material_result' => 20],
|
||||||
|
['raw_material_price_id' => $price2->id, 'material_usage' => 2, 'combination_id' => 999, 'combination_material_result' => 20],
|
||||||
|
],
|
||||||
|
'results' => [
|
||||||
|
[
|
||||||
|
'product_variant_id' => $variant->id,
|
||||||
|
'cutting_result' => 10,
|
||||||
|
'sample' => 8,
|
||||||
|
'original_outside_sample' => 2,
|
||||||
|
],
|
||||||
|
],
|
||||||
|
])
|
||||||
|
->assertRedirect(route('admin.manage.cuttings.index'));
|
||||||
|
|
||||||
|
$cutting->refresh();
|
||||||
|
$combination = $cutting->combinations()->first();
|
||||||
|
expect($combination)->not->toBeNull();
|
||||||
|
expect($combination->material_result)->toBe(20);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -755,6 +900,17 @@ function setupDraftItems(User $user): array
|
|||||||
|
|
||||||
expect($material->trashed())->toBeTrue();
|
expect($material->trashed())->toBeTrue();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('cutting material has material_result cast to integer', function () {
|
||||||
|
$cutting = Cutting::factory()->create();
|
||||||
|
$material = CuttingMaterial::factory()->create([
|
||||||
|
'cutting_id' => $cutting->id,
|
||||||
|
'material_result' => 10,
|
||||||
|
]);
|
||||||
|
|
||||||
|
expect($material->material_result)->toBeInt();
|
||||||
|
expect($material->material_result)->toBe(10);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
// ─── CuttingResult Model ───────────────────────────────────
|
// ─── CuttingResult Model ───────────────────────────────────
|
||||||
@ -842,4 +998,13 @@ function setupDraftItems(User $user): array
|
|||||||
expect($combination->cutting_id)->toBeNull();
|
expect($combination->cutting_id)->toBeNull();
|
||||||
expect($combination->user_id)->toBe($user->id);
|
expect($combination->user_id)->toBe($user->id);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('combination has material_result cast to integer', function () {
|
||||||
|
$combination = CuttingMaterialCombination::factory()->create([
|
||||||
|
'material_result' => 15,
|
||||||
|
]);
|
||||||
|
|
||||||
|
expect($combination->material_result)->toBeInt();
|
||||||
|
expect($combination->material_result)->toBe(15);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user