Files
KanbanGit/app/Actions/Tickets/UpdateProjectTicket.php
T

320 lines
9.8 KiB
PHP

<?php
namespace App\Actions\Tickets;
use App\Models\Ticket;
use App\Models\TicketFieldDefinition;
use App\Models\TicketFieldValue;
use App\Models\User;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\DB;
class UpdateProjectTicket
{
public function __construct(
private readonly CreateTicketWorklogEntry $createTicketWorklogEntry,
) {}
/**
* @param array{
* acceptance_criteria: string|null,
* description: string|null,
* ticket_priority_id: int,
* ticket_type_id: int,
* title: string
* } $ticketAttributes
* @param array<int, array{
* ticket_field_definition_id: int,
* value_number?: float|int|string|null,
* value_user_id?: int|string|null
* }> $fieldValueAttributes
*/
public function handle(
Ticket $ticket,
User $actor,
array $ticketAttributes,
array $fieldValueAttributes,
): bool {
return DB::transaction(function () use ($actor, $fieldValueAttributes, $ticket, $ticketAttributes): bool {
$ticket->loadMissing([
'fieldValues.ticketFieldDefinition',
'fieldValues.userValue',
'parentTicket',
'project',
'ticketPriority',
'ticketType',
]);
/** @var Collection<int, TicketFieldDefinition> $fieldDefinitions */
$fieldDefinitions = $ticket->project->ticketFieldDefinitions()
->get()
->keyBy('id');
$ticketTypeOptions = $ticket->project->ticketTypes()
->get()
->keyBy('id');
$ticketPriorityOptions = $ticket->project->ticketPriorities()
->get()
->keyBy('id');
/** @var Collection<int, TicketFieldValue> $existingFieldValues */
$existingFieldValues = $ticket->fieldValues
->keyBy('ticket_field_definition_id');
$changes = [];
$summaryLabels = [];
$this->trackScalarChange(
$changes,
$summaryLabels,
'title',
'title',
$ticket->title,
$ticketAttributes['title'],
);
$this->trackScalarChange(
$changes,
$summaryLabels,
'description',
'description',
$ticket->description,
$ticketAttributes['description'],
);
$this->trackScalarChange(
$changes,
$summaryLabels,
'acceptance_criteria',
'acceptance criteria',
$ticket->acceptance_criteria,
$ticketAttributes['acceptance_criteria'],
);
if ($ticket->ticket_type_id !== $ticketAttributes['ticket_type_id']) {
$changes['ticket_type'] = [
'from' => $ticket->ticketType?->name,
'to' => $ticketTypeOptions->get($ticketAttributes['ticket_type_id'])?->name,
];
$summaryLabels[] = 'type';
}
if ($ticket->ticket_priority_id !== $ticketAttributes['ticket_priority_id']) {
$changes['ticket_priority'] = [
'from' => $ticket->ticketPriority?->name,
'to' => $ticketPriorityOptions->get($ticketAttributes['ticket_priority_id'])?->name,
];
$summaryLabels[] = 'priority';
}
if ($ticket->parent_ticket_id !== $ticketAttributes['parent_ticket_id']) {
$newParentTicket = $ticketAttributes['parent_ticket_id'] === null
? null
: Ticket::query()->find($ticketAttributes['parent_ticket_id']);
$changes['parent_ticket'] = [
'from' => $ticket->parentTicket?->ticket_key,
'to' => $newParentTicket?->ticket_key,
];
$summaryLabels[] = 'parent ticket';
}
$ticket->fill($ticketAttributes);
if ($ticket->isDirty()) {
$ticket->save();
}
foreach ($fieldValueAttributes as $fieldValueAttribute) {
$fieldDefinition = $fieldDefinitions->get((int) $fieldValueAttribute['ticket_field_definition_id']);
if ($fieldDefinition === null) {
continue;
}
$existingFieldValue = $existingFieldValues->get($fieldDefinition->id);
if ($fieldDefinition->field_type === 'user') {
$this->syncUserFieldValue(
$changes,
$existingFieldValue,
$fieldDefinition,
$fieldValueAttribute,
$summaryLabels,
$ticket,
);
continue;
}
$this->syncNumberFieldValue(
$changes,
$existingFieldValue,
$fieldDefinition,
$fieldValueAttribute,
$summaryLabels,
$ticket,
);
}
if ($summaryLabels === []) {
return false;
}
$this->createTicketWorklogEntry->handle(
$ticket->fresh(),
$actor,
'updated',
$this->buildSummary($summaryLabels),
$changes,
);
return true;
});
}
/**
* @param array<string, mixed> $changes
* @param array<int, string> $summaryLabels
*/
private function trackScalarChange(
array &$changes,
array &$summaryLabels,
string $changeKey,
string $summaryLabel,
?string $currentValue,
?string $newValue,
): void {
if ($currentValue === $newValue) {
return;
}
$changes[$changeKey] = [
'from' => $currentValue,
'to' => $newValue,
];
$summaryLabels[] = $summaryLabel;
}
/**
* @param array<string, mixed> $changes
* @param array{value_user_id?: int|string|null} $fieldValueAttribute
* @param array<int, string> $summaryLabels
*/
private function syncUserFieldValue(
array &$changes,
?TicketFieldValue $existingFieldValue,
TicketFieldDefinition $fieldDefinition,
array $fieldValueAttribute,
array &$summaryLabels,
Ticket $ticket,
): void {
$newUserId = $fieldValueAttribute['value_user_id'] ?? null;
$newUserId = $newUserId === null || $newUserId === '' ? null : (int) $newUserId;
if ($existingFieldValue?->value_user_id === $newUserId) {
return;
}
$oldUserName = $existingFieldValue?->userValue?->name;
$newUserName = $newUserId === null ? null : User::query()->find($newUserId)?->name;
$changes['field_values'][$fieldDefinition->id] = [
'field' => $fieldDefinition->label,
'from' => $oldUserName,
'to' => $newUserName,
];
$summaryLabels[] = "{$fieldDefinition->label} field";
if ($newUserId === null) {
$existingFieldValue?->delete();
return;
}
$ticket->fieldValues()->updateOrCreate(
[
'ticket_field_definition_id' => $fieldDefinition->id,
],
[
'value_number' => null,
'value_user_id' => $newUserId,
],
);
}
/**
* @param array<string, mixed> $changes
* @param array{value_number?: float|int|string|null} $fieldValueAttribute
* @param array<int, string> $summaryLabels
*/
private function syncNumberFieldValue(
array &$changes,
?TicketFieldValue $existingFieldValue,
TicketFieldDefinition $fieldDefinition,
array $fieldValueAttribute,
array &$summaryLabels,
Ticket $ticket,
): void {
$newNumber = $fieldValueAttribute['value_number'] ?? null;
$newNumber = $newNumber === null || $newNumber === '' ? null : (float) $newNumber;
$oldNumber = $existingFieldValue?->value_number;
if ($oldNumber === $newNumber) {
return;
}
$changes['field_values'][$fieldDefinition->id] = [
'field' => $fieldDefinition->label,
'from' => $this->formatNumber($oldNumber),
'to' => $this->formatNumber($newNumber),
];
$summaryLabels[] = "{$fieldDefinition->label} field";
if ($newNumber === null) {
$existingFieldValue?->delete();
return;
}
$ticket->fieldValues()->updateOrCreate(
[
'ticket_field_definition_id' => $fieldDefinition->id,
],
[
'value_number' => $newNumber,
'value_user_id' => null,
],
);
}
/**
* @param array<int, string> $summaryLabels
*/
private function buildSummary(array $summaryLabels): string
{
$uniqueLabels = collect($summaryLabels)
->unique()
->values();
if ($uniqueLabels->count() === 1) {
return 'Updated '.$uniqueLabels->first().'.';
}
if ($uniqueLabels->count() === 2) {
return 'Updated '.$uniqueLabels->first().' and '.$uniqueLabels->last().'.';
}
$lastLabel = $uniqueLabels->pop();
return 'Updated '.$uniqueLabels->implode(', ').', and '.$lastLabel.'.';
}
private function formatNumber(?float $value): ?string
{
if ($value === null) {
return null;
}
return rtrim(rtrim(number_format($value, 2, '.', ''), '0'), '.');
}
}