Compare commits

..

5 Commits

Author SHA1 Message Date
700ddafba4 feat: branch betos — propiedades, oferta de proveedores, chat en contratos y flujo de postulacion renovado
- Sistema de propiedades: tabla, modelo, CRUD API para que el cliente guarde direcciones nombradas
- Postulacion renovada: cliente elige propiedad sin precio ni fecha, sube fotos a GCS/S3
- Proveedores ofertan date_1, date_2 y amount al postularse via pivot; sort por fecha/monto/membresia
- Contratacion: selected_date recibe date_1 o date_2, amount desde pivot (depreca minimun_fee)
- Cancelacion/repostulacion: archive con status=perdido, related_postulation_id para vincular
- Chat en contratos activos: tabla contract_comments, controller, rutas web+API, vista panel, notificaciones bilingues
- PaymentIntent calcula amount desde pivot en vez de recibirlo del front
- getpendingcontracts: sin filtro de tiempo, filtra por status=active, agrega time_created
- Expiracion de postulaciones reducida a 1000 min

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-07-03 11:26:10 -06:00
0ed98c96ee 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>
2026-06-23 17:17:14 -06:00
38da1b56ce 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>
2026-06-18 21:04:14 -06:00
185da1edbb Fix validación en hero() y CORS para ngrok
- SupplierController: corregir respuesta de error del validador, bank_account acepta numeric, rfc/bank/bank_account required al crear
- Cors: permitir header ngrok-skip-browser-warning

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-18 09:46:47 -06:00
47f211e4ab Fix validación en hero() y CORS para ngrok
- SupplierController: corregir respuesta de error del validador, bank_account acepta numeric, rfc/bank/bank_account required al crear
- Cors: permitir header ngrok-skip-browser-warning

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-18 09:45:50 -06:00
60 changed files with 3124 additions and 1182 deletions

BIN
.rnd

Binary file not shown.

View File

@@ -40,6 +40,7 @@ class AuthController extends Controller
'role' => $user->role_id,
'verified' => $user->phone_verified_at,
'reported' => $user->reported,
'profile_photo' => $user->profile_photo,
]);
}
@@ -56,6 +57,7 @@ class AuthController extends Controller
$uid = $verifiedToken->claims()->get('sub');
$email = $verifiedToken->claims()->get('email');
$name = $verifiedToken->claims()->get('name') ?? 'Usuario';
$picture = $verifiedToken->claims()->get('picture');
// Buscar por firebase uid primero, luego por email para hacer merge si ya existía cuenta
$user = User::where('social_id', 'firebase|' . $uid)->first();
@@ -65,18 +67,24 @@ class AuthController extends Controller
}
if ($user) {
// Vincular uid de Firebase si aún no lo tiene (merge de cuenta existente)
$changed = false;
if (!$user->social_id) {
$user->social_id = 'firebase|' . $uid;
$user->save();
$changed = true;
}
if ($picture && $user->profile_photo !== $picture) {
$user->profile_photo = $picture;
$changed = true;
}
if ($changed) $user->save();
} else {
$user = User::create([
'name' => $name,
'email' => $email,
'social_id'=> 'firebase|' . $uid,
'social_id' => 'firebase|' . $uid,
'role_id' => 1,
'password' => null,
'profile_photo' => $picture,
]);
}
@@ -90,6 +98,7 @@ class AuthController extends Controller
'role' => $user->role_id,
'verified' => $user->phone_verified_at,
'reported' => $user->reported,
'profile_photo' => $user->profile_photo,
]);
}

View File

@@ -0,0 +1,132 @@
<?php
namespace App\Http\Controllers;
use OneSignal;
use App\Models\Suppliers;
use App\Models\ContractComment;
use App\Models\CurrentContracts;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Validator;
class ContractCommentController extends Controller
{
public function index(Request $request, $id)
{
$contract = CurrentContracts::with(['user', 'suppliers.user', 'categories', 'status'])->find($id);
if (!$contract) {
abort(404);
}
$comments = ContractComment::with('user')
->where('contract_id', $id)
->orderBy('created_at', 'asc')
->paginate(50);
return view('currentcontracts.comments', compact('comments', 'contract'));
}
public function store(Request $request, $id)
{
$validator = Validator::make($request->all(), [
'comment' => 'required|string',
]);
if ($validator->fails()) {
if ($request->expectsJson()) {
return response()->json($validator->errors(), 422);
}
return redirect()->back()->withErrors($validator);
}
$contract = CurrentContracts::find($id);
if (!$contract) {
if ($request->expectsJson()) {
return response()->json(['message' => 'Contrato no encontrado'], 404);
}
abort(404);
}
$comment = new ContractComment();
$comment->contract_id = $id;
$comment->user_id = Auth::id();
$comment->comment = strip_tags($request->comment);
$comment->save();
$supplier = Suppliers::find($contract->supplier_id);
$isClient = Auth::id() === $contract->user_id;
$recipientId = $isClient ? ($supplier->user_id ?? null) : $contract->user_id;
if ($recipientId) {
try {
OneSignal::sendNotificationCustom([
'include_external_user_ids' => [(string) $recipientId],
'contents' => [
'es' => Auth::user()->name . ': ' . $comment->comment,
'en' => Auth::user()->name . ': ' . $comment->comment,
],
'headings' => [
'es' => 'Nuevo mensaje en tu contrato',
'en' => 'New message on your contract',
],
]);
} catch (\Exception $e) {}
}
if ($request->expectsJson()) {
return response()->json([
'id' => $comment->id,
'sender_id' => Auth::id(),
'sender_name'=> Auth::user()->name,
'comment' => $comment->comment,
'created_at' => $comment->created_at,
], 201);
}
return redirect()->back();
}
public function apiIndex(Request $request, $id)
{
$user = Auth::user();
$contract = CurrentContracts::with('suppliers')->find($id);
if (!$contract) {
return response()->json(['message' => 'Contrato no encontrado'], 404);
}
$supplier = Suppliers::find($contract->supplier_id);
$supplierUserId = $supplier->user_id ?? null;
if ($user->id !== $contract->user_id && $user->id !== $supplierUserId) {
return response()->json(['message' => 'No autorizado'], 403);
}
$comments = ContractComment::with('user')
->where('contract_id', $id)
->orderBy('created_at', 'asc')
->get()
->map(function ($c) use ($contract, $supplierUserId) {
$isSupplier = $c->user_id === $supplierUserId;
return [
'id' => $c->id,
'sender_id' => $c->user_id,
'sender_name' => $c->user->name ?? null,
'role_id' => $c->user->role_id ?? null,
'comment' => $c->comment,
'created_at' => $c->created_at,
];
});
return response()->json($comments);
}
public function destroy($id, $comment_id)
{
ContractComment::destroy($comment_id);
return redirect('currentcontracts/' . $id . '/comments');
}
}

View File

