- 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>
480 lines
19 KiB
PHP
Executable File
480 lines
19 KiB
PHP
Executable File
<?php
|
|
|
|
namespace App\Http\Controllers;
|
|
|
|
use Illuminate\Http\Request;
|
|
use App\Models\User;
|
|
use Carbon\Carbon;
|
|
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;
|
|
|
|
class PostulationController extends Controller
|
|
{
|
|
//
|
|
public function index(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;
|
|
|
|
$postulations = new Postulations();
|
|
$postulations = $postulations->where('id', 'LIKE', '%' . strip_tags($request->session()->get('search')) . '%')
|
|
->orwhere('user_id', 'LIKE', '%' . strip_tags($request->session()->get('search')) . '%')
|
|
->orwhere('address', 'LIKE', '%' . strip_tags($request->session()->get('search')) . '%')
|
|
->orwhere('amount', '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('postulations.index', compact('postulations'));
|
|
} else {
|
|
return view('postulations.ajax', compact('postulations'));
|
|
}
|
|
|
|
}
|
|
|
|
public function map(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;
|
|
|
|
$postulations = new Postulations();
|
|
$postulations = $postulations->where('id', 'LIKE', '%' . strip_tags($request->session()->get('search')) . '%')
|
|
->orwhere('user_id', 'LIKE', '%' . strip_tags($request->session()->get('search')) . '%')
|
|
->orwhere('address', 'LIKE', '%' . strip_tags($request->session()->get('search')) . '%')
|
|
->orwhere('amount', 'LIKE', '%' . strip_tags($request->session()->get('search')) . '%')
|
|
->orderBy(strip_tags($request->session()->get('field')), strip_tags($request->session()->get('sort')))
|
|
->paginate(10);
|
|
|
|
return view('postulations.map', compact('postulations'));
|
|
|
|
}
|
|
|
|
/**
|
|
* Show the form for creating a new resource.
|
|
*
|
|
* @return \Illuminate\Http\Response
|
|
*/
|
|
public function create(Request $request) {
|
|
|
|
$rules = [
|
|
'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);
|
|
if ($validator->fails()) {
|
|
return response()->json($validator->messages());
|
|
} else {
|
|
|
|
$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)
|
|
$suppliers = Suppliers::withinDistanceTo('location', $geometry, $distance)->get();
|
|
|
|
if ($suppliers != '[]') {
|
|
|
|
$postulation = new Postulations();
|
|
$postulation->user_id = $user->id;
|
|
$postulation->category_id = $category->id;
|
|
$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",
|
|
"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()
|
|
// ->name('suppplier_new_postulation')
|
|
// ->language('es_US')
|
|
// ->body(Messages\Components\Body::create([
|
|
// Messages\Components\Parameters\Text::create('Proveedor hay nueva postulación: dirígete a la sección de postulaciones en JobHero para ver más detalles'),
|
|
// ])));
|
|
|
|
}
|
|
}
|
|
|
|
$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,
|
|
null, null, null,
|
|
$delay_UTC,
|
|
"Búsqueda Finalizada"
|
|
);
|
|
} catch (\Exception $e) {}
|
|
|
|
return response()->json([
|
|
'message' => 'Servicio solicitado, espere a que un proveedor se postule'
|
|
]);
|
|
} else {
|
|
return response()->json([
|
|
'message' => 'No Provider'
|
|
]);
|
|
}
|
|
}
|
|
}
|
|
|
|
public function postulate(Request $request) {
|
|
|
|
$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 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 = (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);
|
|
}
|
|
|
|
// 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) {
|
|
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'
|
|
]);
|
|
}
|
|
}
|
|
|
|
public function getpendingcontracts(Request $request) {
|
|
$user = Auth::user();
|
|
$postulations = Postulations::where('user_id', $user->id)
|
|
->where('status', 'active')
|
|
->orderBy('created_at', 'DESC')
|
|
->get();
|
|
$pendingcontracts = array();
|
|
|
|
foreach($postulations as $postulation) {
|
|
$category = Categories::where('id', $postulation->category_id)->first();
|
|
$pendingcontractinfo = array(
|
|
'id' => $postulation->id,
|
|
'category' => $category->name,
|
|
'en_category' => $category->en_name,
|
|
'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);
|
|
}
|
|
|
|
public function getfinishedpostulations(Request $request) {
|
|
$user = Auth::user();
|
|
|
|
if (!$user->suppliers) {
|
|
return response()->json([
|
|
'success' => false,
|
|
'message' => 'No tienes un perfil de proveedor registrado'
|
|
], 400);
|
|
}
|
|
|
|
$supplier = $user->suppliers;
|
|
$postulations = FinishedContracts::with(['technician.user'])
|
|
->where('supplier_id', $supplier->id)->orderBy('created_at', 'DESC')->get();
|
|
$finishedpostulations = array();
|
|
|
|
foreach($postulations as $postulation) {
|
|
$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,
|
|
'technician' => $technician
|
|
);
|
|
$finishedpostulations[] = $finishedpostulationinfo;
|
|
}
|
|
|
|
return response()->json($finishedpostulations);
|
|
}
|
|
|
|
public function getpostulants(Request $request) {
|
|
|
|
$rules = [
|
|
'postulation_id' => 'required|numeric',
|
|
];
|
|
|
|
$validator = Validator::make($request->all(), $rules);
|
|
if ($validator->fails()) {
|
|
return response()->json($validator->errors(), 422);
|
|
}
|
|
|
|
$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 = $postulation->suppliers;
|
|
|
|
$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;
|
|
}
|
|
|
|
$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()
|
|
{
|
|
$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);
|
|
return redirect('postulations');
|
|
}
|
|
|
|
}
|