337 lines
12 KiB
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' => 'خط تولید',
|
|
];
|
|
}
|
|
}
|