feat: profile_photo en contratos activos, notificaciones bilingues en reportes y foto en panel usuarios

- API contratos activos (cliente y proveedor): agrega campo profile_photo (técnico asignado o supplier, null si no tiene)
- Notificaciones push bilingues en comentarios de reporte: "Nueva actividad en tu reporte" / "New activity on your report"
- Moderador en web panel notifica a cliente y proveedor al comentar, con prefijo Moderador/Moderator
- Panel usuarios: columna de foto de perfil entre ID y Nombre

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-06-23 17:17:14 -06:00
parent 38da1b56ce
commit 0ed98c96ee
40 changed files with 1390 additions and 651 deletions

View File

@@ -9,6 +9,9 @@ use App\Models\FinishedContracts;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Validator;
use OneSignal;
use Stripe\Stripe;
use Stripe\Refund;
class ReportController extends Controller
{
@@ -89,27 +92,47 @@ class ReportController extends Controller
*/
public function veredict(Request $request, $id)
{
//
if ($request->isMethod('get'))
return view('reports.veredict', ['report' => Report::find($id)]);
return view('reports.veredict', ['report' => Report::with('finishedcontracts')->find($id)]);
$rules = [
'veredict' => 'required|string',
'veredict' => 'required|string',
'contract_status' => 'nullable|in:8,9',
];
$messages = [
'veredict.required' => 'Se requiere un veredicto',
'veredict.required' => 'Se requiere un veredicto',
];
$validator = Validator::make($request->all(), $rules, $messages);
if ($validator->fails()) {
return redirect()->back()->withInput($request->all())->withErrors($validator);
return redirect()->back()->withInput($request->all())->withErrors($validator);
}
$report = Report::find($id);
$report = Report::with('finishedcontracts')->find($id);
$report->veredict = strip_tags($request->veredict);
$report->save();
if ($request->filled('contract_status')) {
$contract = $report->finishedcontracts;
$newStatus = (int) $request->contract_status;
if ($newStatus === 8) {
$tid = $contract->transaction_id ?? null;
if ($tid && $tid !== 'NO APPLY' && !str_starts_with($tid, 'BYPASS_')) {
try {
Stripe::setApiKey(env('STRIPE_SECRET'));
Refund::create(['payment_intent' => $tid]);
} catch (\Exception $e) {
// reembolso fallido: se registra pero no bloquea el flujo
}
}
}
$contract->status_id = $newStatus;
$contract->save();
}
return redirect('reports');
}
@@ -208,6 +231,23 @@ class ReportController extends Controller
$isSupplier = $user->id === $supplierUserId;
$isClient = $user->id === $contract->user_id;
$recipientId = $isClient ? $supplierUserId : $contract->user_id;
if ($recipientId) {
try {
OneSignal::sendNotificationCustom([
'include_external_user_ids' => [(string) $recipientId],
'contents' => [
'es' => $user->name . ': ' . $comment->comment,
'en' => $user->name . ': ' . $comment->comment,
],
'headings' => [
'es' => 'Nueva actividad en tu reporte',
'en' => 'New activity on your report',
],
]);
} catch (\Exception $e) {}
}
return response()->json([
'id' => $comment->id,
'sender_id' => $user->id,
@@ -232,6 +272,7 @@ class ReportController extends Controller
$reports = Report::with([
'finishedcontracts.suppliers.user',
'finishedcontracts.categories',
'finishedcontracts.technician.user',
])
->whereIn('contract_id', $contractIds)
->orderBy('created_at', 'desc')
@@ -239,9 +280,12 @@ class ReportController extends Controller
$data = $reports->map(function ($report) {
$contract = $report->finishedcontracts;
$technician = $contract->technical_id
? ($contract->technician->user->name ?? null)
: ($contract->suppliers->user->name ?? null);
return [
'id' => $report->id,
'supplier' => $contract->suppliers->user->name ?? null,
'technician' => $technician,
'company' => $contract->suppliers->company_name ?? null,
'category' => $contract->categories->name ?? null,
'en_category' => $contract->categories->en_name ?? null,