584 lines
23 KiB
PHP
584 lines
23 KiB
PHP
<?php
|
|
|
|
namespace Modules\Maintenance\Services;
|
|
|
|
use App\Business;
|
|
use App\Product;
|
|
use App\Transaction;
|
|
use App\User;
|
|
use App\Utils\ProductUtil;
|
|
use App\Utils\TransactionUtil;
|
|
use App\Utils\Util;
|
|
use Illuminate\Support\Collection;
|
|
use Illuminate\Support\Facades\DB;
|
|
use InvalidArgumentException;
|
|
use Modules\Maintenance\Models\Equipment;
|
|
use Modules\Maintenance\Models\EquipmentPart;
|
|
use Modules\Maintenance\Models\MaterialLine;
|
|
use Modules\Maintenance\Models\MaterialList;
|
|
use Modules\Maintenance\Models\MaterialProcurement;
|
|
use Modules\Maintenance\Models\Overhaul;
|
|
use Modules\Maintenance\Models\RebuildProject;
|
|
use Modules\Maintenance\Models\SparePart;
|
|
use Modules\Maintenance\Models\WorkOrder;
|
|
|
|
class MaterialListService
|
|
{
|
|
public const SOURCE_TYPES = ['overhaul', 'rebuild', 'work_order'];
|
|
|
|
public function __construct(
|
|
protected MaterialStockService $stockService,
|
|
protected MaterialPurchaseSuggestionService $suggestionService,
|
|
protected ProductUtil $productUtil,
|
|
protected TransactionUtil $transactionUtil,
|
|
protected Util $commonUtil
|
|
) {}
|
|
|
|
public function getOrCreateList(Business $business, string $sourceType, int $sourceId, ?User $user = null): MaterialList
|
|
{
|
|
$this->assertSourceExists($business->id, $sourceType, $sourceId);
|
|
|
|
return MaterialList::firstOrCreate(
|
|
[
|
|
'business_id' => $business->id,
|
|
'source_type' => $sourceType,
|
|
'source_id' => $sourceId,
|
|
],
|
|
[
|
|
'destination_location_id' => $this->stockService->resolveDestinationLocationId($business->id),
|
|
'status' => 'active',
|
|
'created_by' => $user?->id,
|
|
]
|
|
);
|
|
}
|
|
|
|
public function updateDestination(MaterialList $list, ?int $locationId): MaterialList
|
|
{
|
|
$list->update(['destination_location_id' => $locationId]);
|
|
|
|
return $list->fresh();
|
|
}
|
|
|
|
public function addLine(MaterialList $list, array $data): MaterialLine
|
|
{
|
|
$sparePartId = $data['spare_part_id'] ?? null;
|
|
$productId = $data['product_id'] ?? null;
|
|
$variationId = $data['variation_id'] ?? null;
|
|
$partName = $data['part_name'] ?? null;
|
|
|
|
if ($sparePartId) {
|
|
$part = SparePart::where('business_id', $list->business_id)->findOrFail($sparePartId);
|
|
$productId = $part->product_id;
|
|
$variationId = $variationId ?: app(SparePartInventoryService::class)->resolveVariationId($part);
|
|
$partName = $partName ?: $part->name;
|
|
$data['part_number'] = $data['part_number'] ?? $part->part_number;
|
|
$data['unit'] = $data['unit'] ?? $part->unit;
|
|
$data['suggested_supplier_id'] = $data['suggested_supplier_id']
|
|
?? $this->suggestionService->resolveDefaultSupplierId($list->business_id, $variationId, $part->id);
|
|
}
|
|
|
|
if (! $sparePartId && empty($partName)) {
|
|
throw new InvalidArgumentException('نام قطعه یا قطعه یدکی الزامی است.');
|
|
}
|
|
|
|
$lineType = $data['line_type'] ?? 'supply';
|
|
if (! in_array($lineType, ['supply', 'repair'], true)) {
|
|
$lineType = 'supply';
|
|
}
|
|
|
|
return MaterialLine::create([
|
|
'material_list_id' => $list->id,
|
|
'business_id' => $list->business_id,
|
|
'line_type' => $lineType,
|
|
'spare_part_id' => $sparePartId,
|
|
'product_id' => $productId,
|
|
'variation_id' => $variationId,
|
|
'part_name' => $partName,
|
|
'part_number' => $data['part_number'] ?? null,
|
|
'quantity_required' => $data['quantity_required'] ?? 1,
|
|
'unit' => $data['unit'] ?? null,
|
|
'suggested_supplier_id' => $data['suggested_supplier_id'] ?? null,
|
|
'notes' => $data['notes'] ?? null,
|
|
'status' => 'planned',
|
|
]);
|
|
}
|
|
|
|
public function importFromEquipmentBom(MaterialList $list, int $equipmentId): int
|
|
{
|
|
Equipment::where('business_id', $list->business_id)->findOrFail($equipmentId);
|
|
|
|
$parts = EquipmentPart::where('business_id', $list->business_id)
|
|
->where('equipment_id', $equipmentId)
|
|
->where(function ($q) {
|
|
$q->whereNotNull('spare_part_id')->orWhereNotNull('part_name');
|
|
})
|
|
->get();
|
|
|
|
$count = 0;
|
|
foreach ($parts as $bom) {
|
|
if ($bom->spare_part_id) {
|
|
$exists = $list->lines()->where('spare_part_id', $bom->spare_part_id)->exists();
|
|
if ($exists) {
|
|
continue;
|
|
}
|
|
}
|
|
|
|
$this->addLine($list, [
|
|
'spare_part_id' => $bom->spare_part_id,
|
|
'part_name' => $bom->part_name,
|
|
'part_number' => $bom->part_code,
|
|
'quantity_required' => $bom->quantity ?: 1,
|
|
'unit' => $bom->unit,
|
|
]);
|
|
$count++;
|
|
}
|
|
|
|
return $count;
|
|
}
|
|
|
|
/**
|
|
* @return Collection<int, array<string, mixed>>
|
|
*/
|
|
public function analyzeList(MaterialList $list): Collection
|
|
{
|
|
$list->load(['lines.sparePart', 'lines.suggestedSupplier']);
|
|
|
|
return $list->lines->map(fn (MaterialLine $line) => $this->stockService->analyzeLine(
|
|
$list->business_id,
|
|
$line,
|
|
$list->destination_location_id
|
|
));
|
|
}
|
|
|
|
/**
|
|
* @return array<int, array<string, mixed>>
|
|
*/
|
|
public function buildPurchaseShortfall(MaterialList $list): array
|
|
{
|
|
$items = [];
|
|
foreach ($this->analyzeList($list) as $analysis) {
|
|
if ($analysis['shortfall'] <= 0 || ! $analysis['has_product_link']) {
|
|
continue;
|
|
}
|
|
|
|
$line = MaterialLine::find($analysis['line_id']);
|
|
$supplierId = $line->suggested_supplier_id
|
|
?? $this->suggestionService->resolveDefaultSupplierId(
|
|
$list->business_id,
|
|
$line->variation_id,
|
|
$line->spare_part_id
|
|
);
|
|
|
|
$items[] = array_merge($analysis, [
|
|
'shortfall_qty' => $analysis['shortfall'],
|
|
'variation_id' => $line->variation_id,
|
|
'product_id' => $line->product_id,
|
|
'suggested_supplier_id' => $supplierId,
|
|
'suggested_supplier_name' => $this->suggestionService->supplierName($supplierId),
|
|
'supplier_suggestions' => $line->variation_id
|
|
? $this->suggestionService->suggestSuppliers($list->business_id, (int) $line->variation_id)->all()
|
|
: [],
|
|
]);
|
|
}
|
|
|
|
return $items;
|
|
}
|
|
|
|
public function requestTransfer(
|
|
MaterialList $list,
|
|
MaterialLine $line,
|
|
int $sourceLocationId,
|
|
float $quantity,
|
|
?User $user = null
|
|
): MaterialProcurement {
|
|
$ids = $this->stockService->resolveProductIds($line);
|
|
if (! $ids['product_id'] || ! $ids['variation_id']) {
|
|
throw new InvalidArgumentException('این قطعه به محصول انبار متصل نیست.');
|
|
}
|
|
|
|
$destinationId = $list->destination_location_id
|
|
?: $this->stockService->resolveDestinationLocationId($list->business_id);
|
|
if (! $destinationId) {
|
|
throw new InvalidArgumentException('انبار مقصد (محل تعمیر) مشخص نشده است.');
|
|
}
|
|
|
|
if ($sourceLocationId === $destinationId) {
|
|
throw new InvalidArgumentException('انبار مبدأ و مقصد نمیتوانند یکسان باشند.');
|
|
}
|
|
|
|
$stock = $this->stockService->stockByLocation($list->business_id, $line);
|
|
$sourceRow = $stock->firstWhere('location_id', $sourceLocationId);
|
|
if (! $sourceRow || $sourceRow['qty'] < $quantity) {
|
|
throw new InvalidArgumentException('موجودی کافی در انبار مبدأ وجود ندارد.');
|
|
}
|
|
|
|
$product = Product::where('business_id', $list->business_id)->findOrFail($ids['product_id']);
|
|
$unitPrice = $this->stockService->getUnitPrice($ids['variation_id'], $sourceLocationId);
|
|
|
|
return DB::transaction(function () use (
|
|
$list, $line, $sourceLocationId, $destinationId, $quantity, $ids, $product, $unitPrice, $user
|
|
) {
|
|
$businessId = $list->business_id;
|
|
$userId = $user?->id ?? auth()->id();
|
|
|
|
$refCount = $this->productUtil->setAndGetReferenceCount('stock_transfer');
|
|
$refNo = $this->productUtil->generateReferenceNumber('stock_transfer', $refCount);
|
|
|
|
$sellData = [
|
|
'business_id' => $businessId,
|
|
'location_id' => $sourceLocationId,
|
|
'type' => 'sell_transfer',
|
|
'status' => 'in_transit',
|
|
'payment_status' => 'paid',
|
|
'ref_no' => $refNo,
|
|
'transaction_date' => now()->toDateTimeString(),
|
|
'created_by' => $userId,
|
|
'additional_notes' => 'Maintenance material line #'.$line->id,
|
|
'final_total' => $unitPrice * $quantity,
|
|
'total_before_tax' => $unitPrice * $quantity,
|
|
'shipping_charges' => 0,
|
|
];
|
|
|
|
$sellTransfer = Transaction::create($sellData);
|
|
|
|
$purchaseData = array_merge($sellData, [
|
|
'type' => 'purchase_transfer',
|
|
'location_id' => $destinationId,
|
|
'transfer_parent_id' => $sellTransfer->id,
|
|
'status' => 'in_transit',
|
|
]);
|
|
unset($purchaseData['id']);
|
|
$purchaseTransfer = Transaction::create($purchaseData);
|
|
|
|
$lineArr = [
|
|
'product_id' => $ids['product_id'],
|
|
'variation_id' => $ids['variation_id'],
|
|
'quantity' => $quantity,
|
|
'item_tax' => 0,
|
|
'unit_price' => $unitPrice,
|
|
'unit_price_inc_tax' => $unitPrice,
|
|
];
|
|
|
|
$this->transactionUtil->createOrUpdateSellLines(
|
|
$sellTransfer,
|
|
[$lineArr],
|
|
$sourceLocationId,
|
|
false,
|
|
null,
|
|
[],
|
|
false
|
|
);
|
|
|
|
$purchaseTransfer->purchase_lines()->create([
|
|
'product_id' => $ids['product_id'],
|
|
'variation_id' => $ids['variation_id'],
|
|
'quantity' => $quantity,
|
|
'purchase_price' => $unitPrice,
|
|
'purchase_price_inc_tax' => $unitPrice,
|
|
'item_tax' => 0,
|
|
]);
|
|
|
|
$line->update(['status' => 'transfer_in_transit']);
|
|
|
|
return MaterialProcurement::create([
|
|
'material_line_id' => $line->id,
|
|
'transaction_id' => $sellTransfer->id,
|
|
'procurement_type' => 'stock_transfer',
|
|
'quantity' => $quantity,
|
|
'source_location_id' => $sourceLocationId,
|
|
'destination_location_id' => $destinationId,
|
|
'status' => 'in_transit',
|
|
'meta' => [
|
|
'purchase_transfer_id' => $purchaseTransfer->id,
|
|
'ref_no' => $refNo,
|
|
],
|
|
]);
|
|
});
|
|
}
|
|
|
|
/**
|
|
* @param array<int, array{line_id: int, supplier_id?: int, quantity?: float}> $lineSelections
|
|
* @return Transaction
|
|
*/
|
|
public function createPurchaseRequisition(
|
|
MaterialList $list,
|
|
array $lineSelections,
|
|
?User $user = null
|
|
): Transaction {
|
|
$destinationId = $list->destination_location_id
|
|
?: $this->stockService->resolveDestinationLocationId($list->business_id);
|
|
if (! $destinationId) {
|
|
throw new InvalidArgumentException('انبار مقصد مشخص نشده است.');
|
|
}
|
|
|
|
$purchaseLines = [];
|
|
$lineIds = [];
|
|
|
|
foreach ($lineSelections as $sel) {
|
|
$line = MaterialLine::where('material_list_id', $list->id)->findOrFail($sel['line_id']);
|
|
$analysis = $this->stockService->analyzeLine($list->business_id, $line, $list->destination_location_id);
|
|
$qty = $sel['quantity'] ?? $analysis['shortfall'];
|
|
if ($qty <= 0 || ! $analysis['has_product_link']) {
|
|
continue;
|
|
}
|
|
|
|
$supplierId = $sel['supplier_id'] ?? $line->suggested_supplier_id;
|
|
if ($supplierId) {
|
|
$line->update(['suggested_supplier_id' => $supplierId]);
|
|
}
|
|
|
|
$purchaseLines[] = [
|
|
'product_id' => $line->product_id,
|
|
'variation_id' => $line->variation_id,
|
|
'quantity' => $qty,
|
|
'purchase_price_inc_tax' => 0,
|
|
'item_tax' => 0,
|
|
];
|
|
$lineIds[] = $line->id;
|
|
$line->update(['status' => 'purchase_requested']);
|
|
}
|
|
|
|
if (empty($purchaseLines)) {
|
|
throw new InvalidArgumentException('قطعهای برای درخواست خرید وجود ندارد.');
|
|
}
|
|
|
|
return DB::transaction(function () use ($list, $purchaseLines, $lineIds, $destinationId, $user) {
|
|
$refCount = $this->commonUtil->setAndGetReferenceCount('purchase_requisition');
|
|
$refNo = $this->commonUtil->generateReferenceNumber('purchase_requisition', $refCount);
|
|
|
|
$transaction = Transaction::create([
|
|
'business_id' => $list->business_id,
|
|
'location_id' => $destinationId,
|
|
'type' => 'purchase_requisition',
|
|
'status' => 'ordered',
|
|
'ref_no' => $refNo,
|
|
'transaction_date' => now()->toDateTimeString(),
|
|
'created_by' => $user?->id ?? auth()->id(),
|
|
'additional_notes' => 'Maintenance material list #'.$list->id.' ('.$list->source_type.'#'.$list->source_id.')',
|
|
]);
|
|
|
|
$transaction->purchase_lines()->createMany($purchaseLines);
|
|
|
|
foreach ($lineIds as $lineId) {
|
|
MaterialProcurement::create([
|
|
'material_line_id' => $lineId,
|
|
'transaction_id' => $transaction->id,
|
|
'procurement_type' => 'purchase_requisition',
|
|
'quantity' => 0,
|
|
'destination_location_id' => $destinationId,
|
|
'status' => 'ordered',
|
|
]);
|
|
}
|
|
|
|
return $transaction;
|
|
});
|
|
}
|
|
|
|
/**
|
|
* @param array<int, array{line_id: int, supplier_id: int, quantity?: float}> $lineSelections
|
|
* @return Collection<int, Transaction>
|
|
*/
|
|
public function createPurchaseOrders(
|
|
MaterialList $list,
|
|
array $lineSelections,
|
|
?User $user = null
|
|
): Collection {
|
|
$destinationId = $list->destination_location_id
|
|
?: $this->stockService->resolveDestinationLocationId($list->business_id);
|
|
if (! $destinationId) {
|
|
throw new InvalidArgumentException('انبار مقصد مشخص نشده است.');
|
|
}
|
|
|
|
$bySupplier = [];
|
|
foreach ($lineSelections as $sel) {
|
|
if (empty($sel['supplier_id'])) {
|
|
throw new InvalidArgumentException('تامینکننده برای همه قطعات الزامی است.');
|
|
}
|
|
|
|
$line = MaterialLine::where('material_list_id', $list->id)->findOrFail($sel['line_id']);
|
|
$analysis = $this->stockService->analyzeLine($list->business_id, $line, $list->destination_location_id);
|
|
$qty = $sel['quantity'] ?? $analysis['shortfall'];
|
|
if ($qty <= 0 || ! $analysis['has_product_link']) {
|
|
continue;
|
|
}
|
|
|
|
$supplierId = (int) $sel['supplier_id'];
|
|
$unitPrice = $this->stockService->getUnitPrice((int) $line->variation_id, $destinationId, $supplierId);
|
|
|
|
$bySupplier[$supplierId][] = [
|
|
'line' => $line,
|
|
'quantity' => $qty,
|
|
'unit_price' => $unitPrice,
|
|
];
|
|
}
|
|
|
|
if (empty($bySupplier)) {
|
|
throw new InvalidArgumentException('قطعهای برای سفارش خرید وجود ندارد.');
|
|
}
|
|
|
|
return DB::transaction(function () use ($list, $bySupplier, $destinationId, $user) {
|
|
$orders = collect();
|
|
$userId = $user?->id ?? auth()->id();
|
|
|
|
foreach ($bySupplier as $supplierId => $items) {
|
|
$total = 0;
|
|
$purchaseLines = [];
|
|
$linkedLines = [];
|
|
|
|
foreach ($items as $item) {
|
|
$lineTotal = $item['unit_price'] * $item['quantity'];
|
|
$total += $lineTotal;
|
|
$purchaseLines[] = [
|
|
'product_id' => $item['line']->product_id,
|
|
'variation_id' => $item['line']->variation_id,
|
|
'quantity' => $item['quantity'],
|
|
'pp_without_discount' => $item['unit_price'],
|
|
'purchase_price' => $item['unit_price'],
|
|
'purchase_price_inc_tax' => $item['unit_price'],
|
|
'item_tax' => 0,
|
|
'discount_percent' => 0,
|
|
];
|
|
$linkedLines[] = $item['line'];
|
|
}
|
|
|
|
$refCount = $this->commonUtil->setAndGetReferenceCount('purchase_order');
|
|
$refNo = $this->commonUtil->generateReferenceNumber('purchase_order', $refCount);
|
|
|
|
$transaction = Transaction::create([
|
|
'business_id' => $list->business_id,
|
|
'location_id' => $destinationId,
|
|
'contact_id' => $supplierId,
|
|
'type' => 'purchase_order',
|
|
'status' => 'ordered',
|
|
'ref_no' => $refNo,
|
|
'transaction_date' => now()->toDateTimeString(),
|
|
'created_by' => $userId,
|
|
'total_before_tax' => $total,
|
|
'final_total' => $total,
|
|
'tax_amount' => 0,
|
|
'discount_amount' => 0,
|
|
'shipping_charges' => 0,
|
|
'additional_notes' => 'Maintenance material list #'.$list->id,
|
|
]);
|
|
|
|
$transaction->purchase_lines()->createMany($purchaseLines);
|
|
|
|
foreach ($linkedLines as $line) {
|
|
$line->update(['status' => 'purchase_ordered', 'suggested_supplier_id' => $supplierId]);
|
|
MaterialProcurement::create([
|
|
'material_line_id' => $line->id,
|
|
'transaction_id' => $transaction->id,
|
|
'procurement_type' => 'purchase_order',
|
|
'quantity' => $line->quantity_required,
|
|
'destination_location_id' => $destinationId,
|
|
'supplier_id' => $supplierId,
|
|
'status' => 'ordered',
|
|
]);
|
|
}
|
|
|
|
$orders->push($transaction);
|
|
}
|
|
|
|
return $orders;
|
|
});
|
|
}
|
|
|
|
public function deleteLine(MaterialLine $line): void
|
|
{
|
|
if (in_array($line->status, ['transfer_in_transit', 'purchase_ordered'], true)) {
|
|
throw new InvalidArgumentException('این قطعه در جریان تأمین است و قابل حذف نیست.');
|
|
}
|
|
|
|
$line->procurements()->delete();
|
|
$line->delete();
|
|
}
|
|
|
|
public function resolveEquipmentId(string $sourceType, int $sourceId, int $businessId): ?int
|
|
{
|
|
return match ($sourceType) {
|
|
'overhaul' => Overhaul::where('business_id', $businessId)->find($sourceId)?->equipment_id,
|
|
'work_order' => WorkOrder::where('business_id', $businessId)->find($sourceId)?->equipment_id,
|
|
'rebuild' => null,
|
|
default => null,
|
|
};
|
|
}
|
|
|
|
protected function assertSourceExists(int $businessId, string $sourceType, int $sourceId): void
|
|
{
|
|
if (! in_array($sourceType, self::SOURCE_TYPES, true)) {
|
|
throw new InvalidArgumentException('نوع منبع نامعتبر است.');
|
|
}
|
|
|
|
$exists = match ($sourceType) {
|
|
'overhaul' => Overhaul::where('business_id', $businessId)->where('id', $sourceId)->exists(),
|
|
'rebuild' => RebuildProject::where('business_id', $businessId)->where('id', $sourceId)->exists(),
|
|
'work_order' => WorkOrder::where('business_id', $businessId)->where('id', $sourceId)->exists(),
|
|
default => false,
|
|
};
|
|
|
|
if (! $exists) {
|
|
throw new InvalidArgumentException('رکورد منبع یافت نشد.');
|
|
}
|
|
}
|
|
|
|
public function statusLabels(): array
|
|
{
|
|
return [
|
|
'planned' => __('maintenance::lang.mat_status_planned'),
|
|
'repair_planned' => __('maintenance::lang.mat_status_repair_planned'),
|
|
'transfer_requested' => __('maintenance::lang.mat_status_transfer_requested'),
|
|
'transfer_in_transit' => __('maintenance::lang.mat_status_transfer_in_transit'),
|
|
'purchase_requested' => __('maintenance::lang.mat_status_purchase_requested'),
|
|
'purchase_ordered' => __('maintenance::lang.mat_status_purchase_ordered'),
|
|
'fulfilled' => __('maintenance::lang.mat_status_fulfilled'),
|
|
'cancelled' => __('maintenance::lang.mat_status_cancelled'),
|
|
];
|
|
}
|
|
|
|
/**
|
|
* @return array<string, mixed>
|
|
*/
|
|
public function getPanelData(Business $business, string $sourceType, int $sourceId, ?User $user = null): array
|
|
{
|
|
$list = $this->getOrCreateList($business, $sourceType, $sourceId, $user);
|
|
$list->load([
|
|
'lines.sparePart',
|
|
'lines.suggestedSupplier',
|
|
'lines.procurements.transaction',
|
|
'lines.activeRepairDispatch.holdingEntity',
|
|
'lines.activeRepairDispatch.orgUnit',
|
|
'lines.activeRepairDispatch.serviceProvider',
|
|
'repairDispatches.materialLine',
|
|
'repairDispatches.holdingEntity',
|
|
'repairDispatches.orgUnit',
|
|
'repairDispatches.serviceProvider',
|
|
'destinationLocation',
|
|
]);
|
|
|
|
$supplyList = clone $list;
|
|
$supplyList->setRelation('lines', $list->lines->where('line_type', 'supply')->values());
|
|
|
|
$dispatchService = app(RepairDispatchService::class);
|
|
$util = app(\Modules\Maintenance\Utils\MaintenanceUtil::class);
|
|
|
|
return [
|
|
'material_list' => $list,
|
|
'source_type' => $sourceType,
|
|
'source_id' => $sourceId,
|
|
'analysis' => $this->analyzeList($supplyList)->keyBy('line_id'),
|
|
'purchase_shortfall' => $this->buildPurchaseShortfall($supplyList),
|
|
'repair_lines' => $list->lines->where('line_type', 'repair')->values(),
|
|
'repair_dispatches' => $list->repairDispatches->sortByDesc('id')->values(),
|
|
'equipment_id' => $this->resolveEquipmentId($sourceType, $sourceId, $business->id),
|
|
'locations' => $this->stockService->locationsForBusiness($business->id),
|
|
'status_labels' => $this->statusLabels(),
|
|
'repair_status_labels' => $dispatchService->statusLabels(),
|
|
'route_type_labels' => $dispatchService->routeTypeLabels(),
|
|
'holding_entities' => $util->getHoldingEntitiesDropdown($business->id),
|
|
'external_providers' => $util->getExternalServiceProvidersDropdown($business->id),
|
|
'can_repair_dispatch' => auth()->user()?->can('maintenance.repair_dispatch.manage') ?? false,
|
|
'can_repair_view' => auth()->user()?->can('maintenance.repair_dispatch.view') ?? false,
|
|
];
|
|
}
|
|
}
|