diff --git a/.env.example b/.env.example index 604b401..3bfdab0 100755 --- a/.env.example +++ b/.env.example @@ -42,3 +42,8 @@ PUSHER_APP_CLUSTER=mt1 MIX_PUSHER_APP_KEY="${PUSHER_APP_KEY}" MIX_PUSHER_APP_CLUSTER="${PUSHER_APP_CLUSTER}" + +# OneSignal Push Notifications +ONESIGNAL_APP_ID= +ONESIGNAL_REST_API_KEY= +ONESIGNAL_USER_AUTH_KEY= diff --git a/app/Http/Controllers/PostulationController.php b/app/Http/Controllers/PostulationController.php index f0e142a..fa6aa64 100755 --- a/app/Http/Controllers/PostulationController.php +++ b/app/Http/Controllers/PostulationController.php @@ -124,7 +124,7 @@ class PostulationController extends Controller // 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/Tijuana')->tz('UTC'); + $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->save(); diff --git a/app/Http/Middleware/Cors.php b/app/Http/Middleware/Cors.php index 7c27251..281c491 100755 --- a/app/Http/Middleware/Cors.php +++ b/app/Http/Middleware/Cors.php @@ -12,15 +12,27 @@ class Cors */ public function handle($request, Closure $next) { + $allowedOrigins = [ + 'http://localhost', + 'https://localhost', + 'ionic://localhost', + 'https://jobhero.consultoria-as.com', + 'https://jobhero-api.consultoria-as.com', + 'capacitor://localhost', + 'http://localhost:8100' + ]; - $allowedOrigins = ['http://localhost', 'ionic://localhost']; + $origin = $request->server('HTTP_ORIGIN'); - if (in_array($request->server('HTTP_ORIGIN'), $allowedOrigins)) { - return $next($request) - ->header('Access-Control-Allow-Origin', $request->server('HTTP_ORIGIN')) - ->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'); - } + // Allow requests from mobile apps (no origin or capacitor/ionic) + if (empty($origin) || in_array($origin, $allowedOrigins)) { + $response = $next($request); + $allowOrigin = empty($origin) ? '*' : $origin; + 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'); + } return $next($request); } diff --git a/app/Services/PushNotificationService.php b/app/Services/PushNotificationService.php new file mode 100644 index 0000000..def1482 --- /dev/null +++ b/app/Services/PushNotificationService.php @@ -0,0 +1,207 @@ + 'tag', 'key' => 'iChamba_ID', 'relation' => '=', 'value' => (string) $userId] + ], + null, + null, + null, + null, + $heading, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + $data + ); + } + + /** + * Send notification to multiple users + * + * @param array $userIds + * @param string $message + * @param string $heading + * @param array $data + * @return array + */ + public function sendToUsers(array $userIds, string $message, string $heading = 'JobHero', array $data = []) + { + $results = []; + foreach ($userIds as $userId) { + $results[$userId] = $this->sendToUser($userId, $message, $heading, $data); + } + return $results; + } + + /** + * Send scheduled notification to a user + * + * @param int $userId + * @param string $message + * @param string $sendAt UTC datetime string + * @param string $heading + * @param array $data + * @return mixed + */ + public function sendScheduledToUser(int $userId, string $message, string $sendAt, string $heading = 'JobHero', array $data = []) + { + return OneSignal::sendNotificationUsingTags( + $message, + [ + ['field' => 'tag', 'key' => 'iChamba_ID', 'relation' => '=', 'value' => (string) $userId] + ], + null, + null, + null, + null, + $heading, + null, + null, + null, + null, + null, + null, + null, + $sendAt, + null, + null, + null, + null, + null, + null, + null, + null, + $data + ); + } + + /** + * Send notification to all users with a specific role + * + * @param int $roleId + * @param string $message + * @param string $heading + * @param array $data + * @return mixed + */ + public function sendToRole(int $roleId, string $message, string $heading = 'JobHero', array $data = []) + { + return OneSignal::sendNotificationUsingTags( + $message, + [ + ['field' => 'tag', 'key' => 'iChamba_Role', 'relation' => '=', 'value' => (string) $roleId] + ], + null, + null, + null, + null, + $heading, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + $data + ); + } + + /** + * Notification types for contracts + */ + public function notifyContractHired(int $supplierId, string $address) + { + return $this->sendToUser( + $supplierId, + "Has sido contratado para un servicio. Dirígete a la sección de postulaciones contratadas para más detalles.", + "¡Nuevo Contrato!" + ); + } + + public function notifyContractStarted(int $userId, string $address, string $date) + { + return $this->sendToUser( + $userId, + "El servicio en {$address} el día {$date} ha sido iniciado. Dirígete a la sección de servicios contratados para más detalles.", + "Servicio Iniciado" + ); + } + + public function notifyContractCancelled(int $supplierId, string $address, string $date) + { + return $this->sendToUser( + $supplierId, + "El servicio en {$address} el día {$date} ha sido cancelado. Dirígete a la sección de servicios contratados para más detalles.", + "Servicio Cancelado" + ); + } + + public function notifySupplierArrived(int $clientId, string $address) + { + return $this->sendToUser( + $clientId, + "El proveedor para el servicio en {$address} ha llegado. Dirígete a la sección de contratos confirmados para más detalles.", + "Proveedor ha llegado" + ); + } + + public function notifyAppointmentReminder(int $userId, string $address, int $minutesBefore = 30) + { + return $this->sendToUser( + $userId, + "Tienes un servicio agendado hoy en {$address} en {$minutesBefore} minutos. Dirígete a la sección de contratos confirmados para más detalles.", + "Recordatorio de Cita" + ); + } + + public function notifyNewPostulation(int $supplierId) + { + return $this->sendToUser( + $supplierId, + "Hay una nueva postulación disponible en tu área. Dirígete a la sección de postulaciones para ver más detalles.", + "Nueva Postulación" + ); + } +} diff --git a/config/googlemaps.php b/config/googlemaps.php new file mode 100644 index 0000000..77642fd --- /dev/null +++ b/config/googlemaps.php @@ -0,0 +1,503 @@ + env('GOOGLE_MAPS_API_KEY', ''), + + /* + |-------------------------------------------------------------------------- + | Verify SSL Peer + |-------------------------------------------------------------------------- + | + | Will be used for all web services to verify + | SSL peer (SSL certificate validation) + | + */ + 'ssl_verify_peer' => FALSE, + + /* + |-------------------------------------------------------------------------- + | CURL's connection timeout + |-------------------------------------------------------------------------- + | + | Will be used for all web services to limit + | the maximum time tha connection can take in seconds + | + */ + 'connection_timeout' => 5, + + /* + |-------------------------------------------------------------------------- + | CURL's request timeout + |-------------------------------------------------------------------------- + | + | Will be used for all web services to limit + | the maximum time a request can take + | + */ + 'request_timeout' => 30, + + /* + |-------------------------------------------------------------------------- + | CURL's CURLOPT_ENCODING + |-------------------------------------------------------------------------- + | + | Will be used for all web services to use compression on requests. + | + | Sets the contents of the "Accept-Encoding:" header a containing all + | supported encoding types. + | + */ + 'request_use_compression' => false, + + /* + |-------------------------------------------------------------------------- + | Service URL + |-------------------------------------------------------------------------- + | url - web service URL + | type - request type POST or GET + | key - API key, if different to API key above + | endpoint - boolean, indicates whenever output parameter to be used in the request or not + | responseDefaultKey - specify default field value to be returned when calling getByKey() + | param - accepted request parameters + | + */ + + 'service' => [ + + 'geocoding' => [ + 'url' => 'https://maps.googleapis.com/maps/api/geocode/', + 'type' => 'GET', + 'key' => null, + 'endpoint' => true, + 'responseDefaultKey' => 'place_id', + 'param' => [ + 'address' => null, + 'bounds' => null, + 'key' => null, + 'region' => null, + 'language' => null, + 'result_type' => null, + 'location_type' => null, + 'latlng' => null, + 'place_id' => null, + 'components' => [ + 'route' => null, + 'locality' => null, + 'administrative_area' => null, + 'postal_code' => null, + 'country' => null, + ] + ] + ], + + // https://developers.google.com/maps/documentation/routes/reference/rest + 'routes' => [ + 'url' => 'https://routes.googleapis.com/directions/v2:computeRoutes', + 'type' => 'POST', + 'key' => null, + 'endpoint' => true, + 'decodePolyline' => true, // true = decode overview_polyline.points to an array of points + 'param' => [ + 'origin' => null, // required + 'destination' => null, //required + 'intermediates' => null, + 'travelMode' => null, + 'transitRoutingPreference' => null, + 'polylineQuality' => null, + 'polylineEncoding' => null, + 'departureTime' => null, + 'arrivalTime' => null, + 'computeAlternativeRoutes' => null, + 'routeModifiers' => null, + 'languageCode' => null, + 'regionCode' => null, + 'units' => null, + 'optimizeWaypointOrder' => false, + 'requestedReferenceRoutes' => null, + 'extraComputations' => null, + 'trafficModel' => null, + 'transitPreferences' => null, + ] + ], + + + + // https://developers.google.com/maps/documentation/routes/reference/rest/v2/TopLevel/computeRouteMatrix + 'routematrix' => [ + 'url' => 'https://routes.googleapis.com/distanceMatrix/v2:computeRouteMatrix', + 'type' => 'POST', + 'key' => null, + 'endpoint' => true, + 'param' => [ + 'origins' => null, // required + 'destinations' => null, //required + 'travelMode' => null, + 'routingPreference' => null, + 'departureTime' => null, + 'arrivalTime' => null, + 'languageCode' => null, + 'regionCode' => null, + 'units' => null, + 'extraComputations' => null, + 'trafficModel' => null, + 'transitPreferences' => null, + ] + ], + + + // Deprecated + 'directions' => [ + 'url' => 'https://maps.googleapis.com/maps/api/directions/', + 'type' => 'GET', + 'key' => null, + 'endpoint' => true, + 'responseDefaultKey' => 'geocoded_waypoints', + 'decodePolyline' => true, // true = decode overview_polyline.points to an array of points + 'param' => [ + 'origin' => null, // required + 'destination' => null, //required + 'mode' => null, + 'waypoints' => null, + 'place_id' => null, + 'alternatives' => null, + 'avoid' => null, + 'language' => null, + 'units' => null, + 'region' => null, + 'departure_time' => null, + 'arrival_time' => null, + 'transit_mode' => null, + 'transit_routing_preference' => null, + ] + ], + + // Deprecated + 'distancematrix' => [ + 'url' => 'https://maps.googleapis.com/maps/api/distancematrix/', + 'type' => 'GET', + 'key' => null, + 'endpoint' => true, + 'responseDefaultKey' => 'origin_addresses', + 'param' => [ + 'origins' => null, + 'destinations' => null, + 'key' => null, + 'mode' => null, + 'language' => null, + 'avoid' => null, + 'units' => null, + 'departure_time' => null, + 'arrival_time' => null, + 'transit_mode' => null, + 'transit_routing_preference' => null, + + ] + ], + + + 'elevation' => [ + 'url' => 'https://maps.googleapis.com/maps/api/elevation/', + 'type' => 'GET', + 'key' => null, + 'endpoint' => true, + 'responseDefaultKey' => 'elevation', + 'param' => [ + 'locations' => null, + 'path' => null, + 'samples' => null, + 'key' => null, + ] + ], + + + 'geolocate' => [ + 'url' => 'https://www.googleapis.com/geolocation/v1/geolocate?', + 'type' => 'POST', + 'key' => null, + 'endpoint' => false, + 'responseDefaultKey' => 'location', + 'param' => [ + 'homeMobileCountryCode' => null, + 'homeMobileNetworkCode' => null, + 'radioType' => null, + 'carrier' => null, + 'considerIp' => null, + 'cellTowers' => [ + 'cellId' => null, + 'locationAreaCode' => null, + 'mobileCountryCode' => null, + 'mobileNetworkCode' => null, + 'age' => null, + 'signalStrength' => null, + 'timingAdvance' => null, + ], + 'wifiAccessPoints' => [ + 'macAddress' => null, + 'signalStrength' => null, + 'age' => null, + 'channel' => null, + 'signalToNoiseRatio' => null, + ], + ] + ], + + + + 'snapToRoads' => [ + 'url' => 'https://roads.googleapis.com/v1/snapToRoads?', + 'type' => 'GET', + 'key' => null, + 'endpoint' => false, + 'responseDefaultKey' => 'snappedPoints', + 'param' => [ + 'locations' => null, + 'path' => null, + 'samples' => null, + 'key' => null, + ] + ], + + + 'speedLimits' => [ + 'url' => 'https://roads.googleapis.com/v1/speedLimits?', + 'type' => 'GET', + 'key' => null, + 'endpoint' => false, + 'responseDefaultKey' => 'speedLimits', + 'param' => [ + 'path' => null, + 'placeId' => null, + 'units' => null, + 'key' => null, + ] + ], + + + 'timezone' => [ + 'url' => 'https://maps.googleapis.com/maps/api/timezone/', + 'type' => 'GET', + 'key' => null, + 'endpoint' => true, + 'responseDefaultKey' => 'dstOffset', + 'param' => [ + 'location' => null, + 'timestamp' => null, + 'key' => null, + 'language' => null, + + ] + ], + + + + 'nearbysearch' => [ + 'url' => 'https://maps.googleapis.com/maps/api/place/nearbysearch/', + 'type' => 'GET', + 'key' => null, + 'endpoint' => true, + 'responseDefaultKey' => 'results', + 'param' => [ + 'key' => null, + 'location' => null, + 'radius' => null, + 'keyword' => null, + 'language' => null, + 'minprice' => null, + 'maxprice' => null, + 'name' => null, + 'opennow' => null, + 'rankby' => null, + 'type' => null, // types depricated, one type may be specified + 'pagetoken' => null, + 'zagatselected' => null, + ] + ], + + + + 'textsearch' => [ + 'url' => 'https://maps.googleapis.com/maps/api/place/textsearch/', + 'type' => 'GET', + 'key' => null, + 'endpoint' => true, + 'responseDefaultKey' => 'results', + 'param' => [ + 'key' => null, + 'query' => null, + 'location' => null, + 'radius' => null, + 'language' => null, + 'minprice' => null, + 'maxprice' => null, + 'opennow' => null, + 'type' => null, // types deprecated, one type may be specified + 'pagetoken' => null, + 'zagatselected' => null, + ] + ], + + + + 'radarsearch' => [ + 'url' => 'https://maps.googleapis.com/maps/api/place/radarsearch/', + 'type' => 'GET', + 'key' => null, + 'endpoint' => true, + 'responseDefaultKey' => 'geometry', + 'param' => [ + 'key' => null, + 'radius' => null, + 'location' => null, + 'keyword' => null, + 'minprice' => null, + 'maxprice' => null, + 'opennow' => null, + 'name' => null, + 'type' => null, // types depricated, one type may be specified + 'zagatselected' => null, + ] + ], + + + + 'placedetails' => [ + 'url' => 'https://maps.googleapis.com/maps/api/place/details/', + 'type' => 'GET', + 'key' => null, + 'endpoint' => true, + 'responseDefaultKey' => 'result', + 'param' => [ + 'key' => null, + 'placeid' => null, + 'extensions' => null, + 'language' => null, + ] + ], + + + 'placeadd' => [ + 'url' => 'https://maps.googleapis.com/maps/api/place/add/', + 'type' => 'POST', + 'key' => null, + 'endpoint' => true, + 'responseDefaultKey' => 'place_id', + 'param' => [ + 'key' => null, + 'accuracy' => null, + 'address' => null, + 'language' => null, + 'location' => null, + 'name' => null, + 'phone_number' => null, + 'types' => null, // according to docs types still required as string parameter + 'type' => null, // types deprecated, one type may be specified + 'website' => null, + ] + ], + + + 'placedelete' => [ + 'url' => 'https://maps.googleapis.com/maps/api/place/delete/', + 'type' => 'POST', + 'key' => null, + 'endpoint' => true, + 'responseDefaultKey' => 'status', + 'param' => [ + 'key' => null, + 'place_id' => null, + + ] + ], + + + + + 'placephoto' => [ + 'url' => 'https://maps.googleapis.com/maps/api/place/photo?', + 'type' => 'GET', + 'key' => null, + 'endpoint' => false, + 'responseDefaultKey' => 'image', + 'param' => [ + 'key' => null, + 'photoreference' => null, + 'maxheight' => null, + 'maxwidth' => null, + ] + ], + + + + + + 'placeautocomplete' => [ + 'url' => 'https://maps.googleapis.com/maps/api/place/autocomplete/', + 'type' => 'GET', + 'key' => null, + 'endpoint' => true, + 'responseDefaultKey' => 'predictions', + 'param' => [ + 'key' => null, + 'input' => null, + 'offset' => null, + 'location' => null, + 'radius' => null, + 'language' => null, + 'types' => null, // use string as parameter + 'type' => null, // types deprecated, one type may be specified + 'components' => null, + ] + ], + + + + 'placequeryautocomplete' => [ + 'url' => 'https://maps.googleapis.com/maps/api/place/queryautocomplete/', + 'type' => 'GET', + 'key' => null, + 'endpoint' => true, + 'responseDefaultKey' => 'predictions', + 'param' => [ + 'key' => null, + 'input' => null, + 'offset' => null, + 'location' => null, + 'radius' => null, + 'language' => null, + ] + ], + + ], + + + + + /* + |-------------------------------------------------------------------------- + | End point + |-------------------------------------------------------------------------- + | + | + */ + + 'endpoint' => [ + 'xml' => 'xml?', + 'json' => 'json?', + ], + + + +]; diff --git a/config/onesignal.php b/config/onesignal.php index 69c1892..e070303 100755 --- a/config/onesignal.php +++ b/config/onesignal.php @@ -1,23 +1,33 @@ '00d23dae-1209-42cc-bea7-e1f17cee27fa', + |-------------------------------------------------------------------------- + | One Signal App Id + |-------------------------------------------------------------------------- + | + | Your OneSignal App ID from the OneSignal dashboard + | + */ + 'app_id' => env('ONESIGNAL_APP_ID'), /* - |-------------------------------------------------------------------------- - | Rest API Key - |-------------------------------------------------------------------------- - | + |-------------------------------------------------------------------------- + | Rest API Key + |-------------------------------------------------------------------------- | - | - */ - 'rest_api_key' => 'NTQwMDY4ZjUtODlmMy00NzAzLTg1ZDItMWNhMDgyOGVkYzRk', - 'user_auth_key' => 'YOUR-USER-AUTH-KEY' -); + | Your OneSignal REST API Key from the OneSignal dashboard + | + */ + 'rest_api_key' => env('ONESIGNAL_REST_API_KEY'), + + /* + |-------------------------------------------------------------------------- + | User Auth Key + |-------------------------------------------------------------------------- + | + | Your OneSignal User Auth Key (optional, for user-level API access) + | + */ + 'user_auth_key' => env('ONESIGNAL_USER_AUTH_KEY'), +];