add flutter

This commit is contained in:
Ariska
2026-03-11 15:29:37 +07:00
parent c253e1a370
commit 619d758027
9490 changed files with 135801 additions and 1353 deletions
View File
View File
View File
+26 -1
View File
@@ -124,6 +124,16 @@ class Driver_model extends CI_model
}
}
public function check_banned_by_email($email)
{
$stat = $this->db->query("SELECT id FROM driver WHERE status='3' AND email='" . $this->db->escape_str($email) . "'");
if ($stat->num_rows() == 1) {
return true;
} else {
return false;
}
}
public function check_exist_phone_edit($id, $phone)
{
$cek = $this->db->query("SELECT no_telepon FROM driver where no_telepon='$phone' AND id!='$id'");
@@ -242,6 +252,19 @@ class Driver_model extends CI_model
}
}
/**
* Update driver's FCM token (reg_id) so they receive order request notifications.
*/
public function update_driver_reg_id($id_driver, $reg_id)
{
if (empty($id_driver) || empty(trim((string) $reg_id))) {
return false;
}
$this->db->where('id', $id_driver);
$this->db->update('driver', array('reg_id' => trim((string) $reg_id)));
return $this->db->affected_rows() >= 0;
}
public function get_data_driver($condition)
{
$this->db->select('driver.*, saldo.saldo');
@@ -862,7 +885,9 @@ class Driver_model extends CI_model
$this->db->set('tempat_lahir', $data['tempat_lahir']);
$this->db->set('tgl_lahir', $data['tgl_lahir']);
$this->db->set('alamat_driver', $data['alamat_driver']);
if (isset($data['reg_id'])) {
$this->db->set('reg_id', $data['reg_id']);
}
$this->db->where('id', $data['id']);
$this->db->update('driver', $data);
View File
View File
View File
View File
View File
+162 -124
View File
@@ -4,155 +4,193 @@ class notification_model extends CI_model
{
public function notif_cancel_user($id_driver, $id_transaksi, $token_user)
{
$datanotif = array(
'id_driver' => $id_driver,
if ($this->is_token_empty($token_user) || !$this->is_valid_fcm_token($token_user)) {
log_message('debug', 'notif_cancel_user: skip, no/invalid Firebase token for user');
return true;
}
// Use FCM HTTP v1 via service account, not legacy keyfcm.
$payload = array(
'id_driver' => $id_driver,
'id_transaksi' => $id_transaksi,
'response' => '5',
'type' => 1
'response' => '5',
'type' => 1,
);
$senderdata = array(
'data' => $datanotif,
'to' => $token_user
);
$headers = array(
"Content-Type: application/json",
"Authorization: key=" . keyfcm
);
$curl = curl_init();
curl_setopt_array($curl, array(
CURLOPT_URL => "https://fcm.googleapis.com/fcm/send",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_ENCODING => "",
CURLOPT_MAXREDIRS => 10,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
CURLOPT_CUSTOMREQUEST => "POST",
CURLOPT_POSTFIELDS => json_encode($senderdata),
CURLOPT_HTTPHEADER => $headers,
));
$response = curl_exec($curl);
$err = curl_error($curl);
curl_close($curl);
return $response;
$this->send_generic_to_token($token_user, $payload, array());
return true;
}
public function notif_cancel_driver($id_transaksi, $token_driver)
{
$data = array(
if ($this->is_token_empty($token_driver) || !$this->is_valid_fcm_token($token_driver)) {
log_message('debug', 'notif_cancel_driver: skip, no/invalid Firebase token for driver');
return true;
}
$payload = array(
'id_transaksi' => $id_transaksi,
'response' => '0',
'type' => 1
'response' => '0',
'type' => 1,
);
$senderdata = array(
'data' => $data,
'to' => $token_driver
);
$headers = array(
"Content-Type: application/json",
'Authorization: key=' . keyfcm
);
$curl = curl_init();
curl_setopt_array($curl, array(
CURLOPT_URL => "https://fcm.googleapis.com/fcm/send",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_ENCODING => "",
CURLOPT_MAXREDIRS => 10,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
CURLOPT_CUSTOMREQUEST => "POST",
CURLOPT_POSTFIELDS => json_encode($senderdata),
CURLOPT_HTTPHEADER => $headers,
));
$response = curl_exec($curl);
$err = curl_error($curl);
curl_close($curl);
return $response;
$this->send_generic_to_token($token_driver, $payload, array());
return true;
}
public function send_notif($title, $message, $topic)
/**
* Check if token/target is empty (null, empty string, or whitespace).
* Use before sending FCM to avoid sending when device has no token.
*/
private function is_token_empty($token)
{
return $token === null || trim((string) $token) === '';
}
/**
* Check if string looks like a valid FCM v1 device token (not a placeholder).
* Uses shared fcm_v1_is_valid_device_token helper.
*/
private function is_valid_fcm_token($token)
{
if (!function_exists('fcm_v1_is_valid_device_token')) {
get_instance()->load->helper('fcm_v1_helper');
}
return fcm_v1_is_valid_device_token($token);
}
/**
* Send notification via FCM HTTP v1 to a device token (reg_id, token, token_merchant).
* Uses token API so reg_id can be used as FCM v1 target. Skips invalid/placeholder tokens.
* @param string $target FCM device token (from driver.reg_id, pelanggan.token, etc.)
* @return bool True on success or skip, false on failure
*/
public function send_notif($title, $message, $target)
{
if ($this->is_token_empty($target)) {
log_message('debug', 'send_notif: skip, no token');
return true;
}
if (!$this->is_valid_fcm_token($target)) {
log_message('debug', 'send_notif: skip, invalid/placeholder FCM token');
return true;
}
if (!function_exists('fcm_v1_validate_token')) {
get_instance()->load->helper('fcm_v1_helper');
}
if (!fcm_v1_validate_token()) {
log_message('error', 'send_notif: Firebase token not ready, skip send');
return false;
}
$data = array(
'title' => $title,
'title' => $title,
'message' => $message,
'type' => 4
'type' => 4,
);
$senderdata = array(
'data' => $data,
'to' => '/topics/' . $topic
$options = array(
'title' => $title,
'body' => $message,
);
$headers = array(
'Content-Type : application/json',
'Authorization: key=' . keyfcm
);
$curl = curl_init();
curl_setopt_array($curl, array(
CURLOPT_URL => "https://fcm.googleapis.com/fcm/send",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_ENCODING => "",
CURLOPT_MAXREDIRS => 10,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
CURLOPT_CUSTOMREQUEST => "POST",
CURLOPT_POSTFIELDS => json_encode($senderdata),
CURLOPT_HTTPHEADER => $headers,
));
$response = curl_exec($curl);
$err = curl_error($curl);
curl_close($curl);
return $this->send_generic_to_token($target, $data, $options);
}
public function send_notif_topup($title, $id, $message, $method, $token)
{
if ($this->is_token_empty($token)) {
log_message('debug', 'send_notif_topup: skip, no Firebase token for user id=' . $id);
return;
}
if (!function_exists('fcm_v1_validate_token')) {
get_instance()->load->helper('fcm_v1_helper');
}
if (!fcm_v1_validate_token()) {
log_message('error', 'send_notif_topup: Firebase token not ready, skip send for user id=' . $id);
return;
}
// Direct token notification for topup via HTTP v1.
$data = array(
'title' => $title,
'id' => $id,
'message' => $message,
'title' => $title,
'id' => $id,
'message'=> $message,
'method' => $method,
'type' => 3
'type' => 3,
);
$senderdata = array(
'data' => $data,
'to' => $token
$options = array(
'title' => $title,
'body' => $message,
);
$this->send_generic_to_token($token, $data, $options);
}
$headers = array(
'Content-Type : application/json',
'Authorization: key=' . keyfcm
);
$curl = curl_init();
/**
* Send FCM via HTTP v1 API to a single device token.
* Used by User and Driver apps (notification/send_generic).
* $data must be flat key-value; values are stringified for FCM.
*
* @param string $target FCM device token
* @param array $data Data payload (keys preserved; values cast to string)
* @param array $options Optional 'title', 'body' for notification
* @return bool True on success, false on failure
*/
public function send_generic_to_token($target, $data, $options = array())
{
if ($this->is_token_empty($target)) {
log_message('debug', 'FCM v1 send_generic_to_token: skip, target token empty');
return false;
}
if (!$this->is_valid_fcm_token($target)) {
log_message('debug', 'FCM v1 send_generic_to_token: skip, invalid/placeholder token');
return false;
}
if (!function_exists('fcm_v1_send')) {
get_instance()->load->helper('fcm_v1_helper');
}
if (!fcm_v1_validate_token()) {
log_message('error', 'FCM v1 send_generic_to_token: Firebase token not ready, skip send');
return false;
}
$flat = array();
foreach ($data as $k => $v) {
if (is_array($v) || is_object($v)) {
$flat[$k] = json_encode($v);
} else {
$flat[$k] = (string) $v;
}
}
log_message('debug', 'FCM v1 send_generic_to_token target=' . substr((string)$target, 0, 20) . '... payload=' . json_encode($flat));
$result = fcm_v1_send($target, $flat, false, $options);
if ($result === false) {
log_message('error', 'FCM v1 send_generic_to_token failed for target=' . substr((string)$target, 0, 20) . '...');
}
return $result !== false;
}
curl_setopt_array($curl, array(
CURLOPT_URL => "https://fcm.googleapis.com/fcm/send",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_ENCODING => "",
CURLOPT_MAXREDIRS => 10,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
CURLOPT_CUSTOMREQUEST => "POST",
CURLOPT_POSTFIELDS => json_encode($senderdata),
CURLOPT_HTTPHEADER => $headers,
));
$response = curl_exec($curl);
$err = curl_error($curl);
curl_close($curl);
/**
* Send FCM via HTTP v1 API to a topic.
*
* @param string $target Topic name (without /topics/ prefix)
* @param array $data Data payload
* @param array $options Optional 'title', 'body'
* @return bool True on success, false on failure
*/
public function send_generic_to_topic($target, $data, $options = array())
{
if (!function_exists('fcm_v1_send')) {
get_instance()->load->helper('fcm_v1_helper');
}
if (!fcm_v1_validate_token()) {
log_message('error', 'FCM v1 send_generic_to_topic: Firebase token not ready, skip send');
return false;
}
$flat = array();
foreach ($data as $k => $v) {
if (is_array($v) || is_object($v)) {
$flat[$k] = json_encode($v);
} else {
$flat[$k] = (string) $v;
}
}
log_message('debug', 'FCM v1 send_generic_to_topic topic=' . (string)$target . ' payload=' . json_encode($flat));
$result = fcm_v1_send($target, $flat, true, $options);
if ($result === false) {
log_message('error', 'FCM v1 send_generic_to_topic failed for topic=' . (string)$target);
}
return $result !== false;
}
}
View File
View File
+104 -18
View File
@@ -269,19 +269,45 @@ class Pelanggan_model extends CI_model
return true;
}
public function get_driver_ride($lat, $lng, $fitur)
/**
* Get nearby drivers for ride: purely by GPS distance, no region/wilayah.
* Uses config_driver (driver's real-time lat/long from app) and user (lat, lng).
* Optional $radius_km (1100): when set, used as search radius; else fitur.jarak_minimum with min 10 km.
* Optimized: bounding-box pre-filter + Haversine + LIMIT 50.
*/
public function get_driver_ride($lat, $lng, $fitur, $radius_km = null)
{
$url_foto = base_url() . 'images/fotodriver/';
$lat = (float) $lat;
$lng = (float) $lng;
$fitur = (int) $fitur;
if ($radius_km !== null && $radius_km > 0) {
$radius_km = max(1, min(100, (float) $radius_km));
}
$box_km = isset($radius_km) ? $radius_km : 50;
$delta_lat = $box_km / 111.0;
$cos_lat = max(0.01, cos(deg2rad($lat)));
$delta_lng = $box_km / (111.0 * $cos_lat);
$lat_min = $lat - $delta_lat;
$lat_max = $lat + $delta_lat;
$lng_min = $lng - $delta_lng;
$lng_max = $lng + $delta_lng;
$having_radius = isset($radius_km)
? (float) $radius_km
: 'GREATEST(COALESCE(f.jarak_minimum, 10), 10)';
$result = $this->db->query("
SELECT f.jarak_minimum, f.wallet_minimum, d.id as id, d.nama_driver, ld.latitude, ld.longitude, ld.bearing, ld.update_at,
k.merek, k.nomor_kendaraan, k.warna, k.tipe, s.saldo,
d.no_telepon, CONCAT('$url_foto', d.foto, '') as foto, d.reg_id, dj.driver_job,
(6371 * acos(cos(radians($lat)) * cos(radians( ld.latitude ))"
. " * cos(radians(ld.longitude) - radians($lng))"
. " + sin(radians($lat)) * sin( radians(ld.latitude)))) AS distance
FROM config_driver ld, driver d, driver_job dj, kendaraan k, saldo s,fitur f
WHERE ld.id_driver = d.id
(6371 * acos(cos(radians($lat)) * cos(radians(ld.latitude))
* cos(radians(ld.longitude) - radians($lng))
+ sin(radians($lat)) * sin(radians(ld.latitude)))) AS distance
FROM config_driver ld, driver d, driver_job dj, kendaraan k, saldo s, fitur f
WHERE ld.id_driver = d.id
AND f.id_fitur = $fitur
AND ld.status = '1'
AND dj.id = d.job
@@ -290,35 +316,88 @@ class Pelanggan_model extends CI_model
AND k.id_k = d.kendaraan
AND s.id_user = d.id
AND s.saldo > f.wallet_minimum
HAVING distance <= f.jarak_minimum
ORDER BY distance ASC");
AND ld.latitude BETWEEN $lat_min AND $lat_max
AND ld.longitude BETWEEN $lng_min AND $lng_max
HAVING distance <= $having_radius
ORDER BY distance ASC
LIMIT 50");
$count = $result->num_rows();
log_message('debug', 'get_driver_ride: fitur=' . $fitur . ' lat=' . $lat . ' lng=' . $lng . ' drivers_found=' . $count);
if ($count === 0) {
$this->log_driver_diagnostic('ride', $lat, $lng, $fitur, $lat_min, $lat_max, $lng_min, $lng_max, $radius_km);
}
return $result;
}
public function get_driver_car($lat, $lng, $fitur)
/**
* Diagnostic logging when 0 drivers found: counts online drivers, in-box, by job, etc.
*/
private function log_driver_diagnostic($type, $lat, $lng, $fitur, $lat_min, $lat_max, $lng_min, $lng_max, $radius_km)
{
$r_online = $this->db->query("SELECT COUNT(*) as c FROM config_driver WHERE status='1'")->row();
$cnt_online = $r_online ? (int) $r_online->c : 0;
$r_box = $this->db->query("SELECT COUNT(*) as c FROM config_driver ld JOIN driver d ON ld.id_driver=d.id WHERE ld.status='1' AND d.status='1' AND ld.latitude BETWEEN " . (float)$lat_min . " AND " . (float)$lat_max . " AND ld.longitude BETWEEN " . (float)$lng_min . " AND " . (float)$lng_max)->row();
$cnt_in_box = $r_box ? (int) $r_box->c : 0;
$fitur_row = $this->db->query("SELECT driver_job, jarak_minimum, wallet_minimum FROM fitur WHERE id_fitur=" . (int)$fitur)->row();
$driver_job = $fitur_row ? $fitur_row->driver_job : null;
$cnt_job = 0;
if ($driver_job !== null) {
$r_job = $this->db->query("SELECT COUNT(*) as c FROM driver WHERE status='1' AND job=" . (int)$driver_job)->row();
$cnt_job = $r_job ? (int) $r_job->c : 0;
}
$fitur_exists = $fitur_row ? 'yes' : 'NO_FITUR_NOT_FOUND';
log_message('debug', 'get_driver_' . $type . '_diagnostic: fitur=' . $fitur . ' fitur_exists=' . $fitur_exists . ' driver_job=' . ($driver_job !== null ? $driver_job : 'n/a') . ' radius_km=' . ($radius_km !== null ? $radius_km : 'default') . ' online_drivers=' . $cnt_online . ' in_box=' . $cnt_in_box . ' with_job=' . $cnt_job);
}
/**
* Get nearby drivers for car: purely by GPS distance, no region/wilayah.
* Optional $radius_km (1100); else 10 km. Optimized: bounding-box + LIMIT 50.
*/
public function get_driver_car($lat, $lng, $fitur, $radius_km = null)
{
$range = 10;
$url_foto = base_url() . 'images/fotodriver/';
$lat = (float) $lat;
$lng = (float) $lng;
$fitur = (int) $fitur;
$range = ($radius_km !== null && $radius_km > 0)
? max(1, min(100, (float) $radius_km))
: 10;
$delta_lat = $range / 111.0;
$cos_lat = max(0.01, cos(deg2rad($lat)));
$delta_lng = $range / (111.0 * $cos_lat);
$lat_min = $lat - $delta_lat;
$lat_max = $lat + $delta_lat;
$lng_min = $lng - $delta_lng;
$lng_max = $lng + $delta_lng;
$result = $this->db->query("
SELECT f.jarak_minimum, f.wallet_minimum, d.id as id, d.nama_driver, ld.latitude, ld.longitude, ld.bearing, ld.update_at,
k.merek, k.nomor_kendaraan, k.warna, k.tipe, s.saldo,
d.no_telepon, CONCAT('$url_foto', d.foto, '') as foto, d.reg_id, dj.driver_job,
(6371 * acos(cos(radians($lat)) * cos(radians( ld.latitude ))"
. " * cos(radians(ld.longitude) - radians($lng))"
. " + sin(radians($lat)) * sin( radians(ld.latitude)))) AS distance
FROM config_driver ld, driver d, driver_job dj, kendaraan k, saldo s,fitur f
WHERE ld.id_driver = d.id
(6371 * acos(cos(radians($lat)) * cos(radians(ld.latitude))
* cos(radians(ld.longitude) - radians($lng))
+ sin(radians($lat)) * sin(radians(ld.latitude)))) AS distance
FROM config_driver ld, driver d, driver_job dj, kendaraan k, saldo s, fitur f
WHERE ld.id_driver = d.id
AND f.id_fitur = $fitur
AND ld.status = '1'
AND dj.id = d.job
AND d.job = '2'
AND d.job = f.driver_job
AND d.status = '1'
AND k.id_k = d.kendaraan
AND s.id_user = d.id
AND s.saldo > 500
AND s.saldo > GREATEST(COALESCE(f.wallet_minimum, 0), 500)
AND ld.latitude BETWEEN $lat_min AND $lat_max
AND ld.longitude BETWEEN $lng_min AND $lng_max
HAVING distance <= $range
ORDER BY distance ASC");
ORDER BY distance ASC
LIMIT 50");
$count = $result->num_rows();
log_message('debug', 'get_driver_car: fitur=' . $fitur . ' lat=' . $lat . ' lng=' . $lng . ' drivers_found=' . $count);
if ($count === 0) {
$this->log_driver_diagnostic('car', $lat, $lng, $fitur, $lat_min, $lat_max, $lng_min, $lng_max, $radius_km);
}
return $result;
}
@@ -467,6 +546,7 @@ class Pelanggan_model extends CI_model
$this->delete_transaksi($dataTrans['id_transaksi']);
$stat = FALSE;
}
log_message('debug', 'check_status: id_transaksi=' . $dataTrans['id_transaksi'] . ' raw_status=' . $cek->row('status') . ' stat=' . ($stat ? 'TRUE' : 'FALSE'));
$dataCheck = array(
'message' => 'check status',
'status' => $stat,
@@ -1273,6 +1353,10 @@ class Pelanggan_model extends CI_model
return $this->db->get('category_merchant');
}
/**
* Merchant nearby: purely by GPS (merchant lat/long vs user lat/long), no region.
* Distance <= fitur.jarak_minimum.
*/
public function merchantnearby($long, $lat)
{
$this->db->select("merchant.id_merchant , merchant.nama_merchant , merchant.alamat_merchant , merchant.rate_merchant, merchant.latitude_merchant , merchant.longitude_merchant , merchant.jam_buka , merchant.jam_tutup ,
@@ -1329,6 +1413,7 @@ class Pelanggan_model extends CI_model
return $data;
}
/** Merchant promo nearby: purely GPS, no region. */
public function merchantpromo($long, $lat)
{
$this->db->select("merchant.id_merchant , merchant.nama_merchant , merchant.alamat_merchant , merchant.rate_merchant, merchant.latitude_merchant , merchant.longitude_merchant , merchant.jam_buka , merchant.jam_tutup ,
@@ -1411,6 +1496,7 @@ class Pelanggan_model extends CI_model
return $this->db->get('merchant');
}
/** All merchant nearby by fitur: purely GPS (merchant lat/long vs user), no region. */
public function allmerchantnearby($long, $lat, $fitur)
{
$this->db->select("merchant.id_merchant , merchant.nama_merchant , merchant.alamat_merchant , merchant.rate_merchant, merchant.latitude_merchant , merchant.longitude_merchant , merchant.jam_buka , merchant.jam_tutup ,
View File
View File
View File
View File
View File
View File
+7 -34
View File
@@ -146,39 +146,10 @@ class wallet_model extends CI_model
public function send_notif($title, $message, $topic)
{
$data = array(
'title' => $title,
'message' => $message,
'type' => 3
);
$senderdata = array(
'data' => $data,
'to' => $topic
);
$headers = array(
'Content-Type : application/json',
'Authorization: key=' . keyfcm
);
$curl = curl_init();
curl_setopt_array($curl, array(
CURLOPT_URL => "https://fcm.googleapis.com/fcm/send",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_ENCODING => "",
CURLOPT_MAXREDIRS => 10,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
CURLOPT_CUSTOMREQUEST => "POST",
CURLOPT_POSTFIELDS => json_encode($senderdata),
CURLOPT_HTTPHEADER => $headers,
));
$response = curl_exec($curl);
$err = curl_error($curl);
curl_close($curl);
// Delegate to Notification_model, which uses FCM HTTP v1 + service account.
$CI = &get_instance();
$CI->load->model('notification_model', 'notif');
$CI->notif->send_notif($title, $message, $topic);
}
public function updatesaldowallet($data)
@@ -319,7 +290,9 @@ class wallet_model extends CI_model
$title = 'Dibatalkan';
$desc = 'Pembayaran Kamu telah Dibatalkan';
$this->Notification_model->send_notif_topup($title, $wl['id_user'], $desc, $wl['invoice'], $token);
if ($token !== null && trim((string) $token) !== '') {
$this->Notification_model->send_notif_topup($title, $wl['id_user'], $desc, $wl['invoice'], $token);
}
}
}
}
View File
View File