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