feat: API de reportes, fix distancia espacial y protección OneSignal
- GET/POST /api/contracts/reports y /api/postulations/reports
- GET/POST /api/contracts/reports/{id}/comments con verificación de autoría
- Fix distancia withinDistanceTo: 0.5 grados → 5000 metros (SRID 4326)
- Fix FK Report::finishedcontracts() → contract_id
- Wrap todas las llamadas OneSignal en try/catch para evitar crashes
- Chat UI de comentarios: burbujas por rol, botón veredicto fijo, estilos
- Botón veredicto movido de reports/index a reports/comments
- Ruta POST veredict y restructura reports/{id}/comments en web
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,116 +1,199 @@
|
||||
@extends('layouts.app')
|
||||
|
||||
@section('content')
|
||||
<script src="{{ asset('js/ajaxcrud.js') }}"></script>
|
||||
@if (Auth::user()->role_id >= 5)
|
||||
<div class="container-fluid" style="height:100%">
|
||||
<div>
|
||||
@else
|
||||
<div class="container" style="margin:0 1em">
|
||||
@endif
|
||||
@push('styles')
|
||||
<style>
|
||||
.chat-wrapper {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
padding: 1rem;
|
||||
}
|
||||
.chat-info {
|
||||
background: #f8f9fa;
|
||||
border: 1px solid #dee2e6;
|
||||
border-radius: 8px;
|
||||
padding: 0.75rem 3.5rem 0.75rem 1rem;
|
||||
margin-bottom: 1rem;
|
||||
font-size: 0.85rem;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
gap: 1rem;
|
||||
position: relative;
|
||||
}
|
||||
.chat-info .btn-verdict {
|
||||
position: absolute;
|
||||
right: 1rem;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
}
|
||||
.chat-info span { color: #495057; }
|
||||
.chat-info strong { color: #212529; }
|
||||
.chat-messages {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
padding: 0.5rem;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.75rem;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
.bubble-row {
|
||||
display: flex;
|
||||
align-items: flex-end;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
.bubble-row.right { justify-content: flex-end; }
|
||||
.bubble-row.left { justify-content: flex-start; }
|
||||
.bubble-row.center { justify-content: center; }
|
||||
.bubble {
|
||||
max-width: 60%;
|
||||
padding: 0.6rem 0.9rem;
|
||||
border-radius: 16px;
|
||||
font-size: 0.875rem;
|
||||
line-height: 1.4;
|
||||
position: relative;
|
||||
}
|
||||
.bubble-row.right .bubble {
|
||||
background: #0d6efd;
|
||||
color: #fff;
|
||||
border-bottom-right-radius: 4px;
|
||||
}
|
||||
.bubble-row.left .bubble {
|
||||
background: #198754;
|
||||
color: #fff;
|
||||
border-bottom-left-radius: 4px;
|
||||
}
|
||||
.bubble-row.center .bubble {
|
||||
background: #e9ecef;
|
||||
color: #495057;
|
||||
border-radius: 16px;
|
||||
font-style: italic;
|
||||
text-align: center;
|
||||
max-width: 70%;
|
||||
}
|
||||
.bubble-meta {
|
||||
font-size: 0.7rem;
|
||||
margin-top: 0.25rem;
|
||||
opacity: 0.75;
|
||||
}
|
||||
.bubble-row.right .bubble-meta { text-align: right; color: rgba(255,255,255,0.85); }
|
||||
.bubble-row.left .bubble-meta { text-align: left; color: rgba(255,255,255,0.85); }
|
||||
.bubble-row.center .bubble-meta { text-align: center; color: #6c757d; }
|
||||
.bubble-label {
|
||||
font-size: 0.7rem;
|
||||
font-weight: 600;
|
||||
margin-bottom: 0.2rem;
|
||||
opacity: 0.85;
|
||||
}
|
||||
.chat-input {
|
||||
border-top: 1px solid #dee2e6;
|
||||
padding-top: 0.75rem;
|
||||
}
|
||||
.chat-input form {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
align-items: flex-start;
|
||||
}
|
||||
.chat-input textarea {
|
||||
flex: 1;
|
||||
resize: none;
|
||||
padding: 0.5rem 1rem;
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
.chat-input button {
|
||||
border-radius: 20px;
|
||||
padding: 0.5rem 1.25rem;
|
||||
}
|
||||
.verdict-badge {
|
||||
background: #fff3cd;
|
||||
border: 1px solid #ffc107;
|
||||
border-radius: 8px;
|
||||
padding: 0.5rem 1rem;
|
||||
font-size: 0.85rem;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
</style>
|
||||
@endpush
|
||||
|
||||
<div class="row">
|
||||
<div class="col-sm-7">
|
||||
<h3>Comentarios del reporte</h3>
|
||||
</div>
|
||||
@section('content')
|
||||
@php
|
||||
$clientId = $contract->user_id;
|
||||
$supplierId = $contract->suppliers->user_id ?? null;
|
||||
@endphp
|
||||
|
||||
<div class="chat-wrapper">
|
||||
|
||||
{{-- Info del contrato --}}
|
||||
<div class="chat-info">
|
||||
<span><strong>Contrato #{{ $contract->id }}</strong></span>
|
||||
<span>Cliente: <strong>{{ $contract->user->name ?? '—' }}</strong></span>
|
||||
<span>Proveedor: <strong>{{ $contract->suppliers->company_name ?? '—' }}</strong></span>
|
||||
<span>Categoría: <strong>{{ $contract->categories->name ?? '—' }}</strong></span>
|
||||
<span>Monto: <strong>${{ $contract->amount }}</strong></span>
|
||||
<span>Cita: <strong>{{ $contract->appointment }}</strong></span>
|
||||
@if($report->veredict)
|
||||
<span>Veredicto: <strong>{{ $report->veredict }}</strong></span>
|
||||
@endif
|
||||
<a class="btn btn-secondary btn-xs btn-verdict" title="Veredict"
|
||||
href="{{ url('reports/veredict/' . $report->id) }}">
|
||||
<i class="fa fa-gavel"></i>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<br>
|
||||
<h5>Contrato:</h5>
|
||||
<table class="table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th style="vertical-align: middle">ID</th>
|
||||
{{-- Leyenda --}}
|
||||
<div class="d-flex gap-3 mb-2" style="font-size:0.78rem; gap:1rem;">
|
||||
<span><span style="display:inline-block;width:12px;height:12px;background:#0d6efd;border-radius:3px;"></span> Cliente</span>
|
||||
<span><span style="display:inline-block;width:12px;height:12px;background:#198754;border-radius:3px;"></span> Proveedor</span>
|
||||
<span><span style="display:inline-block;width:12px;height:12px;background:#e9ecef;border:1px solid #ccc;border-radius:3px;"></span> Moderador</span>
|
||||
</div>
|
||||
|
||||
<th style="vertical-align: middle">Usuario</th>
|
||||
{{-- Mensajes --}}
|
||||
<div class="chat-messages">
|
||||
@forelse($comments as $comment)
|
||||
@php
|
||||
if ($comment->user_id == $clientId) {
|
||||
$side = 'right';
|
||||
$label = 'Cliente';
|
||||
} elseif ($comment->user_id == $supplierId) {
|
||||
$side = 'left';
|
||||
$label = 'Proveedor';
|
||||
} else {
|
||||
$side = 'center';
|
||||
$label = 'Moderador';
|
||||
}
|
||||
@endphp
|
||||
|
||||
<th style="vertical-align: middle">Proveedor</th>
|
||||
<div class="bubble-row {{ $side }}">
|
||||
<div>
|
||||
<div class="bubble-label text-muted">{{ $label }} — {{ $comment->user->name ?? '—' }}</div>
|
||||
<div class="bubble">
|
||||
{{ $comment->comment }}
|
||||
<div class="bubble-meta">{{ $comment->created_at->format('d/m/Y H:i') }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@empty
|
||||
<p class="text-center text-muted mt-4">Sin comentarios aún.</p>
|
||||
@endforelse
|
||||
</div>
|
||||
|
||||
<th style="vertical-align: middle">Categoría</th>
|
||||
{{-- Paginación --}}
|
||||
@if($comments->hasPages())
|
||||
<div class="mb-2">{{ $comments->links() }}</div>
|
||||
@endif
|
||||
|
||||
<th style="vertical-align: middle">Dirección</th>
|
||||
{{-- Input del moderador --}}
|
||||
<div class="chat-input">
|
||||
<form method="POST" action="{{ url('reports/' . $report->id . '/comments') }}">
|
||||
@csrf
|
||||
@error('comment')
|
||||
<div class="text-danger mb-1" style="font-size:0.8rem;">{{ $message }}</div>
|
||||
@enderror
|
||||
<textarea name="comment" rows="2" class="form-control" placeholder="Escribir comentario como moderador..."></textarea>
|
||||
<button type="submit" class="btn btn-primary mt-2">Enviar</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<th style="vertical-align: middle">Cita</th>
|
||||
|
||||
<th style="vertical-align: middle">Monto</th>
|
||||
|
||||
<th style="vertical-align: middle">Calificación</th>
|
||||
|
||||
<th style="vertical-align: middle">Comentarios</th>
|
||||
|
||||
<th style="vertical-align: middle">Transacción ID</th>
|
||||
|
||||
<th style="vertical-align: middle">Fecha de creación</th>
|
||||
|
||||
<th style="vertical-align: middle">Fecha de fuera de casa</th>
|
||||
|
||||
<th style="vertical-align: middle">Confirmación de descripción de la casa</th>
|
||||
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@php
|
||||
$i=1;
|
||||
@endphp
|
||||
<tr>
|
||||
<th>{{ $contract->id }}</th>
|
||||
<td>{{ $contract->user->name ?? null }}</td>
|
||||
<td>{{ $contract->suppliers->company_name ?? null }}</td>
|
||||
<td>{{ $contract->categories->name ?? null }}</td>
|
||||
<td>{{ $contract->address }}</td>
|
||||
<td>{{ $contract->appointment }}</td>
|
||||
<td>${{ $contract->amount }}</td>
|
||||
<td>{{ $contract->score }}</td>
|
||||
<td>{{ $contract->comments }}</td>
|
||||
<td>{{ $contract->transaction_id }}</td>
|
||||
<td>{{ $contract->created_at }}</td>
|
||||
<td>{{ isset($nohome->confirmed_at) }}</td>
|
||||
<td>{{ isset($nohome->house_description) }}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<br><br>
|
||||
<h5>Comentarios:</h5>
|
||||
<table class="table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th style="vertical-align: middle"><a href="{{url('comments?field=user_id&sort='.(request()->session()->get('sort')=='asc'?'desc':'asc'))}}">Usuario</a></th>
|
||||
{{request()->session()->get('field')=='user_id'?(request()->session()->get('sort')=='asc'?'':''):''}}
|
||||
|
||||
<th style="vertical-align: middle"><a href="{{url('comments?field=user_id&sort='.(request()->session()->get('sort')=='asc'?'desc':'asc'))}}">Teléfono</a></th>
|
||||
{{request()->session()->get('field')=='user_id'?(request()->session()->get('sort')=='asc'?'':''):''}}
|
||||
|
||||
<th style="vertical-align: middle"><a href="{{url('comments?field=comment&sort='.(request()->session()->get('sort')=='asc'?'desc':'asc'))}}">Comentario</a></th>
|
||||
{{request()->session()->get('field')=='comment'?(request()->session()->get('sort')=='asc'?'':''):''}}
|
||||
|
||||
<th style="vertical-align: middle"><a href="{{url('contracts?field=created_at&sort='.(request()->session()->get('sort')=='asc'?'desc':'asc'))}}">Fecha de creación</a></th>
|
||||
{{request()->session()->get('field')=='created_at'?(request()->session()->get('sort')=='asc'?'':''):''}}
|
||||
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@php
|
||||
$i=1;
|
||||
@endphp
|
||||
@foreach ($comments as $comment)
|
||||
<tr>
|
||||
<td>{{ $comment->user->name }}</td>
|
||||
<td>{{ $comment->user->phone }}</td>
|
||||
<td>{{ $comment->comment }}</td>
|
||||
<td>{{ $comment->created_at }}</td>
|
||||
<td style="width: 3em">
|
||||
<input type="hidden" name="_method" value="delete"/>
|
||||
<a class="btn btn-danger btn-xs" title="Delete"
|
||||
href="javascript:if(confirm('¿Estás seguro de que quieres eliminar este comentario?')) javascript:if(confirm('Usualmente no se deben eliminar comentarios, solo editarlos ¿Estás seguro?')) ajaxDeleteComments('{{url('reports/comments/delete/'.$comment->id.'/'.$contract->id)}}','{{csrf_token()}}')">
|
||||
<i class="fa fa-trash"></i>
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
@endforeach
|
||||
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
{{ $comments->links() }}
|
||||
</div>
|
||||
@endsection
|
||||
|
||||
Reference in New Issue
Block a user