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>
This commit is contained in:
2026-07-03 11:26:10 -06:00
parent 0ed98c96ee
commit 700ddafba4
21 changed files with 944 additions and 180 deletions

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

@@ -165,6 +165,7 @@ class ContractController extends Controller
$rules = [
'postulation_id' => 'required|numeric',
'supplier_id' => 'required|numeric',
'selected_date' => 'required|in:date_1,date_2',
'payment_intent_id' => $paymentBypass ? 'nullable|string' : 'required|string',
'coupon' => 'nullable|string',
];
@@ -192,6 +193,14 @@ class ContractController extends Controller
$ichambafee = iChambaParameter::where('parameter', 'ichamba_fee')->first();
$category = Categories::where('id', $postulation->category_id)->first();
// 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();
@@ -206,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;
@@ -229,40 +239,18 @@ 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
);
}
}
@@ -344,8 +332,9 @@ class ContractController extends Controller
$rules = [
'postulation_id' => 'required|numeric',
'supplier_id' => 'required|numeric',
'coupon' => 'required|string|regex:/(^[A-Za-z0-9 ]+$)+/',
'supplier_id' => 'required|numeric',
'selected_date' => 'required|in:date_1,date_2',
'coupon' => 'required|string|regex:/(^[A-Za-z0-9 ]+$)+/',
];
$validator = Validator::make($request->all(), $rules);
@@ -387,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;

View File