@@ -17,6 +17,7 @@ use App\Models\iChambaParameter;
use App\Models\Suppliers;
use App\Models\Categories;
use App\Models\Cards;
use App\Models\Technician;
use App\Models\Postulations;
use App\Models\CurrentContracts;
use App\Models\FinishedContracts;
@@ -159,28 +160,15 @@ class ContractController extends Controller
public function create(Request $request) {
// Si el bypass está activo, usar reglas relajadas
$paymentBypass = env('PAYMENT_BYPASS', false);
if ($paymentBypass) {
$rules = [
'postulation_id' => 'required|numeric',
'supplier_id' => 'required|numeric',
'card_id' => 'required|string',
'code' => 'required|string',
'device_id' => 'required|string',
'selected_date' => 'required|in:date_1,date_2',
'payment_intent_id' => $paymentBypass ? 'nullable|string' : 'required|string',
'coupon' => 'nullable|string',
];
} else {
$rules = [
'postulation_id' => 'required|numeric',
'supplier_id' => 'required|numeric',
'card_id' => 'required|numeric',
'code' => 'required|numeric',
'device_id' => 'required|string|regex:/(^[A-Za-z0-9 ]+$)+/',
'coupon' => 'nullable|string|regex:/(^[A-Za-z0-9 ]+$)+/',
];
}
$validator = Validator::make($request->all(), $rules);
if ($validator->fails()) {
@@ -195,16 +183,8 @@ class ContractController extends Controller
$postulation = Postulations::where('id', $request->postulation_id)->first();
$coupon = Coupon::where('name', $request->coupon)->first();
if (!$paymentBypass) {
Openpay::setProductionMode(true);
}
if ($user->id == $postulation->user_id) {
$card = null;
if (!$paymentBypass && $request->card_id) {
$card = Cards::where('id', $request->card_id)->first();
}
$supplier = Suppliers::where('id', $request->supplier_id)->first();
$IVA = iChambaParameter::where('id', $supplier->IVA_id)->first();
@@ -213,8 +193,15 @@ class ContractController extends Controller
$ichambafee = iChambaParameter::where('parameter', 'ichamba_fee')->first();
$category = Categories::where('id', $postulation->category_id)->first();
// En modo bypass, saltar la validación de tarjeta
if ($paymentBypass || ($card && $card->user_id == $user->id)) {
// Tomar amount del pivot (oferta del proveedor elegido por el cliente)
$pivotSupplier = $postulation->suppliers()
->where('suppliers.id', $request->supplier_id)
->first();
$minFee = 150;
$pivotAmount = $pivotSupplier ? ($pivotSupplier->pivot->amount ?? $minFee) : $minFee;
$finalAmount = max($pivotAmount, $minFee);
if (true) { // autorización verificada arriba con user_id == postulation->user_id
$contract = new CurrentContracts();
$contract->user_id = $postulation->user_id;
@@ -228,16 +215,17 @@ class ContractController extends Controller
$contract->int_number = 0;
}
$contract->references = $postulation->references;
$contract->appointment = $postulation->appointment;
$contract->amount = ($supplier->minimun_fee < 150 ? 150 : $supplier->minimun_fee);
$selectedDate = $request->selected_date;
$contract->appointment = Carbon::parse($pivotSupplier->pivot->$selectedDate);
$contract->amount = $finalAmount;
if (isset($IVA->num_value) && isset($IVA->num_value)) {
$contract->IVA = $IVA->num_value;
$contract->ISR = $ISR->num_value;
$contract->revenue = ((($supplier->minimun_fee < 150 ? 150 : $supplier->minimun_fee) * ((100 - $ichambafee->num_value) / 100)) * ((100 - $IVA->num_value - $ISR->num_value) / 100));
$contract->revenue = (($finalAmount * ((100 - $ichambafee->num_value) / 100)) * ((100 - $IVA->num_value - $ISR->num_value) / 100));
} else {
$contract->IVA = 0;
$contract->ISR = 0;
$contract->revenue = (($supplier->minimun_fee < 150 ? 150 : $supplier->minimun_fee) * ((100 - $ichambafee->num_value) / 100));
$contract->revenue = ($finalAmount * ((100 - $ichambafee->num_value) / 100));
}
$contract->ichamba_fee = $ichambafee->num_value;
$contract->details = $postulation->details;
@@ -251,90 +239,37 @@ class ContractController extends Controller
if ($coupon->limit > 0) {
if(!isset($checkccontracts) && !isset($checkccontracts)) {
$fee = ($supplier->minimun_fee < 150 ? 150 : $supplier->minimun_fee);
$fee = $finalAmount;
$discount = (($fee*(($coupon->percentage = null ? 0 : $coupon->percentage)/100))+($coupon->amount = null ? 0 : $coupon->amount));
$contract->coupon_id = $coupon->id;
// Solo crear chargeData si no estamos en bypass mode
if (!$paymentBypass && $card) {
$chargeData = array(
'source_id' => $card->token,
'method' => 'card',
'amount' => ((($supplier->minimun_fee < 150 ? 150 : $supplier->minimun_fee)*(1 - (($coupon->percentage = null ? 0 : $coupon->percentage)/100)))-($coupon->amount = null ? 0 : $coupon->amount)),
'description' => ('Contrato del usuario: ' . $user->name . ' del servicio ' . $category->name . ' realizado por el proveedor: ' . $supplier->company_name),
'device_session_id' => $request->device_id,
'cvv2' => $request->code
);
}
}
}
} else {
$fee = ($supplier->minimun_fee < 150 ? 150 : $supplier->minimun_fee);
$fee = $finalAmount;
$discount = 0;
$contract->coupon_id = null;
// Solo crear chargeData si no estamos en bypass mode
if (!$paymentBypass && $card) {
$chargeData = array(
'source_id' => $card->token,
'method' => 'card',
'amount' => ($supplier->minimun_fee < 150 ? 150 : $supplier->minimun_fee),
'description' => ('Contrato del usuario: ' . $user->name . ' del servicio ' . $category->name . ' realizado por el proveedor: ' . $supplier->company_name),
'device_session_id' => $request->device_id,
'cvv2' => $request->code
);
}
}
if (!empty($request->card_id) && !empty($request->device_id) && !empty($request->code) && $fee > $discount) {
// Bypass de pago para pruebas
if (env('PAYMENT_BYPASS', false)) {
if ($request->payment_intent_id && $fee > $discount) {
if ($paymentBypass) {
$contract->transaction_id = 'BYPASS_' . uniqid();
} else {
\Stripe\Stripe::setApiKey(env('STRIPE_SECRET'));
try {
$openpay = Openpay::getInstance(config('app.openpay_id'), config('app.openpay_apikey'));
$customer = $openpay->customers->get($user->openpay_id);
$charge = $customer->charges->create($chargeData);
} catch (OpenpayApiTransactionError $e) {
return response()->json([
'type' => 'error',
'message' => 'No se pudo procesar la transacción'
]);
} catch (OpenpayApiRequestError $e) {
return response()->json([
'type' => 'error',
'message' => 'No se pudo procesar la operación'
]);
} catch (OpenpayApiConnectionError $e) {
return response()->json([
'type' => 'error',
'message' => 'Error al conectarse a Openpay:' . $e->getMessage()
]);
} catch (OpenpayApiAuthError $e) {
return response()->json([
'type' => 'error',
'message' => 'Error al conectarse a Openpay' . $e->getMessage()
]);
} catch (OpenpayApiError $e) {
return response()->json([
'type' => 'error',
'message' => 'Error al conectarse a Openpay' . $e->getMessage()
]);
} catch (Exception $e) {
return response()->json([
'type' => 'error',
'message' => 'Error: ' . $e->getMessage()
]);
$intent = \Stripe\PaymentIntent::retrieve($request->payment_intent_id);
} catch (\Exception $e) {
return response()->json(['type' => 'error', 'message' => 'PaymentIntent inválido'], 422);
}
$contract->transaction_id = $charge->id;
if ($intent->status !== 'succeeded') {
return response()->json(['type' => 'error', 'message' => 'El pago no ha sido confirmado'], 422);
}
$contract->transaction_id = $intent->id;
}
} else if ($coupon) {
@@ -356,22 +291,16 @@ class ContractController extends Controller
Postulations::destroy($request->postulation_id);
//Notify the suppliers that they have been hired
try {
OneSignal::sendNotificationToExternalUser(
"Dirígete a la sección de postulaciones contratadas en la app para ver más detalles",
(string) $supplier->user_id,
null, null, null, null,
"Proveedor: has sido contratado"
);
// TODO: Configurar WhatsApp cuando esté disponible
// Whatsapp::send($supplier->user->phone, Messages\TemplateMessage::create()
// ->name('suppplier_hired')
// ->language('es_US')
// ->body(Messages\Components\Body::create([
// Messages\Components\Parameters\Text::create('Proveedor has sido contratado: dirígete a la sección de postulaciones contratadas en JobHero para ver más detalles'),
// ])));
} catch (\Exception $e) {}
//Schedule a notification for the suppliers about their appointment
try {
OneSignal::sendNotificationToExternalUser(
"Tienes un servicio en " . $contract->address . " hoy en 30 minutos. Dirígete a la sección de postulaciones contratados para más detalles",
(string) $supplier->user_id,
@@ -379,16 +308,9 @@ class ContractController extends Controller
$delay_UTC,
"Proveedor, no olvides tu cita de hoy"
);
// TODO: Configurar WhatsApp cuando esté disponible
// Whatsapp::send($supplier->user->phone, Messages\TemplateMessage::create()
// ->name('suppplier_appointment')
// ->language('es_US')
// ->body(Messages\Components\Body::create([
// Messages\Components\Parameters\Text::create('Proveedor no olvides tu cita de hoy: Tienes un servicio en ' . $contract->address . ' hoy en 30 minutos. Dírigeta a la sección de postulaciones contratados para más detalles'),
// ])));
} catch (\Exception $e) {}
//Schedule a notification for the users about their appointment
try {
OneSignal::sendNotificationToExternalUser(
"Tienes un servicio agendado hoy en " . $contract->address . " en 30 minutos. Dirígete a la sección de contratos confirmados para más detalles",
(string) $user->id,
@@ -396,13 +318,7 @@ class ContractController extends Controller
$delay_UTC,
$user->name . ", no olvides tu cita de hoy"
);
// TODO: Configurar WhatsApp cuando esté disponible
// Whatsapp::send($user->phone, Messages\TemplateMessage::create()
// ->name('user_appointment')
// ->language('es_US')
// ->body(Messages\Components\Body::create([
// Messages\Components\Parameters\Text::create($user->name . ' no olvides tu cita de hoy: Tienes un servicio agendado hoy en ' . $contract->address . ' en 30 minutos. Dírigeta a la sección de contratos confirmados para más detalles'),
// ])));
} catch (\Exception $e) {}
return response()->json([
'message' => 'Servicio contratado exitosamente'
@@ -417,6 +333,7 @@ class ContractController extends Controller
$rules = [
'postulation_id' => 'required|numeric',
'supplier_id' => 'required|numeric',
'selected_date' => 'required|in:date_1,date_2',
'coupon' => 'required|string|regex:/(^[A-Za-z0-9 ]+$)+/',
];
@@ -459,16 +376,23 @@ class ContractController extends Controller
$contract->int_number = 0;
}
$contract->references = $postulation->references;
$contract->appointment = $postulation->appointment;
$contract->amount = ($supplier->minimun_fee < 150 ? 150 : $supplier->minimun_fee);
$pivotForCoupon = $postulation->suppliers()->where('suppliers.id', $request->supplier_id)->first();
$couponDate = $request->selected_date;
$contract->appointment = $pivotForCoupon
? Carbon::parse($pivotForCoupon->pivot->$couponDate)
: Carbon::now();
$minFeeC = 150;
$pivotAmountC = $pivotForCoupon ? ($pivotForCoupon->pivot->amount ?? $minFeeC) : $minFeeC;
$finalAmountC = max($pivotAmountC, $minFeeC);
$contract->amount = $finalAmountC;
if (isset($IVA->num_value) && isset($IVA->num_value)) {
$contract->IVA = $IVA->num_value;
$contract->ISR = $ISR->num_value;
$contract->revenue = ((($supplier->minimun_fee < 150 ? 150 : $supplier->minimun_fee) * ((100 - $ichambafee->num_value) / 100)) * ((100 - $IVA->num_value - $ISR->num_value) / 100));
$contract->revenue = (($finalAmountC * ((100 - $ichambafee->num_value) / 100)) * ((100 - $IVA->num_value - $ISR->num_value) / 100));
} else {
$contract->IVA = 0;
$contract->ISR = 0;
$contract->revenue = (($supplier->minimun_fee < 150 ? 150 : $supplier->minimun_fee) * ((100 - $ichambafee->num_value) / 100));
$contract->revenue = ($finalAmountC * ((100 - $ichambafee->num_value) / 100));
}
$contract->ichamba_fee = $ichambafee->num_value;
$contract->details = $postulation->details;
@@ -483,22 +407,16 @@ class ContractController extends Controller
Postulations::destroy($request->postulation_id);
//Notify the suppliers that they have been hired
try {
OneSignal::sendNotificationToExternalUser(
"Dirígete a la sección de postulaciones contratadas en la app para ver más detalles",
(string) $supplier->user_id,
null, null, null, null,
"Proveedor: has sido contratado"
);
// TODO: Configurar WhatsApp cuando esté disponible
// Whatsapp::send($supplier->user->phone, Messages\TemplateMessage::create()
// ->name('suppplier_hired')
// ->language('es_US')
// ->body(Messages\Components\Body::create([
// Messages\Components\Parameters\Text::create('Proveedor has sido contratado: dirígete a la sección de postulaciones contratadas en JobHero para ver más detalles'),
// ])));
} catch (\Exception $e) {}
//Schedule a notification for the suppliers about their appointment
try {
OneSignal::sendNotificationToExternalUser(
"Tienes un servicio en " . $contract->address . " hoy en 30 minutos. Dirígete a la sección de postulaciones contratados para más detalles",
(string) $supplier->user_id,
@@ -506,16 +424,9 @@ class ContractController extends Controller
$delay_UTC,
"Proveedor, no olvides tu cita de hoy"
);
// TODO: Configurar WhatsApp cuando esté disponible
// Whatsapp::send($supplier->user->phone, Messages\TemplateMessage::create()
// ->name('suppplier_appointment')
// ->language('es_US')
// ->body(Messages\Components\Body::create([
// Messages\Components\Parameters\Text::create('Proveedor no olvides tu cita de hoy: Tienes un servicio en ' . $contract->address . ' hoy en 30 minutos. Dírigeta a la sección de postulaciones contratados para más detalles'),
// ])));
} catch (\Exception $e) {}
//Schedule a notification for the users about their appointment
try {
OneSignal::sendNotificationToExternalUser(
"Tienes un servicio agendado hoy en " . $contract->address . " en 30 minutos. Dirígete a la sección de contratos confirmados para más detalles",
(string) $user->id,
@@ -523,6 +434,7 @@ class ContractController extends Controller
$delay_UTC,
$user->name . ", no olvides tu cita de hoy"
);
} catch (\Exception $e) {}
// TODO: Configurar WhatsApp cuando esté disponible
// Whatsapp::send($user->phone, Messages\TemplateMessage::create()
// ->name('user_appointment')
@@ -643,7 +555,8 @@ class ContractController extends Controller
public function getcurrentcontracts(Request $request) {
$user = Auth::user();
$ccontracts = CurrentContracts::where('user_id', $user->id)->orderBy('created_at', 'DESC')->get();
$ccontracts = CurrentContracts::with(['technician.user'])
->where('user_id', $user->id)->orderBy('created_at', 'DESC')->get();
$currentcontracts = array();
foreach($ccontracts as $ccontract) {
@@ -651,6 +564,12 @@ class ContractController extends Controller
$supplier = Suppliers::where('id', $ccontract->supplier_id)->first();
$time_limit = Carbon::parse($ccontract->appointment);
$day_limit = Carbon::parse($ccontract->created_at);
$technician_name = $ccontract->technical_id
? ($ccontract->technician->user->name ?? null)
: ($supplier ? ($supplier->user->name ?? null) : null);
$profile_photo = $ccontract->technical_id
? ($ccontract->technician->user->profile_photo ?? null)
: ($supplier ? ($supplier->user->profile_photo ?? null) : null);
$currentcontractinfo = array(
'id' => $ccontract->id,
'phone' => $supplier ? ($supplier->user ? $supplier->user->phone : null) : null,
@@ -659,6 +578,8 @@ class ContractController extends Controller
'address' => $ccontract->address,
'date' => $ccontract->appointment,
'supplier' => $supplier ? $supplier->company_name : 'Proveedor no disponible',
'technician' => $technician_name,
'profile_photo' => $profile_photo,
'status' => $ccontract->status_id,
'amount' => $ccontract->amount,
'code' => $ccontract->code,
@@ -691,48 +612,14 @@ class ContractController extends Controller
$time_limit = Carbon::parse($ccontract->appointment);
if ($time_limit->diffInHours(Carbon::now()) >= 24) {
if($ccontract->transaction_id != 'NO APPLY') {
if ($ccontract->transaction_id !== 'NO APPLY' && !str_starts_with($ccontract->transaction_id, 'BYPASS_')) {
try {
$openpay = Openpay::getInstance(config('app.openpay_id'), config('app.openpay_apikey'));
$refundData = array(
'description' => 'Reembolso del contrato con id: ' . $ccontract->id . ', del usuario ' . $user->name . '. Con proveedor: ' . $supplier->id,
);
$customer = $openpay->customers->get($user->openpay_id);
$charge = $customer->charges->get($ccontract->transaction_id);
$charge->refund($refundData);
} catch (OpenpayApiTransactionError $e) {
\Stripe\Stripe::setApiKey(env('STRIPE_SECRET'));
\Stripe\Refund::create(['payment_intent' => $ccontract->transaction_id]);
} catch (\Stripe\Exception\ApiErrorException $e) {
return response()->json([
'type' => 'error',
'message' => 'No se pudo procesar la transacción'
]);
} catch (OpenpayApiRequestError $e) {
return response()->json([
'type' => 'error',
'message' => 'No se pudo procesar la operación'
]);
} catch (OpenpayApiConnectionError $e) {
return response()->json([
'type' => 'error',
'message' => 'Error al conectarse a Openpay:' . $e->getMessage()
]);
} catch (OpenpayApiAuthError $e) {
return response()->json([
'type' => 'error',
'message' => 'Error al conectarse a Openpay' . $e->getMessage()
]);
} catch (OpenpayApiError $e) {
return response()->json([
'type' => 'error',
'message' => 'Error al conectarse a Openpay' . $e->getMessage()
]);
} catch (Exception $e) {
return response()->json([
'type' => 'error',
'message' => 'Error: ' . $e->getMessage()
'message' => 'No se pudo procesar el reembolso: ' . $e->getMessage()
]);
}
}
@@ -754,18 +641,21 @@ class ContractController extends Controller
$fcontract->revenue = $ccontract->revenue;
$fcontract->details = $ccontract->details;
$fcontract->en = $ccontract->en;
$fcontract->transaction_id = (!empty($charge->id) ? $charge->id : $ccontract->transaction_id);
$fcontract->transaction_id = $ccontract->transaction_id;
$fcontract->technical_id = $ccontract->technical_id;
$fcontract->status_id = 4;
$fcontract->save();
CurrentContracts::destroy($request->contract_id);
try {
OneSignal::sendNotificationToExternalUser(
"El servicio en " . $fcontract->address . " el día " . substr($fcontract->appointment, 0, 10) . "ha sido cancelado. Dírigeta a la sección de servicios contratados para más detalles",
(string) $supplier->user_id,
null, null, null, null,
"Proveedor: un servicio ha sido cancelado"
);
} catch (\Exception $e) {}
return response()->json([
'message' => 'Servicio cancelado exitosamente'
@@ -787,15 +677,27 @@ class ContractController extends Controller
$user = Auth::user();
$supplier = $user->suppliers;
$technician = null;
if (!$supplier) {
$technician = Technician::where('user_id', $user->id)->first();
if (!$technician) {
return response()->json([
'success' => false,
'message' => 'No tienes un perfil de proveedor registrado'
'message' => 'No tienes un perfil de proveedor o técnico registrado'
], 400);
}
$supplier = $technician->supplier;
}
$ccontract = CurrentContracts::where('code', $request->contract_pin)->where('supplier_id', $supplier->id)->first();
$ccontract = $technician
? CurrentContracts::where('code', $request->contract_pin)
->where('supplier_id', $technician->supplier_id)
->where('technical_id', $technician->id)
->first()
: CurrentContracts::where('code', $request->contract_pin)
->where('supplier_id', $supplier->id)
->first();
if($ccontract) {
@@ -819,6 +721,7 @@ class ContractController extends Controller
$fcontract->en = $ccontract->en;
$fcontract->coupon_id = $ccontract->coupon_id;
$fcontract->transaction_id = $ccontract->transaction_id;
$fcontract->technical_id = $ccontract->technical_id;
$fcontract->status_id = 3;
$fcontract->score = 5;
$fcontract->save();
@@ -836,12 +739,14 @@ class ContractController extends Controller
$payment->status_id = null;
$payment->save();
try {
OneSignal::sendNotificationToExternalUser(
"El servicio en " . $fcontract->address . " el día " . substr($fcontract->appointment, 0, 10) . " ha sido iniciado. Dírigeta a la sección de servicios contratados para más detalles",
(string) $fcontract->user_id,
null, null, null, null,
"Usuario: el proveedor ha iniciado el servicio"
);
} catch (\Exception $e) {}
return response()->json([
'message' => 'Servicio iniciado exitosamente'
@@ -971,14 +876,17 @@ class ContractController extends Controller
public function getfinishedcontracts(Request $request) {
$user = Auth::user();
$fcontracts = FinishedContracts::where('user_id', $user->id)->orderBy('created_at', 'DESC')->get();
$fcontracts = FinishedContracts::with(['technician.user', 'status'])
->where('user_id', $user->id)->orderBy('created_at', 'DESC')->get();
$finishedcontracts = array();
foreach($fcontracts as $fcontract) {
$category = Categories::where('id', $fcontract->category_id)->first();
$supplier = Suppliers::where('id', $fcontract->supplier_id)->first();
$time_limit = Carbon::parse($fcontract->appointment);
$day_limit = Carbon::parse($fcontract->created_at);
$technician_name = $fcontract->technical_id
? ($fcontract->technician->user->name ?? null)
: ($supplier ? ($supplier->user->name ?? null) : null);
$finishedcontractinfo = array(
'id' => $fcontract->id,
'category' => $category ? $category->name : null,
@@ -987,6 +895,7 @@ class ContractController extends Controller
'date' => $fcontract->appointment,
'date_difference' => $time_limit->diff(Carbon::now(), false)->days,
'supplier' => $supplier ? $supplier->company_name : 'Proveedor no disponible',
'technician' => $technician_name,
'amount' => $fcontract->amount,
'scored' => $fcontract->scored_at,
'parent' => $fcontract->parent_contract_id,

View File

@@ -137,12 +137,14 @@ class NoHomeController extends Controller
if (Carbon::now()->diffInMinutes($contract->appointment, false) < 10) {
return response()->json($contract);
} else {
try {
OneSignal::sendNotificationToExternalUser(
"El proveedor para el servicio en " . $contract->address . " ha llegado. Dírigeta a la sección de contratos confirmados para más detalles",
(string) $client->id,
null, null, null, null,
$client->name . ", tu proveedor del servicio ha llegado"
);
} catch (\Exception $e) {}
return response()->json([
//'message' => 'Por favor espere a los 10 minutos de tolerancia de la hora acordada'
'message' => 'wait'
@@ -227,12 +229,14 @@ class NoHomeController extends Controller
'message' => 'Ausencia registrada con éxito, nos comunicaremos con usted por correo electrónico en caso de alguna circunstancia'
]);
} else {
try {
OneSignal::sendNotificationToExternalUser(
"El proveedor para el servicio en " . $ccontract->address . " ha llegado. Dírigeta a la sección de contratos confirmados para más detalles",
(string) $client->id,
null, null, null, null,
$client->name . ", tu proveedor del servicio ha llegado"
);
} catch (\Exception $e) {}
return response()->json([
'order' => 'wait',
'message' => 'Por favor espere a los 10 minutos de tolerancia de la hora acordada'

View File

@@ -0,0 +1,34 @@
<?php
namespace App\Http\Controllers;
use App\Models\PaymentBatch;
use Illuminate\Http\Request;
class PaymentBatchController extends Controller
{
public function index(Request $request)
{
$batches = PaymentBatch::with('user')
->orderBy('created_at', 'desc')
->paginate(15);
if ($request->ajax()) {
return view('payment-batches.index', compact('batches'));
}
return view('payment-batches.ajax', compact('batches'));
}
public function download($id)
{
$batch = PaymentBatch::findOrFail($id);
$absolutePath = storage_path('app/' . $batch->file_path);
if (!file_exists($absolutePath)) {
return redirect('payment-batches')->with('error', 'El archivo ya no está disponible en el servidor.');
}
return response()->download($absolutePath, $batch->file_name);
}
}

View File

@@ -9,15 +9,15 @@ use Carbon\Carbon;
use App\Models\User;
use App\Models\Suppliers;
use App\Models\Payments;
use App\Models\Postulations;
use App\Models\Coupon;
use App\Models\FinishedContracts;
use App\Models\Cards;
use Openpay;
use Exception;
use OpenpayApiError;
use OpenpayApiAuthError;
use OpenpayApiRequestError;
use OpenpayApiConnectionError;
use OpenpayApiTransactionError;
use App\Models\PaymentBatch;
use Stripe\Stripe;
use Stripe\Customer;
use PhpOffice\PhpSpreadsheet\Spreadsheet;
use PhpOffice\PhpSpreadsheet\Writer\Xlsx;
use Illuminate\Support\Facades\Auth;
class PaymentController extends Controller
{
@@ -33,7 +33,7 @@ class PaymentController extends Controller
if ($request->has('date_to')) $request->session()->put('pay_date_to', $dateTo);
$contracts = FinishedContracts::with(['suppliers.user', 'suppliers.banks'])
->where('status_id', 3)
->whereIn('status_id', [3, 9])
->where('paid', false)
->where('transaction_id', '!=', 'NO APPLY')
->when($dateFrom, fn($q) => $q->whereDate('appointment', '>=', $dateFrom))
@@ -93,327 +93,181 @@ class PaymentController extends Controller
}
}
public function cardsindex(Request $request)
public function intent(Request $request)
{
//
$request->session()->put('search', $request
->has('search') ? strip_tags($request->get('search')) : ($request->session()
->has('search') ? strip_tags($request->session()->get('search')) : ''));
$request->session()->put('field', $request
->has('field') ? strip_tags($request->get('field')) : ($request->session()
->has('field') ? strip_tags($request->session()->get('field')) : 'id'));
$request->session()->put('sort', $request
->has('sort') ? strip_tags($request->get('sort')) : ($request->session()
->has('sort') ? strip_tags($request->session()->get('sort')) : 'asc'));
//$headers = $request->get('header') != '' ? $request->get('header') : -1;
$cards= new Cards();
$cards = $cards->where('id', 'LIKE', '%' . strip_tags($request->session()->get('search')) . '%')
->orwhere('token', 'LIKE', '%' . strip_tags($request->session()->get('search')) . '%')
->orderBy(strip_tags($request->session()->get('field')), strip_tags($request->session()->get('sort')))
->paginate(10);
if ($request->ajax()) {
return view('cards.index', compact('cards'));
} else {
return view('cards.ajax', compact('cards'));
}
}
public function destroy($id)
{
$credit_card = Cards::where('id', $id)->first();
$user = User::where('id', $credit_card->user_id)->first();
Openpay::setProductionMode(true);
try {
$openpay = Openpay::getInstance(config('app.openpay_id'), config('app.openpay_apikey'));
$customer = $openpay->customers->get($user->openpay_id);
$card = $customer->cards->get($credit_card->token);
$card->delete();
} catch (OpenpayApiTransactionError $e) {
return response()->json([
'type' => 'error',
'message' => 'Error en la transacción'
$validator = Validator::make($request->all(), [
'postulation_id' => 'required|numeric|exists:postulations,id',
'supplier_id' => 'required|numeric|exists:suppliers,id',
'coupon' => 'nullable|string',
]);
} catch (OpenpayApiRequestError $e) {
return response()->json([
'type' => 'error',
'message' => 'Error en los datos requeridos'
]);
} catch (OpenpayApiConnectionError $e) {
return response()->json([
'type' => 'error',
'message' => 'Error al conectarse a Openpay: ' . $e->getMessage()
]);
} catch (OpenpayApiAuthError $e) {
return response()->json([
'type' => 'error',
'message' => 'Error al conectarse a Openpay: ' . $e->getMessage()
]);
} catch (OpenpayApiError $e) {
return response()->json([
'type' => 'error',
'message' => 'Error al conectarse a Openpay: ' . $e->getMessage()
]);
} catch (Exception $e) {
return response()->json([
'type' => 'error',
'message' => 'Error: ' . $e->getMessage()
]);
}
Cards::destroy($id);
return redirect('cards');
}
public function addcard(Request $request)
{
$rules = [
'token' => 'required|string|regex:/(^[A-Za-z0-9 ]+$)+/',
'device_id' => 'required|string|regex:/(^[A-Za-z0-9 ]+$)+/',
];
$validator = Validator::make($request->all(), $rules);
if ($validator->fails()) {
return redirect()->back()->withInput($request->all())->withErrors($validator);
return response()->json($validator->errors(), 422);
}
$user = $request->user();
Openpay::setProductionMode(true);
$user = Auth::user();
$postulation = Postulations::find($request->postulation_id);
if ($user->openpay_id == null) {
try {
$openpay = Openpay::getInstance(config('app.openpay_id'), config('app.openpay_apikey'));
$customerData = array(
'external_id' => $user->id,
'name' => $user->name,
if ($postulation->user_id !== $user->id) {
return response()->json(['message' => 'No autorizado'], 403);
}
$pivotSupplier = $postulation->suppliers()
->where('suppliers.id', $request->supplier_id)
->first();
if (!$pivotSupplier) {
return response()->json(['message' => 'El proveedor no se ha postulado a este servicio'], 404);
}
$minFee = 150;
$pivotAmount = $pivotSupplier->pivot->amount ?? $minFee;
$finalAmount = max($pivotAmount, $minFee);
$discount = 0;
if ($request->coupon) {
$coupon = Coupon::where('name', $request->coupon)->first();
if ($coupon && $coupon->limit > 0) {
$discount = ($finalAmount * (($coupon->percentage ?? 0) / 100)) + ($coupon->amount ?? 0);
}
}
$chargeAmount = max($finalAmount - $discount, 0);
Stripe::setApiKey(env('STRIPE_SECRET'));
if (!$user->stripe_customer_id) {
$customer = Customer::create([
'email' => $user->email,
);
$customer = $openpay->customers->add($customerData);
} catch (OpenpayApiTransactionError $e) {
return response()->json([
'type' => 'error',
'message' => 'No se pudo procesar la transacción'
'name' => $user->name,
]);
} catch (OpenpayApiRequestError $e) {
return response()->json([
'type' => 'error',
'message' => 'No se pudo procesar la operación'
]);
} catch (OpenpayApiConnectionError $e) {
return response()->json([
'type' => 'error',
'message' => 'Error al conectarse a Openpay: ' . $e->getMessage()
]);
} catch (OpenpayApiAuthError $e) {
return response()->json([
'type' => 'error',
'message' => 'Error al conectarse a Openpay: ' . $e->getMessage()
]);
} catch (OpenpayApiError $e) {
return response()->json([
'type' => 'error',
'message' => 'Error al conectarse a Openpay: ' . $e->getMessage()
]);
} catch (Exception $e) {
return response()->json([
'type' => 'error',
'message' => 'Error: ' . $e->getMessage()
]);
}
$user->openpay_id = $customer->id;
$user->stripe_customer_id = $customer->id;
$user->save();
}
$cardDataRequest = array(
'token_id' => $request->token,
'device_session_id' => $request->device_id
try {
$ephemeralKey = \Stripe\EphemeralKey::create(
['customer' => $user->stripe_customer_id],
['stripe_version' => \Stripe\Stripe::$apiVersion]
);
try {
$openpay = Openpay::getInstance(config('app.openpay_id'), config('app.openpay_apikey'));
$customer = $openpay->customers->get($user->openpay_id);
$card = $customer->cards->add($cardDataRequest);
} catch (OpenpayApiTransactionError $e) {
return response()->json([
'type' => 'error',
'message' => 'No se pudo procesar la transacción'
]);
} catch (OpenpayApiRequestError $e) {
return response()->json([
'type' => 'error',
'message' => 'No se pudo procesar la operación'
]);
} catch (OpenpayApiConnectionError $e) {
return response()->json([
'type' => 'error',
'message' => 'Error al conectarse a Openpay: ' . $e->getMessage()
]);
} catch (OpenpayApiAuthError $e) {
return response()->json([
'type' => 'error',
'message' => 'Error al conectarse a Openpay: ' . $e->getMessage()
]);
} catch (OpenpayApiError $e) {
return response()->json([
'type' => 'error',
'message' => 'Error al conectarse a Openpay: ' . $e->getMessage()
]);
} catch (Exception $e) {
return response()->json([
'type' => 'error',
'message' => 'Error: ' . $e->getMessage()
$intent = \Stripe\PaymentIntent::create([
'amount' => (int) ($chargeAmount * 100),
'currency' => env('STRIPE_CURRENCY', 'mxn'),
'customer' => $user->stripe_customer_id,
'automatic_payment_methods' => ['enabled' => true],
]);
} catch (\Exception $e) {
return response()->json(['message' => $e->getMessage()], 422);
}
$card = new Cards();
$card->user_id = $user->id;
$card->token = $request->token;
$card->save();
return response()->json([
'message' => 'Tarjeta guardada exitosamente'
'payment_intent_id' => $intent->id,
'client_secret' => $intent->client_secret,
'customer' => $user->stripe_customer_id,
'ephemeral_key' => $ephemeralKey->secret,
'amount' => $chargeAmount,
]);
}
public function deletecard(Request $request)
{
$rules = [
'card_id' => 'required|numeric',
];
$validator = Validator::make($request->all(), $rules);
if ($validator->fails()) {
return redirect()->back()->withInput($request->all())->withErrors($validator);
}
$user = $request->user();
$credit_card = Cards::where('id', $request->card_id)->first();
Openpay::setProductionMode(true);
if ($credit_card->user_id == $user->id) {
try {
$openpay = Openpay::getInstance(config('app.openpay_id'), config('app.openpay_apikey'));
$customer = $openpay->customers->get($user->openpay_id);
$card = $customer->cards->get($credit_card->token);
$card->delete();
} catch (OpenpayApiTransactionError $e) {
return response()->json([
'type' => 'error',
'message' => 'No se pudo procesar la transacción'
]);
} catch (OpenpayApiRequestError $e) {
return response()->json([
'type' => 'error',
'message' => 'No se pudo procesar la operación'
]);
} catch (OpenpayApiConnectionError $e) {
return response()->json([
'type' => 'error',
'message' => 'Error al conectarse a Openpay: ' . $e->getMessage()
]);
} catch (OpenpayApiAuthError $e) {
return response()->json([
'type' => 'error',
'message' => 'Error al conectarse a Openpay: ' . $e->getMessage()
]);
} catch (OpenpayApiError $e) {
return response()->json([
'type' => 'error',
'message' => 'Error al conectarse a Openpay: ' . $e->getMessage()
]);
} catch (Exception $e) {
return response()->json([
'type' => 'error',
'message' => 'Error: ' . $e->getMessage()
]);
}
Cards::destroy($request->card_id);
return response()->json([
'message' => 'Tarjeta eliminada exitosamente'
]);
}
}
public function getcards(Request $request)
{
$user = $request->user();
Openpay::setProductionMode(true);
if ($user->openpay_id) {
try {
$openpay = Openpay::getInstance(config('app.openpay_id'), config('app.openpay_apikey'));
$customer = $openpay->customers->get($user->openpay_id);
} catch (OpenpayApiTransactionError $e) {
return response()->json([
'type' => 'error',
'message' => 'No se pudo procesar la transacción'
]);
} catch (OpenpayApiRequestError $e) {
return response()->json([
'type' => 'error',
'message' => 'No se pudo procesar la operación'
]);
} catch (OpenpayApiConnectionError $e) {
return response()->json([
'type' => 'error',
'message' => 'Error al conectarse a Openpay: ' . $e->getMessage()
$user = Auth::user();
$cards = Cards::where('user_id', $user->id)->get()->map(fn($c) => [
'id' => $c->id,
'payment_method_id' => $c->stripe_pm_id,
'brand' => $c->brand,
'last4' => $c->last4,
'exp_month' => $c->exp_month,
'exp_year' => $c->exp_year,
]);
} catch (OpenpayApiAuthError $e) {
return response()->json([
'type' => 'error',
'message' => 'Error al conectarse a Openpay: ' . $e->getMessage()
]);
} catch (OpenpayApiError $e) {
return response()->json([
'type' => 'error',
'message' => 'Error al conectarse a Openpay: ' . $e->getMessage()
]);
} catch (Exception $e) {
return response()->json([
'type' => 'error',
'message' => 'Error: ' . $e->getMessage()
]);
return response()->json($cards);
}
$cards = Cards::where('user_id', $user->id)->get();
public function generate(Request $request)
{
$dateFrom = $request->input('date_from', '');
$dateTo = $request->input('date_to', '');
$cardsinfo = array();
$contracts = FinishedContracts::with(['suppliers.user', 'suppliers.banks'])
->whereIn('status_id', [3, 9])
->where('paid', false)
->where('transaction_id', '!=', 'NO APPLY')
->when($dateFrom, fn($q) => $q->whereDate('appointment', '>=', $dateFrom))
->when($dateTo, fn($q) => $q->whereDate('appointment', '<=', $dateTo))
->get();
foreach ($cards as $credit_card) {
$card = $customer->cards->get($credit_card->token);
$cardinfo = array(
'id' => $credit_card->id,
'brand' => $card->brand,
'card_number' => $card->card_number,
);
$cardsinfo[] = $cardinfo;
if ($contracts->isEmpty()) {
return redirect('payments')->with('warning', 'No hay contratos pendientes de pago con los filtros seleccionados.');
}
return response()->json($cardsinfo);
$contractIds = $contracts->pluck('id');
$grouped = $contracts->groupBy('supplier_id')->map(function ($group) {
$first = $group->first();
return [
'suppliers' => $first->suppliers,
'contract_count' => $group->count(),
'total_amount' => $group->sum('amount'),
'total_revenue' => $group->sum('revenue'),
'total_iva' => $group->sum('IVA'),
'total_isr' => $group->sum('ISR'),
'total_fee' => $group->sum('ichamba_fee'),
];
})->values();
// Generar Excel
$spreadsheet = new Spreadsheet();
$sheet = $spreadsheet->getActiveSheet();
$sheet->setTitle('Pagos');
$headers = ['Proveedor', 'Email', 'RFC / CURP', 'CLABE', 'Banco', 'Contratos', 'Monto Total', 'Utilidad', 'Ret. IVA', 'Ret. ISR', 'Ret. JobHero'];
foreach ($headers as $i => $header) {
$sheet->setCellValueByColumnAndRow($i + 1, 1, $header);
}
foreach ($grouped as $idx => $data) {
$row = $idx + 2;
$supplier = $data['suppliers'];
$sheet->setCellValueByColumnAndRow(1, $row, optional($supplier)->company_name ?? '');
$sheet->setCellValueByColumnAndRow(2, $row, optional(optional($supplier)->user)->email ?? '');
$sheet->setCellValueByColumnAndRow(3, $row, optional($supplier)->RFC ?: (optional($supplier)->CURP ?? ''));
$sheet->setCellValueByColumnAndRow(4, $row, optional($supplier)->clabe ?? '');
$sheet->setCellValueByColumnAndRow(5, $row, optional(optional($supplier)->banks)->name ?? '');
$sheet->setCellValueByColumnAndRow(6, $row, $data['contract_count']);
$sheet->setCellValueByColumnAndRow(7, $row, $data['total_amount']);
$sheet->setCellValueByColumnAndRow(8, $row, $data['total_revenue']);
$sheet->setCellValueByColumnAndRow(9, $row, $data['total_iva']);
$sheet->setCellValueByColumnAndRow(10, $row, $data['total_isr']);
$sheet->setCellValueByColumnAndRow(11, $row, $data['total_fee']);
}
$now = now();
$fileName = 'pagos_' . $now->format('Y-m-d_H-i-s') . '.xlsx';
$relativePath = 'payment-batches/' . $fileName;
$absolutePath = storage_path('app/' . $relativePath);
if (!is_dir(storage_path('app/payment-batches'))) {
mkdir(storage_path('app/payment-batches'), 0755, true);
}
$writer = new Xlsx($spreadsheet);
$writer->save($absolutePath);
PaymentBatch::create([
'file_name' => $fileName,
'file_path' => $relativePath,
'generated_by' => Auth::id(),
'date_from' => $dateFrom ?: null,
'date_to' => $dateTo ?: null,
]);
FinishedContracts::whereIn('id', $contractIds)->update([
'paid' => true,
'paid_at' => $now,
]);
return response()->download($absolutePath, $fileName);
}
}

View File

@@ -9,10 +9,13 @@ use OneSignal;
use App\Models\Suppliers;
use App\Models\Categories;
use App\Models\Postulations;
use App\Models\Property;
use App\Models\iChambaParameter;
use MissaelAnda\Whatsapp;
use MissaelAnda\Whatsapp\Messages;
use App\Models\FinishedContracts;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Storage;
use Illuminate\Support\Facades\Validator;
use TarfinLabs\LaravelSpatial\Types\Point;
@@ -90,14 +93,12 @@ class PostulationController extends Controller
$rules = [
'category' => 'required|string',
'address' => 'required|string',
'int_number' => 'numeric|nullable',
'property_id' => 'required|numeric',
'references' => 'string|nullable',
'setdate' => 'required|string',
'sethour' => 'required|string',
'details' => 'string|nullable',
'lat' => 'required|numeric',
'lng' => 'required|numeric',
'photos' => 'nullable|array',
'photos.*' => 'nullable|image|max:4096',
'related_postulation_id' => 'nullable|numeric|exists:postulations,id',
];
$validator = Validator::make($request->all(), $rules);
@@ -106,10 +107,16 @@ class PostulationController extends Controller
} else {
$user = Auth::user();
$geometry = new Point($request->lat, $request->lng);
$property = Property::find($request->property_id);
if (!$property || $property->user_id !== $user->id) {
return response()->json(['message' => 'Propiedad no válida'], 403);
}
$geometry = new Point($property->lat, $property->lng);
$category = Categories::where('name', strip_tags($request->category))->orwhere('en_name', strip_tags($request->category))->first();
$distance = 0.5;
$distance = 5000; // metros (5 km)
$suppliers = Suppliers::withinDistanceTo('location', $geometry, $distance)->get();
if ($suppliers != '[]') {
@@ -117,33 +124,49 @@ class PostulationController extends Controller
$postulation = new Postulations();
$postulation->user_id = $user->id;
$postulation->category_id = $category->id;
$postulation->address = strip_tags($request->address);
$postulation->property_id = $property->id;
$postulation->address = $property->address;
$postulation->location = $geometry;
$postulation->int_number = $request->int_number;
$postulation->int_number = $property->int_number;
$postulation->references = preg_replace('/\d+/', '', strip_tags($request->references));
// Parsear fecha y hora del formato ISO que envía el frontend
$dateStr = substr(strip_tags($request->setdate), 0, 10); // "2026-01-28"
$timeStr = substr(strip_tags($request->sethour), 11, 8); // "19:47:00"
$postulation->appointment = Carbon::createFromFormat('Y-m-d H:i:s', $dateStr . ' ' . $timeStr, 'America/Mexico_City')->tz('UTC');
$postulation->amount = 5000;
$postulation->amount = null;
$postulation->details = preg_replace('/\d+/', '', strip_tags($request->details));
$postulation->status = 'active';
$postulation->related_postulation_id = $request->related_postulation_id;
$postulation->save();
// Subir fotos a storage
if ($request->hasFile('photos')) {
$disk = Storage::disk(env('STORAGE_DISK', 'gcs'));
$photoUrls = [];
foreach ($request->file('photos') as $i => $photo) {
$filename = time() . '_' . $i . '.' . $photo->getClientOriginalExtension();
$disk->putFileAs('img/postulations/' . $postulation->id . '/', $photo, $filename, 'public');
$photoUrls[] = $disk->url('img/postulations/' . $postulation->id . '/' . $filename);
}
$postulation->photos = $photoUrls;
$postulation->save();
}
try {
OneSignal::sendNotificationToExternalUser(
"Coméntele al Ing. que hay una postulación",
"128",
null, null, null, null,
"Admin: hay nueva postulación"
);
} catch (\Exception $e) {}
foreach ($suppliers as $supplier) {
if (in_array($category->id, $supplier->categories->pluck('id')->toArray())) {
try {
OneSignal::sendNotificationToExternalUser(
"Dirígete a la sección de postulaciones en la app para ver más detalles",
(string) $supplier->user_id,
null, null, null, null,
"Proveedor: hay nueva postulación"
);
} catch (\Exception $e) {}
// TODO: Configurar WhatsApp cuando esté disponible
// Whatsapp::send($supplier->user->phone, Messages\TemplateMessage::create()
@@ -156,21 +179,9 @@ class PostulationController extends Controller
}
}
$minutes = intval(substr(substr($request->setdate, 14), 0, 2) + 15);
$hours = intval(substr(substr($request->setdate, 11), 0, 2) + 1);
if ($minutes > 59) {
if ($hours > 23){
$delay_msg = Carbon::now()->addDays(1)->toDateString() . ' ' . ($hours - 24) . ':' . ($minutes - 60) . substr(substr($request->setdate, 16), 0, 3);
} else {
$delay_msg = substr($request->sethour, 0, 10) . ' ' . $hours . ':' . ($minutes - 60) . substr(substr($request->setdate, 16), 0, 3);
}
} else {
$delay_msg = substr($request->sethour, 0, 10) . ' ' . $hours . ':' . $minutes . substr(substr($request->setdate, 16), 0, 3);
}
$delay_UTC = Carbon::now()->addMinutes(15)->toString();
try {
OneSignal::sendNotificationToExternalUser(
"Dirígete a la sección de contratos en la app para ver más detalles",
(string) $user->id,
@@ -178,6 +189,7 @@ class PostulationController extends Controller
$delay_UTC,
"Búsqueda Finalizada"
);
} catch (\Exception $e) {}
return response()->json([
'message' => 'Servicio solicitado, espere a que un proveedor se postule'
@@ -194,17 +206,20 @@ class PostulationController extends Controller
$rules = [
'postulation_id' => 'required|numeric',
'date_1' => 'required|date',
'date_2' => 'required|date',
'amount' => 'required|numeric|min:0',
];
$validator = Validator::make($request->all(), $rules);
if ($validator->fails()) {
return redirect()->back()->withInput($request->all())->withErrors($validator);
} else {
return response()->json($validator->errors(), 422);
}
$user = Auth::user();
$postulation = Postulations::where('id', $request->postulation_id)->first();
$time_created = Carbon::parse($postulation->created_at);
$time_limit = (9900 - Carbon::now()->diffInMinutes($time_created));
$time_limit = (1000 - Carbon::now()->diffInMinutes($time_created));
$supplier = Suppliers::where('user_id', $user->id)->first();
if (!$supplier) {
@@ -214,48 +229,53 @@ class PostulationController extends Controller
], 400);
}
// Validar cuota mínima del sistema
$minFeeParam = iChambaParameter::where('parameter', 'ichamba_fee')->first();
if ($minFeeParam && $request->amount < $minFeeParam->num_value) {
return response()->json([
'message' => 'El monto es menor a la cuota mínima del sistema (' . $minFeeParam->num_value . ')'
], 422);
}
if ($time_limit > 0) {
if (in_array($postulation->category_id, $supplier->categories->pluck('id')->toArray())) {
if($supplier->membership == 1) {
if ($supplier->membership == 1) {
try {
OneSignal::sendNotificationToExternalUser(
"Dirígete a la sección de contratos en la app para ver más detalles",
(string) $postulation->user_id,
null, null, null, null,
"Un proveedor certificado se ha postulado"
);
} catch (\Exception $e) {}
}
// TODO: Configurar WhatsApp cuando esté disponible
// Whatsapp::send($postulation->user->phone, Messages\TemplateMessage::create()
// ->name('suppplier_postulated')
// ->language('es_US')
// ->body(Messages\Components\Body::create([
// Messages\Components\Parameters\Text::create('Un proveedor certificado se ha postulado: dirígete a la sección de contratos en JobHero para ver más detalles'),
// ])));
$supplier->postulations()->attach($request->postulation_id);
$supplier->save();
$supplier->postulations()->attach($request->postulation_id, [
'date_1' => Carbon::parse($request->date_1),
'date_2' => Carbon::parse($request->date_2),
'amount' => $request->amount,
]);
return response()->json([
'message' => 'Se ha postulado al servicio exitosamente'
]);
}
} else {
return response()->json([
'message' => 'La postulación ha caducado'
]);
}
}
}
public function getpendingcontracts(Request $request) {
$user = Auth::user();
$postulations = Postulations::where('user_id', $user->id)->orderBy('created_at', 'DESC')->get();
$postulations = Postulations::where('user_id', $user->id)
->where('status', 'active')
->orderBy('created_at', 'DESC')
->get();
$pendingcontracts = array();
foreach($postulations as $postulation) {
$time_limit = Carbon::parse($postulation->appointment);
if ($time_limit->diffInMinutes(Carbon::now(), false) <= -60) {
$category = Categories::where('id', $postulation->category_id)->first();
$pendingcontractinfo = array(
'id' => $postulation->id,
@@ -263,11 +283,14 @@ class PostulationController extends Controller
'en_category' => $category->en_name,
'address' => $postulation->address,
'date' => $postulation->appointment,
'amount' => $postulation->amount
'amount' => $postulation->amount,
'property_id' => $postulation->property_id,
'references' => $postulation->references,
'details' => $postulation->details,
'time_created' => $postulation->created_at,
);
$pendingcontracts[] = $pendingcontractinfo;
}
}
return response()->json($pendingcontracts);
}
@@ -282,19 +305,24 @@ class PostulationController extends Controller
], 400);
}
$postulations = FinishedContracts::where('supplier_id', $user->suppliers->id)->orderBy('created_at', 'DESC')->get();
$supplier = $user->suppliers;
$postulations = FinishedContracts::with(['technician.user'])
->where('supplier_id', $supplier->id)->orderBy('created_at', 'DESC')->get();
$finishedpostulations = array();
foreach($postulations as $postulation) {
$time_limit = Carbon::parse($postulation->appointment);
$category = Categories::where('id', $postulation->category_id)->first();
$technician = $postulation->technical_id
? ($postulation->technician->user->name ?? null)
: ($supplier->user->name ?? null);
$finishedpostulationinfo = array(
'id' => $postulation->id,
'category' => $category->name,
'en_category' => $category->en_name,
'address' => $postulation->address,
'date' => $postulation->appointment,
'amount' => $postulation->amount
'amount' => $postulation->amount,
'technician' => $technician
);
$finishedpostulations[] = $finishedpostulationinfo;
}
@@ -310,42 +338,51 @@ class PostulationController extends Controller
$validator = Validator::make($request->all(), $rules);
if ($validator->fails()) {
return redirect()->back()->withInput($request->all())->withErrors($validator);
} else {
return response()->json($validator->errors(), 422);
}
$user = Auth::user();
$postulation = Postulations::where('id', $request->postulation_id)->first();
$postulation = Postulations::with('suppliers')->where('id', $request->postulation_id)->first();
if ($postulation->user_id == $user->id) {
$category = Categories::where('id', $postulation->category_id)->first();
$suppliers = Suppliers::whereHas('postulations', function($q) use ($request) {
$q->where('postulations_id', $request->postulation_id);
})->get();
$suppliers = $postulation->suppliers;
$pcontractsuppliers = array();
if ($suppliers != '[]') {
foreach($suppliers as $supplier) {
if ($suppliers->isNotEmpty()) {
foreach ($suppliers as $supplier) {
$pivot = $supplier->pivot;
$pcontractsupplier = array(
'id' => $postulation->id,
'category' => $category->name,
'en_category' => $category->en_name,
'address' => $postulation->address,
'date' => $postulation->appointment,
'amount' => $postulation->amount,
'supplier_id' => $supplier->id,
'supplier' => $supplier->company_name,
'tags' => $supplier->tags,
'cover_photo' => $supplier->cover_photo,
'membership' => $supplier->membership,
'fee' => $supplier->minimun_fee,
'score' => round($supplier->total_score/$supplier->finished_jobs, 1),
'fee' => $pivot->amount,
'date_1' => $pivot->date_1,
'date_2' => $pivot->date_2,
'score' => $supplier->finished_jobs > 0
? round($supplier->total_score / $supplier->finished_jobs, 1)
: null,
);
$pcontractsuppliers[] = $pcontractsupplier;
}
$pcontractsuppliercollection = collect($pcontractsuppliers)->sortByDesc('membership')->sortByDesc('score');
$pcontractsupplier = $pcontractsuppliercollection->values()->all();
$collection = collect($pcontractsuppliers);
$sort = $request->get('sort', 'membership');
if ($sort === 'date') {
$pcontractsupplier = $collection->sortBy('date_1')->values()->all();
} elseif ($sort === 'amount') {
$pcontractsupplier = $collection->sortBy('fee')->values()->all();
} else {
$pcontractsupplier = $collection->sortByDesc('membership')->sortByDesc('score')->values()->all();
}
} else {
$pcontractsupplier = array(
@@ -353,14 +390,14 @@ class PostulationController extends Controller
'category' => $category->name,
'en_category' => $category->en_name,
'address' => $postulation->address,
'date' => $postulation->appointment,
'amount' => $postulation->amount,
'supplier_id' => null,
'supplier' => null,
'tags' => null,
'cover_photo' => null,
'membership' => null,
'fee' => null,
'date_1' => null,
'date_2' => null,
'score' => null,
);
@@ -370,13 +407,69 @@ class PostulationController extends Controller
return response()->json($pcontractsupplier);
}
}
}
public function deleteexpired()
{
$postulations = Postulations::whereDate('appointment', '<', Carbon::now())->delete();
}
public function cancelPostulation($id)
{
$user = Auth::user();
$postulation = Postulations::find($id);
if (!$postulation || $postulation->user_id !== $user->id) {
return response()->json(['message' => 'No autorizado'], 403);
}
$postulation->status = 'perdido';
$postulation->save();
return response()->json(['message' => 'Postulación archivada']);
}
public function updatePostulation(Request $request, $id)
{
$user = Auth::user();
$postulation = Postulations::find($id);
if (!$postulation || $postulation->user_id !== $user->id) {
return response()->json(['message' => 'No autorizado'], 403);
}
$rules = [
'references' => 'nullable|string',
'details' => 'nullable|string',
'photos' => 'nullable|array',
'photos.*' => 'nullable|image|max:4096',
];
$validator = Validator::make($request->all(), $rules);
if ($validator->fails()) {
return response()->json($validator->errors(), 422);
}
if ($request->has('references')) {
$postulation->references = preg_replace('/\d+/', '', strip_tags($request->references));
}
if ($request->has('details')) {
$postulation->details = preg_replace('/\d+/', '', strip_tags($request->details));
}
if ($request->hasFile('photos')) {
$disk = Storage::disk(env('STORAGE_DISK', 'gcs'));
$photoUrls = [];
foreach ($request->file('photos') as $i => $photo) {
$filename = time() . '_' . $i . '.' . $photo->getClientOriginalExtension();
$disk->putFileAs('img/postulations/' . $postulation->id . '/', $photo, $filename, 'public');
$photoUrls[] = $disk->url('img/postulations/' . $postulation->id . '/' . $filename);
}
$postulation->photos = $photoUrls;
}
$postulation->save();
return response()->json(['message' => 'Postulación actualizada']);
}
public function destroy($id)
{
Postulations::destroy($id);

View File

@@ -0,0 +1,57 @@
<?php
namespace App\Http\Controllers;
use App\Models\Property;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Validator;
class PropertyController extends Controller
{
public function index()
{
$properties = Auth::user()->properties()->orderBy('name')->get();
return response()->json($properties);
}
public function store(Request $request)
{
$validator = Validator::make($request->all(), [
'name' => 'required|string',
'address' => 'required|string',
'lat' => 'required|numeric',
'lng' => 'required|numeric',
'int_number' => 'nullable|string',
'icon' => 'nullable|in:casa,oficina',
]);
if ($validator->fails()) {
return response()->json($validator->errors(), 422);
}
$property = new Property();
$property->user_id = Auth::id();
$property->name = strip_tags($request->name);
$property->address = strip_tags($request->address);
$property->lat = $request->lat;
$property->lng = $request->lng;
$property->int_number = $request->int_number ? strip_tags($request->int_number) : null;
$property->icon = $request->icon ?? 'casa';
$property->save();
return response()->json($property, 201);
}
public function destroy($id)
{
$property = Property::find($id);
if (!$property || $property->user_id !== Auth::id()) {
return response()->json(['message' => 'No autorizado'], 403);
}
$property->delete();
return response()->json(['message' => 'Propiedad eliminada']);
}
}

View File

@@ -2,111 +2,78 @@
namespace App\Http\Controllers;
use App\Models\Report;
use App\Models\ReportComment;
use App\Models\FinishedContracts;
use App\Models\Suppliers;
use App\Models\NoHome;
use OneSignal;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Validator;
class ReportCommentController extends Controller
{
/**
* Display a listing of the resource.
*
* @return \Illuminate\Http\Response
*/
public function index(Request $request, $id, $contract_id)
public function index(Request $request, $id)
{
//
$request->session()->put('search', $request
->has('search') ? strip_tags($request->get('search')) : ($request->session()
->has('search') ? strip_tags($request->session()->get('search')) : ''));
$report = Report::find($id);
$contract = FinishedContracts::with(['user', 'suppliers.user', 'categories', 'status'])->find($report->contract_id);
$nohome = NoHome::where('contract_id', $contract->id)->first();
$comments = ReportComment::with('user')
->where('report_id', $id)
->orderBy('created_at', 'asc')
->paginate(50);
$request->session()->put('field', $request
->has('field') ? strip_tags($request->get('field')) : ($request->session()
->has('field') ? strip_tags($request->session()->get('field')) : 'id'));
$request->session()->put('sort', $request
->has('sort') ? strip_tags($request->get('sort')) : ($request->session()
->has('sort') ? strip_tags($request->session()->get('sort')) : 'asc'));
//$headers = $request->get('header') != '' ? $request->get('header') : -1;
$contract = FinishedContracts::where('id', $contract_id)->first();
$nohome = NoHome::where('contract_id', $contract_id)->first();
$comments = new ReportComment();
$comments = $comments->where('report_id', $id)
->orderBy(strip_tags($request->session()->get('field')), strip_tags($request->session()->get('sort')))
->paginate(10);
if ($request->isMethod('get'))
return view('reports.comments', compact('comments', 'contract', 'nohome'));
return view('reports.comments', compact('comments', 'contract', 'nohome', 'report'));
}
/**
* Show the form for creating a new resource.
*
* @return \Illuminate\Http\Response
*/
public function create()
public function store(Request $request, $id)
{
//
$validator = Validator::make($request->all(), [
'comment' => 'required|string',
]);
if ($validator->fails()) {
return redirect()->back()->withErrors($validator);
}
/**
* Store a newly created resource in storage.
*
* @param \Illuminate\Http\Request $request
* @return \Illuminate\Http\Response
*/
public function store(Request $request)
{
//
$comment = new ReportComment();
$comment->report_id = $id;
$comment->user_id = Auth::id();
$comment->comment = strip_tags($request->comment);
$comment->save();
$report = Report::find($id);
$contract = FinishedContracts::find($report->contract_id);
$supplier = Suppliers::find($contract->supplier_id);
$recipients = array_filter([
$contract->user_id,
$supplier->user_id ?? null,
]);
foreach ($recipients as $recipientId) {
try {
OneSignal::sendNotificationCustom([
'include_external_user_ids' => [(string) $recipientId],
'contents' => [
'es' => 'Moderador: ' . $comment->comment,
'en' => 'Moderator: ' . $comment->comment,
],
'headings' => [
'es' => 'Nueva actividad en tu reporte',
'en' => 'New activity on your report',
],
]);
} catch (\Exception $e) {}
}
/**
* Display the specified resource.
*
* @param \App\ReportComment $reportComment
* @return \Illuminate\Http\Response
*/
public function show(ReportComment $reportComment)
{
//
return redirect()->back();
}
/**
* Show the form for editing the specified resource.
*
* @param \App\ReportComment $reportComment
* @return \Illuminate\Http\Response
*/
public function edit(ReportComment $reportComment)
public function destroy($id, $comment_id)
{
//
}
/**
* Update the specified resource in storage.
*
* @param \Illuminate\Http\Request $request
* @param \App\ReportComment $reportComment
* @return \Illuminate\Http\Response
*/
public function update(Request $request, ReportComment $reportComment)
{
//
}
/**
* Remove the specified resource from storage.
*
* @param \App\ReportComment $reportComment
* @return \Illuminate\Http\Response
*/
public function destroy($id, $contract_id)
{
//
ReportComment::destroy($id);
return redirect('reports/comments/'.$id.'/'.$contract_id);
ReportComment::destroy($comment_id);
return redirect('reports/' . $id . '/comments');
}
}

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,12 +92,12 @@ 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',
'contract_status' => 'nullable|in:8,9',
];
$messages = [
@@ -106,9 +109,29 @@ class ReportController extends Controller
return redirect()->back()->withInput($request->all())->withErrors($validator);
}
$report = Report::find($id);
$report = Report::with('finishedcontracts')->find($id);
$report->veredict = strip_tags($request->veredict);
$coupon->save();
$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');
}
@@ -138,6 +161,176 @@ class ReportController extends Controller
return redirect('reports');
}
public function getcomments(Request $request, $id)
{
$user = Auth::user();
$report = Report::with('finishedcontracts.suppliers')->find($id);
if (!$report) {
return response()->json(['message' => 'Reporte no encontrado'], 404);
}
$contract = $report->finishedcontracts;
$supplierUserId = $contract->suppliers->user_id ?? null;
if ($user->id !== $contract->user_id && $user->id !== $supplierUserId) {
return response()->json(['message' => 'No autorizado'], 403);
}
$comments = ReportComment::with('user')
->where('report_id', $id)
->orderBy('created_at', 'asc')
->get()
->map(function ($c) use ($contract, $supplierUserId) {
$isSupplier = $c->user_id === $supplierUserId;
$isClient = $c->user_id === $contract->user_id;
return [
'id' => $c->id,
'sender_id' => $c->user_id,
'sender_name' => $isSupplier
? ($contract->suppliers->company_name ?? null)
: ($c->user->name ?? null),
'role_id' => $c->user->role_id ?? null,
'sender_type' => !$isClient && !$isSupplier ? 'moderator' : null,
'comment' => $c->comment,
'created_at' => $c->created_at,
];
});
return response()->json($comments);
}
public function storecomment(Request $request, $id)
{
$validator = Validator::make($request->all(), ['comment' => 'required|string']);
if ($validator->fails()) {
return response()->json($validator->errors(), 422);
}
$user = Auth::user();
$report = Report::with('finishedcontracts.suppliers')->find($id);
if (!$report) {
return response()->json(['message' => 'Reporte no encontrado'], 404);
}
$contract = $report->finishedcontracts;
$supplierUserId = $contract->suppliers->user_id ?? null;
if ($user->id !== $contract->user_id && $user->id !== $supplierUserId) {
return response()->json(['message' => 'No autorizado'], 403);
}
$comment = new ReportComment();
$comment->report_id = $id;
$comment->user_id = $user->id;
$comment->comment = strip_tags($request->comment);
$comment->save();
$supplierUserId = $contract->suppliers->user_id ?? null;
$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,
'sender_name' => $isSupplier ? ($contract->suppliers->company_name ?? null) : $user->name,
'role_id' => $user->role_id,
'sender_type' => !$isClient && !$isSupplier ? 'moderator' : null,
'comment' => $comment->comment,
'created_at' => $comment->created_at,
], 201);
}
public function getsupplierreports(Request $request)
{
$user = Auth::user();
if (!$user->suppliers) {
return response()->json(['success' => false, 'message' => 'No tienes un perfil de proveedor registrado'], 400);
}
$contractIds = FinishedContracts::where('supplier_id', $user->suppliers->id)->pluck('id');
$reports = Report::with([
'finishedcontracts.suppliers.user',
'finishedcontracts.categories',
'finishedcontracts.technician.user',
])
->whereIn('contract_id', $contractIds)
->orderBy('created_at', 'desc')
->get();
$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,
'technician' => $technician,
'company' => $contract->suppliers->company_name ?? null,
'category' => $contract->categories->name ?? null,
'en_category' => $contract->categories->en_name ?? null,
'appointment' => $contract->appointment,
'address' => $contract->address,
'amount' => $contract->amount,
'veredict' => $report->veredict,
];
});
return response()->json($data);
}
public function getreports(Request $request)
{
$user = Auth::user();
$contractIds = FinishedContracts::where('user_id', $user->id)->pluck('id');
$reports = Report::with([
'finishedcontracts.suppliers.user',
'finishedcontracts.categories',
])
->whereIn('contract_id', $contractIds)
->orderBy('created_at', 'desc')
->get();
$data = $reports->map(function ($report) {
$contract = $report->finishedcontracts;
return [
'id' => $report->id,
'supplier' => $contract->suppliers->user->name ?? null,
'company' => $contract->suppliers->company_name ?? null,
'category' => $contract->categories->name ?? null,
'en_category' => $contract->categories->en_name ?? null,
'appointment' => $contract->appointment,
'address' => $contract->address,
'amount' => $contract->amount,
'veredict' => $report->veredict,
];
});
return response()->json($data);
}
public function report(Request $request) {
$rules = [

View File

@@ -101,7 +101,7 @@ class SupplierController extends Controller
'company_name' => 'required|string',
'cover_photo' => 'mimetypes:image/jpeg,image/png,image/jpg|max:2048',
'categories' => 'required|string',
'tags' => 'required|string',
'tags' => 'nullable|string',
'CURP' => 'unique:suppliers|nullable|string|size:18',
'RFC' => 'string|size:14|nullable',
'clabe' => 'string|nullable|regex:/(^[0-9 ]+$)+/',
@@ -117,7 +117,7 @@ class SupplierController extends Controller
'company_name' => 'required|string',
'cover_photo' => 'mimetypes:image/jpeg,image/png,image/jpg|max:2048',
'categories' => 'required|string',
'tags' => 'required|string',
'tags' => 'nullable|string',
'CURP' => 'string|size:18|nullable',
'RFC' => 'unique:suppliers|nullable|string|size:13',
'clabe' => 'string|nullable|regex:/(^[0-9 ]+$)+/',
@@ -148,7 +148,7 @@ class SupplierController extends Controller
'company_name' => 'required|string',
'cover_photo' => 'required|mimetypes:image/jpeg,image/png,image/jpg|max:2048',
'categories' => 'required|string',
'tags' => 'required|string',
'tags' => 'nullable|string',
'CURP' => 'unique:suppliers|nullable|string|size:18',
'RFC' => 'string|size:14|nullable',
'clabe' => 'string|nullable|regex:/(^[0-9 ]+$)+/',
@@ -164,7 +164,7 @@ class SupplierController extends Controller
'company_name' => 'required|string',
'cover_photo' => 'required|mimetypes:image/jpeg,image/png,image/jpg|max:2048',
'categories' => 'required|string',
'tags' => 'required|string',
'tags' => 'nullable|string',
'CURP' => 'string|size:18|nullable',
'RFC' => 'unique:suppliers|nullable|string|size:13',
'clabe' => 'string|nullable|regex:/(^[0-9 ]+$)+/',
@@ -182,7 +182,7 @@ class SupplierController extends Controller
'company_name.required' => 'Se requiere el nombre de la empresa',
'cover_photo.required' => 'Se requiere una foto de portada',
'categories.required' => 'Se requiere una categoría',
'tags.required' => 'Se requiere un tag',
//'tags.required' => 'Se requiere un tag',
'RFC.required' => 'Se requiere un RFC',
'CURP.required' => 'Se requiere un CURP',
'RFC.unique' => 'RFC ya registrado',
@@ -371,7 +371,7 @@ class SupplierController extends Controller
'categories' => 'required|string',
'tags' => 'nullable|string',
'bank' => 'nullable|numeric',
'bank_account' => 'nullable|string',
'bank_account' => 'nullable|numeric',
'fee' => 'nullable|numeric',
'address' => 'string',
'lat' => 'numeric|nullable',
@@ -419,7 +419,7 @@ class SupplierController extends Controller
'categories' => 'required|string',
'tags' => 'nullable|string',
'bank' => 'nullable|numeric',
'bank_account' => 'nullable|string',
'bank_account' => 'nullable|numeric',
'fee' => 'nullable|numeric',
'address' => 'required|string',
'lat' => 'required|numeric',
@@ -431,11 +431,11 @@ class SupplierController extends Controller
} else {
$rules = [
'name' => 'required|string',
'rfc' => 'nullable|string',
'rfc' => 'required|string',
'categories' => 'required|string',
'tags' => 'nullable|string',
'bank' => 'nullable|numeric',
'bank_account' => 'nullable|string',
'bank' => 'required|numeric',
'bank_account' => 'required|numeric',
'fee' => 'nullable|numeric',
'address' => 'required|string',
'lat' => 'required|numeric',
@@ -482,16 +482,16 @@ class SupplierController extends Controller
$messages = [
'name.required' => 'Se requiere el nombre de la empresa',
'categories.required' => 'Se requiere una categoría',
'tags.required' => 'Se requiere un tag',
//'RFC.required' => 'Se requiere un RFC',
//'tags.required' => 'Se requiere un tag',
'RFC.required' => 'Se requiere un RFC',
//'CURP.required' => 'Se requiere un CURP',
//'RFC.unique' => 'RFC ya registrado',
'RFC.unique' => 'RFC ya registrado',
//'CURP.unique' => 'CURP ya registrado',
//'RFC.size' => 'RFC no valido',
'RFC.size' => 'RFC no valido',
//'CURP.size' => 'CURP no valido',
//'taxes_id.required' => 'Se requiere un regimen fiscal',
//'clabe.required' => 'Se requiere una CLABE interbancaria',
//'minimun_fee.required' => 'Se requiere un monto mínimo a cobrar',
'minimun_fee.required' => 'Se requiere un monto mínimo a cobrar',
'address.required' => 'Se requiere una dirección',
'lat.required' => 'Se requiere una dirección válida, si ya hay una dirección escrita, favor de volverla a escribir',
'lng.required' => 'Se requiere una dirección válida, si ya hay una dirección escrita, favor de volverla a escribir',
@@ -499,7 +499,7 @@ class SupplierController extends Controller
$validator = Validator::make($request->all(), $rules, $messages);
if ($validator->fails()) {
return response()->json($validator);
return response()->json($validator->errors(), 422);
}
$supplier = Suppliers::where('user_id', $user->id)->first();
@@ -550,7 +550,7 @@ class SupplierController extends Controller
*/
$supplier->RFC = strip_tags($request->rfc);
$supplier->clabe = strip_tags($request->bank_account);
$supplier->clabe = (string) $request->bank_account;
$supplier->bank_id = $request->bank;
$supplier->minimun_fee = $request->fee ?? 150;
$supplier->address = strip_tags($request->address);
@@ -611,7 +611,7 @@ class SupplierController extends Controller
*/
$supplier->RFC = strip_tags($request->rfc);
$supplier->clabe = strip_tags($request->bank_account);
$supplier->clabe = (string) $request->bank_account;
$supplier->bank_id = $request->bank;
$supplier->minimun_fee = $request->fee ?? 150;
$supplier->address = strip_tags($request->address);
@@ -674,7 +674,7 @@ class SupplierController extends Controller
'company_name' => 'required|string',
'cover_photo' => 'mimetypes:image/jpeg,image/png,image/jpg|max:2048',
'categories' => 'required|string',
'tags' => 'required|string',
'tags' => 'nullable|string',
'CURP' => 'unique:suppliers|nullable|string|size:18',
'RFC' => 'string|size:14|nullable',
'clabe' => 'string|nullable|regex:/(^[0-9 ]+$)+/',
@@ -690,7 +690,7 @@ class SupplierController extends Controller
'company_name' => 'required|string',
'cover_photo' => 'mimetypes:image/jpeg,image/png,image/jpg|max:2048',
'categories' => 'required|string',
'tags' => 'required|string',
'tags' => 'nullable|string',
'CURP' => 'string|size:18|nullable',
'RFC' => 'unique:suppliers|nullable|string|size:13',
'clabe' => 'string|nullable|regex:/(^[0-9 ]+$)+/',
@@ -721,7 +721,7 @@ class SupplierController extends Controller
'company_name' => 'required|string',
'cover_photo' => 'required|mimetypes:image/jpeg,image/png,image/jpg|max:2048',
'categories' => 'required|string',
'tags' => 'required|string',
'tags' => 'nullable|string',
'CURP' => 'unique:suppliers|nullable|string|size:18',
'RFC' => 'string|size:14|nullable',
'clabe' => 'string|nullable|regex:/(^[0-9 ]+$)+/',
@@ -737,7 +737,7 @@ class SupplierController extends Controller
'company_name' => 'required|string',
'cover_photo' => 'required|mimetypes:image/jpeg,image/png,image/jpg|max:2048',
'categories' => 'required|string',
'tags' => 'required|string',
'tags' => 'nullable|string',
'CURP' => 'string|size:18|nullable',
'RFC' => 'unique:suppliers|nullable|string|size:13',
'clabe' => 'string|nullable|regex:/(^[0-9 ]+$)+/',
@@ -755,7 +755,7 @@ class SupplierController extends Controller
'company_name.required' => 'Se requiere el nombre de la empresa',
'cover_photo.required' => 'Se requiere una foto de portada',
'categories.required' => 'Se requiere una categoría',
'tags.required' => 'Se requiere un tag',
//'tags.required' => 'Se requiere un tag',
'RFC.required' => 'Se requiere un RFC',
'CURP.required' => 'Se requiere un CURP',
'RFC.unique' => 'RFC ya registrado',
@@ -997,7 +997,7 @@ class SupplierController extends Controller
'company_name' => 'required|string',
'cover_photo' => 'mimetypes:image/jpeg,image/png,image/jpg|max:2048',
'categories' => 'required|string',
'tags' => 'required|string',
'tags' => 'nullable|string',
'CURP' => 'unique:suppliers|nullable|string|size:18',
'RFC' => 'string|size:14|nullable',
'clabe' => 'string|nullable|regex:/(^[0-9 ]+$)+/',
@@ -1013,7 +1013,7 @@ class SupplierController extends Controller
'company_name' => 'required|string',
'cover_photo' => 'mimetypes:image/jpeg,image/png,image/jpg|max:2048',
'categories' => 'required|string',
'tags' => 'required|string',
'tags' => 'nullable|string',
'CURP' => 'string|size:18|nullable',
'RFC' => 'unique:suppliers|nullable|string|size:13',
'clabe' => 'string|nullable|regex:/(^[0-9 ]+$)+/',
@@ -1044,7 +1044,7 @@ class SupplierController extends Controller
'company_name' => 'required|string',
'cover_photo' => 'required|mimetypes:image/jpeg,image/png,image/jpg|max:2048',
'categories' => 'required|string',
'tags' => 'required|string',
'tags' => 'nullable|string',
'CURP' => 'unique:suppliers|nullable|string|size:18',
'RFC' => 'string|size:14|nullable',
'clabe' => 'string|nullable|regex:/(^[0-9 ]+$)+/',
@@ -1060,7 +1060,7 @@ class SupplierController extends Controller
'company_name' => 'required|string',
'cover_photo' => 'required|mimetypes:image/jpeg,image/png,image/jpg|max:2048',
'categories' => 'required|string',
'tags' => 'required|string',
'tags' => 'nullable|string',
'CURP' => 'string|size:18|nullable',
'RFC' => 'unique:suppliers|nullable|string|size:13',
'clabe' => 'string|nullable|regex:/(^[0-9 ]+$)+/',
@@ -1078,7 +1078,7 @@ class SupplierController extends Controller
'company_name.required' => 'Se requiere el nombre de la empresa',
'cover_photo.required' => 'Se requiere una foto de portada',
'categories.required' => 'Se requiere una categoría',
'tags.required' => 'Se requiere un tag',
//'tags.required' => 'Se requiere un tag',
'RFC.required' => 'Se requiere un RFC',
'CURP.required' => 'Se requiere un CURP',
'RFC.unique' => 'RFC ya registrado',
@@ -1237,7 +1237,7 @@ class SupplierController extends Controller
], 400);
}
$distance = 0.5;
$distance = 5000; // metros (5 km)
$postulations = Postulations::withinDistanceTo('location', $supplier->location, $distance)->orderBy('created_at', 'DESC')->get();
$postulationsinfo = array();
@@ -1280,12 +1280,19 @@ class SupplierController extends Controller
], 400);
}
$contracts = CurrentContracts::where('supplier_id', $supplier->id)->orderBy('created_at', 'DESC')->get();
$contracts = CurrentContracts::with(['technician.user'])
->where('supplier_id', $supplier->id)->orderBy('created_at', 'DESC')->get();
$contractsinfo = array();
foreach ($contracts as $contract) {
$category = Categories::where('id', $contract->category_id)->first();
$technician = $contract->technical_id
? ($contract->technician->user->name ?? null)
: ($supplier->user->name ?? null);
$profile_photo = $contract->technical_id
? ($contract->technician->user->profile_photo ?? null)
: ($supplier->user->profile_photo ?? null);
$contractinfo = array(
'id' => $contract->id,
'phone' => $contract->user->phone,
@@ -1297,7 +1304,9 @@ class SupplierController extends Controller
'lat' => $contract->location->getLat(),
'lng' => $contract->location->getLng(),
'amount' => $contract->amount,
'details' => $contract->details
'details' => $contract->details,
'technician' => $technician,
'profile_photo' => $profile_photo
);
$contractsinfo[] = $contractinfo;
}

View File

@@ -0,0 +1,192 @@
<?php
namespace App\Http\Controllers;
use App\Models\Technician;
use App\Models\User;
use App\Models\CurrentContracts;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Validator;
class TechnicianController extends Controller
{
public function index(Request $request)
{
$user = Auth::user();
$supplier = $user->suppliers;
if (!$supplier) {
return response()->json(['message' => 'No tienes un perfil de proveedor'], 400);
}
$technicians = Technician::with('user')
->where('supplier_id', $supplier->id)
->get()
->map(fn($t) => [
'id' => $t->id,
'user_id' => $t->user_id,
'name' => $t->user->name ?? null,
'email' => $t->user->email ?? null,
'phone' => $t->user->phone ?? null,
]);
return response()->json($technicians);
}
public function store(Request $request)
{
$validator = Validator::make($request->all(), [
'user_id' => 'required|numeric|exists:users,id',
]);
if ($validator->fails()) {
return response()->json($validator->errors(), 422);
}
$user = Auth::user();
$supplier = $user->suppliers;
if (!$supplier) {
return response()->json(['message' => 'No tienes un perfil de proveedor'], 400);
}
$targetUser = User::find($request->user_id);
if ($targetUser->id === $user->id) {
return response()->json(['message' => 'No puedes agregarte a ti mismo como técnico'], 422);
}
$exists = Technician::where('user_id', $request->user_id)
->where('supplier_id', $supplier->id)
->exists();
if ($exists) {
return response()->json(['message' => 'Este usuario ya es técnico de tu empresa'], 422);
}
$technician = Technician::create([
'user_id' => $request->user_id,
'supplier_id' => $supplier->id,
]);
$targetUser->role_id = 8;
$targetUser->save();
return response()->json([
'id' => $technician->id,
'user_id' => $technician->user_id,
'name' => $targetUser->name,
'email' => $targetUser->email,
'phone' => $targetUser->phone,
], 201);
}
public function destroy($id)
{
$user = Auth::user();
$supplier = $user->suppliers;
if (!$supplier) {
return response()->json(['message' => 'No tienes un perfil de proveedor'], 400);
}
$technician = Technician::where('id', $id)
->where('supplier_id', $supplier->id)
->first();
if (!$technician) {
return response()->json(['message' => 'Técnico no encontrado'], 404);
}
// Revertir role a "Usuario" solo si no pertenece a otro proveedor
$otherSuppliers = Technician::where('user_id', $technician->user_id)
->where('supplier_id', '!=', $supplier->id)
->exists();
if (!$otherSuppliers) {
$targetUser = User::find($technician->user_id);
if ($targetUser && $targetUser->role_id === 8) {
$targetUser->role_id = 1;
$targetUser->save();
}
}
$technician->delete();
return response()->json(['message' => 'Técnico eliminado exitosamente']);
}
public function find(Request $request)
{
$validator = Validator::make($request->all(), [
'email' => 'required|email',
]);
if ($validator->fails()) {
return response()->json($validator->errors(), 422);
}
$user = Auth::user();
$supplier = $user->suppliers;
if (!$supplier) {
return response()->json(['message' => 'No tienes un perfil de proveedor'], 400);
}
$targetUser = User::where('email', $request->email)->first();
if (!$targetUser) {
return response()->json(['message' => 'Usuario no encontrado'], 404);
}
$alreadyInTeam = Technician::where('user_id', $targetUser->id)
->where('supplier_id', $supplier->id)
->exists();
return response()->json([
'id' => $targetUser->id,
'name' => $targetUser->name,
'email' => $targetUser->email,
'phone' => $targetUser->phone,
'already_in_team' => $alreadyInTeam,
]);
}
public function assign(Request $request)
{
$validator = Validator::make($request->all(), [
'contract_id' => 'required|numeric|exists:current_contracts,id',
'technician_id' => 'nullable|numeric|exists:technicians,id',
]);
if ($validator->fails()) {
return response()->json($validator->errors(), 422);
}
$user = Auth::user();
$supplier = $user->suppliers;
if (!$supplier) {
return response()->json(['message' => 'No tienes un perfil de proveedor'], 400);
}
$contract = CurrentContracts::find($request->contract_id);
if ($contract->supplier_id !== $supplier->id) {
return response()->json(['message' => 'No autorizado'], 403);
}
if ($request->technician_id) {
$technician = Technician::where('id', $request->technician_id)
->where('supplier_id', $supplier->id)
->first();
if (!$technician) {
return response()->json(['message' => 'El técnico no pertenece a tu empresa'], 422);
}
}
$contract->technical_id = $request->technician_id ?? null;
$contract->save();
return response()->json(['message' => 'Técnico asignado exitosamente']);
}
}

View File

@@ -107,7 +107,6 @@ class UserController extends Controller
'name' => 'required|string',
'email' => 'required|string|email',
'role' => 'required|numeric',
'openpay_id' => 'string|nullable',
];
$messages = [
@@ -125,9 +124,6 @@ class UserController extends Controller
$user->name = strip_tags($request->name);
$user->email = $request->email;
$user->role_id = $request->role;
if ($request->openpay_id == "null" OR $request->openpay_id == null){
$user->openpay_id = null;
}
$user->save();
return redirect('users');

View File

@@ -31,7 +31,7 @@ class Cors
return $response
->header('Access-Control-Allow-Origin', $allowOrigin)
->header('Access-Control-Allow-Methods', 'GET, POST, PUT, PATCH, DELETE, OPTIONS')
->header('Access-Control-Allow-Headers', 'Content-Type, Authorization, X-Requested-With, X-XSRF-TOKEN');
->header('Access-Control-Allow-Headers', 'Content-Type, Authorization, X-Requested-With, X-XSRF-TOKEN, ngrok-skip-browser-warning');
}
return $next($request);

View File

@@ -0,0 +1,22 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class ContractComment extends Model
{
protected $fillable = [
'contract_id', 'user_id', 'comment',
];
public function contract()
{
return $this->belongsTo(CurrentContracts::class, 'contract_id');
}
public function user()
{
return $this->belongsTo(User::class);
}
}

View File

@@ -7,6 +7,8 @@ use App\Models\Categories;
use App\Models\Suppliers;
use App\Models\Coupon;
use App\Models\Status;
use App\Models\Technician;
use App\Models\ContractComment;
use Illuminate\Database\Eloquent\Model;
use TarfinLabs\LaravelSpatial\Casts\LocationCast;
use TarfinLabs\LaravelSpatial\Traits\HasSpatial;
@@ -30,6 +32,7 @@ class CurrentContracts extends Model
'coupon_id',
'transaction_id',
'status_id',
'technical_id',
];
protected $casts = [
@@ -61,4 +64,13 @@ class CurrentContracts extends Model
return $this->belongsTo(Status::class);
}
public function technician()
{
return $this->belongsTo(Technician::class, 'technical_id');
}
public function contractComments()
{
return $this->hasMany(ContractComment::class, 'contract_id');
}
}

View File

@@ -10,6 +10,7 @@ use App\Models\Status;
use App\Models\Report;
use App\Models\Payments;
use App\Models\NoHome;
use App\Models\Technician;
use Illuminate\Database\Eloquent\Model;
use TarfinLabs\LaravelSpatial\Casts\LocationCast;
use TarfinLabs\LaravelSpatial\Traits\HasSpatial;
@@ -34,6 +35,7 @@ class FinishedContracts extends Model
'transaction_id',
'score',
'status_id',
'technical_id',
];
protected $casts = [
@@ -80,4 +82,9 @@ class FinishedContracts extends Model
return $this->hasOne(NoHome::class);
}
public function technician()
{
return $this->belongsTo(Technician::class, 'technical_id');
}
}

View File

@@ -0,0 +1,22 @@
<?php
namespace App\Models;
use App\Models\User;
use Illuminate\Database\Eloquent\Model;
class PaymentBatch extends Model
{
protected $fillable = [
'file_name',
'file_path',
'generated_by',
'date_from',
'date_to',
];
public function user()
{
return $this->belongsTo(User::class, 'generated_by');
}
}

View File

@@ -6,6 +6,7 @@ use App\Models\User;
use App\Models\Categories;
use App\Models\Suppliers;
use App\Models\Status;
use App\Models\Property;
use Illuminate\Database\Eloquent\Model;
use TarfinLabs\LaravelSpatial\Casts\LocationCast;
use TarfinLabs\LaravelSpatial\Traits\HasSpatial;
@@ -18,17 +19,22 @@ class Postulations extends Model
protected $fillable = [
'user_id',
'category_id',
'property_id',
'address',
'int_number',
'references',
'appointment',
'amount',
'details',
'photos',
'status',
'related_postulation_id',
'status_id',
];
protected $casts = [
'location' => LocationCast::class
'location' => LocationCast::class,
'photos' => 'array',
];
public function user()
@@ -44,7 +50,19 @@ class Postulations extends Model
public function suppliers()
{
return $this->belongsToMany(Suppliers::class, 'postulations_suppliers', 'postulations_id', 'suppliers_id')->withTimestamps();
return $this->belongsToMany(Suppliers::class, 'postulations_suppliers', 'postulations_id', 'suppliers_id')
->withPivot('date_1', 'date_2', 'amount')
->withTimestamps();
}
public function property()
{
return $this->belongsTo(Property::class);
}
public function relatedPostulation()
{
return $this->belongsTo(Postulations::class, 'related_postulation_id');
}
public function status()

7
app/Models/Cards.php → app/Models/Property.php Executable file → Normal file
View File

@@ -2,15 +2,14 @@
namespace App\Models;
use App\Models\User;
use Illuminate\Database\Eloquent\Model;
class Cards extends Model
class Property extends Model
{
protected $fillable = [
'user_id',
'token',
'user_id', 'name', 'address', 'int_number', 'lat', 'lng', 'icon',
];
public function user()
{
return $this->belongsTo(User::class);

View File

@@ -20,7 +20,7 @@ class Report extends Model
public function finishedcontracts()
{
return $this->belongsTo(FinishedContracts::class);
return $this->belongsTo(FinishedContracts::class, 'contract_id');
}
public function user()

View File

@@ -10,6 +10,7 @@ use App\Models\Postulations;
use App\Models\FinishedContracts;
use App\Models\CurrentContracts;
use App\Models\Payments;
use App\Models\Technician;
use TarfinLabs\LaravelSpatial\Casts\LocationCast;
use TarfinLabs\LaravelSpatial\Traits\HasSpatial;
use Illuminate\Database\Eloquent\Model;
@@ -55,7 +56,9 @@ class Suppliers extends Model
public function postulations()
{
return $this->belongsToMany(Postulations::class, 'postulations_suppliers', 'suppliers_id', 'postulations_id')->withTimestamps();
return $this->belongsToMany(Postulations::class, 'postulations_suppliers', 'suppliers_id', 'postulations_id')
->withPivot('date_1', 'date_2', 'amount')
->withTimestamps();
}
public function payments()
@@ -73,6 +76,11 @@ class Suppliers extends Model
return $this->hasMany(FinishedContracts::class);
}
public function technicians()
{
return $this->hasMany(Technician::class, 'supplier_id');
}
public function ichambaparameters()
{
return $this->belongsTo(iChambaParameter::class);

25
app/Models/Technician.php Normal file
View File

@@ -0,0 +1,25 @@
<?php
namespace App\Models;
use App\Models\User;
use App\Models\Suppliers;
use Illuminate\Database\Eloquent\Model;
class Technician extends Model
{
protected $fillable = [
'user_id',
'supplier_id',
];
public function user()
{
return $this->belongsTo(User::class);
}
public function supplier()
{
return $this->belongsTo(Suppliers::class, 'supplier_id');
}
}

View File

@@ -4,11 +4,12 @@ namespace App\Models;
use App\Models\Role;
use App\Models\LinkedSocialAccount;
use App\Models\Cards;
use App\Models\Suppliers;
use App\Models\Postulations;
use App\Models\Report;
use App\Models\ReportComment;
use App\Models\Property;
use App\Models\ContractComment;
use App\Models\CurrentContracts;
use App\Models\FinishedContracts;
use Laravel\Passport\HasApiTokens;
@@ -27,10 +28,9 @@ class User extends Authenticatable
'profile_photo',
'role_id',
'social_id',
'openpay_id',
'stripe_customer_id',
'password',
'phone',
'openpay_id',
'phone_verified_at'
];
protected $hidden = [
@@ -54,11 +54,6 @@ class User extends Authenticatable
return $this->hasMany(LinkedSocialAccount::class);
}
public function cards()
{
return $this->hasMany(Cards::class);
}
public function suppliers()
{
return $this->hasOne(Suppliers::class);
@@ -89,6 +84,16 @@ class User extends Authenticatable
return $this->hasMany(ReportComment::class);
}
public function properties()
{
return $this->hasMany(Property::class);
}
public function contractcomments()
{
return $this->hasMany(ContractComment::class);
}
public function roles()
{
return $this->belongsTo(Role::class, 'role_id');

View File

@@ -0,0 +1,65 @@
<?php
namespace App\Providers;
use App\Models\User;
use Illuminate\Support\Facades\Gate;
use Laravel\Telescope\IncomingEntry;
use Laravel\Telescope\Telescope;
use Laravel\Telescope\TelescopeApplicationServiceProvider;
class TelescopeServiceProvider extends TelescopeApplicationServiceProvider
{
/**
* Register any application services.
*/
public function register(): void
{
// Telescope::night();
$this->hideSensitiveRequestDetails();
$isLocal = $this->app->environment('local');
Telescope::filter(function (IncomingEntry $entry) use ($isLocal) {
return $isLocal ||
$entry->isReportableException() ||
$entry->isFailedRequest() ||
$entry->isFailedJob() ||
$entry->isScheduledTask() ||
$entry->hasMonitoredTag();
});
}
/**
* Prevent sensitive request details from being logged by Telescope.
*/
protected function hideSensitiveRequestDetails(): void
{
if ($this->app->environment('local')) {
return;
}
Telescope::hideRequestParameters(['_token']);
Telescope::hideRequestHeaders([
'cookie',
'x-csrf-token',
'x-xsrf-token',
]);
}
/**
* Register the Telescope gate.
*
* This gate determines who can access Telescope in non-local environments.
*/
protected function gate(): void
{
Gate::define('viewTelescope', function (User $user) {
return in_array($user->email, [
//
]);
});
}
}

View File

@@ -17,6 +17,7 @@ class PushNotificationService
*/
public function sendToUser(int $userId, string $message, string $heading = 'JobHero', array $data = [])
{
try {
return OneSignal::sendNotificationToExternalUser(
$message,
(string) $userId,
@@ -26,6 +27,7 @@ class PushNotificationService
null,
$heading
);
} catch (\Exception $e) {}
}
/**
@@ -58,6 +60,7 @@ class PushNotificationService
*/
public function sendScheduledToUser(int $userId, string $message, string $sendAt, string $heading = 'JobHero', array $data = [])
{
try {
return OneSignal::sendNotificationToExternalUser(
$message,
(string) $userId,
@@ -67,6 +70,7 @@ class PushNotificationService
$sendAt,
$heading
);
} catch (\Exception $e) {}
}
/**
@@ -80,7 +84,7 @@ class PushNotificationService
*/
public function sendToRole(int $roleId, string $message, string $heading = 'JobHero', array $data = [])
{
return OneSignal::sendNotificationUsingTags(
try { return OneSignal::sendNotificationUsingTags(
$message,
[
['field' => 'tag', 'key' => 'iChamba_Role', 'relation' => '=', 'value' => (string) $roleId]
@@ -107,7 +111,7 @@ class PushNotificationService
null,
null,
$data
);
); } catch (\Exception $e) {}
}
/**

14
betos_branch.md Normal file
View File

@@ -0,0 +1,14 @@
Este documento tiene notas tanto para back como para front. El backend está hecho en Laravel y el front en Ionic, adecua las notas a tu área correspondiente. Los cambios a continuación son bastante radicales, asi que sugiero crear un branch que se llame “betos” para trabajar esto.
Los clientes deben de tener una tabla de propiedades (properties), que guarda las direcciones de sus propiedades (con coordenadas y numero exterior). Esto va a ser un cambio en category.ts, porque ahora donde dice Dirección va a ser Propiedad, con un listbox que se alimente de las propiedades ligadas al usuario de la siguente forma “Nombre de la propiedad: Dirección”, si no hay ninguna o el servicio no es para ninguna de estas, debe de abrirse el modal “Agregar Propiedad”, que pida la dirección (con autocomplete), guarde las coordenadas y el numero exterior (inputs hidden igual que en category.ts), y la guarde con un nombre personalizado asignado por el usuario (E incluso le puede poner un icono para designar si es casa u oficina).
Para las postulaciones vamos a tener cambios radicales; ahora el cliente solo pondrá la categoría, la propiedad y comentarios cuando quiera postular un servicio. Adiós a definir un precio y fecha. También es importante que el cliente pueda adjuntar fotos de lo que necesita que se repare o se instale.
Cuando el proveedor se postula a una postulación, debe definir una fecha en la que puede realizar el servicio y a que costo (ojo, la cuota mínima a nivel sistema se mantiene). Nota para back: eso también quiere decir que ahora ya no debe de haber un mínimum fee en la tabla de suppliers, y que la tabla pivote postulations_suppliers debe de guardar al menos dos fechas tentativas en las que puede dar el servicio y el monto que desea cobrar por el servicio.
En viewsuppliers el cliente debe de poder filtrar a los postulantes por fecha más pronta en la que se puede realizar el servicio, monto más barato y si el proveedor está certificado. Definir si es más fácil hacer estos sorts en front o en back
Una vez que la postulación se vuelva un CurrentContract, debemos de implementar la posibilidad de que el cliente y el proveedor se comuniquen dentro de la app de la misma manera que lo hacen en ReportDiscussion. Nota para back: Hay que agregar un botón en los Contratos Actuales que te permita ver la discusión de cada contrato y los detalles del contrato igual que lo hacemos ya en los Reportes.
Por último, con estos nuevos cambios se vuelve complicado determinar como manejaremos el NoHome, si tienes alguna idea eres libre de comentar.
Dime si estoy omitiendo algo que sea importante o nos pueda dar problemas.

View File

@@ -39,6 +39,8 @@
"openpay/sdk": "^2.0",
"spatie/laravel-google-cloud-storage": "^2.3",
"spatie/laravel-html": "^3.0",
"phpoffice/phpspreadsheet": "^2.0",
"stripe/stripe-php": "^20.2",
"tarfin-labs/laravel-spatial": "^3.0",
"timehunter/laravel-google-recaptcha-v3": "^2.4"
},

431
composer.lock generated
View File

@@ -4,7 +4,7 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
"content-hash": "c61511f0eed8a14cd184f0bae9104e5e",
"content-hash": "36319ee9cf7a1e9c627e9e9d19baf6e6",
"packages": [
{
"name": "berkayk/onesignal-laravel",
@@ -398,6 +398,82 @@
],
"time": "2024-02-09T16:56:22+00:00"
},
{
"name": "composer/pcre",
"version": "3.4.0",
"source": {
"type": "git",
"url": "https://github.com/composer/pcre.git",
"reference": "d5a341b3fb61f3001970940afb1d332968a183ed"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/composer/pcre/zipball/d5a341b3fb61f3001970940afb1d332968a183ed",
"reference": "d5a341b3fb61f3001970940afb1d332968a183ed",
"shasum": ""
},
"require": {
"php": "^7.4 || ^8.0"
},
"conflict": {
"phpstan/phpstan": "<2.2.2"
},
"require-dev": {
"phpstan/phpstan": "^2",
"phpstan/phpstan-deprecation-rules": "^2",
"phpstan/phpstan-strict-rules": "^2",
"phpunit/phpunit": "^9"
},
"type": "library",
"extra": {
"phpstan": {
"includes": [
"extension.neon"
]
},
"branch-alias": {
"dev-main": "3.x-dev"
}
},
"autoload": {
"psr-4": {
"Composer\\Pcre\\": "src"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Jordi Boggiano",
"email": "j.boggiano@seld.be",
"homepage": "http://seld.be"
}
],
"description": "PCRE wrapping library that offers type-safe preg_* replacements.",
"keywords": [
"PCRE",
"preg",
"regex",
"regular expression"
],
"support": {
"issues": "https://github.com/composer/pcre/issues",
"source": "https://github.com/composer/pcre/tree/3.4.0"
},
"funding": [
{
"url": "https://packagist.com",
"type": "custom"
},
{
"url": "https://github.com/composer",
"type": "github"
}
],
"time": "2026-06-07T11:47:49+00:00"
},
{
"name": "cuyz/valinor",
"version": "2.4.0",
@@ -4249,6 +4325,191 @@
],
"time": "2026-03-08T20:05:35+00:00"
},
{
"name": "maennchen/zipstream-php",
"version": "3.2.2",
"source": {
"type": "git",
"url": "https://github.com/maennchen/ZipStream-PHP.git",
"reference": "77bebeb4c6c340bb3c11c843b2cffd8bbfde4d5e"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/maennchen/ZipStream-PHP/zipball/77bebeb4c6c340bb3c11c843b2cffd8bbfde4d5e",
"reference": "77bebeb4c6c340bb3c11c843b2cffd8bbfde4d5e",
"shasum": ""
},
"require": {
"ext-mbstring": "*",
"ext-zlib": "*",
"php-64bit": "^8.3"
},
"require-dev": {
"brianium/paratest": "^7.7",
"ext-zip": "*",
"friendsofphp/php-cs-fixer": "^3.86",
"guzzlehttp/guzzle": "^7.5",
"mikey179/vfsstream": "^1.6",
"php-coveralls/php-coveralls": "^2.5",
"phpunit/phpunit": "^12.0",
"vimeo/psalm": "^6.0"
},
"suggest": {
"guzzlehttp/psr7": "^2.4",
"psr/http-message": "^2.0"
},
"type": "library",
"autoload": {
"psr-4": {
"ZipStream\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Paul Duncan",
"email": "pabs@pablotron.org"
},
{
"name": "Jonatan Männchen",
"email": "jonatan@maennchen.ch"
},
{
"name": "Jesse Donat",
"email": "donatj@gmail.com"
},
{
"name": "András Kolesár",
"email": "kolesar@kolesar.hu"
}
],
"description": "ZipStream is a library for dynamically streaming dynamic zip files from PHP without writing to the disk at all on the server.",
"keywords": [
"stream",
"zip"
],
"support": {
"issues": "https://github.com/maennchen/ZipStream-PHP/issues",
"source": "https://github.com/maennchen/ZipStream-PHP/tree/3.2.2"
},
"funding": [
{
"url": "https://github.com/maennchen",
"type": "github"
}
],
"time": "2026-04-11T18:38:28+00:00"
},
{
"name": "markbaker/complex",
"version": "3.0.2",
"source": {
"type": "git",
"url": "https://github.com/MarkBaker/PHPComplex.git",
"reference": "95c56caa1cf5c766ad6d65b6344b807c1e8405b9"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/MarkBaker/PHPComplex/zipball/95c56caa1cf5c766ad6d65b6344b807c1e8405b9",
"reference": "95c56caa1cf5c766ad6d65b6344b807c1e8405b9",
"shasum": ""
},
"require": {
"php": "^7.2 || ^8.0"
},
"require-dev": {
"dealerdirect/phpcodesniffer-composer-installer": "dev-master",
"phpcompatibility/php-compatibility": "^9.3",
"phpunit/phpunit": "^7.0 || ^8.0 || ^9.0",
"squizlabs/php_codesniffer": "^3.7"
},
"type": "library",
"autoload": {
"psr-4": {
"Complex\\": "classes/src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Mark Baker",
"email": "mark@lange.demon.co.uk"
}
],
"description": "PHP Class for working with complex numbers",
"homepage": "https://github.com/MarkBaker/PHPComplex",
"keywords": [
"complex",
"mathematics"
],
"support": {
"issues": "https://github.com/MarkBaker/PHPComplex/issues",
"source": "https://github.com/MarkBaker/PHPComplex/tree/3.0.2"
},
"time": "2022-12-06T16:21:08+00:00"
},
{
"name": "markbaker/matrix",
"version": "3.0.1",
"source": {
"type": "git",
"url": "https://github.com/MarkBaker/PHPMatrix.git",
"reference": "728434227fe21be27ff6d86621a1b13107a2562c"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/MarkBaker/PHPMatrix/zipball/728434227fe21be27ff6d86621a1b13107a2562c",
"reference": "728434227fe21be27ff6d86621a1b13107a2562c",
"shasum": ""
},
"require": {
"php": "^7.1 || ^8.0"
},
"require-dev": {
"dealerdirect/phpcodesniffer-composer-installer": "dev-master",
"phpcompatibility/php-compatibility": "^9.3",
"phpdocumentor/phpdocumentor": "2.*",
"phploc/phploc": "^4.0",
"phpmd/phpmd": "2.*",
"phpunit/phpunit": "^7.0 || ^8.0 || ^9.0",
"sebastian/phpcpd": "^4.0",
"squizlabs/php_codesniffer": "^3.7"
},
"type": "library",
"autoload": {
"psr-4": {
"Matrix\\": "classes/src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Mark Baker",
"email": "mark@demon-angel.eu"
}
],
"description": "PHP Class for working with matrices",
"homepage": "https://github.com/MarkBaker/PHPMatrix",
"keywords": [
"mathematics",
"matrix",
"vector"
],
"support": {
"issues": "https://github.com/MarkBaker/PHPMatrix/issues",
"source": "https://github.com/MarkBaker/PHPMatrix/tree/3.0.1"
},
"time": "2022-12-02T22:17:43+00:00"
},
{
"name": "mercadopago/dx-php",
"version": "3.10.0",
@@ -5115,6 +5376,112 @@
},
"time": "2024-10-02T11:20:13+00:00"
},
{
"name": "phpoffice/phpspreadsheet",
"version": "2.4.6",
"source": {
"type": "git",
"url": "https://github.com/PHPOffice/PhpSpreadsheet.git",
"reference": "0bbef382b7d9c1dbda10c8113d564ff9159a7e79"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/PHPOffice/PhpSpreadsheet/zipball/0bbef382b7d9c1dbda10c8113d564ff9159a7e79",
"reference": "0bbef382b7d9c1dbda10c8113d564ff9159a7e79",
"shasum": ""
},
"require": {
"composer/pcre": "^1 || ^2 || ^3",
"ext-ctype": "*",
"ext-dom": "*",
"ext-fileinfo": "*",
"ext-gd": "*",
"ext-iconv": "*",
"ext-libxml": "*",
"ext-mbstring": "*",
"ext-simplexml": "*",
"ext-xml": "*",
"ext-xmlreader": "*",
"ext-xmlwriter": "*",
"ext-zip": "*",
"ext-zlib": "*",
"maennchen/zipstream-php": "^2.1 || ^3.0",
"markbaker/complex": "^3.0",
"markbaker/matrix": "^3.0",
"php": ">=8.1.0 <8.6.0",
"psr/simple-cache": "^1.0 || ^2.0 || ^3.0"
},
"require-dev": {
"dealerdirect/phpcodesniffer-composer-installer": "dev-main",
"dompdf/dompdf": "^2.0 || ^3.0",
"friendsofphp/php-cs-fixer": "^3.2",
"mitoteam/jpgraph": "^10.5",
"mpdf/mpdf": "^8.1.1",
"phpcompatibility/php-compatibility": "^9.3",
"phpstan/phpstan": "^1.1",
"phpstan/phpstan-phpunit": "^1.0",
"phpunit/phpunit": "^9.6 || ^10.5",
"squizlabs/php_codesniffer": "^3.7",
"tecnickcom/tcpdf": "^6.5"
},
"suggest": {
"dompdf/dompdf": "Option for rendering PDF with PDF Writer",
"ext-intl": "PHP Internationalization Functions, required for NumberFormatter Wizard",
"mitoteam/jpgraph": "Option for rendering charts, or including charts with PDF or HTML Writers",
"mpdf/mpdf": "Option for rendering PDF with PDF Writer",
"tecnickcom/tcpdf": "Option for rendering PDF with PDF Writer"
},
"type": "library",
"autoload": {
"psr-4": {
"PhpOffice\\PhpSpreadsheet\\": "src/PhpSpreadsheet"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Maarten Balliauw",
"homepage": "https://blog.maartenballiauw.be"
},
{
"name": "Mark Baker",
"homepage": "https://markbakeruk.net"
},
{
"name": "Franck Lefevre",
"homepage": "https://rootslabs.net"
},
{
"name": "Erik Tilt"
},
{
"name": "Adrien Crivelli"
},
{
"name": "Owen Leibman"
}
],
"description": "PHPSpreadsheet - Read, Create and Write Spreadsheet documents in PHP - Spreadsheet engine",
"homepage": "https://github.com/PHPOffice/PhpSpreadsheet",
"keywords": [
"OpenXML",
"excel",
"gnumeric",
"ods",
"php",
"spreadsheet",
"xls",
"xlsx"
],
"support": {
"issues": "https://github.com/PHPOffice/PhpSpreadsheet/issues",
"source": "https://github.com/PHPOffice/PhpSpreadsheet/tree/2.4.6"
},
"time": "2026-06-07T02:31:12+00:00"
},
{
"name": "phpoption/phpoption",
"version": "1.9.5",
@@ -6365,6 +6732,68 @@
],
"time": "2026-02-22T09:05:56+00:00"
},
{
"name": "stripe/stripe-php",
"version": "v20.2.1",
"source": {
"type": "git",
"url": "https://github.com/stripe/stripe-php.git",
"reference": "c628cfa0b3de4ef5110b2c2bfbf881a33a52fdd5"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/stripe/stripe-php/zipball/c628cfa0b3de4ef5110b2c2bfbf881a33a52fdd5",
"reference": "c628cfa0b3de4ef5110b2c2bfbf881a33a52fdd5",
"shasum": ""
},
"require": {
"ext-curl": "*",
"ext-json": "*",
"ext-mbstring": "*",
"php": ">=7.2.0"
},
"require-dev": {
"friendsofphp/php-cs-fixer": "3.94.0",
"phpstan/phpstan": "^1.2",
"phpunit/phpunit": "^8.0 || ^9.0"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "2.0-dev"
}
},
"autoload": {
"files": [
"lib/version_check.php"
],
"psr-4": {
"Stripe\\": "lib/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Stripe and contributors",
"homepage": "https://github.com/stripe/stripe-php/contributors"
}
],
"description": "Stripe PHP Library",
"homepage": "https://stripe.com/",
"keywords": [
"api",
"payment processing",
"stripe"
],
"support": {
"issues": "https://github.com/stripe/stripe-php/issues",
"source": "https://github.com/stripe/stripe-php/tree/v20.2.1"
},
"time": "2026-06-12T22:41:56+00:00"
},
{
"name": "symfony/cache",
"version": "v8.1.0",

212
config/telescope.php Normal file
View File

@@ -0,0 +1,212 @@
<?php
use Laravel\Telescope\Http\Middleware\Authorize;
use Laravel\Telescope\Watchers;
return [
/*
|--------------------------------------------------------------------------
| Telescope Master Switch
|--------------------------------------------------------------------------
|
| This option may be used to disable all Telescope watchers regardless
| of their individual configuration, which simply provides a single
| and convenient way to enable or disable Telescope data storage.
|
*/
'enabled' => env('TELESCOPE_ENABLED', true),
/*
|--------------------------------------------------------------------------
| Telescope Domain
|--------------------------------------------------------------------------
|
| This is the subdomain where Telescope will be accessible from. If the
| setting is null, Telescope will reside under the same domain as the
| application. Otherwise, this value will be used as the subdomain.
|
*/
'domain' => env('TELESCOPE_DOMAIN'),
/*
|--------------------------------------------------------------------------
| Telescope Path
|--------------------------------------------------------------------------
|
| This is the URI path where Telescope will be accessible from. Feel free
| to change this path to anything you like. Note that the URI will not
| affect the paths of its internal API that aren't exposed to users.
|
*/
'path' => env('TELESCOPE_PATH', 'telescope'),
/*
|--------------------------------------------------------------------------
| Telescope Storage Driver
|--------------------------------------------------------------------------
|
| This configuration options determines the storage driver that will
| be used to store Telescope's data. In addition, you may set any
| custom options as needed by the particular driver you choose.
|
*/
'driver' => env('TELESCOPE_DRIVER', 'database'),
'storage' => [
'database' => [
'connection' => env('DB_CONNECTION', 'mysql'),
'chunk' => 1000,
],
],
/*
|--------------------------------------------------------------------------
| Telescope Queue
|--------------------------------------------------------------------------
|
| This configuration options determines the queue connection and queue
| which will be used to process ProcessPendingUpdate jobs. This can
| be changed if you would prefer to use a non-default connection.
|
*/
'queue' => [
'connection' => env('TELESCOPE_QUEUE_CONNECTION'),
'queue' => env('TELESCOPE_QUEUE'),
'delay' => env('TELESCOPE_QUEUE_DELAY', 10),
],
/*
|--------------------------------------------------------------------------
| Telescope Route Middleware
|--------------------------------------------------------------------------
|
| These middleware will be assigned to every Telescope route, giving you
| the chance to add your own middleware to this list or change any of
| the existing middleware. Or, you can simply stick with this list.
|
*/
'middleware' => [
'web',
Authorize::class,
],
/*
|--------------------------------------------------------------------------
| Allowed / Ignored Paths & Commands
|--------------------------------------------------------------------------
|
| The following array lists the URI paths and Artisan commands that will
| not be watched by Telescope. In addition to this list, some Laravel
| commands, like migrations and queue commands, are always ignored.
|
*/
'only_paths' => [
// 'api/*'
],
'ignore_paths' => [
'livewire*',
'nova-api*',
'pulse*',
'_boost*',
'.well-known*',
],
'ignore_commands' => [
//
],
/*
|--------------------------------------------------------------------------
| Telescope Watchers
|--------------------------------------------------------------------------
|
| The following array lists the "watchers" that will be registered with
| Telescope. The watchers gather the application's profile data when
| a request or task is executed. Feel free to customize this list.
|
*/
'watchers' => [
Watchers\BatchWatcher::class => env('TELESCOPE_BATCH_WATCHER', true),
Watchers\CacheWatcher::class => [
'enabled' => env('TELESCOPE_CACHE_WATCHER', true),
'hidden' => [],
'ignore' => [],
],
Watchers\ClientRequestWatcher::class => [
'enabled' => env('TELESCOPE_CLIENT_REQUEST_WATCHER', true),
'ignore_hosts' => [],
],
Watchers\CommandWatcher::class => [
'enabled' => env('TELESCOPE_COMMAND_WATCHER', true),
'ignore' => [],
],
Watchers\DumpWatcher::class => [
'enabled' => env('TELESCOPE_DUMP_WATCHER', true),
'always' => env('TELESCOPE_DUMP_WATCHER_ALWAYS', false),
],
Watchers\EventWatcher::class => [
'enabled' => env('TELESCOPE_EVENT_WATCHER', true),
'ignore' => [],
],
Watchers\ExceptionWatcher::class => env('TELESCOPE_EXCEPTION_WATCHER', true),
Watchers\GateWatcher::class => [
'enabled' => env('TELESCOPE_GATE_WATCHER', true),
'ignore_abilities' => [],
'ignore_packages' => true,
'ignore_paths' => [],
],
Watchers\JobWatcher::class => env('TELESCOPE_JOB_WATCHER', true),
Watchers\LogWatcher::class => [
'enabled' => env('TELESCOPE_LOG_WATCHER', true),
'level' => 'error',
],
Watchers\MailWatcher::class => env('TELESCOPE_MAIL_WATCHER', true),
Watchers\ModelWatcher::class => [
'enabled' => env('TELESCOPE_MODEL_WATCHER', true),
'events' => ['eloquent.*'],
'hydrations' => true,
],
Watchers\NotificationWatcher::class => env('TELESCOPE_NOTIFICATION_WATCHER', true),
Watchers\QueryWatcher::class => [
'enabled' => env('TELESCOPE_QUERY_WATCHER', true),
'ignore_packages' => true,
'ignore_paths' => [],
'slow' => 100,
],
Watchers\RedisWatcher::class => env('TELESCOPE_REDIS_WATCHER', true),
Watchers\RequestWatcher::class => [
'enabled' => env('TELESCOPE_REQUEST_WATCHER', true),
'size_limit' => env('TELESCOPE_RESPONSE_SIZE_LIMIT', 64),
'ignore_http_methods' => [],
'ignore_status_codes' => [],
],
Watchers\ScheduleWatcher::class => env('TELESCOPE_SCHEDULE_WATCHER', true),
Watchers\ViewWatcher::class => env('TELESCOPE_VIEW_WATCHER', true),
],
];

View File

@@ -20,7 +20,7 @@ class CreateUsersTable extends Migration
$table->string('profile_photo')->nullable();
$table->BigInteger('role_id')->unsigned()->nullable();
$table->string('social_id')->unique()->nullable();
$table->string('openpay_id')->unique()->nullable();
$table->string('stripe_customer_id')->unique()->nullable();
$table->string('phone')->unique()->nullable();
$table->timestamp('phone_verified_at')->nullable();
$table->string('password')->nullable();

View File

@@ -16,7 +16,11 @@ class CreateCardsTable extends Migration
Schema::create('cards', function (Blueprint $table) {
$table->bigIncrements('id');
$table->unsignedBigInteger('user_id');
$table->string('token');
$table->string('stripe_pm_id')->unique();
$table->string('brand');
$table->string('last4', 4);
$table->unsignedTinyInteger('exp_month');
$table->unsignedSmallInteger('exp_year');
$table->timestamps();
$table->foreign('user_id')->references('id')->on('users')->onDelete('cascade');
});

View File

@@ -36,7 +36,9 @@ class CreateFinishedcontractsTable extends Migration
$table->unsignedBigInteger('parent_contract_id')->nullable();
$table->Integer('score')->nullable();
$table->timestamp('scored_at')->nullable();
$table->unsignedBigInteger('technical_id')->nullable();
$table->boolean('paid')->default(false);
$table->timestamp('paid_at')->nullable();
$table->timestamps();
$table->foreign('user_id')->references('id')->on('users')->onDelete('set null');
$table->foreign('supplier_id')->references('id')->on('suppliers')->onDelete('set null');

View File

@@ -34,6 +34,7 @@ class CreateCurrentContractsTable extends Migration
$table->unsignedBigInteger('status_id')->nullable()->default('1');
$table->BigInteger('code')->nullable();
$table->unsignedBigInteger('parent_contract_id')->nullable();
$table->unsignedBigInteger('technical_id')->nullable();
$table->timestamps();
$table->foreign('user_id')->references('id')->on('users')->onDelete('set null');
$table->foreign('supplier_id')->references('id')->on('suppliers')->onDelete('set null');

View File

@@ -0,0 +1,27 @@
<?php
use Illuminate\Support\Facades\Schema;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;
class CreatePaymentBatchesTable extends Migration
{
public function up()
{
Schema::create('payment_batches', function (Blueprint $table) {
$table->bigIncrements('id');
$table->string('file_name');
$table->string('file_path');
$table->unsignedBigInteger('generated_by')->nullable();
$table->date('date_from')->nullable();
$table->date('date_to')->nullable();
$table->timestamps();
$table->foreign('generated_by')->references('id')->on('users')->onDelete('set null');
});
}
public function down()
{
Schema::dropIfExists('payment_batches');
}
}

View File

@@ -0,0 +1,42 @@
<?php
use Illuminate\Support\Facades\Schema;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;
class CreateTechniciansTable extends Migration
{
public function up()
{
Schema::create('technicians', function (Blueprint $table) {
$table->bigIncrements('id');
$table->unsignedBigInteger('user_id');
$table->unsignedBigInteger('supplier_id');
$table->timestamps();
$table->unique(['user_id', 'supplier_id']);
$table->foreign('user_id')->references('id')->on('users')->onDelete('cascade');
$table->foreign('supplier_id')->references('id')->on('suppliers')->onDelete('cascade');
});
Schema::table('finished_contracts', function (Blueprint $table) {
$table->foreign('technical_id')->references('id')->on('technicians')->onDelete('set null');
});
Schema::table('current_contracts', function (Blueprint $table) {
$table->foreign('technical_id')->references('id')->on('technicians')->onDelete('set null');
});
}
public function down()
{
Schema::table('finished_contracts', function (Blueprint $table) {
$table->dropForeign(['technical_id']);
});
Schema::table('current_contracts', function (Blueprint $table) {
$table->dropForeign(['technical_id']);
});
Schema::dropIfExists('technicians');
}
}

View File

@@ -0,0 +1,27 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up(): void
{
Schema::dropIfExists('cards');
}
public function down(): void
{
Schema::create('cards', function (Blueprint $table) {
$table->id();
$table->unsignedBigInteger('user_id');
$table->string('stripe_pm_id')->nullable();
$table->string('brand')->nullable();
$table->string('last4')->nullable();
$table->unsignedTinyInteger('exp_month')->nullable();
$table->unsignedSmallInteger('exp_year')->nullable();
$table->timestamps();
});
}
};

View File

@@ -0,0 +1,30 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
class CreatePropertiesTable extends Migration
{
public function up()
{
Schema::create('properties', function (Blueprint $table) {
$table->bigIncrements('id');
$table->unsignedBigInteger('user_id');
$table->string('name');
$table->string('address');
$table->string('int_number')->nullable();
$table->decimal('lat', 10, 7);
$table->decimal('lng', 10, 7);
$table->enum('icon', ['casa', 'oficina'])->default('casa');
$table->timestamps();
$table->foreign('user_id')->references('id')->on('users')->onDelete('cascade');
});
}
public function down()
{
Schema::dropIfExists('properties');
}
}

View File

@@ -0,0 +1,28 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
class AlterPostulationsAddPropertyNullableAmount extends Migration
{
public function up()
{
Schema::table('postulations', function (Blueprint $table) {
$table->unsignedBigInteger('property_id')->nullable()->after('category_id');
$table->json('photos')->nullable()->after('details');
$table->float('amount')->nullable()->change();
$table->foreign('property_id')->references('id')->on('properties')->onDelete('set null');
});
}
public function down()
{
Schema::table('postulations', function (Blueprint $table) {
$table->dropForeign(['property_id']);
$table->dropColumn(['property_id', 'photos']);
$table->float('amount')->nullable(false)->change();
});
}
}

View File

@@ -0,0 +1,24 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
class AlterPostulationsSuppliersAddOfferFields extends Migration
{
public function up()
{
Schema::table('postulations_suppliers', function (Blueprint $table) {
$table->timestamp('date_1')->nullable()->after('suppliers_id');
$table->timestamp('date_2')->nullable()->after('date_1');
$table->float('amount')->nullable()->after('date_2');
});
}
public function down()
{
Schema::table('postulations_suppliers', function (Blueprint $table) {
$table->dropColumn(['date_1', 'date_2', 'amount']);
});
}
}

View File

@@ -0,0 +1,27 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
class CreateContractCommentsTable extends Migration
{
public function up()
{
Schema::create('contract_comments', function (Blueprint $table) {
$table->bigIncrements('id');
$table->unsignedBigInteger('contract_id');
$table->unsignedBigInteger('user_id')->nullable();
$table->text('comment')->nullable();
$table->timestamps();
$table->foreign('contract_id')->references('id')->on('current_contracts')->onDelete('cascade');
$table->foreign('user_id')->references('id')->on('users')->onDelete('set null');
});
}
public function down()
{
Schema::dropIfExists('contract_comments');
}
}

View File

@@ -0,0 +1,27 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up(): void
{
Schema::table('postulations', function (Blueprint $table) {
$table->string('status')->default('active')->after('photos');
$table->unsignedBigInteger('related_postulation_id')->nullable()->after('status');
$table->foreign('related_postulation_id')
->references('id')->on('postulations')
->onDelete('set null');
});
}
public function down(): void
{
Schema::table('postulations', function (Blueprint $table) {
$table->dropForeign(['related_postulation_id']);
$table->dropColumn(['status', 'related_postulation_id']);
});
}
};

View File

@@ -44,6 +44,10 @@ class DatabaseSeeder extends Seeder
'name' => 'SuperAdmin',
]);
DB::table('roles')->insert([
'name' => 'Técnico',
]);
DB::table('users')->insert([
'name' => 'Admin',
'email' => 'torch2196@gmail.com',
@@ -100,6 +104,10 @@ class DatabaseSeeder extends Seeder
DB::table('statuses')->insert([
'name' => 'cancelado',
'en_name' => 'canceled',
]);
DB::table('statuses')->insert([
'name' => 'ausente',
'en_name' => 'absent',
]);
DB::table('statuses')->insert([
'name' => 'fuera de casa',
@@ -110,9 +118,14 @@ class DatabaseSeeder extends Seeder
'en_name' => 'reported',
]);
DB::table('statuses')->insert([
'name' => 'ausente',
'en_name' => 'absent',
'name' => 'devuelto al cliente',
'en_name' => 'returned to client',
]);
DB::table('statuses')->insert([
'name' => 'disptuta terminada',
'en_name' => 'complain ended',
]);
$this->call([
BanksSeeder::class,

View File

@@ -1,47 +0,0 @@
@extends('layouts.app')
@section('content')
<div class="container">
<div class="col-md-8 offset-md-2">
<h1>{{isset($bank)?'Editar':'Nuevo'}} Banco</h1>
<hr>
@if(isset($bank))
{!! Form::model($bank,['method'=>'put','id'=>'frm']) !!}
@else
{!! Form::open(['id'=>'frm']) !!}
@endif
<div class="form-group row required">
{!! Form::label("code","Código",["class"=>"col-form-label col-md-3 col-lg-2"]) !!}
<div class="col-md-8">
{!! Form::number("code",null,["class"=>"form-control".($errors->has('code')?" is-invalid":""),"autofocus",'placeholder'=>'Código del banco']) !!}
<span id="error-name" class="invalid-feedback"></span>
</div>
</div>
<div class="form-group row required">
{!! Form::label("name","Banco",["class"=>"col-form-label col-md-3 col-lg-2"]) !!}
<div class="col-md-8">
{!! Form::text("name",null,["class"=>"form-control".($errors->has('name')?" is-invalid":""),"autofocus",'placeholder'=>'Nombre del banco']) !!}
<span id="error-name" class="invalid-feedback"></span>
</div>
</div>
@if ($errors->any())
<div class="alert alert-danger">
<ul>
@foreach ($errors->all() as $error)
<li>{{ $error }}</li>
@endforeach
</ul>
</div>
@endif
<div class="form-group row">
<div class="col-md-3 col-lg-2"></div>
<div class="col-md-4">
<a href="{{url('banks')}}" class="btn btn-danger btn-xs">
Atrás</a>
{!! Form::button("Guardar",["type" => "submit","class"=>"btn btn-primary btn-xs"])!!}
</div>
</div>
{!! Form::close() !!}
</div>
</div>
@endsection

View File

@@ -1,72 +0,0 @@
@if (Auth::user()->role_id >= 5)
<div class="container-fluid" style="height:100%">
<div>
@else
<div class="container" style="margin:0 1em">
@endif
<div class="row">
<div class="col-sm-7">
<h3>Tarjetas</h3>
</div>
<div class="col-sm-5">
<div class="pull-right">
{!! Form::open(['method'=>'GET','url'=>'cards','class'=>'navbar-form navbar-left','role'=>'search']) !!}
<div class="input-group">
<input class="form-control" id="search"
value="{{ request()->session()->get('search') }}"
placeholder="Buscar" name="search"
type="text" id="search"/>
<div class="input-group-btn">
<button type="submit" class="btn btn-primary">
<i class="fa fa-search" aria-hidden="true"></i>
</button>
</div>
</div>
{!! Form::close() !!}
</div>
</div>
</div>
<table class="table">
<thead>
<tr>
<th style="vertical-align: middle"><a href="{{url('cards?field=id&sort='.(request()->session()->get('sort')=='asc'?'desc':'asc'))}}">ID</a></th>
{{request()->session()->get('field')=='id'?(request()->session()->get('sort')=='asc'?'':''):''}}
<th style="vertical-align: middle"><a href="{{url('cards?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('cards?field=user_id&sort='.(request()->session()->get('sort')=='asc'?'desc':'asc'))}}">Openpay ID</a></th>
{{request()->session()->get('field')=='user_id'?(request()->session()->get('sort')=='asc'?'':''):''}}
<th style="vertical-align: middle"><a href="{{url('cards?field=token&sort='.(request()->session()->get('sort')=='asc'?'desc':'asc'))}}">Token</a></th>
{{request()->session()->get('field')=='token'?(request()->session()->get('sort')=='asc'?'':''):''}}
</tr>
</thead>
<tbody>
@php
$i=1;
@endphp
@foreach ($cards as $card)
<tr>
<th>{{ $card->id }}</th>
<th>{{ $card->user->name }}</th>
<td>{{ $card->user->openpay_id }}</td>
<td>{{ $card->token}}</td>
<td>
<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 esta tarjeta?')) javascript:if(confirm('Usualmente no se deben eliminar tarjetas, ¿Estás seguro?')) ajaxDelete('{{url('cards/delete/'.$card->id)}}','{{csrf_token()}}')">
<i class="fa fa-trash"></i>
</a>
</td>
</tr>
@endforeach
</tbody>
</table>
{{ $cards->links() }}
</div>

View File

@@ -0,0 +1,230 @@
@extends('layouts.app')
@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-actions {
position: absolute;
right: 1rem;
top: 50%;
transform: translateY(-50%);
display: flex;
gap: 0.4rem;
}
.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;
}
</style>
@endpush
@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 Activo #{{ $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>
<span>Status: <strong>{{ $contract->status->name ?? $contract->status_id }}</strong></span>
<div class="btn-actions">
<button type="button" class="btn btn-info btn-xs" title="Ver detalles"
data-toggle="modal" data-target="#modalDetalles">
<i class="fa fa-info-circle"></i>
</button>
</div>
</div>
{{-- 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>
</div>
{{-- 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
<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 mensajes aún.</p>
@endforelse
</div>
{{-- Paginación --}}
@if($comments->hasPages())
<div class="mb-2">{{ $comments->links() }}</div>
@endif
{{-- Input del moderador --}}
<div class="chat-input">
<form method="POST" action="{{ url('currentcontracts/' . $contract->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 mensaje como moderador..."></textarea>
<button type="submit" class="btn btn-primary mt-2">Enviar</button>
</form>
</div>
</div>
{{-- Modal fuera del chat-wrapper --}}
<div class="modal fade" id="modalDetalles" tabindex="-1" role="dialog">
<div class="modal-dialog modal-lg" role="document">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">Detalles del Contrato Activo #{{ $contract->id }}</h5>
<button type="button" class="close" data-dismiss="modal">
<span>&times;</span>
</button>
</div>
<div class="modal-body">
<table class="table table-sm table-borderless mb-4">
<tr><th style="width:35%">Cliente</th><td>{{ $contract->user->name ?? '—' }}</td></tr>
<tr><th>Proveedor</th><td>{{ $contract->suppliers->company_name ?? '—' }}</td></tr>
<tr><th>Categoría</th><td>{{ $contract->categories->name ?? '—' }}</td></tr>
<tr><th>Dirección</th><td>{{ $contract->address }}</td></tr>
<tr><th>Cita</th><td>{{ $contract->appointment }}</td></tr>
<tr><th>Monto</th><td>${{ $contract->amount }}</td></tr>
<tr><th>PIN</th><td><code>{{ $contract->code }}</code></td></tr>
<tr><th>Status</th><td>{{ $contract->status->name ?? $contract->status_id }}</td></tr>
<tr><th>Transaction ID</th><td><code>{{ $contract->transaction_id ?? '—' }}</code></td></tr>
</table>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-dismiss="modal">Cerrar</button>
</div>
</div>
</div>
</div>
@endsection
@section('js')
<script>
$(document).ready(function () {
$('#modalDetalles').appendTo('body');
});
</script>
@endsection

View File

@@ -101,6 +101,10 @@
<td>{{ $currentcontract->created_at }}</td>
<td>{{ $currentcontract->updated_at }}</td>
<td>
<a href="{{ url('currentcontracts/' . $currentcontract->id . '/comments') }}"
class="btn btn-info btn-xs" title="Chat">
<i class="fa fa-comments"></i>
</a>
<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 esta contrato?')) javascript:if(confirm('Usualmente no se deben eliminar contratos, ¿Estás seguro?')) ajaxDelete('{{url('currentcontracts/delete/'.$currentcontract->id)}}','{{csrf_token()}}')">

View File

@@ -111,7 +111,7 @@
@else
<li class="nav-item dropdown">
<a id="navbarDropdown" class="nav-link dropdown-toggle" href="#" role="button" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false" v-pre style="padding-left:55px;">
<img src="{{ isset(Auth::user()->profile_photo) ? asset('img/users/' . Auth::user()->id . '/' . Auth::user()->profile_photo) : asset('img/users/default.png') }}" style="width:48px; height:auto; position:absolute; top:-5px; left:-1px; border-radius:50%"/>
<img src="{{ Auth::user()->profile_photo ?? asset('img/users/default.png') }}" style="width:48px; height:auto; position:absolute; top:-5px; left:-1px; border-radius:50%"/>
{{ Auth::user()->name }} <span class="caret"></span>
</a>

View File

@@ -2,7 +2,7 @@
@section('content')
<div id="content">
@include('cards.index')
@include('payment-batches.index')
</div>
<div class="loading">
<i class="fa fa-refresh fa-spin fa-2x fa-tw"></i>

View File

@@ -0,0 +1,56 @@
<div class="container-fluid">
<div class="row mb-3">
<div class="col">
<h3>Pagos Realizados</h3>
</div>
</div>
@if(session('error'))
<div class="alert alert-danger">{{ session('error') }}</div>
@endif
<table class="table">
<thead>
<tr>
<th>#</th>
<th>Archivo</th>
<th>Generado por</th>
<th>Fecha de generación</th>
<th>Rango de fechas</th>
<th></th>
</tr>
</thead>
<tbody>
@forelse($batches as $batch)
<tr>
<td>{{ $batch->id }}</td>
<td>{{ $batch->file_name }}</td>
<td>{{ optional($batch->user)->name ?? '—' }}</td>
<td>{{ $batch->created_at->format('d/m/Y H:i') }}</td>
<td>
@if($batch->date_from || $batch->date_to)
{{ $batch->date_from ? \Carbon\Carbon::parse($batch->date_from)->format('d/m/Y') : '—' }}
al
{{ $batch->date_to ? \Carbon\Carbon::parse($batch->date_to)->format('d/m/Y') : '—' }}
@else
Todos los registros
@endif
</td>
<td>
<a href="{{ url('payment-batches/' . $batch->id . '/download') }}"
class="btn btn-sm btn-outline-primary">
<i class="fa fa-download"></i> Descargar
</a>
</td>
</tr>
@empty
<tr>
<td colspan="6" class="text-center text-muted">Sin pagos realizados aún.</td>
</tr>
@endforelse
</tbody>
</table>
{{ $batches->links() }}
</div>

View File

@@ -13,27 +13,53 @@
$sortUrl = fn($f) => url('payments') . '?' . http_build_query(array_filter(['date_from' => $dateFrom, 'date_to' => $dateTo, 'field' => $f, 'sort' => $currentField === $f ? $nextSort : 'desc']));
@endphp
<div class="row">
<div class="col-sm-5">
<h3>Pagos a Proveedores</h3>
@if(session('warning'))
<div class="alert alert-warning">{{ session('warning') }}</div>
@endif
<div class="row align-items-center mb-2">
<div class="col-sm-4">
<h3 class="mb-0">Pagos a Proveedores</h3>
</div>
<div class="col-sm-7">
<div class="pull-right">
{!! Form::open(['method' => 'GET', 'url' => 'payments', 'class' => 'form-inline', 'role' => 'search']) !!}
<label class="mr-2 mb-0">Filtrar</label>
<input type="date" class="form-control mr-1" name="date_from" value="{{ $dateFrom }}">
<input type="date" class="form-control mr-1" name="date_to" value="{{ $dateTo }}">
<button type="submit" class="btn btn-primary mr-1">
<i class="fa fa-filter" aria-hidden="true"></i>
<div class="col-sm-8">
<div class="d-flex justify-content-end align-items-center flex-wrap" style="gap:.5rem">
{!! Form::open(['method' => 'GET', 'url' => 'payments', 'class' => 'd-flex align-items-center', 'style' => 'gap:.4rem']) !!}
<label class="mb-0 mr-1">Filtrar</label>
<input type="date" class="form-control form-control-sm" name="date_from" value="{{ $dateFrom }}">
<input type="date" class="form-control form-control-sm" name="date_to" value="{{ $dateTo }}">
<button type="submit" class="btn btn-sm btn-primary">
<i class="fa fa-filter"></i>
</button>
<a href="{{ url('payments') }}" class="btn btn-default">
<i class="fa fa-times" aria-hidden="true"></i>
<a href="{{ url('payments') }}" class="btn btn-sm btn-default">
<i class="fa fa-times"></i>
</a>
{!! Form::close() !!}
<form method="POST" action="{{ url('payments/generate') }}" id="generateForm">
@csrf
<input type="hidden" name="date_from" value="{{ $dateFrom }}">
<input type="hidden" name="date_to" value="{{ $dateTo }}">
<button type="button" class="btn btn-sm btn-success" onclick="confirmGenerate()">
<i class="fa fa-file-excel-o"></i> Generar Pagos
</button>
</form>
</div>
</div>
</div>
<script>
function confirmGenerate() {
var total = {{ $payments->total() }};
if (total === 0) {
alert('No hay contratos pendientes de pago.');
return;
}
if (confirm('Se generará un archivo Excel con ' + total + ' proveedor(es) y se marcarán sus contratos como pagados. ¿Continuar?')) {
document.getElementById('generateForm').submit();
}
}
</script>
<table class="table">
<thead>
<tr>

View File

@@ -1,116 +1,279 @@
@extends('layouts.app')
@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-actions {
position: absolute;
right: 1rem;
top: 50%;
transform: translateY(-50%);
display: flex;
gap: 0.4rem;
}
.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
@section('content')
<script src="{{ asset('js/ajaxcrud.js') }}"></script>
@if (Auth::user()->role_id >= 5)
<div class="container-fluid" style="height:100%">
@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
<div class="btn-actions">
<button type="button" class="btn btn-info btn-xs" title="Ver detalles"
data-toggle="modal" data-target="#modalDetalles">
<i class="fa fa-info-circle"></i>
</button>
<a class="btn btn-secondary btn-xs" title="Veredicto"
href="{{ url('reports/veredict/' . $report->id) }}">
<i class="fa fa-gavel"></i>
</a>
</div>
</div>
{{-- 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>
{{-- 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
<div class="bubble-row {{ $side }}">
<div>
@else
<div class="container" style="margin:0 1em">
<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>
{{-- Paginación --}}
@if($comments->hasPages())
<div class="mb-2">{{ $comments->links() }}</div>
@endif
<div class="row">
<div class="col-sm-7">
<h3>Comentarios del reporte</h3>
</div>
{{-- 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>
<br>
<h5>Contrato:</h5>
<table class="table">
<thead>
<tr>
<th style="vertical-align: middle">ID</th>
</div>
<th style="vertical-align: middle">Usuario</th>
{{-- Modal fuera del chat-wrapper para evitar stacking context del flex --}}
<div class="modal fade" id="modalDetalles" tabindex="-1" role="dialog">
<div class="modal-dialog modal-lg" role="document">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">Detalles del Contrato #{{ $contract->id }}</h5>
<button type="button" class="close" data-dismiss="modal">
<span>&times;</span>
</button>
</div>
<div class="modal-body">
<th style="vertical-align: middle">Proveedor</th>
<th style="vertical-align: middle">Categoría</th>
<th style="vertical-align: middle">Dirección</th>
<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>
<h6 class="font-weight-bold mb-2">Información del contrato</h6>
<table class="table table-sm table-borderless mb-4">
<tr><th style="width:35%">Cliente</th><td>{{ $contract->user->name ?? '—' }}</td></tr>
<tr><th>Proveedor</th><td>{{ $contract->suppliers->company_name ?? '—' }}</td></tr>
<tr><th>Categoría</th><td>{{ $contract->categories->name ?? '—' }}</td></tr>
<tr><th>Dirección</th><td>{{ $contract->address }}</td></tr>
<tr><th>Cita</th><td>{{ $contract->appointment }}</td></tr>
<tr><th>Monto</th><td>${{ $contract->amount }}</td></tr>
<tr><th>Estado</th><td>{{ $contract->status->name ?? $contract->status_id }}</td></tr>
<tr><th>Transaction ID</th><td><code>{{ $contract->transaction_id ?? '—' }}</code></td></tr>
</table>
<br><br>
<h5>Comentarios:</h5>
<table class="table">
<thead>
@if($nohome)
<hr>
<h6 class="font-weight-bold mb-2">Evidencia del proveedor (No Home)</h6>
<table class="table table-sm table-borderless mb-3">
<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'?'':''):''}}
<th style="width:35%">Descripción</th>
<td>{{ $nohome->house_description ?? '' }}</td>
</tr>
</thead>
<tbody>
@php
$i=1;
@endphp
@foreach ($comments as $comment)
@if($nohome->location)
<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>
<th>Coordenadas GPS</th>
<td>
{{ $nohome->location->getLatitude() }}, {{ $nohome->location->getLongitude() }}
<a href="https://www.google.com/maps?q={{ $nohome->location->getLatitude() }},{{ $nohome->location->getLongitude() }}"
target="_blank" class="btn btn-xs btn-outline-secondary ml-2">
<i class="fa fa-map-marker"></i> Ver en mapa
</a>
</td>
</tr>
@endforeach
</tbody>
@endif
</table>
@if($nohome->house_photo)
<img src="{{ $nohome->house_photo }}" alt="Foto del domicilio"
class="img-fluid rounded" style="max-height:400px;">
@endif
@else
<hr>
<p class="text-muted mb-0"><em>El proveedor no registró evidencia de visita.</em></p>
@endif
{{ $comments->links() }}
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-dismiss="modal">Cerrar</button>
</div>
</div>
</div>
</div>
@endsection
@section('js')
<script>
// main-content tiene overflow-y:auto que crea stacking context
// Mover el modal a body para que Bootstrap pueda posicionarlo correctamente
$(document).ready(function () {
$('#modalDetalles').appendTo('body');
});
</script>
@endsection

View File

@@ -57,13 +57,9 @@
<td>{{ $report->veredict }}</td>
<td style="width: 10em">
<a class="btn btn-primary btn-xs" title="Comments"
href="{{url('reports/comments/'.$report->id.'/'.$report->contract_id)}}">
href="{{url('reports/'.$report->id.'/comments')}}">
<i class="fa fa-comments"></i>
</a>
<a class="btn btn-secondary btn-xs" title="Veredict"
href="{{url('reports/veredict/'.$report->id)}}">
<i class="fa fa-gavel"></i>
</a>
<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 reporte?')) javascript:if(confirm('Usualmente no se deben eliminar reportes, solo editarlos ¿Estás seguro?')) ajaxDelete('{{url('reports/delete/'.$report->id)}}','{{csrf_token()}}')">

View File

@@ -17,6 +17,20 @@
<span id="error-name" class="invalid-feedback"></span>
</div>
</div>
<div class="form-group row">
{!! Form::label("contract_status","Acción sobre el contrato",["class"=>"col-form-label col-md-3 col-lg-2"]) !!}
<div class="col-md-8">
{!! Form::select("contract_status", [
'' => '— Sin cambio de estatus —',
'8' => 'Devuelto al cliente (reembolso Stripe)',
'9' => 'Disputa terminada (pagar al proveedor)',
], null, ["class"=>"form-control"]) !!}
<small class="text-muted">
"Devuelto al cliente" emite el reembolso en Stripe automáticamente.
"Disputa terminada" aparece en la sección de Pagos.
</small>
</div>
</div>
@if ($errors->any())
<div class="alert alert-danger">
<ul>

View File

@@ -15,12 +15,12 @@
<a href="/banks" class="sidebar-link {{ Request::is('banks*') ? 'active' : '' }}">
<i class="fa fa-university"></i> Bancos
</a>
<a href="/cards" class="sidebar-link {{ Request::is('cards*') ? 'active' : '' }}">
<i class="fa fa-credit-card"></i> Tarjetas
</a>
<a href="/payments" class="sidebar-link {{ Request::is('payments*') ? 'active' : '' }}">
<a href="/payments" class="sidebar-link {{ Request::is('payments') ? 'active' : '' }}">
<i class="fa fa-money"></i> Pagos
</a>
<a href="/payment-batches" class="sidebar-link {{ Request::is('payment-batches*') ? 'active' : '' }}">
<i class="fa fa-history"></i> Pagos Realizados
</a>
<a href="/postulations" class="sidebar-link {{ Request::is('postulations*') ? 'active' : '' }}">
<i class="fa fa-file-text"></i> Postulaciones
</a>

View File

@@ -28,7 +28,8 @@
'4' => 'Facturación',
'5' => 'Moderador',
'6' => 'Administrador',
'7' => 'SuperAdmin'
'7' => 'SuperAdmin',
'8' => 'Técnico'
), $user->roles->id) !!}
<span id="error-name" class="invalid-feedback"></span>
</div>
@@ -42,13 +43,7 @@
</div>
</div>
<div class="form-group row required">
{!! Form::label("openpay_id","Openpay ID",["class"=>"col-form-label col-md-3 col-lg-2"]) !!}
<div class="col-md-8">
{!! Form::text("openpay_id",null,["class"=>"form-control".($errors->has('openpay_id')?" is-invalid":""),'placeholder'=>'Openpay ID']) !!}
<span id="error-openpay_id" class="invalid-feedback"></span>
</div>
</div>
@if ($errors->any())
<div class="alert alert-danger">
<ul>

View File

@@ -35,6 +35,8 @@
<th style="vertical-align: middle"><a href="{{url('users?field=id&sort='.(request()->session()->get('sort')=='asc'?'desc':'asc'))}}">ID</a></th>
{{request()->session()->get('field')=='id'?(request()->session()->get('sort')=='asc'?'':''):''}}
<th style="vertical-align: middle">Foto</th>
<th style="vertical-align: middle"><a href="{{url('users?field=name&sort='.(request()->session()->get('sort')=='asc'?'desc':'asc'))}}">Nombre</a></th>
{{request()->session()->get('field')=='name'?(request()->session()->get('sort')=='asc'?'':''):''}}
@@ -70,6 +72,13 @@
@foreach ($users as $user)
<tr>
<th>{{ $user->id }}</th>
<td>
@if($user->profile_photo)
<img src="{{ $user->profile_photo }}" style="width:36px;height:36px;object-fit:cover;border-radius:50%;">
@else
<span class="text-muted"></span>
@endif
</td>
<td>{{ $user->name }}</td>
<td >{{ $user->email }}</td>
<td >{{ $user->roles->name }}</td>

View File

@@ -37,9 +37,7 @@ Route::group([
'prefix' => 'payments',
'middleware' => 'auth:api'
], function() {
Route::post('addcard', 'PaymentController@addcard');
Route::post('deletecard', 'PaymentController@deletecard');
Route::get('getcards', 'PaymentController@getcards');
Route::post('intent', 'PaymentController@intent');
});
Route::group([
@@ -57,6 +55,9 @@ Route::group([
Route::post('review', 'ContractController@reviewcontract');
Route::post('extra', 'ContractController@extra');
Route::post('report', 'ReportController@report');
Route::get('reports', 'ReportController@getreports');
Route::get('reports/{id}/comments', 'ReportController@getcomments');
Route::post('reports/{id}/comments', 'ReportController@storecomment');
Route::get('nohome-check', 'NoHomeController@nohomecheck');
Route::post('nohome-confirm', 'NoHomeController@nohomeconfirm');
//Route::post('nohome-test', 'NoHomeController@test');
@@ -74,8 +75,27 @@ Route::group([
Route::get('get-postulations', 'SupplierController@getpostulation');
Route::get('get-contracted-postulations', 'SupplierController@getcontractedpostulation');
Route::get('get-finished-postulations', 'PostulationController@getfinishedpostulations');
Route::get('postulations/reports', 'ReportController@getsupplierreports');
Route::get('get-postulants', 'PostulationController@getpostulants');
Route::post('postulate', 'PostulationController@postulate');
Route::delete('postulations/{id}', 'PostulationController@cancelPostulation');
Route::put('postulations/{id}', 'PostulationController@updatePostulation');
// Técnicos
Route::get('technicians', 'TechnicianController@index');
Route::get('technicians/find', 'TechnicianController@find');
Route::post('technicians/add', 'TechnicianController@store');
Route::delete('technicians/{id}', 'TechnicianController@destroy');
Route::post('contracts/assign-technician', 'TechnicianController@assign');
// Propiedades
Route::get('properties', 'PropertyController@index');
Route::post('properties', 'PropertyController@store');
Route::delete('properties/{id}', 'PropertyController@destroy');
// Chat de contratos activos
Route::get('contracts/{id}/comments', 'ContractCommentController@apiIndex');
Route::post('contracts/{id}/comments', 'ContractCommentController@store');
});
Route::get('/parameters', 'IChambaParameterController@parameters');

View File

@@ -56,14 +56,6 @@ Route::group([
Route::match(['get', 'put'], 'update/{id}', 'BanksController@update')->middleware('rolecheck:6');
Route::match(['get', 'post'], 'create', 'BanksController@create')->middleware('rolecheck:6');
});
Route::group([
'prefix' => 'cards'
], function() {
Route::get('/', 'PaymentController@cardsindex')->middleware('rolecheck:6');
Route::delete('delete/{id}', 'PaymentController@destroy')->middleware('superadmin');
//Route::match(['get', 'put'], 'update/{id}', 'PaymentController@update')->middleware('rolecheck:6');
//Route::match(['get', 'post'], 'create', 'PaymentController@create')->middleware('rolecheck:6');
});
Route::group([
'prefix' => 'coupons'
], function() {
@@ -92,10 +84,17 @@ Route::group([
'prefix' => 'payments'
], function() {
Route::get('/', 'PaymentController@index')->middleware('rolecheck:6');
Route::post('generate', 'PaymentController@generate')->middleware('rolecheck:6');
Route::delete('delete/{id}', 'StatusController@destroy')->middleware('superadmin');
Route::match(['get', 'put'], 'update/{id}', 'StatusController@update')->middleware('rolecheck:6');
Route::match(['get', 'post'], 'create', 'StatusController@create')->middleware('rolecheck:6');
});
Route::group([
'prefix' => 'payment-batches'
], function() {
Route::get('/', 'PaymentBatchController@index')->middleware('rolecheck:6');
Route::get('{id}/download', 'PaymentBatchController@download')->middleware('rolecheck:6');
});
Route::group([
'prefix' => 'postulations'
], function() {
@@ -112,6 +111,9 @@ Route::group([
Route::get('/delete-missed', 'ContractController@deletemissed')->middleware('appenginecron');
Route::get('/map', 'ContractController@mapcurrentcontracts')->middleware('rolecheck:6');
Route::delete('delete/{id}', 'ContractController@currentdestroy')->middleware('superadmin');
Route::get('{id}/comments', 'ContractCommentController@index')->middleware('rolecheck:6');
Route::post('{id}/comments', 'ContractCommentController@store')->middleware('rolecheck:6');
Route::delete('{id}/comments/{comment_id}', 'ContractCommentController@destroy')->middleware('superadmin');
});
Route::group([
@@ -126,9 +128,10 @@ Route::group([
], function() {
Route::get('/', 'ReportController@index')->middleware('rolecheck:6');
Route::delete('delete/{id}', 'ReportController@destroy')->middleware('superadmin');
Route::get('comments/{id}/{contract_id}', 'ReportCommentController@index')->middleware('rolecheck:6');
Route::delete('comments/delete/{id}/{contract_id}', 'ReportCommentController@destroy')->middleware('superadmin');
Route::get('veredict/{id}', 'ReportController@veredict')->middleware('rolecheck:6');
Route::get('{id}/comments', 'ReportCommentController@index')->middleware('rolecheck:6');
Route::post('{id}/comments', 'ReportCommentController@store')->middleware('rolecheck:6');
Route::delete('{id}/comments/{comment_id}', 'ReportCommentController@destroy')->middleware('superadmin');
Route::match(['get', 'post'], 'veredict/{id}', 'ReportController@veredict')->middleware('rolecheck:6');
//Route::match(['get', 'post'], 'create', 'CouponController@create')->middleware('rolecheck:6');
});
});