feat: enhance raw material management with stock status filtering and improved form structure
This commit is contained in:
parent
d3a33c46f6
commit
98e3ffefc7
@ -12,11 +12,11 @@ enum RawMaterialUnit: string
|
|||||||
case METER = 'meter';
|
case METER = 'meter';
|
||||||
case KILOGRAM = 'kilogram';
|
case KILOGRAM = 'kilogram';
|
||||||
|
|
||||||
private const MIN_STOCK_YARD = 5.0;
|
private const MIN_STOCK_YARD = 20;
|
||||||
|
|
||||||
private const MIN_STOCK_METER = 5.0;
|
private const MIN_STOCK_METER = 10;
|
||||||
|
|
||||||
private const MIN_STOCK_KILOGRAM = 2.0;
|
private const MIN_STOCK_KILOGRAM = 5;
|
||||||
|
|
||||||
private const CM_PER_YARD = 91.44;
|
private const CM_PER_YARD = 91.44;
|
||||||
|
|
||||||
|
|||||||
@ -7,6 +7,7 @@
|
|||||||
use App\Http\Controllers\Concerns\ParsesDataTableQuery;
|
use App\Http\Controllers\Concerns\ParsesDataTableQuery;
|
||||||
use App\Http\Controllers\Controller;
|
use App\Http\Controllers\Controller;
|
||||||
use App\Http\Requests\Admin\Master\RawMaterialRequest;
|
use App\Http\Requests\Admin\Master\RawMaterialRequest;
|
||||||
|
use App\Http\Requests\Admin\ToggleStatusRequest;
|
||||||
use App\Models\RawMaterial;
|
use App\Models\RawMaterial;
|
||||||
use App\Models\RawMaterialPrice;
|
use App\Models\RawMaterialPrice;
|
||||||
use App\Services\Master\RawMaterialService;
|
use App\Services\Master\RawMaterialService;
|
||||||
@ -28,13 +29,13 @@ public function index(Request $request): Response
|
|||||||
{
|
{
|
||||||
$tableQuery = $this->parseDataTableQuery($request);
|
$tableQuery = $this->parseDataTableQuery($request);
|
||||||
$isActive = $request->string('is_active')->toString();
|
$isActive = $request->string('is_active')->toString();
|
||||||
|
$stockStatus = $request->string('stock_status')->toString();
|
||||||
|
|
||||||
return Inertia::render('admin/master/raw-materials/Index', [
|
return Inertia::render('admin/master/raw-materials/Index', [
|
||||||
'rawMaterials' => $this->rawMaterialService->paginateForIndex($tableQuery, $isActive),
|
'rawMaterials' => $this->rawMaterialService->paginateForIndex($tableQuery, $isActive, $stockStatus),
|
||||||
'outOfStockGroups' => $this->rawMaterialService->outOfStockGroups(),
|
|
||||||
'stockWarningGroups' => $this->rawMaterialService->stockWarningGroups(),
|
|
||||||
'filters' => $this->dataTableFilters($tableQuery, [
|
'filters' => $this->dataTableFilters($tableQuery, [
|
||||||
'is_active' => $isActive,
|
'is_active' => $isActive,
|
||||||
|
'stock_status' => $stockStatus,
|
||||||
]),
|
]),
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
@ -80,13 +81,9 @@ public function update(RawMaterialRequest $request, RawMaterial $rawMaterial): R
|
|||||||
return redirect()->route('admin.master.raw-materials.index');
|
return redirect()->route('admin.master.raw-materials.index');
|
||||||
}
|
}
|
||||||
|
|
||||||
public function toggleStatus(Request $request, RawMaterial $rawMaterial): RedirectResponse
|
public function toggleStatus(ToggleStatusRequest $request, RawMaterial $rawMaterial): RedirectResponse
|
||||||
{
|
{
|
||||||
$validated = $request->validate([
|
$this->rawMaterialService->toggleStatus($rawMaterial, $request->validated());
|
||||||
'is_active' => ['required', 'boolean'],
|
|
||||||
]);
|
|
||||||
|
|
||||||
$this->rawMaterialService->toggleStatus($rawMaterial, $validated['is_active']);
|
|
||||||
|
|
||||||
$this->flashStatusUpdated('bahan baku');
|
$this->flashStatusUpdated('bahan baku');
|
||||||
|
|
||||||
|
|||||||
@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
namespace App\Services\Master;
|
namespace App\Services\Master;
|
||||||
|
|
||||||
|
use App\Enums\RawMaterialUnit;
|
||||||
use App\Models\RawMaterial;
|
use App\Models\RawMaterial;
|
||||||
use App\Models\RawMaterialPrice;
|
use App\Models\RawMaterialPrice;
|
||||||
use App\Services\Media\MediaService;
|
use App\Services\Media\MediaService;
|
||||||
@ -18,119 +19,10 @@ public function __construct(
|
|||||||
private readonly MediaService $mediaService,
|
private readonly MediaService $mediaService,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
/**
|
|
||||||
* @return list<array{
|
|
||||||
* id: int,
|
|
||||||
* name: string,
|
|
||||||
* unit_label: string,
|
|
||||||
* edit_url: string,
|
|
||||||
* variants: list<array{
|
|
||||||
* id: int,
|
|
||||||
* name: string,
|
|
||||||
* stock_formatted: string,
|
|
||||||
* images: list<array<string, mixed>>,
|
|
||||||
* }>,
|
|
||||||
* }>
|
|
||||||
*/
|
|
||||||
public function outOfStockGroups(): array
|
|
||||||
{
|
|
||||||
return $this->inventoryAlertGroups(
|
|
||||||
fn (RawMaterialPrice $price): bool => (float) $price->stock <= 0,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @return list<array{
|
|
||||||
* id: int,
|
|
||||||
* name: string,
|
|
||||||
* unit_label: string,
|
|
||||||
* edit_url: string,
|
|
||||||
* variants: list<array{
|
|
||||||
* id: int,
|
|
||||||
* name: string,
|
|
||||||
* stock_formatted: string,
|
|
||||||
* images: list<array<string, mixed>>,
|
|
||||||
* }>,
|
|
||||||
* }>
|
|
||||||
*/
|
|
||||||
public function stockWarningGroups(): array
|
|
||||||
{
|
|
||||||
return $this->inventoryAlertGroups(function (RawMaterialPrice $price): bool {
|
|
||||||
$stock = (float) $price->stock;
|
|
||||||
|
|
||||||
if ($stock <= 0) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
$minStock = $price->rawMaterial->unit->minStock();
|
|
||||||
|
|
||||||
return $stock < $minStock;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @return list<array{
|
|
||||||
* id: int,
|
|
||||||
* name: string,
|
|
||||||
* unit_label: string,
|
|
||||||
* edit_url: string,
|
|
||||||
* variants: list<array{
|
|
||||||
* id: int,
|
|
||||||
* name: string,
|
|
||||||
* stock_formatted: string,
|
|
||||||
* images: list<array<string, mixed>>,
|
|
||||||
* }>,
|
|
||||||
* }>
|
|
||||||
*/
|
|
||||||
private function inventoryAlertGroups(callable $filter): array
|
|
||||||
{
|
|
||||||
return RawMaterialPrice::query()
|
|
||||||
->whereHas('rawMaterial', fn (Builder $query) => $query->where('is_active', true))
|
|
||||||
->with(['rawMaterial', 'media'])
|
|
||||||
->get()
|
|
||||||
->filter($filter)
|
|
||||||
->sortBy([
|
|
||||||
fn (RawMaterialPrice $price) => $price->rawMaterial->name,
|
|
||||||
fn (RawMaterialPrice $price) => $price->variant,
|
|
||||||
])
|
|
||||||
->groupBy('raw_material_id')
|
|
||||||
->map(function ($prices, $rawMaterialId) {
|
|
||||||
$material = $prices->first()->rawMaterial;
|
|
||||||
|
|
||||||
return [
|
|
||||||
'id' => (int) $rawMaterialId,
|
|
||||||
'name' => $material->name,
|
|
||||||
'unit_label' => $material->unit->label(),
|
|
||||||
'edit_url' => route('admin.master.raw-materials.edit', $rawMaterialId),
|
|
||||||
'variants' => $prices->map(function (RawMaterialPrice $price) use ($material): array {
|
|
||||||
return [
|
|
||||||
'id' => $price->id,
|
|
||||||
'name' => $price->variant,
|
|
||||||
'stock_formatted' => $this->formatStockAmount((float) $price->stock, $material->unit->abbreviation()),
|
|
||||||
'images' => MediaPresenter::collection($price, 'images'),
|
|
||||||
];
|
|
||||||
})->values()->all(),
|
|
||||||
];
|
|
||||||
})
|
|
||||||
->values()
|
|
||||||
->all();
|
|
||||||
}
|
|
||||||
|
|
||||||
private function formatStockAmount(float $amount, ?string $unit = null): string
|
|
||||||
{
|
|
||||||
$formatted = rtrim(rtrim(number_format($amount, 4, ',', '.'), '0'), ',');
|
|
||||||
|
|
||||||
if ($unit === null) {
|
|
||||||
return $formatted;
|
|
||||||
}
|
|
||||||
|
|
||||||
return "{$formatted} {$unit}";
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param array{search: string, sort: string, direction: 'asc'|'desc'} $tableQuery
|
* @param array{search: string, sort: string, direction: 'asc'|'desc'} $tableQuery
|
||||||
*/
|
*/
|
||||||
public function paginateForIndex(array $tableQuery, string $isActive): LengthAwarePaginator
|
public function paginateForIndex(array $tableQuery, string $isActive, string $stockStatus = ''): LengthAwarePaginator
|
||||||
{
|
{
|
||||||
$query = RawMaterial::query()
|
$query = RawMaterial::query()
|
||||||
->with([
|
->with([
|
||||||
@ -145,10 +37,22 @@ public function paginateForIndex(array $tableQuery, string $isActive): LengthAwa
|
|||||||
->orWhereHas('prices', fn (Builder $query) => $query->where('variant', 'like', "%{$search}%"));
|
->orWhereHas('prices', fn (Builder $query) => $query->where('variant', 'like', "%{$search}%"));
|
||||||
});
|
});
|
||||||
})
|
})
|
||||||
->when(
|
->when($isActive !== '', fn (Builder $query) => $query->where('is_active', $isActive === '1'))
|
||||||
$isActive !== '',
|
->when($stockStatus === 'out_of_stock', function (Builder $query): void {
|
||||||
fn (Builder $query) => $query->where('is_active', $isActive === '1')
|
$query->whereHas('prices', fn (Builder $q) => $q->where('stock', '<=', 0));
|
||||||
);
|
})
|
||||||
|
->when($stockStatus === 'low_stock', function (Builder $query): void {
|
||||||
|
$query->whereHas('prices', function (Builder $q): void {
|
||||||
|
$q->where('stock', '>', 0)->where(function (Builder $q): void {
|
||||||
|
foreach (RawMaterialUnit::cases() as $unit) {
|
||||||
|
$q->orWhere(function (Builder $q) use ($unit): void {
|
||||||
|
$q->whereHas('rawMaterial', fn (Builder $rm) => $rm->where('unit', $unit))
|
||||||
|
->where('stock', '<', $unit->minStock());
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
$this->applySorting($query, $tableQuery['sort'], $tableQuery['direction']);
|
$this->applySorting($query, $tableQuery['sort'], $tableQuery['direction']);
|
||||||
|
|
||||||
@ -224,9 +128,9 @@ public function update(RawMaterial $rawMaterial, array $validated): void
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public function toggleStatus(RawMaterial $rawMaterial, bool $isActive): void
|
public function toggleStatus(RawMaterial $rawMaterial, array $validated): void
|
||||||
{
|
{
|
||||||
$rawMaterial->is_active = $isActive;
|
$rawMaterial->is_active = $validated['is_active'];
|
||||||
$rawMaterial->save();
|
$rawMaterial->save();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -1,10 +1,10 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { Head, Link } from '@inertiajs/vue3';
|
import { Head, Link } from '@inertiajs/vue3';
|
||||||
import { ArrowLeft } from '@lucide/vue';
|
import { ArrowLeft } from '@lucide/vue';
|
||||||
import RawMaterialForm from '@/components/admin/master/raw-materials/RawMaterialForm.vue';
|
|
||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
import AdminLayout from '@/layouts/AdminLayout.vue';
|
import AdminLayout from '@/layouts/AdminLayout.vue';
|
||||||
import type { EnumOption } from '@/types/raw-material';
|
import type { EnumOption } from '@/types/raw-material';
|
||||||
|
import RawMaterialForm from './form/RawMaterialForm.vue';
|
||||||
|
|
||||||
defineProps<{
|
defineProps<{
|
||||||
units: EnumOption[];
|
units: EnumOption[];
|
||||||
|
|||||||
@ -2,10 +2,10 @@
|
|||||||
import { Head, Link } from '@inertiajs/vue3';
|
import { Head, Link } from '@inertiajs/vue3';
|
||||||
import { ArrowLeft } from '@lucide/vue';
|
import { ArrowLeft } from '@lucide/vue';
|
||||||
import { computed } from 'vue';
|
import { computed } from 'vue';
|
||||||
import RawMaterialForm from '@/components/admin/master/raw-materials/RawMaterialForm.vue';
|
|
||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
import AdminLayout from '@/layouts/AdminLayout.vue';
|
import AdminLayout from '@/layouts/AdminLayout.vue';
|
||||||
import type { EnumOption, RawMaterialListItem } from '@/types/raw-material';
|
import type { EnumOption, RawMaterialListItem } from '@/types/raw-material';
|
||||||
|
import RawMaterialForm from './form/RawMaterialForm.vue';
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
rawMaterial: RawMaterialListItem;
|
rawMaterial: RawMaterialListItem;
|
||||||
|
|||||||
@ -2,8 +2,6 @@
|
|||||||
import { Head, Link } from '@inertiajs/vue3';
|
import { Head, Link } from '@inertiajs/vue3';
|
||||||
import { Plus } from '@lucide/vue';
|
import { Plus } from '@lucide/vue';
|
||||||
import { computed, ref, watch } from 'vue';
|
import { computed, ref, watch } from 'vue';
|
||||||
import MasterOutOfStockCatalogSection from '@/components/admin/master/MasterOutOfStockCatalogSection.vue';
|
|
||||||
import RawMaterialGroupedTable from '@/components/admin/master/raw-materials/RawMaterialGroupedTable.vue';
|
|
||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
import { Card, CardContent } from '@/components/ui/card';
|
import { Card, CardContent } from '@/components/ui/card';
|
||||||
import { useCan } from '@/composables/useCan';
|
import { useCan } from '@/composables/useCan';
|
||||||
@ -13,18 +11,17 @@ import {
|
|||||||
} from '@/composables/useDataTableQuery';
|
} from '@/composables/useDataTableQuery';
|
||||||
import AdminLayout from '@/layouts/AdminLayout.vue';
|
import AdminLayout from '@/layouts/AdminLayout.vue';
|
||||||
import type { DataTableFilterDef } from '@/types/data-table';
|
import type { DataTableFilterDef } from '@/types/data-table';
|
||||||
import type { MasterOutOfStockGroup } from '@/types/master-inventory';
|
|
||||||
import type { PaginatedRawMaterials } from '@/types/raw-material';
|
import type { PaginatedRawMaterials } from '@/types/raw-material';
|
||||||
|
import RawMaterialGroupedTable from './table/RawMaterialGroupedTable.vue';
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
rawMaterials: PaginatedRawMaterials;
|
rawMaterials: PaginatedRawMaterials;
|
||||||
outOfStockGroups: MasterOutOfStockGroup[];
|
|
||||||
stockWarningGroups: MasterOutOfStockGroup[];
|
|
||||||
filters: {
|
filters: {
|
||||||
search: string;
|
search: string;
|
||||||
sort?: string;
|
sort?: string;
|
||||||
direction?: 'asc' | 'desc';
|
direction?: 'asc' | 'desc';
|
||||||
is_active?: string;
|
is_active?: string;
|
||||||
|
stock_status?: string;
|
||||||
};
|
};
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
@ -35,7 +32,7 @@ const { query, setSearch, setFilter, resetFilters, syncFromServer } =
|
|||||||
useDataTableQuery({
|
useDataTableQuery({
|
||||||
url: '/admin/master/raw-materials',
|
url: '/admin/master/raw-materials',
|
||||||
initial: { ...props.filters },
|
initial: { ...props.filters },
|
||||||
filterKeys: ['is_active'],
|
filterKeys: ['is_active', 'stock_status'],
|
||||||
});
|
});
|
||||||
|
|
||||||
useDataTableQuerySync(() => props.filters, syncFromServer);
|
useDataTableQuerySync(() => props.filters, syncFromServer);
|
||||||
@ -50,13 +47,23 @@ const filterDefs = computed<DataTableFilterDef[]>(() => [
|
|||||||
{ value: '0', label: 'Nonaktif' },
|
{ value: '0', label: 'Nonaktif' },
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
key: 'stock_status',
|
||||||
|
label: 'Stok',
|
||||||
|
type: 'select',
|
||||||
|
options: [
|
||||||
|
{ value: 'out_of_stock', label: 'Stok Habis' },
|
||||||
|
{ value: 'low_stock', label: 'Stok Menipis' },
|
||||||
|
],
|
||||||
|
},
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const filterValues = computed(() => ({
|
const filterValues = computed(() => ({
|
||||||
is_active: query.value.is_active ?? '',
|
is_active: query.value.is_active ?? '',
|
||||||
|
stock_status: query.value.stock_status ?? '',
|
||||||
}));
|
}));
|
||||||
|
|
||||||
const tablePagination = computed(() => ({
|
const pagination = computed(() => ({
|
||||||
currentPage: props.rawMaterials.current_page,
|
currentPage: props.rawMaterials.current_page,
|
||||||
perPage: props.rawMaterials.per_page,
|
perPage: props.rawMaterials.per_page,
|
||||||
lastPage: props.rawMaterials.last_page,
|
lastPage: props.rawMaterials.last_page,
|
||||||
@ -100,16 +107,10 @@ watch(
|
|||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<MasterOutOfStockCatalogSection severity="warning" title="Stok Menipis" empty-title="Tidak ada peringatan stok"
|
|
||||||
empty-description="Stok bahan baku dibawah 5 pcs." :groups="stockWarningGroups" />
|
|
||||||
|
|
||||||
<MasterOutOfStockCatalogSection title="Stok Habis" empty-title="Tidak ada stok habis"
|
|
||||||
empty-description="Stok bahan baku dibawah 0 pcs." :groups="outOfStockGroups" />
|
|
||||||
|
|
||||||
<Card class="min-w-0">
|
<Card class="min-w-0">
|
||||||
<CardContent class="min-w-0">
|
<CardContent class="min-w-0">
|
||||||
<RawMaterialGroupedTable v-model:search="search" :materials="rawMaterials.data" :first-item="firstItem"
|
<RawMaterialGroupedTable v-model:search="search" :materials="rawMaterials.data" :first-item="firstItem"
|
||||||
:pagination="tablePagination" :pagination-links="rawMaterials.links" :filter-defs="filterDefs"
|
:pagination="pagination" :pagination-links="rawMaterials.links" :filter-defs="filterDefs"
|
||||||
:filter-values="filterValues" @filter-change="setFilter" @filters-reset="resetFilters" />
|
:filter-values="filterValues" @filter-change="setFilter" @filters-reset="resetFilters" />
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { useForm } from '@inertiajs/vue3';
|
import { useForm } from '@inertiajs/vue3';
|
||||||
import { Copy, Plus, Save, Trash2 } from '@lucide/vue';
|
import { Copy, Plus, Save, Trash2 } from '@lucide/vue';
|
||||||
import { ref, computed } from 'vue';
|
import { ref } from 'vue';
|
||||||
import { toast } from 'vue-sonner';
|
import { toast } from 'vue-sonner';
|
||||||
import { DecimalInput } from '@/components/form/decimal-input';
|
import { DecimalInput } from '@/components/form/decimal-input';
|
||||||
import { MultipleImageUploadField } from '@/components/form/image-upload-field';
|
import { MultipleImageUploadField } from '@/components/form/image-upload-field';
|
||||||
@ -94,14 +94,6 @@ const form = useForm({
|
|||||||
unit: props.initialData?.unit ?? '',
|
unit: props.initialData?.unit ?? '',
|
||||||
});
|
});
|
||||||
|
|
||||||
const isEditMode = computed(() => props.method === 'put');
|
|
||||||
|
|
||||||
const unitLabel = computed(() => {
|
|
||||||
const option = props.units.find((item) => item.value === form.unit);
|
|
||||||
|
|
||||||
return option?.label ?? form.unit;
|
|
||||||
});
|
|
||||||
|
|
||||||
function allPricesHaveSameValue(items: RawMaterialPriceFormItem[]): boolean {
|
function allPricesHaveSameValue(items: RawMaterialPriceFormItem[]): boolean {
|
||||||
if (items.length <= 1) {
|
if (items.length <= 1) {
|
||||||
return true;
|
return true;
|
||||||
@ -198,20 +190,6 @@ function buildFormData(): FormData {
|
|||||||
return formData;
|
return formData;
|
||||||
}
|
}
|
||||||
|
|
||||||
function formError(key: string): string | undefined {
|
|
||||||
return (form.errors as Record<string, string>)[key];
|
|
||||||
}
|
|
||||||
|
|
||||||
function priceError(clientId: string, field: string): string | undefined {
|
|
||||||
const index = prices.value.findIndex((price) => price.client_id === clientId);
|
|
||||||
|
|
||||||
if (index === -1) {
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
return formError(`prices.${index}.${field}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
function submit() {
|
function submit() {
|
||||||
const options = {
|
const options = {
|
||||||
forceFormData: true,
|
forceFormData: true,
|
||||||
@ -281,7 +259,7 @@ function submit() {
|
|||||||
<RupiahInput :id="`shared_price_${prices[0].client_id}`" :model-value="prices[0].price"
|
<RupiahInput :id="`shared_price_${prices[0].client_id}`" :model-value="prices[0].price"
|
||||||
@update:model-value="setSharedPrice" />
|
@update:model-value="setSharedPrice" />
|
||||||
<FieldError
|
<FieldError
|
||||||
:errors="priceError(prices[0].client_id, 'price') ? [priceError(prices[0].client_id, 'price')!] : []" />
|
:errors="formErrors(form, 'prices.0.price')" />
|
||||||
</Field>
|
</Field>
|
||||||
</FieldGroup>
|
</FieldGroup>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
@ -314,7 +292,7 @@ function submit() {
|
|||||||
placeholder="Contoh: Premium / 40s" :maxlength="FIELD_LIMITS.variantName"
|
placeholder="Contoh: Premium / 40s" :maxlength="FIELD_LIMITS.variantName"
|
||||||
@update:model-value="setPriceField(price.client_id, 'variant', String($event))" />
|
@update:model-value="setPriceField(price.client_id, 'variant', String($event))" />
|
||||||
<FieldError
|
<FieldError
|
||||||
:errors="priceError(price.client_id, 'variant') ? [priceError(price.client_id, 'variant')!] : []" />
|
:errors="formErrors(form, `prices.${index}.variant`)" />
|
||||||
</Field>
|
</Field>
|
||||||
<Field v-if="method !== 'put'">
|
<Field v-if="method !== 'put'">
|
||||||
<FieldLabel :for="`stock_${price.client_id}`" required>
|
<FieldLabel :for="`stock_${price.client_id}`" required>
|
||||||
@ -323,7 +301,7 @@ function submit() {
|
|||||||
<DecimalInput :id="`stock_${price.client_id}`" :model-value="price.stock"
|
<DecimalInput :id="`stock_${price.client_id}`" :model-value="price.stock"
|
||||||
@update:model-value="setPriceField(price.client_id, 'stock', String($event))" />
|
@update:model-value="setPriceField(price.client_id, 'stock', String($event))" />
|
||||||
<FieldError
|
<FieldError
|
||||||
:errors="priceError(price.client_id, 'stock') ? [priceError(price.client_id, 'stock')!] : []" />
|
:errors="formErrors(form, `prices.${index}.stock`)" />
|
||||||
</Field>
|
</Field>
|
||||||
<Field v-if="!useSamePrice || prices.length === 1">
|
<Field v-if="!useSamePrice || prices.length === 1">
|
||||||
<FieldLabel :for="`price_${price.client_id}`" required>
|
<FieldLabel :for="`price_${price.client_id}`" required>
|
||||||
@ -332,14 +310,14 @@ function submit() {
|
|||||||
<RupiahInput :id="`price_${price.client_id}`" :model-value="price.price"
|
<RupiahInput :id="`price_${price.client_id}`" :model-value="price.price"
|
||||||
@update:model-value="setPriceValue(price.client_id, $event)" />
|
@update:model-value="setPriceValue(price.client_id, $event)" />
|
||||||
<FieldError
|
<FieldError
|
||||||
:errors="priceError(price.client_id, 'price') ? [priceError(price.client_id, 'price')!] : []" />
|
:errors="formErrors(form, `prices.${index}.price`)" />
|
||||||
</Field>
|
</Field>
|
||||||
</FieldSet>
|
</FieldSet>
|
||||||
|
|
||||||
<div class="mt-4">
|
<div class="mt-4">
|
||||||
<MultipleImageUploadField :id="`price_images_${price.client_id}`" v-model="price.media"
|
<MultipleImageUploadField :id="`price_images_${price.client_id}`" v-model="price.media"
|
||||||
label="Foto Varian" :max-files="5" required
|
label="Foto Varian" :max-files="5" required
|
||||||
:errors="priceError(price.client_id, 'images') ? [priceError(price.client_id, 'images')!] : []" />
|
:errors="formErrors(form, `prices.${index}.images`)" />
|
||||||
</div>
|
</div>
|
||||||
</FieldGroup>
|
</FieldGroup>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
@ -1,8 +1,6 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { Link } from '@inertiajs/vue3';
|
import { Link } from '@inertiajs/vue3';
|
||||||
import { computed } from 'vue';
|
import { computed } from 'vue';
|
||||||
import DataTableActions from '@/components/admin/master/raw-materials/data-table-actions.vue';
|
|
||||||
import RawMaterialStatusToggle from '@/components/admin/master/raw-materials/raw-material-status-toggle.vue';
|
|
||||||
import DataTableToolbar from '@/components/data-table/DataTableToolbar.vue';
|
import DataTableToolbar from '@/components/data-table/DataTableToolbar.vue';
|
||||||
import MediaThumbnailCell from '@/components/media/MediaThumbnailCell.vue';
|
import MediaThumbnailCell from '@/components/media/MediaThumbnailCell.vue';
|
||||||
import { Badge } from '@/components/ui/badge';
|
import { Badge } from '@/components/ui/badge';
|
||||||
@ -28,6 +26,8 @@ import type {
|
|||||||
DataTablePaginationLink,
|
DataTablePaginationLink,
|
||||||
} from '@/types/data-table';
|
} from '@/types/data-table';
|
||||||
import type { RawMaterialListItem } from '@/types/raw-material';
|
import type { RawMaterialListItem } from '@/types/raw-material';
|
||||||
|
import DataTableActions from './data-table-actions.vue';
|
||||||
|
import RawMaterialStatusToggle from './raw-material-status-toggle.vue';
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
materials: RawMaterialListItem[];
|
materials: RawMaterialListItem[];
|
||||||
@ -90,15 +90,17 @@ function rowNumber(index: number): number {
|
|||||||
</div>
|
</div>
|
||||||
<div class="flex flex-wrap items-center gap-x-4 gap-y-1 text-sm">
|
<div class="flex flex-wrap items-center gap-x-4 gap-y-1 text-sm">
|
||||||
<span>
|
<span>
|
||||||
Total stok <strong class="text-primary"> {{material.prices.reduce((acc, price) => acc +
|
Total stok <strong class="text-primary"> {{material.prices.reduce((acc, price) =>
|
||||||
|
acc +
|
||||||
Number(price.stock), 0)
|
Number(price.stock), 0)
|
||||||
}}
|
}}
|
||||||
{{ material.unit_abbreviation }}
|
{{ material.unit_abbreviation }}
|
||||||
</strong>
|
</strong>
|
||||||
</span>
|
</span>
|
||||||
<span>
|
<span>
|
||||||
Total harga <strong class="text-primary"> Rp {{formatRupiah(material.prices.reduce((acc, price) => acc +
|
Total harga <strong class="text-primary"> Rp
|
||||||
Number(price.stock) * Number(price.price), 0))
|
{{formatRupiah(material.prices.reduce((acc, price) => acc +
|
||||||
|
Number(price.stock) * Number(price.price), 0))
|
||||||
}}
|
}}
|
||||||
</strong>
|
</strong>
|
||||||
</span>
|
</span>
|
||||||
@ -118,7 +120,7 @@ function rowNumber(index: number): number {
|
|||||||
<TableHead>Varian</TableHead>
|
<TableHead>Varian</TableHead>
|
||||||
<TableHead>Foto</TableHead>
|
<TableHead>Foto</TableHead>
|
||||||
<TableHead>Stok</TableHead>
|
<TableHead>Stok</TableHead>
|
||||||
<TableHead>Harga</TableHead>
|
<TableHead>Harga per Satuan</TableHead>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
</TableHeader>
|
</TableHeader>
|
||||||
<TableBody>
|
<TableBody>
|
||||||
@ -51,12 +51,8 @@ function destroyRawMaterial() {
|
|||||||
|
|
||||||
<Tooltip v-if="can('raw-materials.delete')">
|
<Tooltip v-if="can('raw-materials.delete')">
|
||||||
<TooltipTrigger as-child>
|
<TooltipTrigger as-child>
|
||||||
<Button
|
<Button variant="ghost" size="icon" class="text-destructive hover:text-destructive size-8"
|
||||||
variant="ghost"
|
@click="deleteConfirmOpen = true">
|
||||||
size="icon"
|
|
||||||
class="text-destructive hover:text-destructive size-8"
|
|
||||||
@click="deleteConfirmOpen = true"
|
|
||||||
>
|
|
||||||
<Trash2 class="size-4" />
|
<Trash2 class="size-4" />
|
||||||
<span class="sr-only">Hapus</span>
|
<span class="sr-only">Hapus</span>
|
||||||
</Button>
|
</Button>
|
||||||
@ -65,15 +61,8 @@ function destroyRawMaterial() {
|
|||||||
</Tooltip>
|
</Tooltip>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<ConfirmDialog
|
<ConfirmDialog v-if="can('raw-materials.delete')" v-model:open="deleteConfirmOpen" title="Hapus bahan baku?"
|
||||||
v-if="can('raw-materials.delete')"
|
|
||||||
v-model:open="deleteConfirmOpen"
|
|
||||||
title="Hapus bahan baku?"
|
|
||||||
:description="`Bahan baku ${material.name} akan dihapus beserta seluruh variannya. Tindakan ini tidak dapat dibatalkan.`"
|
:description="`Bahan baku ${material.name} akan dihapus beserta seluruh variannya. Tindakan ini tidak dapat dibatalkan.`"
|
||||||
confirm-label="Hapus"
|
confirm-label="Hapus" cancel-label="Batal" destructive :loading="deleteProcessing"
|
||||||
cancel-label="Batal"
|
@confirm="destroyRawMaterial" />
|
||||||
destructive
|
|
||||||
:loading="deleteProcessing"
|
|
||||||
@confirm="destroyRawMaterial"
|
|
||||||
/>
|
|
||||||
</template>
|
</template>
|
||||||
@ -49,11 +49,8 @@ function toggleStatus(checked: boolean) {
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="flex items-center gap-2">
|
<div class="flex items-center gap-2">
|
||||||
<Switch
|
<Switch :model-value="isActive" :disabled="processing || !can('raw-materials.toggle-status')"
|
||||||
:model-value="isActive"
|
@update:model-value="toggleStatus" />
|
||||||
:disabled="processing || !can('raw-materials.toggle-status')"
|
|
||||||
@update:model-value="toggleStatus"
|
|
||||||
/>
|
|
||||||
<Badge :variant="isActive ? 'default' : 'secondary'">
|
<Badge :variant="isActive ? 'default' : 'secondary'">
|
||||||
{{ isActive ? 'Aktif' : 'Nonaktif' }}
|
{{ isActive ? 'Aktif' : 'Nonaktif' }}
|
||||||
</Badge>
|
</Badge>
|
||||||
Loading…
Reference in New Issue
Block a user