Add provider tag verification

This commit is contained in:
Codex
2026-07-10 17:55:52 -05:00
parent 3b856b5e46
commit 71434c1ac4
14 changed files with 504 additions and 11 deletions
+47
View File
@@ -91,6 +91,53 @@
<x-rating-form type="provider_profile" :id="$profile->id" category="provider_overall" />
</section>
<section class="tse-panel p-6">
<div class="flex flex-col gap-2 sm:flex-row sm:items-start sm:justify-between">
<div>
<h3 class="font-semibold text-slate-950 dark:text-white">Completed-work competency evidence</h3>
<p class="mt-1 text-sm text-slate-500 dark:text-slate-400">Buyer confirmations from completed TSE work orders. These support profile tags without replacing provider-controlled profile claims.</p>
</div>
<x-badge tone="sky">{{ $profile->tagVerifications->count() }} records</x-badge>
</div>
@forelse ($profile->tagVerifications->sortByDesc('created_at')->take(6) as $verification)
@php
$confirmedTerms = $profile->taxonomyTerms->whereIn('id', $verification->confirmed_term_ids ?? []);
$confirmedLevel = $verification->confirmedLevelDefinition();
@endphp
<div class="mt-4 rounded-md border border-slate-200 p-4 text-sm dark:border-slate-800">
<div class="flex flex-col gap-2 sm:flex-row sm:items-start sm:justify-between">
<div>
<p class="font-semibold text-slate-950 dark:text-white">{{ $verification->workOrder->jobPost->title ?? 'Completed work order' }}</p>
<p class="mt-1 text-slate-600 dark:text-slate-400">{{ $verification->buyer->name }} | {{ str_replace('_', ' ', $verification->level_verdict) }} | {{ $verification->created_at->format('M j, Y') }}</p>
</div>
@if ($confirmedLevel)
<x-badge tone="sky">{{ $confirmedLevel['short_name'] }}</x-badge>
@endif
</div>
@if ($confirmedTerms->isNotEmpty())
<div class="mt-3 flex flex-wrap gap-2">
@foreach ($confirmedTerms as $term)
<x-badge tone="emerald">{{ $term->name }}</x-badge>
@endforeach
</div>
@endif
@if ($verification->suggested_tags)
<div class="mt-3 flex flex-wrap gap-2">
@foreach ($verification->suggested_tags as $tag)
<x-badge tone="slate">{{ $tag }}</x-badge>
@endforeach
</div>
@endif
@if ($verification->notes)
<p class="mt-3 whitespace-pre-line text-slate-700 dark:text-slate-300">{{ $verification->notes }}</p>
@endif
</div>
@empty
<p class="mt-3 text-sm text-slate-600 dark:text-slate-400">No completed-work competency confirmations have been recorded yet.</p>
@endforelse
</section>
@if ($profile->visible('imports'))
<section class="tse-panel p-6">
<h3 class="font-semibold text-slate-950 dark:text-white">Imported profile and review history</h3>
+133
View File
@@ -6,6 +6,13 @@
$changeReasonCodes = \App\Models\WorkOrderChangeRequest::REASON_CODES;
$contactEventTypes = \App\Models\WorkOrderContactEvent::EVENT_TYPES;
$disputeReasonCodes = \App\Models\Dispute::REASON_CODES;
$providerProfile = $workOrder->provider->providerProfile;
$providerTerms = $providerProfile?->taxonomyTerms ?? collect();
$tagVerification = $workOrder->providerTagVerification;
$verificationCompleteStatuses = ['completed', 'buyer_approved', 'closed'];
$canVerifyProviderTags = (auth()->id() === $workOrder->buyer_id || auth()->user()->hasRole('admin'))
&& in_array($workOrder->status, $verificationCompleteStatuses, true)
&& $providerProfile;
@endphp
<x-app-layout>
@@ -416,6 +423,132 @@
@endforeach
</section>
<section class="tse-panel p-6">
<div class="flex flex-col gap-2 sm:flex-row sm:items-start sm:justify-between">
<div>
<h3 class="font-semibold text-slate-950 dark:text-white">Provider tag verification</h3>
<p class="mt-1 text-sm text-slate-500 dark:text-slate-400">After completed work, buyers can confirm whether the provider level and self-declared tags matched what was observed on this job.</p>
</div>
@if ($tagVerification)
<x-badge tone="emerald">Verification recorded</x-badge>
@else
<x-badge tone="slate">No verification yet</x-badge>
@endif
</div>
@if ($tagVerification)
@php
$confirmedTerms = $providerTerms->whereIn('id', $tagVerification->confirmed_term_ids ?? []);
$disputedTerms = $providerTerms->whereIn('id', $tagVerification->disputed_term_ids ?? []);
$confirmedLevel = $tagVerification->confirmedLevelDefinition();
@endphp
<div class="mt-4 rounded-md border border-slate-200 p-4 text-sm dark:border-slate-800">
<p class="font-semibold text-slate-950 dark:text-white">
{{ $tagVerification->buyer->name }} marked level evidence as {{ str_replace('_', ' ', $tagVerification->level_verdict) }}.
</p>
<p class="mt-1 text-slate-600 dark:text-slate-400">
Declared: {{ config('technician-levels')[$tagVerification->declared_level]['short_name'] ?? 'Not recorded' }}
@if ($confirmedLevel)
| Observed: {{ $confirmedLevel['short_name'] }}
@endif
</p>
@if ($confirmedTerms->isNotEmpty())
<div class="mt-3">
<p class="text-xs font-semibold uppercase text-slate-500 dark:text-slate-400">Confirmed tags</p>
<div class="mt-2 flex flex-wrap gap-2">
@foreach ($confirmedTerms as $term)
<x-badge tone="emerald">{{ $term->name }}</x-badge>
@endforeach
</div>
</div>
@endif
@if ($disputedTerms->isNotEmpty())
<div class="mt-3">
<p class="text-xs font-semibold uppercase text-slate-500 dark:text-slate-400">Disputed tags</p>
<div class="mt-2 flex flex-wrap gap-2">
@foreach ($disputedTerms as $term)
<x-badge tone="amber">{{ $term->name }}</x-badge>
@endforeach
</div>
</div>
@endif
@if ($tagVerification->suggested_tags)
<div class="mt-3">
<p class="text-xs font-semibold uppercase text-slate-500 dark:text-slate-400">Buyer suggested tags</p>
<div class="mt-2 flex flex-wrap gap-2">
@foreach ($tagVerification->suggested_tags as $tag)
<x-badge tone="sky">{{ $tag }}</x-badge>
@endforeach
</div>
</div>
@endif
@if ($tagVerification->notes)
<p class="mt-3 whitespace-pre-line text-slate-700 dark:text-slate-300">{{ $tagVerification->notes }}</p>
@endif
</div>
@endif
@if ($canVerifyProviderTags)
<form method="POST" action="{{ route('provider-tag-verifications.store', $workOrder) }}" class="mt-4 space-y-4 rounded-md border border-slate-200 p-4 dark:border-slate-800">
@csrf
<div class="grid gap-4 md:grid-cols-2">
<div>
<label for="level_verdict" class="block text-sm font-medium text-slate-800 dark:text-slate-200">Level verdict</label>
<select id="level_verdict" name="level_verdict" 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 (\App\Models\ProviderTagVerification::LEVEL_VERDICTS as $code => $label)
<option value="{{ $code }}" @selected(($tagVerification?->level_verdict ?? 'not_observed') === $code)>{{ $label }}</option>
@endforeach
</select>
</div>
<div>
<label for="confirmed_level" class="block text-sm font-medium text-slate-800 dark:text-slate-200">Observed technician level</label>
<select id="confirmed_level" name="confirmed_level" 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">
<option value="">Not observed</option>
@foreach (config('technician-levels') as $level => $definition)
<option value="{{ $level }}" @selected((int) ($tagVerification?->confirmed_level ?? 0) === $level)>{{ $definition['name'] }}</option>
@endforeach
</select>
</div>
</div>
<div class="grid gap-4 md:grid-cols-2">
<div>
<p class="text-sm font-medium text-slate-800 dark:text-slate-200">Confirm observed tags</p>
<div class="mt-2 space-y-2">
@forelse ($providerTerms as $term)
<label class="flex items-start gap-2 rounded-md border border-slate-200 p-2 text-sm dark:border-slate-800">
<input type="checkbox" name="confirmed_term_ids[]" value="{{ $term->id }}" @checked(in_array($term->id, $tagVerification?->confirmed_term_ids ?? [], true)) class="mt-1 rounded border-slate-300 text-sky-600 shadow-sm focus:ring-sky-500 dark:border-slate-700 dark:bg-slate-950">
<span>{{ $term->name }} <span class="text-xs text-slate-500">({{ str_replace('_', ' ', $term->type) }})</span></span>
</label>
@empty
<p class="text-sm text-slate-600 dark:text-slate-400">The provider has not declared tags yet.</p>
@endforelse
</div>
</div>
<div>
<p class="text-sm font-medium text-slate-800 dark:text-slate-200">Dispute tags that did not match this job</p>
<div class="mt-2 space-y-2">
@forelse ($providerTerms as $term)
<label class="flex items-start gap-2 rounded-md border border-slate-200 p-2 text-sm dark:border-slate-800">
<input type="checkbox" name="disputed_term_ids[]" value="{{ $term->id }}" @checked(in_array($term->id, $tagVerification?->disputed_term_ids ?? [], true)) class="mt-1 rounded border-slate-300 text-amber-600 shadow-sm focus:ring-amber-500 dark:border-slate-700 dark:bg-slate-950">
<span>{{ $term->name }} <span class="text-xs text-slate-500">({{ str_replace('_', ' ', $term->type) }})</span></span>
</label>
@empty
<p class="text-sm text-slate-600 dark:text-slate-400">No tags are available to dispute.</p>
@endforelse
</div>
</div>
</div>
<x-field name="suggested_tags_text" label="Suggested tags, one per line" :value="implode(PHP_EOL, $tagVerification?->suggested_tags ?? [])" textarea />
<x-field name="notes" label="Notes about observed preparation, tools, or fit" :value="$tagVerification?->notes" textarea />
<x-primary-button>Save provider tag verification</x-primary-button>
</form>
@elseif (! in_array($workOrder->status, $verificationCompleteStatuses, true))
<p class="mt-3 text-sm text-slate-600 dark:text-slate-400">Provider tag verification opens after the provider marks the work completed.</p>
@endif
</section>
<section class="tse-panel p-6">
<h3 class="font-semibold text-slate-950 dark:text-white">Community rating</h3>
<x-rating-summary :ratings="$workOrder->ratings" />