@@ -9,6 +9,8 @@ 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\PaymentBatch;
use Stripe\Stripe;
@@ -94,19 +96,43 @@ class PaymentController extends Controller
public function intent(Request $request)
{
$validator = Validator::make($request->all(), [
'supplier_id' => 'required|numeric',
'postulation_id' => 'required|numeric|exists:postulations,id',
'supplier_id' => 'required|numeric|exists:suppliers,id',
'coupon' => 'nullable|string',
]);
if ($validator->fails()) {
return response()->json($validator->errors(), 422);
}
$user = Auth::user();
$supplier = Suppliers::find($request->supplier_id);
$user = Auth::user();
$postulation = Postulations::find($request->postulation_id);
if (!$supplier) {
return response()->json(['message' => 'Proveedor no encontrado'], 404);
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) {
@@ -118,8 +144,6 @@ class PaymentController extends Controller
$user->save();
}
$amount = max((int) $supplier->minimun_fee, 150);
try {
$ephemeralKey = \Stripe\EphemeralKey::create(
['customer' => $user->stripe_customer_id],
@@ -127,7 +151,7 @@ class PaymentController extends Controller
);
$intent = \Stripe\PaymentIntent::create([
'amount' => $amount * 100,
'amount' => (int) ($chargeAmount * 100),
'currency' => env('STRIPE_CURRENCY', 'mxn'),
'customer' => $user->stripe_customer_id,
'automatic_payment_methods' => ['enabled' => true],
@@ -141,6 +165,7 @@ class PaymentController extends Controller
'client_secret' => $intent->client_secret,
'customer' => $user->stripe_customer_id,
'ephemeral_key' => $ephemeralKey->secret,
'amount' => $chargeAmount,
]);
}

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;
@@ -89,15 +92,13 @@ class PostulationController extends Controller
public function create(Request $request) {
$rules = [
'category' => 'required|string',
'address' => 'required|string',
'int_number' => 'numeric|nullable',
'references' => 'string|nullable',
'setdate' => 'required|string',
'sethour' => 'required|string',
'details' => 'string|nullable',
'lat' => 'required|numeric',
'lng' => 'required|numeric',
'category' => 'required|string',
'property_id' => 'required|numeric',
'references' => 'string|nullable',
'details' => 'string|nullable',
'photos' => 'nullable|array',
'photos.*' => 'nullable|image|max:4096',
'related_postulation_id' => 'nullable|numeric|exists:postulations,id',
];
$validator = Validator::make($request->all(), $rules);
@@ -105,8 +106,14 @@ class PostulationController extends Controller
return response()->json($validator->messages());
} else {
$user = Auth::user();
$geometry = new Point($request->lat, $request->lng);
$user = Auth::user();
$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 = 5000; // metros (5 km)
@@ -115,20 +122,32 @@ class PostulationController extends Controller
if ($suppliers != '[]') {
$postulation = new Postulations();
$postulation->user_id = $user->id;
$postulation->user_id = $user->id;
$postulation->category_id = $category->id;
$postulation->address = strip_tags($request->address);
$postulation->location = $geometry;
$postulation->int_number = $request->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->details = preg_replace('/\d+/', '', strip_tags($request->details));
$postulation->property_id = $property->id;
$postulation->address = $property->address;
$postulation->location = $geometry;
$postulation->int_number = $property->int_number;
$postulation->references = preg_replace('/\d+/', '', strip_tags($request->references));
$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",
@@ -160,19 +179,6 @@ class PostulationController extends Controller
}
}
$minutes = intval(substr(substr($request->sethour, 14), 0, 2) + 15);
$hours = intval(substr(substr($request->sethour, 11), 0, 2) + 1);
if ($minutes > 59) {
if ($hours > 23){
$delay_msg = Carbon::now()->addDays(1)->toDateString() . ' ' . ($hours - 24) . ':' . ($minutes - 60) . substr(substr($request->sethour, 16), 0, 3);
} else {
$delay_msg = substr($request->sethour, 0, 10) . ' ' . $hours . ':' . ($minutes - 60) . substr(substr($request->sethour, 16), 0, 3);
}
} else {
$delay_msg = substr($request->sethour, 0, 10) . ' ' . $hours . ':' . $minutes . substr(substr($request->sethour, 16), 0, 3);
}
$delay_UTC = Carbon::now()->addMinutes(15)->toString();
try {
@@ -200,81 +206,90 @@ 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));
$supplier = Suppliers::where('user_id', $user->id)->first();
$user = Auth::user();
$postulation = Postulations::where('id', $request->postulation_id)->first();
$time_created = Carbon::parse($postulation->created_at);
$time_limit = (1000 - Carbon::now()->diffInMinutes($time_created));
$supplier = Suppliers::where('user_id', $user->id)->first();
if (!$supplier) {
return response()->json([
'success' => false,
'message' => 'No tienes un perfil de proveedor registrado'
], 400);
}
if (!$supplier) {
return response()->json([
'success' => false,
'message' => 'No tienes un perfil de proveedor registrado'
], 400);
}
if ($time_limit > 0) {
if (in_array($postulation->category_id, $supplier->categories->pluck('id')->toArray())) {
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'),
// ])));
// 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);
}
$supplier->postulations()->attach($request->postulation_id);
$supplier->save();
if ($time_limit > 0) {
if (in_array($postulation->category_id, $supplier->categories->pluck('id')->toArray())) {
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) {}
}
$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'
]);
}
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,
'category' => $category->name,
'id' => $postulation->id,
'category' => $category->name,
'en_category' => $category->en_name,
'address' => $postulation->address,
'date' => $postulation->appointment,
'amount' => $postulation->amount
'address' => $postulation->address,
'date' => $postulation->appointment,
'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);
@@ -323,66 +338,74 @@ 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();
$user = Auth::user();
$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();
if ($postulation->user_id == $user->id) {
$category = Categories::where('id', $postulation->category_id)->first();
$suppliers = $postulation->suppliers;
$pcontractsuppliers = array();
if ($suppliers != '[]') {
foreach($suppliers as $supplier) {
$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),
);
$pcontractsuppliers[] = $pcontractsupplier;
}
$pcontractsuppliercollection = collect($pcontractsuppliers)->sortByDesc('membership')->sortByDesc('score');
$pcontractsupplier = $pcontractsuppliercollection->values()->all();
} else {
$pcontractsupplier = array(
'id' => $postulation->id,
'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,
'score' => null,
);
$pcontractsuppliers = array();
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,
'supplier_id' => $supplier->id,
'supplier' => $supplier->company_name,
'tags' => $supplier->tags,
'cover_photo' => $supplier->cover_photo,
'membership' => $supplier->membership,
'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;
$pcontractsupplier = $pcontractsuppliers;
}
$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(
'id' => $postulation->id,
'category' => $category->name,
'en_category' => $category->en_name,
'address' => $postulation->address,
'supplier_id' => null,
'supplier' => null,
'tags' => null,
'cover_photo' => null,
'membership' => null,
'fee' => null,
'date_1' => null,
'date_2' => null,
'score' => null,
);
$pcontractsuppliers[] = $pcontractsupplier;
$pcontractsupplier = $pcontractsuppliers;
}
return response()->json($pcontractsupplier);
}
}
}
public function deleteexpired()
@@ -390,6 +413,63 @@ class PostulationController extends Controller
$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']);
}
}