{{ $post->user->name }} | {{ $post->created_at->diffForHumans() }}
+ @if ($post->title){{ $post->title }}
@endif +{{ $post->body }}
+diff --git a/README.md b/README.md index 5ad1377..8d251df 100644 --- a/README.md +++ b/README.md @@ -1,58 +1,80 @@ -
+# Provider Exchange - +Provider Exchange is an early Laravel prototype for a provider-centered field service network and work-order exchange. -## About Laravel +The product goal is not to clone Field Nation. The first version focuses on: -Laravel is a web application framework with expressive, elegant syntax. We believe development must be an enjoyable and creative experience to be truly fulfilling. Laravel takes the pain out of development by easing common tasks used in many web projects, such as: +- provider and buyer profiles; +- community posts; +- buyer job posts; +- provider quotes; +- work-order assignment and status tracking; +- mutual reviews; +- peer-review dispute records; +- external profile/rating snapshots from Field Nation, WorkMarket, and similar platforms. -- [Simple, fast routing engine](https://laravel.com/docs/routing). -- [Powerful dependency injection container](https://laravel.com/docs/container). -- Multiple back-ends for [session](https://laravel.com/docs/session) and [cache](https://laravel.com/docs/cache) storage. -- Expressive, intuitive [database ORM](https://laravel.com/docs/eloquent). -- Database agnostic [schema migrations](https://laravel.com/docs/migrations). -- [Robust background job processing](https://laravel.com/docs/queues). -- [Real-time event broadcasting](https://laravel.com/docs/broadcasting). +## Guardrails -Laravel is accessible, powerful, and provides tools required for large, robust applications. +- The platform does not hold money in v1. +- The platform does not set or recommend market rates. +- Providers control their own rate cards and terms. +- Buyers control their own posted offer terms. +- Payment remains direct between buyer and provider unless a compliant payment partner is added later. +- Reputation is mutual: providers review buyers and buyers review providers. +- Dispute records are reputational/peer-review records, not legal adjudication. -## Learning Laravel - -Laravel has the most extensive and thorough [documentation](https://laravel.com/docs) and video tutorial library of all modern web application frameworks, making it a breeze to get started with the framework. - -In addition, [Laracasts](https://laracasts.com) contains thousands of video tutorials on a range of topics including Laravel, modern PHP, unit testing, and JavaScript. Boost your skills by digging into our comprehensive video library. - -You can also watch bite-sized lessons with real-world projects on [Laravel Learn](https://laravel.com/learn), where you will be guided through building a Laravel application from scratch while learning PHP fundamentals. - -## Agentic Development - -Laravel's predictable structure and conventions make it ideal for AI coding agents like Claude Code, Cursor, and GitHub Copilot. Install [Laravel Boost](https://laravel.com/docs/ai) to supercharge your AI workflow: +## Local Setup ```bash -composer require laravel/boost --dev - -php artisan boost:install +composer install +npm install +cp .env.example .env +php artisan key:generate +touch database/database.sqlite +php artisan migrate:fresh --seed +npm run build +php artisan serve --host=0.0.0.0 --port=8123 ``` -Boost provides your agent 15+ tools and skills that help agents build Laravel applications while following best practices. +## Demo Accounts -## Contributing +All seeded demo accounts use password `password`. -Thank you for considering contributing to the Laravel framework! The contribution guide can be found in the [Laravel documentation](https://laravel.com/docs/contributions). +- `admin@example.com` +- `provider@example.com` +- `buyer@example.com` +- `hybrid@example.com` -## Code of Conduct +## Test Commands -In order to ensure that the Laravel community is welcoming to all, please review and abide by the [Code of Conduct](https://laravel.com/docs/contributions#code-of-conduct). +```bash +php artisan test +npm run build +``` -## Security Vulnerabilities +## Current Roadmap Status -If you discover a security vulnerability within Laravel, please send an e-mail to Taylor Otwell via [taylor@laravel.com](mailto:taylor@laravel.com). All security vulnerabilities will be promptly addressed. +1. Foundation: Laravel app, auth, roles, seeded demo users. +2. Profiles: provider and buyer profiles. +3. Social layer: community feed posts. +4. Job posts: buyer job-post creation and listing. +5. Quote flow: provider quotes and buyer acceptance. +6. Work orders: assignment, status transitions, deliverables, completion notes. +7. Reviews: mutual buyer/provider reviews. +8. Disputes: peer-review dispute records. +9. API prep: Sanctum installed with starter API routes. +10. Hardening: tests, seed data, setup docs, and no-payment/no-rate-setting guardrails. -## License +## External Profile Imports -The Laravel framework is open-sourced software licensed under the [MIT license](https://opensource.org/licenses/MIT). +Provider profiles support manual external snapshots. For Field Nation, the first version records: + +- platform name; +- external provider ID; +- profile URL if available; +- rating; +- review count; +- completed jobs; +- notes or copied review summaries. + +Public search did not expose a Field Nation profile for provider ID `172-630`, so any automated Field Nation import should be treated as a later authenticated/export-assisted workflow rather than a public scrape assumption. diff --git a/app/Http/Controllers/BuyerProfileController.php b/app/Http/Controllers/BuyerProfileController.php new file mode 100644 index 0000000..9b6245e --- /dev/null +++ b/app/Http/Controllers/BuyerProfileController.php @@ -0,0 +1,61 @@ + BuyerProfile::with('user')->latest()->paginate(20), + ]); + } + + public function edit(Request $request): View + { + abort_unless($request->user()->hasRole('buyer'), 403); + + return view('buyers.edit', [ + 'profile' => $request->user()->buyerProfile, + ]); + } + + public function update(Request $request): RedirectResponse + { + abort_unless($request->user()->hasRole('buyer'), 403); + + $data = $request->validate([ + 'company_name' => ['required', 'string', 'max:255'], + 'headline' => ['nullable', 'string', 'max:255'], + 'description' => ['nullable', 'string'], + 'service_categories' => ['nullable', 'string'], + 'hiring_regions' => ['nullable', 'string'], + 'vendor_onboarding' => ['nullable', 'string'], + 'payment_terms' => ['nullable', 'string'], + 'website_url' => ['nullable', 'url', 'max:255'], + 'contact_email' => ['nullable', 'email', 'max:255'], + 'public_contact' => ['nullable', 'boolean'], + ]); + + $data['public_contact'] = $request->boolean('public_contact'); + + $request->user()->buyerProfile()->updateOrCreate( + ['user_id' => $request->user()->id], + $data + ); + + return redirect()->route('buyers.edit')->with('status', 'Buyer profile saved.'); + } + + public function show(BuyerProfile $buyer): View + { + return view('buyers.show', [ + 'profile' => $buyer->load('user'), + ]); + } +} diff --git a/app/Http/Controllers/DisputeController.php b/app/Http/Controllers/DisputeController.php new file mode 100644 index 0000000..292fe73 --- /dev/null +++ b/app/Http/Controllers/DisputeController.php @@ -0,0 +1,45 @@ +load('workOrder.buyer', 'workOrder.provider', 'openedBy'); + + abort_unless( + in_array($request->user()->id, [$dispute->workOrder->buyer_id, $dispute->workOrder->provider_id], true) + || $request->user()->hasRole('admin'), + 403 + ); + + return view('disputes.show', ['dispute' => $dispute]); + } + + public function store(Request $request, WorkOrder $workOrder): RedirectResponse + { + abort_unless(in_array($request->user()->id, [$workOrder->buyer_id, $workOrder->provider_id], true), 403); + + $data = $request->validate([ + 'summary' => ['required', 'string', 'max:255'], + 'claim' => ['required', 'string'], + 'evidence_notes' => ['nullable', 'string'], + ]); + + $dispute = $workOrder->disputes()->create($data + [ + 'opened_by_id' => $request->user()->id, + 'status' => 'open', + ]); + + $workOrder->update(['status' => 'disputed']); + + return redirect()->route('disputes.show', $dispute)->with('status', 'Dispute opened for peer review.'); + } +} diff --git a/app/Http/Controllers/ExternalProfileImportController.php b/app/Http/Controllers/ExternalProfileImportController.php new file mode 100644 index 0000000..5d31a71 --- /dev/null +++ b/app/Http/Controllers/ExternalProfileImportController.php @@ -0,0 +1,33 @@ +user()->providerProfile; + + abort_unless($request->user()->hasRole('provider') && $profile, 403); + + $data = $request->validate([ + 'platform' => ['required', 'string', 'max:255'], + 'external_id' => ['nullable', 'string', 'max:255'], + 'profile_url' => ['nullable', 'url', 'max:255'], + 'rating' => ['nullable', 'numeric', 'min:0', 'max:5'], + 'review_count' => ['nullable', 'integer', 'min:0'], + 'completed_jobs' => ['nullable', 'integer', 'min:0'], + 'notes' => ['nullable', 'string'], + ]); + + $profile->externalImports()->create($data + [ + 'status' => 'manual', + 'imported_at' => now(), + ]); + + return redirect()->route('providers.edit')->with('status', 'External profile snapshot saved.'); + } +} diff --git a/app/Http/Controllers/JobPostController.php b/app/Http/Controllers/JobPostController.php new file mode 100644 index 0000000..f617fa5 --- /dev/null +++ b/app/Http/Controllers/JobPostController.php @@ -0,0 +1,56 @@ + JobPost::with('buyer.buyerProfile')->latest()->paginate(20), + ]); + } + + public function create(Request $request): View + { + abort_unless($request->user()->hasRole('buyer'), 403); + + return view('jobs.create'); + } + + public function store(Request $request): RedirectResponse + { + abort_unless($request->user()->hasRole('buyer'), 403); + + $data = $request->validate([ + 'title' => ['required', 'string', 'max:255'], + 'service_category' => ['nullable', 'string', 'max:255'], + 'location' => ['required', 'string', 'max:255'], + 'starts_at' => ['nullable', 'date'], + 'time_window' => ['nullable', 'string', 'max:255'], + 'scope' => ['required', 'string'], + 'required_skills' => ['nullable', 'string'], + 'required_tools' => ['nullable', 'string'], + 'deliverables' => ['nullable', 'string'], + 'payment_terms' => ['nullable', 'string'], + 'vendor_onboarding' => ['nullable', 'string'], + 'visibility' => ['required', 'string', 'in:public,members'], + ]); + + $job = $request->user()->jobPosts()->create($data); + + return redirect()->route('jobs.show', $job)->with('status', 'Job posted.'); + } + + public function show(JobPost $job): View + { + return view('jobs.show', [ + 'job' => $job->load('buyer.buyerProfile', 'quotes.provider.providerProfile', 'workOrder'), + ]); + } +} diff --git a/app/Http/Controllers/ProviderProfileController.php b/app/Http/Controllers/ProviderProfileController.php new file mode 100644 index 0000000..54c1c39 --- /dev/null +++ b/app/Http/Controllers/ProviderProfileController.php @@ -0,0 +1,65 @@ + ProviderProfile::with('user', 'externalImports')->latest()->paginate(20), + ]); + } + + public function edit(Request $request): View + { + abort_unless($request->user()->hasRole('provider'), 403); + + return view('providers.edit', [ + 'profile' => $request->user()->providerProfile, + ]); + } + + public function update(Request $request): RedirectResponse + { + abort_unless($request->user()->hasRole('provider'), 403); + + $data = $request->validate([ + 'business_name' => ['required', 'string', 'max:255'], + 'headline' => ['nullable', 'string', 'max:255'], + 'bio' => ['nullable', 'string'], + 'service_area' => ['nullable', 'string', 'max:255'], + 'skills' => ['nullable', 'string'], + 'tools' => ['nullable', 'string'], + 'certifications' => ['nullable', 'string'], + 'insurance_status' => ['nullable', 'string', 'max:255'], + 'rate_card' => ['nullable', 'string'], + 'travel_policy' => ['nullable', 'string'], + 'availability_notes' => ['nullable', 'string'], + 'website_url' => ['nullable', 'url', 'max:255'], + 'phone' => ['nullable', 'string', 'max:255'], + 'public_contact' => ['nullable', 'boolean'], + ]); + + $data['public_contact'] = $request->boolean('public_contact'); + + $request->user()->providerProfile()->updateOrCreate( + ['user_id' => $request->user()->id], + $data + ); + + return redirect()->route('providers.edit')->with('status', 'Provider profile saved.'); + } + + public function show(ProviderProfile $provider): View + { + return view('providers.show', [ + 'profile' => $provider->load('user', 'externalImports'), + ]); + } +} diff --git a/app/Http/Controllers/QuoteController.php b/app/Http/Controllers/QuoteController.php new file mode 100644 index 0000000..03dbc86 --- /dev/null +++ b/app/Http/Controllers/QuoteController.php @@ -0,0 +1,62 @@ +user()->hasRole('provider'), 403); + abort_if($job->buyer_id === $request->user()->id, 403); + abort_unless($job->status === 'open', 422); + + $data = $request->validate([ + 'requested_amount' => ['nullable', 'numeric', 'min:0'], + 'rate_summary' => ['nullable', 'string', 'max:255'], + 'message' => ['nullable', 'string'], + 'terms' => ['nullable', 'string'], + ]); + + $job->quotes()->create($data + [ + 'provider_id' => $request->user()->id, + 'status' => 'submitted', + ]); + + return redirect()->route('jobs.show', $job)->with('status', 'Quote submitted.'); + } + + public function accept(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' => 'accepted']); + $quote->jobPost->quotes()->whereKeyNot($quote->id)->update(['status' => 'declined']); + $quote->jobPost->update(['status' => 'assigned']); + + $workOrder = WorkOrder::create([ + 'job_post_id' => $quote->job_post_id, + 'buyer_id' => $quote->jobPost->buyer_id, + 'provider_id' => $quote->provider_id, + 'accepted_quote_id' => $quote->id, + 'status' => 'assigned', + 'agreed_terms' => trim(($quote->rate_summary ?? '')."\n\n".($quote->terms ?? '')), + 'deliverables_checklist' => $quote->jobPost->deliverables, + 'status_history' => [[ + 'status' => 'assigned', + 'user_id' => $request->user()->id, + 'at' => now()->toIso8601String(), + ]], + ]); + + return redirect()->route('work-orders.show', $workOrder)->with('status', 'Quote accepted and work order created.'); + } +} diff --git a/app/Http/Controllers/ReviewController.php b/app/Http/Controllers/ReviewController.php new file mode 100644 index 0000000..1df0f5f --- /dev/null +++ b/app/Http/Controllers/ReviewController.php @@ -0,0 +1,36 @@ +user()->id, [$workOrder->buyer_id, $workOrder->provider_id], true), 403); + + $revieweeId = $request->user()->id === $workOrder->buyer_id + ? $workOrder->provider_id + : $workOrder->buyer_id; + + $data = $request->validate([ + 'rating' => ['required', 'integer', 'min:1', 'max:5'], + 'body' => ['nullable', 'string'], + ]); + + $workOrder->reviews()->updateOrCreate( + [ + 'reviewer_id' => $request->user()->id, + 'reviewee_id' => $revieweeId, + ], + $data + [ + 'review_type' => $request->user()->id === $workOrder->buyer_id ? 'buyer_to_provider' : 'provider_to_buyer', + ] + ); + + return redirect()->route('work-orders.show', $workOrder)->with('status', 'Review saved.'); + } +} diff --git a/app/Http/Controllers/SocialPostController.php b/app/Http/Controllers/SocialPostController.php new file mode 100644 index 0000000..b3b0ef8 --- /dev/null +++ b/app/Http/Controllers/SocialPostController.php @@ -0,0 +1,31 @@ + SocialPost::with('user')->latest()->paginate(20), + ]); + } + + public function store(Request $request): RedirectResponse + { + $data = $request->validate([ + 'title' => ['nullable', 'string', 'max:255'], + 'body' => ['required', 'string'], + 'visibility' => ['required', 'string', 'in:public,members'], + ]); + + $request->user()->socialPosts()->create($data); + + return redirect()->route('feed.index')->with('status', 'Post published.'); + } +} diff --git a/app/Http/Controllers/WorkOrderController.php b/app/Http/Controllers/WorkOrderController.php new file mode 100644 index 0000000..3857843 --- /dev/null +++ b/app/Http/Controllers/WorkOrderController.php @@ -0,0 +1,91 @@ +where(fn ($query) => $query + ->where('buyer_id', $request->user()->id) + ->orWhere('provider_id', $request->user()->id) + ) + ->latest() + ->paginate(20); + + return view('work-orders.index', ['orders' => $orders]); + } + + public function show(Request $request, WorkOrder $workOrder): View + { + $this->authorizeParticipant($request, $workOrder); + + return view('work-orders.show', [ + 'workOrder' => $workOrder->load( + 'jobPost', + 'buyer.buyerProfile', + 'provider.providerProfile', + 'acceptedQuote', + 'reviews.reviewer', + 'reviews.reviewee', + 'disputes.openedBy' + ), + ]); + } + + public function transition(Request $request, WorkOrder $workOrder): RedirectResponse + { + $this->authorizeParticipant($request, $workOrder); + + $data = $request->validate([ + 'status' => ['required', 'string', 'in:'.implode(',', WorkOrder::STATUSES)], + 'completion_notes' => ['nullable', 'string'], + ]); + + $status = $data['status']; + $timestamps = [ + 'en_route' => 'en_route_at', + 'on_site' => 'on_site_at', + 'in_progress' => 'started_at', + 'completed' => 'completed_at', + 'buyer_approved' => 'approved_at', + 'closed' => 'closed_at', + ]; + + $history = $workOrder->status_history ?? []; + $history[] = [ + 'status' => $status, + 'user_id' => $request->user()->id, + 'at' => now()->toIso8601String(), + ]; + + $updates = [ + 'status' => $status, + 'status_history' => $history, + 'completion_notes' => $data['completion_notes'] ?? $workOrder->completion_notes, + ]; + + if (isset($timestamps[$status]) && ! $workOrder->{$timestamps[$status]}) { + $updates[$timestamps[$status]] = now(); + } + + $workOrder->update($updates); + + return redirect()->route('work-orders.show', $workOrder)->with('status', 'Work order updated.'); + } + + private function authorizeParticipant(Request $request, WorkOrder $workOrder): void + { + abort_unless( + in_array($request->user()->id, [$workOrder->buyer_id, $workOrder->provider_id], true) + || $request->user()->hasRole('admin'), + 403 + ); + } +} diff --git a/app/Models/BuyerProfile.php b/app/Models/BuyerProfile.php new file mode 100644 index 0000000..a778c3d --- /dev/null +++ b/app/Models/BuyerProfile.php @@ -0,0 +1,20 @@ +belongsTo(User::class); + } +} diff --git a/app/Models/Dispute.php b/app/Models/Dispute.php new file mode 100644 index 0000000..d3596fd --- /dev/null +++ b/app/Models/Dispute.php @@ -0,0 +1,32 @@ + 'array', + 'resolved_at' => 'datetime', + ]; + } + + public function workOrder(): BelongsTo + { + return $this->belongsTo(WorkOrder::class); + } + + public function openedBy(): BelongsTo + { + return $this->belongsTo(User::class, 'opened_by_id'); + } +} diff --git a/app/Models/ExternalProfileImport.php b/app/Models/ExternalProfileImport.php new file mode 100644 index 0000000..44c2a31 --- /dev/null +++ b/app/Models/ExternalProfileImport.php @@ -0,0 +1,29 @@ + 'array', + 'review_snapshots' => 'array', + 'imported_at' => 'datetime', + ]; + } + + public function providerProfile(): BelongsTo + { + return $this->belongsTo(ProviderProfile::class); + } +} diff --git a/app/Models/JobPost.php b/app/Models/JobPost.php new file mode 100644 index 0000000..3ad5b78 --- /dev/null +++ b/app/Models/JobPost.php @@ -0,0 +1,39 @@ + 'datetime', + ]; + } + + public function buyer(): BelongsTo + { + return $this->belongsTo(User::class, 'buyer_id'); + } + + public function quotes(): HasMany + { + return $this->hasMany(Quote::class); + } + + public function workOrder(): HasOne + { + return $this->hasOne(WorkOrder::class); + } +} diff --git a/app/Models/NotificationPreference.php b/app/Models/NotificationPreference.php new file mode 100644 index 0000000..acb521a --- /dev/null +++ b/app/Models/NotificationPreference.php @@ -0,0 +1,32 @@ + 'boolean', + 'quote_updates' => 'boolean', + 'work_order_updates' => 'boolean', + 'review_updates' => 'boolean', + 'dispute_updates' => 'boolean', + 'email_enabled' => 'boolean', + 'push_enabled' => 'boolean', + ]; + } + + public function user(): BelongsTo + { + return $this->belongsTo(User::class); + } +} diff --git a/app/Models/ProviderProfile.php b/app/Models/ProviderProfile.php new file mode 100644 index 0000000..a31da7c --- /dev/null +++ b/app/Models/ProviderProfile.php @@ -0,0 +1,26 @@ +belongsTo(User::class); + } + + public function externalImports(): HasMany + { + return $this->hasMany(ExternalProfileImport::class); + } +} diff --git a/app/Models/Quote.php b/app/Models/Quote.php new file mode 100644 index 0000000..d6c57e0 --- /dev/null +++ b/app/Models/Quote.php @@ -0,0 +1,21 @@ +belongsTo(JobPost::class); + } + + public function provider(): BelongsTo + { + return $this->belongsTo(User::class, 'provider_id'); + } +} diff --git a/app/Models/Review.php b/app/Models/Review.php new file mode 100644 index 0000000..d26a92e --- /dev/null +++ b/app/Models/Review.php @@ -0,0 +1,26 @@ +belongsTo(WorkOrder::class); + } + + public function reviewer(): BelongsTo + { + return $this->belongsTo(User::class, 'reviewer_id'); + } + + public function reviewee(): BelongsTo + { + return $this->belongsTo(User::class, 'reviewee_id'); + } +} diff --git a/app/Models/SocialPost.php b/app/Models/SocialPost.php new file mode 100644 index 0000000..c1d793f --- /dev/null +++ b/app/Models/SocialPost.php @@ -0,0 +1,16 @@ +belongsTo(User::class); + } +} diff --git a/app/Models/User.php b/app/Models/User.php index 7243957..53d31c4 100644 --- a/app/Models/User.php +++ b/app/Models/User.php @@ -8,7 +8,10 @@ use Illuminate\Database\Eloquent\Attributes\Fillable; use Illuminate\Database\Eloquent\Attributes\Hidden; use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Foundation\Auth\User as Authenticatable; +use Illuminate\Database\Eloquent\Relations\HasMany; +use Illuminate\Database\Eloquent\Relations\HasOne; use Illuminate\Notifications\Notifiable; +use Laravel\Sanctum\HasApiTokens; use Spatie\Permission\Traits\HasRoles; #[Fillable(['name', 'email', 'password'])] @@ -16,7 +19,7 @@ use Spatie\Permission\Traits\HasRoles; class User extends Authenticatable { /** @use HasFactory{{ $profile->headline }}
+{{ $profile->hiring_regions }}
+ + @endforeach + {{ $profiles->links() }} +{{ $profile->headline }}
+{{ $profile->description }}
+{{ $message }}
+ @enderror +Opened by {{ $dispute->openedBy->name }} | {{ $dispute->status }}
+{{ $dispute->claim }}
+{{ $dispute->evidence_notes }}
+{{ $dispute->recommended_resolution ?? 'Pending peer review.' }}
+{{ $post->user->name }} | {{ $post->created_at->diffForHumans() }}
+ @if ($post->title){{ $post->body }}
+{{ $job->location }} | {{ $job->service_category }} | {{ $job->status }}
+{{ $job->scope }}
+ + @endforeach + {{ $jobs->links() }} +{{ $job->location }} | {{ $job->service_category }} | {{ $job->status }}
+{{ $job->scope }}
+{{ $quote->provider->providerProfile?->business_name ?? $quote->provider->name }}
+{{ $quote->status }} | {{ $quote->rate_summary }} | {{ $quote->requested_amount }}
+{{ $quote->message }}
+ @auth + @if (auth()->id() === $job->buyer_id && $job->status === 'open') + + @endif + @endauth +No quotes yet.
+ @endforelse +{{ $profile->headline }}
+{{ $profile->service_area }}
+ @foreach ($profile->externalImports as $import) + {{ $import->platform }} {{ $import->rating ? $import->rating.'/5' : '' }} + @endforeach + + @endforeach + {{ $profiles->links() }} +{{ $profile->headline }}
+{{ $profile->bio }}
+{{ $import->platform }} {{ $import->external_id ? '#'.$import->external_id : '' }}
+Rating: {{ $import->rating ?? 'n/a' }} | Reviews: {{ $import->review_count ?? 'n/a' }} | Completed: {{ $import->completed_jobs ?? 'n/a' }}
+{{ $import->notes }}
+No external profile snapshots yet.
+ @endforelse +{{ $order->status }} | Buyer: {{ $order->buyer->name }} | Provider: {{ $order->provider->name }}
+ + @endforeach + {{ $orders->links() }} +Status: {{ $workOrder->status }}
+{{ $workOrder->agreed_terms }}
+{{ $workOrder->deliverables_checklist }}
+ +{{ $review->reviewer->name }} rated {{ $review->reviewee->name }} {{ $review->rating }}/5
+{{ $review->body }}
+