Phase 6 and 7 scope contact safeguards

This commit is contained in:
Codex
2026-07-10 14:14:23 -05:00
parent 6ce2b294d0
commit 5009269907
27 changed files with 1158 additions and 60 deletions
+12
View File
@@ -3,6 +3,9 @@
<div class="mx-auto max-w-5xl space-y-6 p-6">
<section class="rounded border bg-white p-6">
<p class="text-sm text-gray-500">Opened by {{ $dispute->openedBy->name }} | {{ $dispute->status }}</p>
@if ($dispute->reason_code)
<p class="mt-2 text-sm font-semibold text-slate-700">Reason: {{ \App\Models\Dispute::REASON_CODES[$dispute->reason_code] ?? str_replace('_', ' ', $dispute->reason_code) }}</p>
@endif
<h3 class="mt-4 font-semibold">Claim</h3>
<p class="mt-2 whitespace-pre-line">{{ $dispute->claim }}</p>
<h3 class="mt-4 font-semibold">Evidence notes</h3>
@@ -38,12 +41,21 @@
<option value="split">Split resolution</option>
<option value="insufficient_evidence">Insufficient evidence</option>
</select>
<label class="block text-sm font-medium">Primary reason</label>
<select name="reason_code" class="rounded-md border-gray-300">
@foreach (\App\Models\Dispute::REASON_CODES as $code => $label)
<option value="{{ $code }}">{{ $label }}</option>
@endforeach
</select>
<x-field name="reason" label="Reason" textarea />
<x-primary-button>Save vote</x-primary-button>
</form>
@foreach ($dispute->votes as $vote)
<div class="mt-3 rounded bg-gray-50 p-3 text-sm">
<p class="font-semibold">{{ $vote->user->name }} | {{ str_replace('_', ' ', $vote->recommendation) }}</p>
@if ($vote->reason_code)
<p class="text-gray-500">{{ \App\Models\Dispute::REASON_CODES[$vote->reason_code] ?? str_replace('_', ' ', $vote->reason_code) }}</p>
@endif
<p class="whitespace-pre-line">{{ $vote->reason }}</p>
</div>
@endforeach
+84 -11
View File
@@ -1,19 +1,92 @@
<x-app-layout>
<x-slot name="header"><h2 class="text-xl font-semibold">Post Job</h2></x-slot>
<div class="mx-auto max-w-5xl p-6">
<form method="POST" action="{{ route('jobs.store') }}" class="space-y-4 rounded border bg-white p-6">
<form method="POST" action="{{ route('jobs.store') }}" class="space-y-6 rounded border bg-white p-6 dark:border-slate-800 dark:bg-slate-900">
@csrf
<x-field name="title" label="Title" />
<x-field name="service_category" label="Service category" />
<x-field name="location" label="Location" />
<x-field name="starts_at" label="Start date/time" type="datetime-local" />
<x-field name="time_window" label="Time window" />
<x-field name="scope" label="Scope" textarea />
<x-field name="required_skills" label="Required skills" textarea />
<x-field name="required_tools" label="Required tools" textarea />
<x-field name="deliverables" label="Deliverables checklist" textarea />
<section class="grid gap-4 md:grid-cols-2">
<x-field name="title" label="Title" />
<x-field name="service_category" label="Service category" />
<x-field name="location" label="Location" />
<x-field name="starts_at" label="Start date/time" type="datetime-local" />
<x-field name="time_window" label="Time window" />
<div>
<label for="schedule_type" class="block text-sm font-medium text-slate-800 dark:text-slate-200">Schedule type</label>
<select id="schedule_type" name="schedule_type" class="mt-1 block w-full rounded-md border-slate-300 bg-white text-slate-950 shadow-sm focus:border-sky-500 focus:ring-sky-500 dark:border-slate-700 dark:bg-slate-950 dark:text-slate-100">
@foreach (['hard_start' => 'Hard start', 'flex_window' => 'Flexible window', 'appointment' => 'Appointment', 'remote' => 'Remote'] as $value => $label)
<option value="{{ $value }}">{{ $label }}</option>
@endforeach
</select>
</div>
<label class="flex items-center gap-2 rounded-md border border-slate-200 px-3 py-2 text-sm text-slate-700 dark:border-slate-800 dark:text-slate-300">
<input type="checkbox" name="remote_eligible" value="1" class="rounded border-slate-300 text-sky-600 shadow-sm focus:ring-sky-500 dark:border-slate-700 dark:bg-slate-950">
Remote eligible
</label>
</section>
<section class="space-y-4">
<div>
<h3 class="font-semibold text-slate-950 dark:text-white">Structured scope</h3>
<p class="text-sm text-slate-500 dark:text-slate-400">These fields define the job. Supplemental instructions cannot override these boundaries.</p>
</div>
<x-field name="primary_objective" label="Primary objective" textarea />
<div class="grid gap-4 md:grid-cols-2">
<x-field name="included_work" label="Included work" textarea />
<x-field name="excluded_work" label="Excluded work" textarea />
<x-field name="maximum_onsite_expectations" label="Maximum onsite expectations" textarea />
<x-field name="expected_duration" label="Expected duration" />
</div>
<x-field name="scope" label="Legacy summary scope" textarea help="Short summary for compatibility with the first prototype views." />
<x-field name="supplemental_instructions" label="Supplemental instructions" textarea help="Long instructions are preserved, but they are supplemental and do not create undefined onsite obligations." />
</section>
<section class="space-y-4">
<h3 class="font-semibold text-slate-950 dark:text-white">Requirements and closeout</h3>
<div class="grid gap-4 md:grid-cols-2">
<x-field name="required_skills" label="Required skills" textarea />
<x-field name="required_tools" label="Required tools" textarea />
<x-field name="required_certifications" label="Required certifications" textarea />
<x-field name="required_safety_gear" label="Required safety gear" textarea />
<x-field name="deliverables" label="Deliverables checklist" textarea />
<x-field name="closeout_conditions" label="Closeout conditions" textarea />
<x-field name="buyer_provided_equipment" label="Buyer-provided equipment" textarea />
<x-field name="provider_provided_equipment" label="Provider-provided equipment" textarea />
<x-field name="return_shipment_expectations" label="Return shipment expectations" textarea />
<x-field name="parking_access_notes" label="Parking and access notes" textarea />
<x-field name="onsite_restrictions" label="Onsite restrictions" textarea />
<x-field name="vendor_onboarding" label="Vendor onboarding" textarea />
</div>
</section>
<section class="space-y-4">
<div>
<h3 class="font-semibold text-slate-950 dark:text-white">Contact and support availability</h3>
<p class="text-sm text-slate-500 dark:text-slate-400">Certify real support coverage for the scheduled work window before dispatch.</p>
</div>
<div class="grid gap-4 md:grid-cols-3">
<x-field name="primary_contact_name" label="Primary contact name" />
<x-field name="primary_contact_phone" label="Primary contact phone" />
<x-field name="primary_contact_email" label="Primary contact email" type="email" />
<x-field name="backup_contact_name" label="Backup contact name" />
<x-field name="backup_contact_phone" label="Backup contact phone" />
<x-field name="backup_contact_email" label="Backup contact email" type="email" />
<x-field name="dispatch_contact_name" label="Dispatch contact name" />
<x-field name="dispatch_contact_phone" label="Dispatch contact phone" />
<x-field name="dispatch_contact_email" label="Dispatch contact email" type="email" />
</div>
<div class="grid gap-4 md:grid-cols-2">
<x-field name="technical_bridge" label="Technical bridge or meeting link" />
<x-field name="escalation_contact" label="Escalation contact" />
<x-field name="support_channel" label="Support channel" />
<x-field name="support_expected_response_time" label="Expected response time" />
<x-field name="support_availability_window" label="Support availability window" />
<label class="flex items-center gap-2 rounded-md border border-slate-200 px-3 py-2 text-sm text-slate-700 dark:border-slate-800 dark:text-slate-300">
<input type="checkbox" name="contact_certified" value="1" class="rounded border-slate-300 text-sky-600 shadow-sm focus:ring-sky-500 dark:border-slate-700 dark:bg-slate-950">
I certify these contacts and support channels will be available during the work window.
</label>
</div>
</section>
<x-field name="payment_terms" label="Payment terms" textarea />
<x-field name="vendor_onboarding" label="Vendor onboarding" textarea />
<input type="hidden" name="visibility" value="public">
<x-primary-button>Post job</x-primary-button>
</form>
+17 -2
View File
@@ -14,8 +14,23 @@
@auth @role('buyer')<a class="rounded bg-indigo-600 px-4 py-2 text-white" href="{{ route('jobs.create') }}">Post job</a>@endrole @endauth
@foreach ($jobs as $job)
<a href="{{ route('jobs.show', $job) }}" class="block rounded border bg-white p-4 shadow-sm">
<h3 class="font-semibold">{{ $job->title }}</h3>
<p class="text-sm text-gray-600">{{ $job->location }} | {{ $job->service_category }} | {{ $job->status }}</p>
<div class="flex flex-col gap-2 sm:flex-row sm:items-start sm:justify-between">
<div>
<h3 class="font-semibold">{{ $job->title }}</h3>
<p class="text-sm text-gray-600">{{ $job->location }} | {{ $job->service_category }} | {{ $job->status }}</p>
</div>
<div class="flex flex-wrap gap-2">
<x-badge tone="{{ $job->scope_clarity_status === 'clear' ? 'emerald' : 'amber' }}">Scope {{ str_replace('_', ' ', $job->scope_clarity_status) }}</x-badge>
<x-badge tone="{{ $job->contact_certified ? 'emerald' : 'rose' }}">{{ $job->contact_certified ? 'Support certified' : 'Support not certified' }}</x-badge>
</div>
</div>
@if (is_array($job->risk_flags) && $job->risk_flags)
<div class="mt-2 flex flex-wrap gap-2">
@foreach ($job->risk_flags as $flag)
<x-badge tone="amber">{{ str_replace('_', ' ', $flag) }}</x-badge>
@endforeach
</div>
@endif
<p class="mt-2 line-clamp-2 text-sm">{{ $job->scope }}</p>
<x-rating-summary :ratings="$job->ratings" />
</a>
+69 -3
View File
@@ -1,15 +1,81 @@
<x-app-layout>
<x-slot name="header"><h2 class="text-xl font-semibold">{{ $job->title }}</h2></x-slot>
<div class="mx-auto max-w-6xl space-y-6 p-6">
<section class="rounded border bg-white p-6">
<p class="text-sm text-gray-500">{{ $job->location }} | {{ $job->service_category }} | {{ $job->status }}</p>
<p class="mt-4 whitespace-pre-line">{{ $job->scope }}</p>
<section class="rounded border bg-white p-6 dark:border-slate-800 dark:bg-slate-900">
<div class="flex flex-col gap-3 sm:flex-row sm:items-start sm:justify-between">
<div>
<p class="text-sm text-gray-500">{{ $job->location }} | {{ $job->service_category }} | {{ $job->status }}</p>
<div class="mt-3 flex flex-wrap gap-2">
<x-badge tone="{{ $job->scope_clarity_status === 'clear' ? 'emerald' : 'amber' }}">Scope {{ str_replace('_', ' ', $job->scope_clarity_status) }}</x-badge>
@if ($job->contact_certified)
<x-badge tone="emerald">Support certified</x-badge>
@else
<x-badge tone="rose">Support not certified</x-badge>
@endif
@if ($job->remote_eligible)
<x-badge tone="sky">Remote eligible</x-badge>
@endif
</div>
</div>
<div class="text-sm text-slate-600 dark:text-slate-400 sm:text-right">
<p>{{ $job->starts_at?->format('M j, Y g:i A') ?? 'No start set' }}</p>
<p>{{ $job->time_window ?: 'No window set' }} | {{ str_replace('_', ' ', $job->schedule_type ?: 'schedule not set') }}</p>
</div>
</div>
@if (is_array($job->risk_flags) && $job->risk_flags)
<div class="mt-4 rounded-md border border-amber-200 bg-amber-50 p-4 text-sm text-amber-950 dark:border-amber-900 dark:bg-amber-950/40 dark:text-amber-100">
<p class="font-semibold">Risk flags</p>
<div class="mt-2 flex flex-wrap gap-2">
@foreach ($job->risk_flags as $flag)
<x-badge tone="amber">{{ str_replace('_', ' ', $flag) }}</x-badge>
@endforeach
</div>
</div>
@endif
<div class="mt-6 rounded-md border border-sky-200 bg-sky-50 p-4 text-sm text-sky-950 dark:border-sky-900 dark:bg-sky-950/40 dark:text-sky-100">
<p class="font-semibold">Anti-catch-all rule</p>
<p class="mt-1">The structured scope fields define this job. Supplemental instructions are preserved for reference, but they do not override included work, excluded work, closeout conditions, or change-request requirements.</p>
</div>
<dl class="mt-6 grid gap-4 md:grid-cols-2">
<div><dt class="text-sm text-gray-500">Primary objective</dt><dd class="whitespace-pre-line">{{ $job->primary_objective ?: $job->scope }}</dd></div>
<div><dt class="text-sm text-gray-500">Expected duration</dt><dd class="whitespace-pre-line">{{ $job->expected_duration ?: 'Not specified' }}</dd></div>
<div><dt class="text-sm text-gray-500">Included work</dt><dd class="whitespace-pre-line">{{ $job->included_work ?: 'Not specified' }}</dd></div>
<div><dt class="text-sm text-gray-500">Excluded work</dt><dd class="whitespace-pre-line">{{ $job->excluded_work ?: 'Not specified' }}</dd></div>
<div><dt class="text-sm text-gray-500">Maximum onsite expectations</dt><dd class="whitespace-pre-line">{{ $job->maximum_onsite_expectations ?: 'Not specified' }}</dd></div>
<div><dt class="text-sm text-gray-500">Closeout conditions</dt><dd class="whitespace-pre-line">{{ $job->closeout_conditions ?: 'Not specified' }}</dd></div>
<div><dt class="text-sm text-gray-500">Required skills</dt><dd class="whitespace-pre-line">{{ $job->required_skills }}</dd></div>
<div><dt class="text-sm text-gray-500">Required tools</dt><dd class="whitespace-pre-line">{{ $job->required_tools }}</dd></div>
<div><dt class="text-sm text-gray-500">Required certifications</dt><dd class="whitespace-pre-line">{{ $job->required_certifications ?: 'None listed' }}</dd></div>
<div><dt class="text-sm text-gray-500">Safety gear</dt><dd class="whitespace-pre-line">{{ $job->required_safety_gear ?: 'None listed' }}</dd></div>
<div><dt class="text-sm text-gray-500">Deliverables</dt><dd class="whitespace-pre-line">{{ $job->deliverables }}</dd></div>
<div><dt class="text-sm text-gray-500">Payment terms</dt><dd class="whitespace-pre-line">{{ $job->payment_terms }}</dd></div>
<div><dt class="text-sm text-gray-500">Buyer-provided equipment</dt><dd class="whitespace-pre-line">{{ $job->buyer_provided_equipment ?: 'Not specified' }}</dd></div>
<div><dt class="text-sm text-gray-500">Provider-provided equipment</dt><dd class="whitespace-pre-line">{{ $job->provider_provided_equipment ?: 'Not specified' }}</dd></div>
<div><dt class="text-sm text-gray-500">Return shipment</dt><dd class="whitespace-pre-line">{{ $job->return_shipment_expectations ?: 'Not specified' }}</dd></div>
<div><dt class="text-sm text-gray-500">Parking/access</dt><dd class="whitespace-pre-line">{{ $job->parking_access_notes ?: 'Not specified' }}</dd></div>
<div><dt class="text-sm text-gray-500">Onsite restrictions</dt><dd class="whitespace-pre-line">{{ $job->onsite_restrictions ?: 'Not specified' }}</dd></div>
<div><dt class="text-sm text-gray-500">Vendor onboarding</dt><dd class="whitespace-pre-line">{{ $job->vendor_onboarding ?: 'Not specified' }}</dd></div>
</dl>
@if ($job->supplemental_instructions)
<details class="mt-6 rounded-md border border-slate-200 p-4 text-sm dark:border-slate-800">
<summary class="cursor-pointer font-semibold text-slate-950 dark:text-white">Supplemental instructions</summary>
<p class="mt-3 whitespace-pre-line text-slate-700 dark:text-slate-300">{{ $job->supplemental_instructions }}</p>
</details>
@endif
<section class="mt-6 rounded-md border border-slate-200 p-4 dark:border-slate-800">
<h3 class="font-semibold text-slate-950 dark:text-white">Contact and support coverage</h3>
<dl class="mt-3 grid gap-4 text-sm md:grid-cols-3">
<div><dt class="text-gray-500">Primary contact</dt><dd>{{ $job->primary_contact_name ?: 'Not set' }}<br>{{ $job->primary_contact_phone }} {{ $job->primary_contact_email }}</dd></div>
<div><dt class="text-gray-500">Backup contact</dt><dd>{{ $job->backup_contact_name ?: 'Not set' }}<br>{{ $job->backup_contact_phone }} {{ $job->backup_contact_email }}</dd></div>
<div><dt class="text-gray-500">Dispatch contact</dt><dd>{{ $job->dispatch_contact_name ?: 'Not set' }}<br>{{ $job->dispatch_contact_phone }} {{ $job->dispatch_contact_email }}</dd></div>
<div><dt class="text-gray-500">Technical bridge</dt><dd>{{ $job->technical_bridge ?: 'Not set' }}</dd></div>
<div><dt class="text-gray-500">Escalation</dt><dd>{{ $job->escalation_contact ?: 'Not set' }}</dd></div>
<div><dt class="text-gray-500">Support availability</dt><dd>{{ $job->support_availability_window ?: 'Not set' }}<br>{{ $job->support_channel }} {{ $job->support_expected_response_time }}</dd></div>
</dl>
</section>
<x-attachments :attachments="$job->attachments" />
<x-rating-summary :ratings="$job->ratings" />
<x-rating-form type="job_post" :id="$job->id" category="job_quality" />
@@ -39,6 +39,55 @@
<p>{{ $workOrder->scheduled_at?->format('M j, Y g:i A') ?? 'Not set' }} | {{ $workOrder->appointment_window ?: 'No window set' }}</p>
</section>
<section>
<h2>Scope Safeguards</h2>
<p class="muted">Scope clarity: {{ str_replace('_', ' ', $workOrder->scope_clarity_status) }}</p>
@if (is_array($workOrder->risk_flags) && $workOrder->risk_flags)
<p class="muted">Risk flags: {{ collect($workOrder->risk_flags)->map(fn ($flag) => str_replace('_', ' ', $flag))->implode(', ') }}</p>
@endif
<div class="grid">
<div class="box">
<h3>Primary Objective</h3>
<p>{!! nl2br(e($workOrder->scopeSnapshotValue('primary_objective') ?: $workOrder->jobPost->scope)) !!}</p>
</div>
<div class="box">
<h3>Included Work</h3>
<p>{!! nl2br(e($workOrder->scopeSnapshotValue('included_work') ?: 'Not specified')) !!}</p>
</div>
<div class="box">
<h3>Excluded Work</h3>
<p>{!! nl2br(e($workOrder->scopeSnapshotValue('excluded_work') ?: 'Not specified')) !!}</p>
</div>
<div class="box">
<h3>Closeout Conditions</h3>
<p>{!! nl2br(e($workOrder->scopeSnapshotValue('closeout_conditions') ?: 'Not specified')) !!}</p>
</div>
</div>
</section>
<section>
<h2>Contact And Support</h2>
<p class="muted">Certified: {{ $workOrder->contactSnapshotValue('contact_certified') ? 'Yes' : 'No' }}</p>
<div class="grid">
<div class="box">
<h3>Primary</h3>
<p>{{ $workOrder->contactSnapshotValue('primary_contact_name') ?: 'Not set' }}<br>{{ $workOrder->contactSnapshotValue('primary_contact_phone') }} {{ $workOrder->contactSnapshotValue('primary_contact_email') }}</p>
</div>
<div class="box">
<h3>Backup</h3>
<p>{{ $workOrder->contactSnapshotValue('backup_contact_name') ?: 'Not set' }}<br>{{ $workOrder->contactSnapshotValue('backup_contact_phone') }} {{ $workOrder->contactSnapshotValue('backup_contact_email') }}</p>
</div>
<div class="box">
<h3>Dispatch</h3>
<p>{{ $workOrder->contactSnapshotValue('dispatch_contact_name') ?: 'Not set' }}<br>{{ $workOrder->contactSnapshotValue('dispatch_contact_phone') }} {{ $workOrder->contactSnapshotValue('dispatch_contact_email') }}</p>
</div>
<div class="box">
<h3>Escalation</h3>
<p>{{ $workOrder->contactSnapshotValue('escalation_contact') ?: 'Not set' }}<br>{{ $workOrder->contactSnapshotValue('support_channel') }} {{ $workOrder->contactSnapshotValue('support_expected_response_time') }}</p>
</div>
</div>
</section>
<section>
<h2>Agreed Terms</h2>
<p>{!! nl2br(e($workOrder->agreed_terms ?: 'No agreed terms recorded.')) !!}</p>
@@ -67,6 +116,28 @@
@endif
</section>
<section>
<h2>Change Requests</h2>
<ul>
@forelse ($workOrder->changeRequests() as $changeRequest)
<li>{{ $changeRequest['summary'] }} | {{ $changeRequest['reason_label'] ?? 'Change' }} | {{ $changeRequest['status'] ?? 'open' }}</li>
@empty
<li>No change requests recorded.</li>
@endforelse
</ul>
</section>
<section>
<h2>Contact / Support Events</h2>
<ul>
@forelse ($workOrder->contactEvents as $event)
<li>{{ \App\Models\WorkOrderContactEvent::EVENT_TYPES[$event->event_type] ?? str_replace('_', ' ', $event->event_type) }} | {{ $event->attempted_at?->format('M j, Y g:i A') ?? $event->created_at->format('M j, Y g:i A') }} | {{ $event->result ?: 'No result' }}</li>
@empty
<li>No contact or support failures recorded.</li>
@endforelse
</ul>
</section>
<section>
<h2>Attachments</h2>
<ul>
+129 -3
View File
@@ -2,6 +2,10 @@
$progress = $workOrder->checklistProgress();
$completed = $workOrder->checklistCompleted();
$evidenceRules = is_array($workOrder->evidence_rules) ? $workOrder->evidence_rules : [];
$riskFlags = is_array($workOrder->risk_flags) ? $workOrder->risk_flags : [];
$changeReasonCodes = \App\Models\WorkOrderChangeRequest::REASON_CODES;
$contactEventTypes = \App\Models\WorkOrderContactEvent::EVENT_TYPES;
$disputeReasonCodes = \App\Models\Dispute::REASON_CODES;
@endphp
<x-app-layout>
@@ -34,6 +38,7 @@
<x-stat-card label="Checklist" :value="$progress['done'].'/'.$progress['total']" description="Completed deliverables" />
<x-stat-card label="Attachments" :value="$workOrder->attachments->count()" description="Evidence and files" />
<x-stat-card label="Changes" :value="count($workOrder->changeRequests())" description="Requested scope changes" />
<x-stat-card label="Contact issues" :value="$workOrder->contactFailureCount()" description="Logged support failures" />
</div>
<div class="mt-6 grid gap-6 lg:grid-cols-[1.1fr_.9fr]">
@@ -55,6 +60,49 @@
</div>
</section>
<section class="tse-panel p-6">
<div class="flex flex-col gap-3 sm:flex-row sm:items-start sm:justify-between">
<div>
<h3 class="font-semibold text-slate-950 dark:text-white">Scope and support safeguards</h3>
<p class="mt-1 text-sm text-slate-500 dark:text-slate-400">Structured scope governs the work order. Supplemental instructions cannot create undefined onsite obligations.</p>
</div>
<div class="flex flex-wrap gap-2">
<x-badge tone="{{ $workOrder->scope_clarity_status === 'clear' ? 'emerald' : 'amber' }}">Scope {{ str_replace('_', ' ', $workOrder->scope_clarity_status) }}</x-badge>
@if ($workOrder->contactSnapshotValue('contact_certified'))
<x-badge tone="emerald">Support certified</x-badge>
@else
<x-badge tone="rose">Support not certified</x-badge>
@endif
</div>
</div>
@if ($riskFlags)
<div class="mt-4 flex flex-wrap gap-2">
@foreach ($riskFlags as $flag)
<x-badge tone="amber">{{ str_replace('_', ' ', $flag) }}</x-badge>
@endforeach
</div>
@endif
<dl class="mt-6 grid gap-4 text-sm md:grid-cols-2">
<div><dt class="text-slate-500">Primary objective</dt><dd class="whitespace-pre-line text-slate-800 dark:text-slate-200">{{ $workOrder->scopeSnapshotValue('primary_objective') ?: $workOrder->jobPost->scope }}</dd></div>
<div><dt class="text-slate-500">Expected duration</dt><dd class="whitespace-pre-line text-slate-800 dark:text-slate-200">{{ $workOrder->scopeSnapshotValue('expected_duration') ?: 'Not specified' }}</dd></div>
<div><dt class="text-slate-500">Included work</dt><dd class="whitespace-pre-line text-slate-800 dark:text-slate-200">{{ $workOrder->scopeSnapshotValue('included_work') ?: 'Not specified' }}</dd></div>
<div><dt class="text-slate-500">Excluded work</dt><dd class="whitespace-pre-line text-slate-800 dark:text-slate-200">{{ $workOrder->scopeSnapshotValue('excluded_work') ?: 'Not specified' }}</dd></div>
<div><dt class="text-slate-500">Maximum onsite expectations</dt><dd class="whitespace-pre-line text-slate-800 dark:text-slate-200">{{ $workOrder->scopeSnapshotValue('maximum_onsite_expectations') ?: 'Not specified' }}</dd></div>
<div><dt class="text-slate-500">Closeout conditions</dt><dd class="whitespace-pre-line text-slate-800 dark:text-slate-200">{{ $workOrder->scopeSnapshotValue('closeout_conditions') ?: 'Not specified' }}</dd></div>
</dl>
<div class="mt-6 rounded-md border border-slate-200 p-4 dark:border-slate-800">
<h4 class="font-semibold text-slate-950 dark:text-white">Certified contact path</h4>
<dl class="mt-3 grid gap-4 text-sm md:grid-cols-3">
<div><dt class="text-slate-500">Primary</dt><dd>{{ $workOrder->contactSnapshotValue('primary_contact_name') ?: 'Not set' }}<br>{{ $workOrder->contactSnapshotValue('primary_contact_phone') }} {{ $workOrder->contactSnapshotValue('primary_contact_email') }}</dd></div>
<div><dt class="text-slate-500">Backup</dt><dd>{{ $workOrder->contactSnapshotValue('backup_contact_name') ?: 'Not set' }}<br>{{ $workOrder->contactSnapshotValue('backup_contact_phone') }} {{ $workOrder->contactSnapshotValue('backup_contact_email') }}</dd></div>
<div><dt class="text-slate-500">Dispatch</dt><dd>{{ $workOrder->contactSnapshotValue('dispatch_contact_name') ?: 'Not set' }}<br>{{ $workOrder->contactSnapshotValue('dispatch_contact_phone') }} {{ $workOrder->contactSnapshotValue('dispatch_contact_email') }}</dd></div>
<div><dt class="text-slate-500">Technical bridge</dt><dd>{{ $workOrder->contactSnapshotValue('technical_bridge') ?: 'Not set' }}</dd></div>
<div><dt class="text-slate-500">Escalation</dt><dd>{{ $workOrder->contactSnapshotValue('escalation_contact') ?: 'Not set' }}</dd></div>
<div><dt class="text-slate-500">Support window</dt><dd>{{ $workOrder->contactSnapshotValue('support_availability_window') ?: 'Not set' }}<br>{{ $workOrder->contactSnapshotValue('support_channel') }} {{ $workOrder->contactSnapshotValue('support_expected_response_time') }}</dd></div>
</dl>
</div>
</section>
@if (auth()->id() === $workOrder->buyer_id || auth()->user()->hasRole('admin'))
<section class="tse-panel p-6">
<h3 class="font-semibold text-slate-950 dark:text-white">Work-order setup</h3>
@@ -129,18 +177,49 @@
<h3 class="font-semibold text-slate-950 dark:text-white">Change requests</h3>
<form method="POST" action="{{ route('work-orders.change-requests', $workOrder) }}" class="mt-4 space-y-4">
@csrf
<div>
<label for="reason_code" class="block text-sm font-medium text-slate-800 dark:text-slate-200">Reason</label>
<select id="reason_code" name="reason_code" class="mt-1 block w-full rounded-md border-slate-300 bg-white text-slate-950 shadow-sm focus:border-sky-500 focus:ring-sky-500 dark:border-slate-700 dark:bg-slate-950 dark:text-slate-100">
@foreach ($changeReasonCodes as $code => $label)
<option value="{{ $code }}">{{ $label }}</option>
@endforeach
</select>
</div>
<x-field name="summary" label="Summary" />
<x-field name="details" label="Details" textarea />
<div class="grid gap-4 md:grid-cols-3">
<x-field name="scope_impact" label="Scope impact" textarea />
<x-field name="schedule_impact" label="Schedule impact" textarea />
<x-field name="terms_impact" label="Terms impact" textarea />
</div>
<x-secondary-button type="submit">Record change request</x-secondary-button>
</form>
<div class="mt-4 space-y-3">
@forelse ($workOrder->changeRequests() as $requestRecord)
<div class="rounded-md border border-slate-200 p-4 text-sm dark:border-slate-800">
<p class="font-semibold text-slate-950 dark:text-white">{{ $requestRecord['summary'] }}</p>
<p class="mt-1 text-slate-500 dark:text-slate-400">{{ $requestRecord['requested_by_name'] ?? 'User' }} | {{ $requestRecord['status'] ?? 'open' }} | {{ $requestRecord['requested_at'] ?? '' }}</p>
<p class="mt-1 text-slate-500 dark:text-slate-400">{{ $requestRecord['requested_by_name'] ?? 'User' }} | {{ $requestRecord['reason_label'] ?? 'Change' }} | {{ $requestRecord['status'] ?? 'open' }} | {{ $requestRecord['requested_at'] ?? '' }}</p>
@if (! empty($requestRecord['details']))
<p class="mt-2 whitespace-pre-line text-slate-700 dark:text-slate-300">{{ $requestRecord['details'] }}</p>
@endif
<div class="mt-3 grid gap-3 text-xs md:grid-cols-3">
<p><span class="font-semibold">Scope:</span> {{ $requestRecord['scope_impact'] ?? 'Not specified' }}</p>
<p><span class="font-semibold">Schedule:</span> {{ $requestRecord['schedule_impact'] ?? 'Not specified' }}</p>
<p><span class="font-semibold">Terms:</span> {{ $requestRecord['terms_impact'] ?? 'Not specified' }}</p>
</div>
@if (! empty($requestRecord['id']) && ($requestRecord['status'] ?? 'open') === 'open' && (auth()->id() !== ($requestRecord['requested_by_id'] ?? null) || auth()->user()->hasRole('admin')))
<form method="POST" action="{{ route('work-orders.change-requests.resolve', [$workOrder, $requestRecord['id']]) }}" class="mt-3 grid gap-3 border-t border-slate-200 pt-3 dark:border-slate-800 md:grid-cols-[.6fr_1.4fr_auto]">
@csrf
@method('PATCH')
<select name="status" class="rounded-md border-slate-300 bg-white text-sm text-slate-950 shadow-sm focus:border-sky-500 focus:ring-sky-500 dark:border-slate-700 dark:bg-slate-950 dark:text-slate-100">
<option value="accepted">Accept</option>
<option value="declined">Decline</option>
<option value="withdrawn">Withdraw</option>
</select>
<input name="resolution_notes" class="rounded-md border-slate-300 bg-white text-sm text-slate-950 shadow-sm focus:border-sky-500 focus:ring-sky-500 dark:border-slate-700 dark:bg-slate-950 dark:text-slate-100" placeholder="Resolution notes">
<x-secondary-button type="submit">Update</x-secondary-button>
</form>
@endif
</div>
@empty
<p class="text-sm text-slate-600 dark:text-slate-400">No change requests yet.</p>
@@ -148,6 +227,43 @@
</div>
</section>
<section class="tse-panel p-6">
<h3 class="font-semibold text-slate-950 dark:text-white">Contact and support event log</h3>
@if (auth()->id() === $workOrder->provider_id || auth()->user()->hasRole('admin'))
<form method="POST" action="{{ route('work-orders.contact-events.store', $workOrder) }}" class="mt-4 space-y-4">
@csrf
<div class="grid gap-4 md:grid-cols-2">
<div>
<label for="event_type" class="block text-sm font-medium text-slate-800 dark:text-slate-200">Event type</label>
<select id="event_type" name="event_type" class="mt-1 block w-full rounded-md border-slate-300 bg-white text-slate-950 shadow-sm focus:border-sky-500 focus:ring-sky-500 dark:border-slate-700 dark:bg-slate-950 dark:text-slate-100">
@foreach ($contactEventTypes as $code => $label)
<option value="{{ $code }}">{{ $label }}</option>
@endforeach
</select>
</div>
<x-field name="attempted_channel" label="Attempted channel" />
<x-field name="attempted_at" label="Attempted at" type="datetime-local" />
<x-field name="result" label="Result" />
</div>
<x-field name="notes" label="Notes" textarea />
<x-secondary-button type="submit">Log contact/support event</x-secondary-button>
</form>
@endif
<div class="mt-4 space-y-3">
@forelse ($workOrder->contactEvents as $event)
<div class="rounded-md border border-slate-200 p-4 text-sm dark:border-slate-800">
<p class="font-semibold text-slate-950 dark:text-white">{{ $contactEventTypes[$event->event_type] ?? str_replace('_', ' ', $event->event_type) }}</p>
<p class="mt-1 text-slate-500 dark:text-slate-400">{{ $event->user->name }} | {{ $event->attempted_at?->format('M j, Y g:i A') ?? $event->created_at->format('M j, Y g:i A') }} | {{ $event->attempted_channel ?: 'No channel' }} | {{ $event->result ?: 'No result' }}</p>
@if ($event->notes)
<p class="mt-2 whitespace-pre-line text-slate-700 dark:text-slate-300">{{ $event->notes }}</p>
@endif
</div>
@empty
<p class="text-sm text-slate-600 dark:text-slate-400">No contact/support events have been logged.</p>
@endforelse
</div>
</section>
<section class="tse-panel p-6">
<h3 class="font-semibold text-slate-950 dark:text-white">Messages</h3>
<form method="POST" action="{{ route('work-order-messages.store', $workOrder) }}" class="mt-4 space-y-4">
@@ -213,9 +329,11 @@
<x-field name="rating" label="Overall rating 1-5" type="number" />
</div>
@foreach ($reviewFields as $field => $label)
@php($definitionKey = $fieldDefinitionKeys[$field] ?? null)
@php
$definitionKey = $fieldDefinitionKeys[$field] ?? null;
@endphp
<div>
<x-field name="{{ $field }}" label="{{ $label }} 1-5" type="number" />
<x-field :name="$field" :label="$label.' 1-5'" type="number" />
@if ($definitionKey && ! empty($definitions[$definitionKey]))
<p class="mt-1 text-xs text-slate-500 dark:text-slate-400">{{ $definitions[$definitionKey] }}</p>
@endif
@@ -308,6 +426,14 @@
<form method="POST" action="{{ route('disputes.store', $workOrder) }}" class="mt-4 space-y-4">
@csrf
<x-field name="summary" label="Summary" />
<div>
<label for="dispute_reason_code" class="block text-sm font-medium text-slate-800 dark:text-slate-200">Reason code</label>
<select id="dispute_reason_code" name="reason_code" class="mt-1 block w-full rounded-md border-slate-300 bg-white text-slate-950 shadow-sm focus:border-sky-500 focus:ring-sky-500 dark:border-slate-700 dark:bg-slate-950 dark:text-slate-100">
@foreach ($disputeReasonCodes as $code => $label)
<option value="{{ $code }}">{{ $label }}</option>
@endforeach
</select>
</div>
<x-field name="claim" label="Claim" textarea />
<x-field name="evidence_notes" label="Evidence notes" textarea />
<x-primary-button>Open dispute</x-primary-button>