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 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;
|
||||
|
||||
|
||||
@ -7,6 +7,7 @@
|
||||
use App\Http\Controllers\Concerns\ParsesDataTableQuery;
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Http\Requests\Admin\Master\RawMaterialRequest;
|
||||
use App\Http\Requests\Admin\ToggleStatusRequest;
|
||||
use App\Models\RawMaterial;
|
||||
use App\Models\RawMaterialPrice;
|
||||
use App\Services\Master\RawMaterialService;
|
||||
@ -28,13 +29,13 @@ public function index(Request $request): Response
|
||||
{
|
||||
$tableQuery = $this->parseDataTableQuery($request);
|
||||
$isActive = $request->string('is_active')->toString();
|
||||
$stockStatus = $request->string('stock_status')->toString();
|
||||
|
||||
return Inertia::render('admin/master/raw-materials/Index', [
|
||||
'rawMaterials' => $this->rawMaterialService->paginateForIndex($tableQuery, $isActive),
|
||||
'outOfStockGroups' => $this->rawMaterialService->outOfStockGroups(),
|
||||
'stockWarningGroups' => $this->rawMaterialService->stockWarningGroups(),
|
||||
'rawMaterials' => $this->rawMaterialService->paginateForIndex($tableQuery, $isActive, $stockStatus),
|
||||
'filters' => $this->dataTableFilters($tableQuery, [
|
||||
'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');
|
||||
}
|
||||
|
||||
public function toggleStatus(Request $request, RawMaterial $rawMaterial): RedirectResponse
|
||||
public function toggleStatus(ToggleStatusRequest $request, RawMaterial $rawMaterial): RedirectResponse
|
||||
{
|
||||
$validated = $request->validate([
|
||||
'is_active' => ['required', 'boolean'],
|
||||
]);
|
||||
|
||||
$this->rawMaterialService->toggleStatus($rawMaterial, $validated['is_active']);
|
||||
$this->rawMaterialService->toggleStatus($rawMaterial, $request->validated());
|
||||
|
||||
$this->flashStatusUpdated('bahan baku');
|
||||
|
||||
|
||||
@ -2,6 +2,7 @@
|
||||
|
||||
namespace App\Services\Master;
|
||||
|
||||
use App\Enums\RawMaterialUnit;
|
||||
use App\Models\RawMaterial;
|
||||
use App\Models\RawMaterialPrice;
|
||||
use App\Services\Media\MediaService;
|
||||
@ -18,119 +19,10 @@ public function __construct(
|
||||
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
|
||||
*/
|
||||
public function paginateForIndex(array $tableQuery, string $isActive): LengthAwarePaginator
|
||||
public function paginateForIndex(array $tableQuery, string $isActive, string $stockStatus = ''): LengthAwarePaginator
|
||||
{
|
||||
$query = RawMaterial::query()
|
||||
->with([
|
||||
@ -145,10 +37,22 @@ public function paginateForIndex(array $tableQuery, string $isActive): LengthAwa
|
||||
->orWhereHas('prices', fn (Builder $query) => $query->where('variant', 'like', "%{$search}%"));
|
||||
});
|
||||
})
|
||||
->when(
|
||||
$isActive !== '',
|
||||
fn (Builder $query) => $query->where('is_active', $isActive === '1')
|
||||
);
|
||||
->when($isActive !== '', fn (Builder $query) => $query->where('is_active', $isActive === '1'))
|
||||
->when($stockStatus === 'out_of_stock', function (Builder $query): void {
|
||||
$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']);
|
||||
|
||||
@ -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();
|
||||
}
|
||||
|
||||
|
||||
@ -1,10 +1,10 @@
|
||||
<script setup lang="ts">
|
||||
import { Head, Link } from '@inertiajs/vue3';
|
||||
import { ArrowLeft } from '@lucide/vue';
|
||||
import RawMaterialForm from '@/components/admin/master/raw-materials/RawMaterialForm.vue';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import AdminLayout from '@/layouts/AdminLayout.vue';
|
||||
import type { EnumOption } from '@/types/raw-material';
|
||||
import RawMaterialForm from './form/RawMaterialForm.vue';
|
||||
|
||||
defineProps<{
|
||||
units: EnumOption[];
|
||||
|
||||
@ -2,10 +2,10 @@
|
||||
import { Head, Link } from '@inertiajs/vue3';
|
||||
import { ArrowLeft } from '@lucide/vue';
|
||||
import { computed } from 'vue';
|
||||
import RawMaterialForm from '@/components/admin/master/raw-materials/RawMaterialForm.vue';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import AdminLayout from '@/layouts/AdminLayout.vue';
|
||||
import type { EnumOption, RawMaterialListItem } from '@/types/raw-material';
|
||||
import RawMaterialForm from './form/RawMaterialForm.vue';
|
||||
|
||||
const props = defineProps<{
|
||||
rawMaterial: RawMaterialListItem;
|
||||
|
||||
@ -2,8 +2,6 @@
|
||||
import { Head, Link } from '@inertiajs/vue3';
|
||||
import { Plus } from '@lucide/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 { Card, CardContent } from '@/components/ui/card';
|
||||
import { useCan } from '@/composables/useCan';
|
||||
@ -13,18 +11,17 @@ import {
|
||||
} from '@/composables/useDataTableQuery';
|
||||
import AdminLayout from '@/layouts/AdminLayout.vue';
|
||||
import type { DataTableFilterDef } from '@/types/data-table';
|
||||
import type { MasterOutOfStockGroup } from '@/types/master-inventory';
|
||||
import type { PaginatedRawMaterials } from '@/types/raw-material';
|
||||
import RawMaterialGroupedTable from './table/RawMaterialGroupedTable.vue';
|
||||
|
||||
const props = defineProps<{
|
||||
rawMaterials: PaginatedRawMaterials;
|
||||
outOfStockGroups: MasterOutOfStockGroup[];
|
||||
stockWarningGroups: MasterOutOfStockGroup[];
|
||||
filters: {
|
||||
search: string;
|
||||
sort?: string;
|
||||
direction?: 'asc' | 'desc';
|
||||
is_active?: string;
|
||||
stock_status?: string;
|
||||
};
|
||||
}>();
|
||||
|
||||
@ -35,7 +32,7 @@ const { query, setSearch, setFilter, resetFilters, syncFromServer } =
|
||||
useDataTableQuery({
|
||||
url: '/admin/master/raw-materials',
|
||||
initial: { ...props.filters },
|
||||
filterKeys: ['is_active'],
|
||||
filterKeys: ['is_active', 'stock_status'],
|
||||
});
|
||||
|
||||
useDataTableQuerySync(() => props.filters, syncFromServer);
|
||||
@ -50,13 +47,23 @@ const filterDefs = computed<DataTableFilterDef[]>(() => [
|
||||
{ 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(() => ({
|
||||
is_active: query.value.is_active ?? '',
|
||||
stock_status: query.value.stock_status ?? '',
|
||||
}));
|
||||
|
||||
const tablePagination = computed(() => ({
|
||||
const pagination = computed(() => ({
|
||||
currentPage: props.rawMaterials.current_page,
|
||||
perPage: props.rawMaterials.per_page,
|
||||
lastPage: props.rawMaterials.last_page,
|
||||
@ -100,16 +107,10 @@ watch(
|
||||
</Button>
|
||||
</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">
|
||||
<CardContent class="min-w-0">
|
||||
<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" />
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
<script setup lang="ts">
|
||||
import { useForm } from '@inertiajs/vue3';
|
||||
import { Copy, Plus, Save, Trash2 } from '@lucide/vue';
|
||||
import { ref, computed } from 'vue';
|
||||
import { ref } from 'vue';
|
||||
import { toast } from 'vue-sonner';
|
||||
import { DecimalInput } from '@/components/form/decimal-input';
|
||||
import { MultipleImageUploadField } from '@/components/form/image-upload-field';
|
||||
@ -94,14 +94,6 @@ const form = useForm({
|
||||
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 {
|
||||
if (items.length <= 1) {
|
||||
return true;
|
||||
@ -198,20 +190,6 @@ function buildFormData(): 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() {
|
||||
const options = {
|
||||
forceFormData: true,
|
||||
@ -281,7 +259,7 @@ function submit() {
|
||||
<RupiahInput :id="`shared_price_${prices[0].client_id}`" :model-value="prices[0].price"
|
||||
@update:model-value="setSharedPrice" />
|
||||
<FieldError
|
||||
:errors="priceError(prices[0].client_id, 'price') ? [priceError(prices[0].client_id, 'price')!] : []" />
|
||||
:errors="formErrors(form, 'prices.0.price')" />
|
||||
</Field>
|
||||
</FieldGroup>
|
||||
</CardContent>
|
||||
@ -314,7 +292,7 @@ function submit() {
|
||||
placeholder="Contoh: Premium / 40s" :maxlength="FIELD_LIMITS.variantName"
|
||||
@update:model-value="setPriceField(price.client_id, 'variant', String($event))" />
|
||||
<FieldError
|
||||
:errors="priceError(price.client_id, 'variant') ? [priceError(price.client_id, 'variant')!] : []" />
|
||||
:errors="formErrors(form, `prices.${index}.variant`)" />
|
||||
</Field>
|
||||
<Field v-if="method !== 'put'">
|
||||
<FieldLabel :for="`stock_${price.client_id}`" required>
|
||||
@ -323,7 +301,7 @@ function submit() {
|
||||
<DecimalInput :id="`stock_${price.client_id}`" :model-value="price.stock"
|
||||
@update:model-value="setPriceField(price.client_id, 'stock', String($event))" />
|
||||
<FieldError
|
||||
:errors="priceError(price.client_id, 'stock') ? [priceError(price.client_id, 'stock')!] : []" />
|
||||
:errors="formErrors(form, `prices.${index}.stock`)" />
|
||||
</Field>
|
||||
<Field v-if="!useSamePrice || prices.length === 1">
|
||||
<FieldLabel :for="`price_${price.client_id}`" required>
|
||||
@ -332,14 +310,14 @@ function submit() {
|
||||
<RupiahInput :id="`price_${price.client_id}`" :model-value="price.price"
|
||||
@update:model-value="setPriceValue(price.client_id, $event)" />
|
||||
<FieldError
|
||||
:errors="priceError(price.client_id, 'price') ? [priceError(price.client_id, 'price')!] : []" />
|
||||
:errors="formErrors(form, `prices.${index}.price`)" />
|
||||
</Field>
|
||||
</FieldSet>
|
||||
|
||||
<div class="mt-4">
|
||||
<MultipleImageUploadField :id="`price_images_${price.client_id}`" v-model="price.media"
|
||||
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>
|
||||
</FieldGroup>
|
||||
</CardContent>
|
||||
@ -1,8 +1,6 @@
|
||||
<script setup lang="ts">
|
||||
import { Link } from '@inertiajs/vue3';
|
||||
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 MediaThumbnailCell from '@/components/media/MediaThumbnailCell.vue';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
@ -28,6 +26,8 @@ import type {
|
||||
DataTablePaginationLink,
|
||||
} from '@/types/data-table';
|
||||
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<{
|
||||
materials: RawMaterialListItem[];
|
||||
@ -90,15 +90,17 @@ function rowNumber(index: number): number {
|
||||
</div>
|
||||
<div class="flex flex-wrap items-center gap-x-4 gap-y-1 text-sm">
|
||||
<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)
|
||||
}}
|
||||
{{ material.unit_abbreviation }}
|
||||
</strong>
|
||||
</span>
|
||||
<span>
|
||||
Total harga <strong class="text-primary"> Rp {{formatRupiah(material.prices.reduce((acc, price) => acc +
|
||||
Number(price.stock) * Number(price.price), 0))
|
||||
Total harga <strong class="text-primary"> Rp
|
||||
{{formatRupiah(material.prices.reduce((acc, price) => acc +
|
||||
Number(price.stock) * Number(price.price), 0))
|
||||
}}
|
||||
</strong>
|
||||
</span>
|
||||
@ -118,7 +120,7 @@ function rowNumber(index: number): number {
|
||||
<TableHead>Varian</TableHead>
|
||||
<TableHead>Foto</TableHead>
|
||||
<TableHead>Stok</TableHead>
|
||||
<TableHead>Harga</TableHead>
|
||||
<TableHead>Harga per Satuan</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
@ -51,12 +51,8 @@ function destroyRawMaterial() {
|
||||
|
||||
<Tooltip v-if="can('raw-materials.delete')">
|
||||
<TooltipTrigger as-child>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
class="text-destructive hover:text-destructive size-8"
|
||||
@click="deleteConfirmOpen = true"
|
||||
>
|
||||
<Button variant="ghost" size="icon" class="text-destructive hover:text-destructive size-8"
|
||||
@click="deleteConfirmOpen = true">
|
||||
<Trash2 class="size-4" />
|
||||
<span class="sr-only">Hapus</span>
|
||||
</Button>
|
||||
@ -65,15 +61,8 @@ function destroyRawMaterial() {
|
||||
</Tooltip>
|
||||
</div>
|
||||
|
||||
<ConfirmDialog
|
||||
v-if="can('raw-materials.delete')"
|
||||
v-model:open="deleteConfirmOpen"
|
||||
title="Hapus bahan baku?"
|
||||
<ConfirmDialog 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.`"
|
||||
confirm-label="Hapus"
|
||||
cancel-label="Batal"
|
||||
destructive
|
||||
:loading="deleteProcessing"
|
||||
@confirm="destroyRawMaterial"
|
||||
/>
|
||||
confirm-label="Hapus" cancel-label="Batal" destructive :loading="deleteProcessing"
|
||||
@confirm="destroyRawMaterial" />
|
||||
</template>
|
||||
@ -49,11 +49,8 @@ function toggleStatus(checked: boolean) {
|
||||
|
||||
<template>
|
||||
<div class="flex items-center gap-2">
|
||||
<Switch
|
||||
:model-value="isActive"
|
||||
:disabled="processing || !can('raw-materials.toggle-status')"
|
||||
@update:model-value="toggleStatus"
|
||||
/>
|
||||
<Switch :model-value="isActive" :disabled="processing || !can('raw-materials.toggle-status')"
|
||||
@update:model-value="toggleStatus" />
|
||||
<Badge :variant="isActive ? 'default' : 'secondary'">
|
||||
{{ isActive ? 'Aktif' : 'Nonaktif' }}
|
||||
</Badge>
|
||||
Loading…
Reference in New Issue
Block a user