Refactor: Enforce type safety, fix close deadlock, implement assignee restrictions and notify admin fallbacks
This commit is contained in:
@@ -177,6 +177,9 @@ class EditFeedback extends EditRecord
|
||||
|
||||
$newStatus = $data['status'] ?? null;
|
||||
if ($newStatus === 'closed' && $this->getRecord()->status !== 'closed') {
|
||||
// Save pending attachments first so ClosingService validation sees them
|
||||
$this->savePendingAttachments();
|
||||
|
||||
$closingService = App::make(ClosingService::class);
|
||||
$closingService->close($this->getRecord(), auth()->user());
|
||||
}
|
||||
@@ -184,7 +187,7 @@ class EditFeedback extends EditRecord
|
||||
return $data;
|
||||
}
|
||||
|
||||
protected function afterSave(): void
|
||||
protected function savePendingAttachments(): void
|
||||
{
|
||||
if (empty($this->pendingAttachments)) {
|
||||
return;
|
||||
@@ -213,6 +216,13 @@ class EditFeedback extends EditRecord
|
||||
'size' => $this->safeFileSize($disk, $path),
|
||||
]);
|
||||
}
|
||||
|
||||
$this->pendingAttachments = []; // Clear to prevent double saving in afterSave
|
||||
}
|
||||
|
||||
protected function afterSave(): void
|
||||
{
|
||||
$this->savePendingAttachments();
|
||||
}
|
||||
|
||||
protected function safeMimeType($disk, string $path): ?string
|
||||
|
||||
@@ -59,7 +59,25 @@ class FeedbackForm
|
||||
)
|
||||
->visible(fn ($get): bool => ! $get('is_general'))
|
||||
->nullable()
|
||||
->live(),
|
||||
->live()
|
||||
->afterStateUpdated(function ($state, $set): void {
|
||||
if (empty($state)) {
|
||||
$set('contract_id', null);
|
||||
return;
|
||||
}
|
||||
$cp = CustomerProduct::find($state);
|
||||
if (! $cp) {
|
||||
return;
|
||||
}
|
||||
$activeContract = \App\Models\Contract::where('product_id', $cp->product_id)
|
||||
->where('status', 'active')
|
||||
->first();
|
||||
if ($activeContract) {
|
||||
$set('contract_id', $activeContract->id);
|
||||
} else {
|
||||
$set('contract_id', null);
|
||||
}
|
||||
}),
|
||||
|
||||
Select::make('contract_id')
|
||||
->label(__('app.contract'))
|
||||
|
||||
@@ -7,6 +7,9 @@ use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||
|
||||
use App\Enums\ContractType;
|
||||
use App\Enums\ContractStatus;
|
||||
|
||||
#[Fillable(['product_id', 'customer_id', 'type', 'start_date', 'end_date', 'status', 'printed_at', 'signed_status'])]
|
||||
class Contract extends Model
|
||||
{
|
||||
@@ -16,6 +19,8 @@ class Contract extends Model
|
||||
'start_date' => 'date',
|
||||
'end_date' => 'date',
|
||||
'printed_at' => 'date',
|
||||
'type' => ContractType::class,
|
||||
'status' => ContractStatus::class,
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
@@ -15,6 +15,11 @@ class Department extends Model
|
||||
return $this->belongsTo(User::class, 'manager_id');
|
||||
}
|
||||
|
||||
public function members(): HasMany
|
||||
{
|
||||
return $this->hasMany(User::class);
|
||||
}
|
||||
|
||||
public function feedbacks(): HasMany
|
||||
{
|
||||
return $this->hasMany(Feedback::class, 'current_department_id');
|
||||
|
||||
@@ -6,9 +6,18 @@ use Illuminate\Database\Eloquent\Attributes\Fillable;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
|
||||
use App\Enums\HandoverStatus;
|
||||
|
||||
#[Fillable(['product_id', 'customer_id', 'handover_date', 'status', 'remark'])]
|
||||
class Handover extends Model
|
||||
{
|
||||
protected function casts(): array
|
||||
{
|
||||
return [
|
||||
'status' => HandoverStatus::class,
|
||||
'handover_date' => 'date',
|
||||
];
|
||||
}
|
||||
public function product(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(Product::class);
|
||||
|
||||
@@ -6,11 +6,23 @@ use Illuminate\Database\Eloquent\Attributes\Fillable;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
|
||||
use App\Enums\ServiceType;
|
||||
use App\Enums\ContractStatus;
|
||||
|
||||
#[Fillable(['product_id', 'service_type', 'status', 'actual_collection_date', 'remark'])]
|
||||
class ProductService extends Model
|
||||
{
|
||||
protected $table = 'product_services';
|
||||
|
||||
protected function casts(): array
|
||||
{
|
||||
return [
|
||||
'service_type' => ServiceType::class,
|
||||
'status' => ContractStatus::class,
|
||||
'actual_collection_date' => 'date',
|
||||
];
|
||||
}
|
||||
|
||||
public function product(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(Product::class);
|
||||
|
||||
@@ -7,6 +7,7 @@ use Illuminate\Database\Eloquent\Attributes\Fillable;
|
||||
use Illuminate\Database\Eloquent\Attributes\Hidden;
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
use Illuminate\Foundation\Auth\User as Authenticatable;
|
||||
use Illuminate\Notifications\Notifiable;
|
||||
use Spatie\Permission\Traits\HasRoles;
|
||||
@@ -14,7 +15,7 @@ use Illuminate\Contracts\Auth\Access\Authorizable;
|
||||
use Filament\Models\Contracts\FilamentUser;
|
||||
use Filament\Panel;
|
||||
|
||||
#[Fillable(['name', 'email', 'password', 'role'])]
|
||||
#[Fillable(['name', 'email', 'password', 'role', 'department_id'])]
|
||||
#[Hidden(['password', 'remember_token'])]
|
||||
class User extends Authenticatable implements FilamentUser
|
||||
{
|
||||
@@ -29,6 +30,11 @@ class User extends Authenticatable implements FilamentUser
|
||||
];
|
||||
}
|
||||
|
||||
public function department(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(Department::class);
|
||||
}
|
||||
|
||||
public function canAccessPanel(Panel $panel): bool
|
||||
{
|
||||
return true;
|
||||
|
||||
@@ -23,9 +23,12 @@ class ClosingService
|
||||
throw new HttpException(403, __('feedback.close_permission_denied'));
|
||||
}
|
||||
|
||||
if ($actor->hasRole('manager') && ! $actor->hasRole('admin')) {
|
||||
$managedDeptIds = Department::where('manager_id', $actor->id)->pluck('id');
|
||||
if (! $managedDeptIds->contains($feedback->current_department_id)) {
|
||||
if (! $actor->hasRole('admin')) {
|
||||
$managedDeptIds = Department::where('manager_id', $actor->id)->pluck('id')->toArray();
|
||||
$actorDeptId = $actor->department_id;
|
||||
$allowedDeptIds = array_filter(array_merge([$actorDeptId], $managedDeptIds));
|
||||
|
||||
if (! in_array($feedback->current_department_id, $allowedDeptIds)) {
|
||||
throw new HttpException(403, __('feedback.close_department_only'));
|
||||
}
|
||||
}
|
||||
@@ -68,13 +71,26 @@ class ClosingService
|
||||
|
||||
protected function notifyStakeholders(Feedback $feedback, User $actor): void
|
||||
{
|
||||
$notifiedUserIds = [];
|
||||
|
||||
if ($feedback->assignedTo) {
|
||||
$feedback->assignedTo->notify(new \App\Notifications\TicketClosed($feedback, $actor));
|
||||
$notifiedUserIds[] = $feedback->assignedTo->id;
|
||||
}
|
||||
|
||||
$department = $feedback->currentDepartment;
|
||||
if ($department && $department->manager && $department->manager->id !== ($feedback->assignedTo?->id ?? null)) {
|
||||
$department->manager->notify(new \App\Notifications\TicketClosed($feedback, $actor));
|
||||
if ($department && $department->manager) {
|
||||
if (! in_array($department->manager->id, $notifiedUserIds)) {
|
||||
$department->manager->notify(new \App\Notifications\TicketClosed($feedback, $actor));
|
||||
}
|
||||
} else {
|
||||
// No manager! Notify admins
|
||||
$admins = User::whereHas('roles', fn ($q) => $q->where('name', 'admin'))->get();
|
||||
foreach ($admins as $admin) {
|
||||
if (! in_array($admin->id, $notifiedUserIds)) {
|
||||
$admin->notify(new \App\Notifications\TicketClosed($feedback, $actor));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -24,6 +24,20 @@ class TransferService
|
||||
]);
|
||||
}
|
||||
|
||||
if ($newHandlerId !== null) {
|
||||
$handler = User::find($newHandlerId);
|
||||
if (! $handler) {
|
||||
throw ValidationException::withMessages([
|
||||
'new_assignee_id' => __('feedback.user_not_found'),
|
||||
]);
|
||||
}
|
||||
if ($handler->department_id !== $toDepartment->id && $toDepartment->manager_id !== $handler->id) {
|
||||
throw ValidationException::withMessages([
|
||||
'new_assignee_id' => __('feedback.handler_not_in_department'),
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
$fromDepartmentId = $feedback->current_department_id;
|
||||
$fromDepartmentName = $feedback->currentDepartment?->name ?? 'N/A';
|
||||
|
||||
@@ -67,6 +81,12 @@ class TransferService
|
||||
|
||||
if ($manager) {
|
||||
$manager->notify(new \App\Notifications\TicketTransferred($feedback, $log, $sender));
|
||||
} else {
|
||||
// No manager! Notify admins
|
||||
$admins = User::whereHas('roles', fn ($q) => $q->where('name', 'admin'))->get();
|
||||
foreach ($admins as $admin) {
|
||||
$admin->notify(new \App\Notifications\TicketTransferred($feedback, $log, $sender));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user