This commit is contained in:
Codex
2026-07-10 10:38:02 -05:00
parent 63820994f8
commit 42db078e47
51 changed files with 1173 additions and 26 deletions
+33
View File
@@ -0,0 +1,33 @@
<?php
namespace App\Http\Controllers;
use App\Models\Attachment;
use App\Models\Dispute;
use App\Models\JobPost;
use App\Models\User;
use App\Models\WorkOrder;
use Illuminate\Http\Request;
use Illuminate\View\View;
class AdminController extends Controller
{
public function index(Request $request): View
{
abort_unless($request->user()->hasRole('admin'), 403);
return view('admin.index', [
'counts' => [
'users' => User::count(),
'jobs' => JobPost::count(),
'work_orders' => WorkOrder::count(),
'disputes' => Dispute::count(),
'attachments' => Attachment::count(),
],
'users' => User::with('roles')->latest()->limit(10)->get(),
'jobs' => JobPost::with('buyer')->latest()->limit(10)->get(),
'workOrders' => WorkOrder::with('jobPost', 'buyer', 'provider')->latest()->limit(10)->get(),
'disputes' => Dispute::with('workOrder.jobPost', 'openedBy')->latest()->limit(10)->get(),
]);
}
}
@@ -0,0 +1,80 @@
<?php
namespace App\Http\Controllers;
use App\Models\Dispute;
use App\Models\ExternalProfileImport;
use App\Models\JobPost;
use App\Models\ProviderProfile;
use App\Models\SocialPost;
use App\Models\WorkOrder;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Str;
class AttachmentController extends Controller
{
public function store(Request $request): RedirectResponse
{
$data = $request->validate([
'attachable_type' => ['required', 'string', 'in:provider_profile,buyer_profile,social_post,job_post,work_order,dispute,external_profile_import'],
'attachable_id' => ['required', 'integer'],
'kind' => ['nullable', 'string', 'max:255'],
'caption' => ['nullable', 'string', 'max:1000'],
'file' => ['required', 'file', 'max:10240'],
]);
$attachable = $this->resolveAttachable($data['attachable_type'], (int) $data['attachable_id']);
$this->authorizeAttachment($request, $attachable);
$file = $request->file('file');
$path = $file->store('attachments/'.Str::slug($data['attachable_type']), 'public');
$attachable->attachments()->create([
'user_id' => $request->user()->id,
'kind' => $data['kind'] ?? 'general',
'disk' => 'public',
'path' => $path,
'original_name' => $file->getClientOriginalName(),
'mime_type' => $file->getClientMimeType(),
'size' => $file->getSize() ?: 0,
'caption' => $data['caption'] ?? null,
]);
return back()->with('status', 'File uploaded.');
}
private function resolveAttachable(string $type, int $id): mixed
{
return match ($type) {
'provider_profile' => ProviderProfile::findOrFail($id),
'buyer_profile' => \App\Models\BuyerProfile::findOrFail($id),
'social_post' => SocialPost::findOrFail($id),
'job_post' => JobPost::findOrFail($id),
'work_order' => WorkOrder::findOrFail($id),
'dispute' => Dispute::findOrFail($id),
'external_profile_import' => ExternalProfileImport::findOrFail($id),
};
}
private function authorizeAttachment(Request $request, mixed $attachable): void
{
$user = $request->user();
if ($attachable instanceof ProviderProfile) {
abort_unless($attachable->user_id === $user->id || $user->hasRole('admin'), 403);
} elseif ($attachable instanceof \App\Models\BuyerProfile) {
abort_unless($attachable->user_id === $user->id || $user->hasRole('admin'), 403);
} elseif ($attachable instanceof SocialPost) {
abort_unless($attachable->user_id === $user->id || $user->hasRole('admin'), 403);
} elseif ($attachable instanceof JobPost) {
abort_unless($attachable->buyer_id === $user->id || $user->hasRole('admin'), 403);
} elseif ($attachable instanceof WorkOrder) {
abort_unless(in_array($user->id, [$attachable->buyer_id, $attachable->provider_id], true) || $user->hasRole('admin'), 403);
} elseif ($attachable instanceof Dispute) {
abort_unless(in_array($user->id, [$attachable->workOrder->buyer_id, $attachable->workOrder->provider_id], true) || $user->hasRole('admin'), 403);
} elseif ($attachable instanceof ExternalProfileImport) {
abort_unless($attachable->providerProfile->user_id === $user->id || $user->hasRole('admin'), 403);
}
}
}
@@ -12,7 +12,7 @@ class BuyerProfileController extends Controller
public function index(): View
{
return view('buyers.index', [
'profiles' => BuyerProfile::with('user')->latest()->paginate(20),
'profiles' => BuyerProfile::with('user', 'attachments')->latest()->paginate(20),
]);
}
@@ -21,7 +21,7 @@ class BuyerProfileController extends Controller
abort_unless($request->user()->hasRole('buyer'), 403);
return view('buyers.edit', [
'profile' => $request->user()->buyerProfile,
'profile' => $request->user()->buyerProfile?->load('attachments'),
]);
}
@@ -55,7 +55,7 @@ class BuyerProfileController extends Controller
public function show(BuyerProfile $buyer): View
{
return view('buyers.show', [
'profile' => $buyer->load('user'),
'profile' => $buyer->load('user', 'attachments'),
]);
}
}
@@ -0,0 +1,42 @@
<?php
namespace App\Http\Controllers;
use App\Models\Dispute;
use App\Models\JobPost;
use App\Models\SocialPost;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
class CommentController extends Controller
{
public function store(Request $request): RedirectResponse
{
$data = $request->validate([
'commentable_type' => ['required', 'string', 'in:social_post,job_post,dispute'],
'commentable_id' => ['required', 'integer'],
'body' => ['required', 'string', 'max:5000'],
]);
$commentable = match ($data['commentable_type']) {
'social_post' => SocialPost::findOrFail($data['commentable_id']),
'job_post' => JobPost::findOrFail($data['commentable_id']),
'dispute' => Dispute::findOrFail($data['commentable_id']),
};
if ($commentable instanceof Dispute) {
abort_unless(
in_array($request->user()->id, [$commentable->workOrder->buyer_id, $commentable->workOrder->provider_id], true)
|| $request->user()->hasRole('admin'),
403
);
}
$commentable->comments()->create([
'user_id' => $request->user()->id,
'body' => $data['body'],
]);
return back()->with('status', 'Comment saved.');
}
}
+13 -1
View File
@@ -4,6 +4,7 @@ namespace App\Http\Controllers;
use App\Models\Dispute;
use App\Models\WorkOrder;
use App\Notifications\ExchangeEventNotification;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\View\View;
@@ -12,7 +13,7 @@ class DisputeController extends Controller
{
public function show(Request $request, Dispute $dispute): View
{
$dispute->load('workOrder.buyer', 'workOrder.provider', 'openedBy');
$dispute->load('workOrder.buyer', 'workOrder.provider', 'openedBy', 'votes.user', 'comments.user', 'attachments');
abort_unless(
in_array($request->user()->id, [$dispute->workOrder->buyer_id, $dispute->workOrder->provider_id], true)
@@ -40,6 +41,17 @@ class DisputeController extends Controller
$workOrder->update(['status' => 'disputed']);
$recipient = $request->user()->id === $workOrder->buyer_id
? $workOrder->provider
: $workOrder->buyer;
$recipient->notify(new ExchangeEventNotification(
'Dispute opened',
$request->user()->name.' opened a dispute on '.$workOrder->jobPost->title.'.',
route('disputes.show', $dispute),
'dispute_opened'
));
return redirect()->route('disputes.show', $dispute)->with('status', 'Dispute opened for peer review.');
}
}
@@ -0,0 +1,27 @@
<?php
namespace App\Http\Controllers;
use App\Models\Dispute;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
class DisputeVoteController extends Controller
{
public function store(Request $request, Dispute $dispute): RedirectResponse
{
abort_unless($request->user()->hasAnyRole(['provider', 'buyer', 'admin']), 403);
$data = $request->validate([
'recommendation' => ['required', 'string', 'in:provider,buyer,split,insufficient_evidence'],
'reason' => ['nullable', 'string', 'max:2000'],
]);
$dispute->votes()->updateOrCreate(
['user_id' => $request->user()->id],
$data
);
return redirect()->route('disputes.show', $dispute)->with('status', 'Peer vote saved.');
}
}
+9 -2
View File
@@ -12,7 +12,7 @@ class JobPostController extends Controller
public function index(): View
{
return view('jobs.index', [
'jobs' => JobPost::with('buyer.buyerProfile')->latest()->paginate(20),
'jobs' => JobPost::with('buyer.buyerProfile', 'attachments')->latest()->paginate(20),
]);
}
@@ -50,7 +50,14 @@ class JobPostController extends Controller
public function show(JobPost $job): View
{
return view('jobs.show', [
'job' => $job->load('buyer.buyerProfile', 'quotes.provider.providerProfile', 'workOrder'),
'job' => $job->load(
'buyer.buyerProfile',
'quotes.provider.providerProfile',
'quotes.revisions.user',
'workOrder',
'attachments',
'comments.user'
),
]);
}
}
@@ -12,7 +12,7 @@ class ProviderProfileController extends Controller
public function index(): View
{
return view('providers.index', [
'profiles' => ProviderProfile::with('user', 'externalImports')->latest()->paginate(20),
'profiles' => ProviderProfile::with('user', 'externalImports', 'attachments')->latest()->paginate(20),
]);
}
@@ -21,7 +21,7 @@ class ProviderProfileController extends Controller
abort_unless($request->user()->hasRole('provider'), 403);
return view('providers.edit', [
'profile' => $request->user()->providerProfile,
'profile' => $request->user()->providerProfile?->load('attachments', 'externalImports.attachments'),
]);
}
@@ -59,7 +59,7 @@ class ProviderProfileController extends Controller
public function show(ProviderProfile $provider): View
{
return view('providers.show', [
'profile' => $provider->load('user', 'externalImports'),
'profile' => $provider->load('user', 'externalImports.attachments', 'attachments'),
]);
}
}
+79 -1
View File
@@ -5,6 +5,7 @@ namespace App\Http\Controllers;
use App\Models\JobPost;
use App\Models\Quote;
use App\Models\WorkOrder;
use App\Notifications\ExchangeEventNotification;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
@@ -23,14 +24,80 @@ class QuoteController extends Controller
'terms' => ['nullable', 'string'],
]);
$job->quotes()->create($data + [
$quote = $job->quotes()->create($data + [
'provider_id' => $request->user()->id,
'status' => 'submitted',
]);
$quote->revisions()->create($data + [
'user_id' => $request->user()->id,
'action' => 'submitted',
]);
$job->buyer->notify(new ExchangeEventNotification(
'New quote received',
$request->user()->name.' submitted a quote for '.$job->title.'.',
route('jobs.show', $job),
'quote_submitted'
));
return redirect()->route('jobs.show', $job)->with('status', 'Quote submitted.');
}
public function update(Request $request, Quote $quote): RedirectResponse
{
$quote->load('jobPost');
abort_unless($quote->provider_id === $request->user()->id, 403);
abort_unless(in_array($quote->status, ['submitted', 'countered', 'revised'], true), 422);
abort_unless($quote->jobPost->status === 'open', 422);
$data = $request->validate([
'requested_amount' => ['nullable', 'numeric', 'min:0'],
'rate_summary' => ['nullable', 'string', 'max:255'],
'message' => ['nullable', 'string'],
'terms' => ['nullable', 'string'],
]);
$quote->update($data + ['status' => 'revised']);
$quote->revisions()->create($data + [
'user_id' => $request->user()->id,
'action' => 'revised',
]);
$quote->jobPost->buyer->notify(new ExchangeEventNotification(
'Quote revised',
$request->user()->name.' revised a quote for '.$quote->jobPost->title.'.',
route('jobs.show', $quote->jobPost),
'quote_revised'
));
return redirect()->route('jobs.show', $quote->jobPost)->with('status', 'Quote revised.');
}
public function decline(Request $request, Quote $quote): RedirectResponse
{
$quote->load('jobPost');
abort_unless($quote->jobPost->buyer_id === $request->user()->id, 403);
abort_unless($quote->jobPost->status === 'open', 422);
$quote->update(['status' => 'declined']);
$quote->revisions()->create([
'user_id' => $request->user()->id,
'action' => 'declined',
]);
$quote->provider->notify(new ExchangeEventNotification(
'Quote declined',
$quote->jobPost->buyer->name.' declined your quote for '.$quote->jobPost->title.'.',
route('jobs.show', $quote->jobPost),
'quote_declined'
));
return redirect()->route('jobs.show', $quote->jobPost)->with('status', 'Quote declined.');
}
public function accept(Request $request, Quote $quote): RedirectResponse
{
$quote->load('jobPost');
@@ -39,6 +106,10 @@ class QuoteController extends Controller
abort_unless($quote->jobPost->status === 'open', 422);
$quote->update(['status' => 'accepted']);
$quote->revisions()->create([
'user_id' => $request->user()->id,
'action' => 'accepted',
]);
$quote->jobPost->quotes()->whereKeyNot($quote->id)->update(['status' => 'declined']);
$quote->jobPost->update(['status' => 'assigned']);
@@ -57,6 +128,13 @@ class QuoteController extends Controller
]],
]);
$quote->provider->notify(new ExchangeEventNotification(
'Quote accepted',
$quote->jobPost->buyer->name.' accepted your quote for '.$quote->jobPost->title.'.',
route('work-orders.show', $workOrder),
'quote_accepted'
));
return redirect()->route('work-orders.show', $workOrder)->with('status', 'Quote accepted and work order created.');
}
}
+17
View File
@@ -3,6 +3,7 @@
namespace App\Http\Controllers;
use App\Models\WorkOrder;
use App\Notifications\ExchangeEventNotification;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
@@ -18,6 +19,11 @@ class ReviewController extends Controller
$data = $request->validate([
'rating' => ['required', 'integer', 'min:1', 'max:5'],
'communication_rating' => ['nullable', 'integer', 'min:1', 'max:5'],
'scope_accuracy_rating' => ['nullable', 'integer', 'min:1', 'max:5'],
'payment_reliability_rating' => ['nullable', 'integer', 'min:1', 'max:5'],
'workmanship_rating' => ['nullable', 'integer', 'min:1', 'max:5'],
'timeliness_rating' => ['nullable', 'integer', 'min:1', 'max:5'],
'body' => ['nullable', 'string'],
]);
@@ -31,6 +37,17 @@ class ReviewController extends Controller
]
);
$reviewee = $request->user()->id === $workOrder->buyer_id
? $workOrder->provider
: $workOrder->buyer;
$reviewee->notify(new ExchangeEventNotification(
'New review received',
$request->user()->name.' reviewed a completed work order.',
route('work-orders.show', $workOrder),
'review_received'
));
return redirect()->route('work-orders.show', $workOrder)->with('status', 'Review saved.');
}
}
@@ -12,7 +12,7 @@ class SocialPostController extends Controller
public function index(): View
{
return view('feed.index', [
'posts' => SocialPost::with('user')->latest()->paginate(20),
'posts' => SocialPost::with('user', 'attachments', 'comments.user')->latest()->paginate(20),
]);
}
+35 -1
View File
@@ -3,6 +3,7 @@
namespace App\Http\Controllers;
use App\Models\WorkOrder;
use App\Notifications\ExchangeEventNotification;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\View\View;
@@ -34,7 +35,9 @@ class WorkOrderController extends Controller
'acceptedQuote',
'reviews.reviewer',
'reviews.reviewee',
'disputes.openedBy'
'disputes.openedBy',
'messages.user',
'attachments',
),
]);
}
@@ -49,6 +52,9 @@ class WorkOrderController extends Controller
]);
$status = $data['status'];
abort_unless($workOrder->canTransitionTo($status), 422, 'This status transition is not allowed.');
$this->authorizeTransitionRole($request, $workOrder, $status);
$timestamps = [
'en_route' => 'en_route_at',
'on_site' => 'on_site_at',
@@ -77,6 +83,17 @@ class WorkOrderController extends Controller
$workOrder->update($updates);
$recipient = $request->user()->id === $workOrder->buyer_id
? $workOrder->provider
: $workOrder->buyer;
$recipient->notify(new ExchangeEventNotification(
'Work order updated',
$request->user()->name.' changed '.$workOrder->jobPost->title.' to '.str_replace('_', ' ', $status).'.',
route('work-orders.show', $workOrder),
'work_order_status'
));
return redirect()->route('work-orders.show', $workOrder)->with('status', 'Work order updated.');
}
@@ -88,4 +105,21 @@ class WorkOrderController extends Controller
403
);
}
private function authorizeTransitionRole(Request $request, WorkOrder $workOrder, string $status): void
{
$userId = $request->user()->id;
$providerStatuses = ['en_route', 'on_site', 'in_progress', 'completed'];
$buyerStatuses = ['buyer_approved', 'closed', 'cancelled'];
$sharedStatuses = ['disputed'];
if (in_array($status, $providerStatuses, true)) {
abort_unless($workOrder->provider_id === $userId || $request->user()->hasRole('admin'), 403);
} elseif (in_array($status, $buyerStatuses, true)) {
abort_unless($workOrder->buyer_id === $userId || $request->user()->hasRole('admin'), 403);
} elseif (in_array($status, $sharedStatuses, true)) {
abort_unless(in_array($userId, [$workOrder->buyer_id, $workOrder->provider_id], true) || $request->user()->hasRole('admin'), 403);
}
}
}
@@ -0,0 +1,42 @@
<?php
namespace App\Http\Controllers;
use App\Models\WorkOrder;
use App\Notifications\ExchangeEventNotification;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
class WorkOrderMessageController extends Controller
{
public function store(Request $request, WorkOrder $workOrder): RedirectResponse
{
abort_unless(
in_array($request->user()->id, [$workOrder->buyer_id, $workOrder->provider_id], true)
|| $request->user()->hasRole('admin'),
403
);
$data = $request->validate([
'body' => ['required', 'string', 'max:5000'],
]);
$workOrder->messages()->create([
'user_id' => $request->user()->id,
'body' => $data['body'],
]);
$recipient = $request->user()->id === $workOrder->buyer_id
? $workOrder->provider
: $workOrder->buyer;
$recipient->notify(new ExchangeEventNotification(
'New work-order message',
$request->user()->name.' sent a message on '.$workOrder->jobPost->title.'.',
route('work-orders.show', $workOrder),
'work_order_message'
));
return redirect()->route('work-orders.show', $workOrder)->with('status', 'Message sent.');
}
}
+22
View File
@@ -0,0 +1,22 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Attributes\Fillable;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\MorphTo;
use Illuminate\Database\Eloquent\Model;
#[Fillable(['user_id', 'kind', 'disk', 'path', 'original_name', 'mime_type', 'size', 'caption'])]
class Attachment extends Model
{
public function attachable(): MorphTo
{
return $this->morphTo();
}
public function user(): BelongsTo
{
return $this->belongsTo(User::class);
}
}
+6
View File
@@ -4,6 +4,7 @@ namespace App\Models;
use Illuminate\Database\Eloquent\Attributes\Fillable;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\MorphMany;
use Illuminate\Database\Eloquent\Model;
#[Fillable([
@@ -17,4 +18,9 @@ class BuyerProfile extends Model
{
return $this->belongsTo(User::class);
}
public function attachments(): MorphMany
{
return $this->morphMany(Attachment::class, 'attachable');
}
}
+27
View File
@@ -0,0 +1,27 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Attributes\Fillable;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\MorphTo;
use Illuminate\Database\Eloquent\Model;
#[Fillable(['user_id', 'body', 'is_hidden'])]
class Comment extends Model
{
protected function casts(): array
{
return ['is_hidden' => 'boolean'];
}
public function commentable(): MorphTo
{
return $this->morphTo();
}
public function user(): BelongsTo
{
return $this->belongsTo(User::class);
}
}
+17
View File
@@ -4,6 +4,8 @@ namespace App\Models;
use Illuminate\Database\Eloquent\Attributes\Fillable;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Database\Eloquent\Relations\MorphMany;
use Illuminate\Database\Eloquent\Model;
#[Fillable([
@@ -29,4 +31,19 @@ class Dispute extends Model
{
return $this->belongsTo(User::class, 'opened_by_id');
}
public function votes(): HasMany
{
return $this->hasMany(DisputeVote::class);
}
public function comments(): MorphMany
{
return $this->morphMany(Comment::class, 'commentable');
}
public function attachments(): MorphMany
{
return $this->morphMany(Attachment::class, 'attachable');
}
}
+21
View File
@@ -0,0 +1,21 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Attributes\Fillable;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Model;
#[Fillable(['dispute_id', 'user_id', 'recommendation', 'reason'])]
class DisputeVote extends Model
{
public function dispute(): BelongsTo
{
return $this->belongsTo(Dispute::class);
}
public function user(): BelongsTo
{
return $this->belongsTo(User::class);
}
}
+6
View File
@@ -4,6 +4,7 @@ namespace App\Models;
use Illuminate\Database\Eloquent\Attributes\Fillable;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\MorphMany;
use Illuminate\Database\Eloquent\Model;
#[Fillable([
@@ -26,4 +27,9 @@ class ExternalProfileImport extends Model
{
return $this->belongsTo(ProviderProfile::class);
}
public function attachments(): MorphMany
{
return $this->morphMany(Attachment::class, 'attachable');
}
}
+11
View File
@@ -6,6 +6,7 @@ use Illuminate\Database\Eloquent\Attributes\Fillable;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Database\Eloquent\Relations\HasOne;
use Illuminate\Database\Eloquent\Relations\MorphMany;
use Illuminate\Database\Eloquent\Model;
#[Fillable([
@@ -36,4 +37,14 @@ class JobPost extends Model
{
return $this->hasOne(WorkOrder::class);
}
public function comments(): MorphMany
{
return $this->morphMany(Comment::class, 'commentable');
}
public function attachments(): MorphMany
{
return $this->morphMany(Attachment::class, 'attachable');
}
}
+6
View File
@@ -5,6 +5,7 @@ namespace App\Models;
use Illuminate\Database\Eloquent\Attributes\Fillable;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Database\Eloquent\Relations\MorphMany;
use Illuminate\Database\Eloquent\Model;
#[Fillable([
@@ -23,4 +24,9 @@ class ProviderProfile extends Model
{
return $this->hasMany(ExternalProfileImport::class);
}
public function attachments(): MorphMany
{
return $this->morphMany(Attachment::class, 'attachable');
}
}
+6
View File
@@ -4,6 +4,7 @@ namespace App\Models;
use Illuminate\Database\Eloquent\Attributes\Fillable;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Database\Eloquent\Model;
#[Fillable(['job_post_id', 'provider_id', 'status', 'requested_amount', 'rate_summary', 'message', 'terms'])]
@@ -18,4 +19,9 @@ class Quote extends Model
{
return $this->belongsTo(User::class, 'provider_id');
}
public function revisions(): HasMany
{
return $this->hasMany(QuoteRevision::class);
}
}
+21
View File
@@ -0,0 +1,21 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Attributes\Fillable;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Model;
#[Fillable(['quote_id', 'user_id', 'action', 'requested_amount', 'rate_summary', 'message', 'terms'])]
class QuoteRevision extends Model
{
public function quote(): BelongsTo
{
return $this->belongsTo(Quote::class);
}
public function user(): BelongsTo
{
return $this->belongsTo(User::class);
}
}
+5 -1
View File
@@ -6,7 +6,11 @@ use Illuminate\Database\Eloquent\Attributes\Fillable;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Model;
#[Fillable(['work_order_id', 'reviewer_id', 'reviewee_id', 'rating', 'body', 'review_type'])]
#[Fillable([
'work_order_id', 'reviewer_id', 'reviewee_id', 'rating', 'communication_rating',
'scope_accuracy_rating', 'payment_reliability_rating', 'workmanship_rating',
'timeliness_rating', 'body', 'review_type',
])]
class Review extends Model
{
public function workOrder(): BelongsTo
+11
View File
@@ -4,6 +4,7 @@ namespace App\Models;
use Illuminate\Database\Eloquent\Attributes\Fillable;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\MorphMany;
use Illuminate\Database\Eloquent\Model;
#[Fillable(['user_id', 'title', 'body', 'visibility'])]
@@ -13,4 +14,14 @@ class SocialPost extends Model
{
return $this->belongsTo(User::class);
}
public function comments(): MorphMany
{
return $this->morphMany(Comment::class, 'commentable');
}
public function attachments(): MorphMany
{
return $this->morphMany(Attachment::class, 'attachable');
}
}
+28
View File
@@ -5,6 +5,7 @@ namespace App\Models;
use Illuminate\Database\Eloquent\Attributes\Fillable;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Database\Eloquent\Relations\MorphMany;
use Illuminate\Database\Eloquent\Model;
#[Fillable([
@@ -19,6 +20,18 @@ class WorkOrder extends Model
'buyer_approved', 'disputed', 'closed', 'cancelled',
];
public const ALLOWED_TRANSITIONS = [
'assigned' => ['en_route', 'cancelled', 'disputed'],
'en_route' => ['on_site', 'cancelled', 'disputed'],
'on_site' => ['in_progress', 'completed', 'disputed'],
'in_progress' => ['completed', 'disputed'],
'completed' => ['buyer_approved', 'disputed'],
'buyer_approved' => ['closed', 'disputed'],
'disputed' => ['closed'],
'closed' => [],
'cancelled' => [],
];
protected function casts(): array
{
return [
@@ -61,4 +74,19 @@ class WorkOrder extends Model
{
return $this->hasMany(Dispute::class);
}
public function messages(): HasMany
{
return $this->hasMany(WorkOrderMessage::class);
}
public function attachments(): MorphMany
{
return $this->morphMany(Attachment::class, 'attachable');
}
public function canTransitionTo(string $status): bool
{
return in_array($status, self::ALLOWED_TRANSITIONS[$this->status] ?? [], true);
}
}
+26
View File
@@ -0,0 +1,26 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Attributes\Fillable;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Model;
#[Fillable(['work_order_id', 'user_id', 'body', 'is_system'])]
class WorkOrderMessage extends Model
{
protected function casts(): array
{
return ['is_system' => 'boolean'];
}
public function workOrder(): BelongsTo
{
return $this->belongsTo(WorkOrder::class);
}
public function user(): BelongsTo
{
return $this->belongsTo(User::class);
}
}
@@ -0,0 +1,47 @@
<?php
namespace App\Notifications;
use Illuminate\Bus\Queueable;
use Illuminate\Notifications\Notification;
class ExchangeEventNotification extends Notification
{
use Queueable;
/**
* Create a new notification instance.
*/
public function __construct(
public string $title,
public string $body,
public ?string $url = null,
public string $type = 'exchange_event',
) {
}
/**
* Get the notification's delivery channels.
*
* @return array<int, string>
*/
public function via(object $notifiable): array
{
return ['database'];
}
/**
* Get the array representation of the notification.
*
* @return array<string, mixed>
*/
public function toArray(object $notifiable): array
{
return [
'type' => $this->type,
'title' => $this->title,
'body' => $this->body,
'url' => $this->url,
];
}
}