ultimatepos/Modules/Maintenance/Services/RebuildProcessService.php

337 lines
12 KiB
PHP

<?php
namespace Modules\Maintenance\Services;
use App\Business;
use App\User;
use Carbon\Carbon;
use Illuminate\Support\Facades\DB;
use Modules\Maintenance\Models\Equipment;
use Modules\Maintenance\Models\RebuildPhase;
use Modules\Maintenance\Models\RebuildProject;
class RebuildProcessService
{
public const PHASES = [
['key' => 'assessment', 'title' => 'ارزیابی و بازرسی اولیه'],
['key' => 'planning', 'title' => 'برنامه‌ریزی و تأیید'],
['key' => 'dismantling', 'title' => 'دمونتاژ و تخلیه'],
['key' => 'rebuilding', 'title' => 'بازسازی و مونتاژ'],
['key' => 'testing', 'title' => 'تست و راه‌اندازی آزمایشی'],
['key' => 'commissioning', 'title' => 'تحویل و بهره‌برداری'],
];
public const WORKFLOW_STATUSES = [
'draft',
'assessment',
'planning',
'approved',
'dismantling',
'rebuilding',
'testing',
'commissioning',
'completed',
'on_hold',
'cancelled',
];
public function generateNumber(Business $business): string
{
$prefix = 'RB-'.now()->format('ym');
$last = RebuildProject::where('business_id', $business->id)
->where('rebuild_number', 'like', $prefix.'%')
->orderByDesc('id')
->value('rebuild_number');
$seq = 1;
if ($last && preg_match('/(\d+)$/', $last, $m)) {
$seq = (int) $m[1] + 1;
}
return $prefix.str_pad((string) $seq, 4, '0', STR_PAD_LEFT);
}
public function resolveEquipmentIds(string $scopeType, array $data): array
{
if ($scopeType === 'production_line') {
if (empty($data['production_line_id'])) {
return [];
}
return Equipment::where('business_id', $data['business_id'])
->where('production_line_id', $data['production_line_id'])
->pluck('id')
->map(fn ($id) => (int) $id)
->all();
}
if (! empty($data['equipment_ids']) && is_array($data['equipment_ids'])) {
return array_values(array_unique(array_map('intval', $data['equipment_ids'])));
}
if (! empty($data['equipment_id'])) {
return [(int) $data['equipment_id']];
}
return [];
}
public function createProject(Business $business, array $data, array $equipmentIds, ?User $user = null): RebuildProject
{
return DB::transaction(function () use ($business, $data, $equipmentIds, $user) {
$status = $data['status'] ?? 'draft';
$project = RebuildProject::create(array_merge($data, [
'business_id' => $business->id,
'rebuild_number' => $data['rebuild_number'] ?? $this->generateNumber($business),
'status' => $status,
'progress_percent' => $status === 'draft' ? 0 : 5,
]));
$this->initializePhases($project);
$this->syncEquipmentPivot($project, $equipmentIds);
if ($status !== 'draft') {
$this->startWorkflow($project, $user);
}
return $project->fresh(['phases', 'equipmentItems', 'productionLine', 'supervisor']);
});
}
public function initializePhases(RebuildProject $project): void
{
if ($project->phases()->exists()) {
return;
}
foreach (self::PHASES as $index => $phase) {
RebuildPhase::create([
'rebuild_project_id' => $project->id,
'phase_key' => $phase['key'],
'title' => $phase['title'],
'sort_order' => $index + 1,
'status' => 'pending',
]);
}
}
public function syncEquipmentPivot(RebuildProject $project, array $equipmentIds): void
{
$sync = [];
foreach ($equipmentIds as $equipmentId) {
$equipment = Equipment::find($equipmentId);
$sync[$equipmentId] = [
'pre_rebuild_health' => $equipment?->health_percentage,
'status' => 'pending',
];
}
$project->equipmentItems()->sync($sync);
}
public function startWorkflow(RebuildProject $project, ?User $user = null): RebuildProject
{
return DB::transaction(function () use ($project, $user) {
if ($project->status === 'cancelled' || $project->status === 'completed') {
throw new \InvalidArgumentException('پروژه بازسازی در وضعیت فعلی قابل شروع نیست.');
}
if ($project->equipmentItems()->count() === 0) {
throw new \InvalidArgumentException('حداقل یک تجهیز برای شروع بازسازی لازم است.');
}
$project->update([
'status' => 'assessment',
'actual_start_date' => $project->actual_start_date ?? now()->toDateString(),
'progress_percent' => max($project->progress_percent, 10),
]);
$this->activatePhase($project, 'assessment', $user);
$this->syncEquipmentOperationalStatus($project);
return $project->fresh(['phases', 'equipmentItems']);
});
}
public function advancePhase(RebuildProject $project, ?string $notes = null, ?User $user = null): RebuildProject
{
return DB::transaction(function () use ($project, $notes, $user) {
$currentKey = $this->currentPhaseKey($project);
if (! $currentKey) {
throw new \InvalidArgumentException('فاز فعالی برای پیش‌برد یافت نشد.');
}
$this->completePhase($project, $currentKey, $notes, $user);
$nextKey = $this->nextPhaseKey($currentKey);
if ($nextKey === null) {
return $this->completeProject($project, $user);
}
$statusMap = [
'planning' => ['status' => 'planning', 'progress' => 25],
'dismantling' => ['status' => 'dismantling', 'progress' => 45],
'rebuilding' => ['status' => 'rebuilding', 'progress' => 60],
'testing' => ['status' => 'testing', 'progress' => 80],
'commissioning' => ['status' => 'commissioning', 'progress' => 95],
];
if (isset($statusMap[$nextKey])) {
$project->update([
'status' => $statusMap[$nextKey]['status'],
'progress_percent' => $statusMap[$nextKey]['progress'],
]);
$this->activatePhase($project, $nextKey, $user);
$this->syncEquipmentOperationalStatus($project);
}
return $project->fresh(['phases', 'equipmentItems']);
});
}
public function completeProject(RebuildProject $project, ?User $user = null): RebuildProject
{
$project->update([
'status' => 'completed',
'progress_percent' => 100,
'actual_end_date' => now()->toDateString(),
]);
foreach ($project->equipmentItems as $equipment) {
$postHealth = $equipment->pivot->post_rebuild_health ?? $equipment->health_percentage ?? 100;
$equipment->update([
'status' => 'active',
'health_percentage' => $postHealth,
]);
$project->equipmentItems()->updateExistingPivot($equipment->id, ['status' => 'completed']);
}
return $project->fresh(['phases', 'equipmentItems']);
}
public function holdProject(RebuildProject $project): RebuildProject
{
$project->update(['status' => 'on_hold']);
return $project;
}
public function cancelProject(RebuildProject $project): RebuildProject
{
return DB::transaction(function () use ($project) {
$project->update([
'status' => 'cancelled',
'actual_end_date' => now()->toDateString(),
]);
foreach ($project->equipmentItems as $equipment) {
if (in_array($equipment->status, ['under_rebuild', 'under_maintenance'], true)) {
$equipment->update(['status' => 'active']);
}
}
$project->phases()->where('status', 'in_progress')->update(['status' => 'pending']);
return $project->fresh(['phases', 'equipmentItems']);
});
}
protected function currentPhaseKey(RebuildProject $project): ?string
{
$phase = $project->phases()->where('status', 'in_progress')->orderBy('sort_order')->first();
return $phase?->phase_key;
}
protected function nextPhaseKey(string $currentKey): ?string
{
$keys = array_column(self::PHASES, 'key');
$index = array_search($currentKey, $keys, true);
if ($index === false || $index >= count($keys) - 1) {
return null;
}
return $keys[$index + 1];
}
protected function activatePhase(RebuildProject $project, string $phaseKey, ?User $user = null): void
{
$phase = $project->phases()->where('phase_key', $phaseKey)->first();
if (! $phase) {
return;
}
$phase->update([
'status' => 'in_progress',
'actual_start' => $phase->actual_start ?? now()->toDateString(),
]);
}
protected function completePhase(RebuildProject $project, string $phaseKey, ?string $notes, ?User $user): void
{
$phase = $project->phases()->where('phase_key', $phaseKey)->first();
if (! $phase) {
return;
}
$phase->update([
'status' => 'completed',
'actual_end' => now()->toDateString(),
'notes' => $notes ? trim(($phase->notes ? $phase->notes."\n" : '').$notes) : $phase->notes,
'completed_by' => $user?->id,
]);
if ($phaseKey === 'planning') {
$project->update(['status' => 'approved', 'progress_percent' => 40]);
}
}
protected function syncEquipmentOperationalStatus(RebuildProject $project): void
{
$rebuildStatuses = ['dismantling', 'rebuilding', 'testing', 'commissioning', 'approved'];
$equipmentStatus = in_array($project->status, $rebuildStatuses, true) ? 'under_rebuild' : 'under_maintenance';
foreach ($project->equipmentItems as $equipment) {
if ($project->status === 'completed' || $project->status === 'cancelled') {
continue;
}
$equipment->update(['status' => $equipmentStatus]);
$project->equipmentItems()->updateExistingPivot($equipment->id, [
'status' => in_array($project->status, ['testing', 'commissioning'], true) ? 'in_progress' : 'pending',
]);
}
}
public function phaseLabels(): array
{
return collect(self::PHASES)->mapWithKeys(fn ($p) => [$p['key'] => $p['title']])->all();
}
public function rebuildStatusLabels(): array
{
return [
'draft' => 'پیش‌نویس',
'assessment' => 'ارزیابی',
'planning' => 'برنامه‌ریزی',
'approved' => 'تأیید‌شده',
'dismantling' => 'دمونتاژ',
'rebuilding' => 'بازسازی',
'testing' => 'تست',
'commissioning' => 'تحویل',
'completed' => 'تکمیل‌شده',
'on_hold' => 'معلق',
'cancelled' => 'لغو‌شده',
];
}
public function scopeTypeLabels(): array
{
return [
'equipment' => 'تجهیز',
'production_line' => 'خط تولید',
];
}
}