diff --git a/CHANGELOG.md b/CHANGELOG.md index 717b42c8..25c78165 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,7 @@ - Add pppoe customer and pppoe IP to make static username and IP - Add Sync button - Allow Mac Address Username +- Router Maps ## 2024.8.1 diff --git a/install/phpnuxbill.sql b/install/phpnuxbill.sql index 4bf59e16..e3b66ef4 100644 --- a/install/phpnuxbill.sql +++ b/install/phpnuxbill.sql @@ -137,6 +137,8 @@ CREATE TABLE `tbl_routers` ( `username` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL, `password` varchar(60) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL, `description` varchar(256) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL, + `coordinates` VARCHAR(50) NOT NULL DEFAULT '', + `coverage` VARCHAR(8) NOT NULL DEFAULT '0', `enabled` tinyint(1) NOT NULL DEFAULT '1' COMMENT '0 disabled' ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci; diff --git a/system/autoload/Paginator.php b/system/autoload/Paginator.php index 3899d7b9..3d8b9014 100644 --- a/system/autoload/Paginator.php +++ b/system/autoload/Paginator.php @@ -8,7 +8,7 @@ class Paginator { - public static function findMany($query, $search = [], $per_page = '10', $append_url = "") + public static function findMany($query, $search = [], $per_page = '10', $append_url = "", $toArray = false) { global $routes, $ui; $adjacents = "2"; @@ -71,7 +71,11 @@ class Paginator if ($ui) { $ui->assign('paginator', $result); } - return $query->offset($startpoint)->limit($per_page)->find_many(); + if($toArray){ + return $query->offset($startpoint)->limit($per_page)->find_array(); + }else{ + return $query->offset($startpoint)->limit($per_page)->find_many(); + } } } diff --git a/system/controllers/customers.php b/system/controllers/customers.php index baf166cd..5aa83e80 100644 --- a/system/controllers/customers.php +++ b/system/controllers/customers.php @@ -713,7 +713,6 @@ switch ($action) { die(); } $d = Paginator::findMany($query, ['search' => $search], 30, $append_url); - $ui->assign('xheader', '<link rel="stylesheet" type="text/css" href="https://cdn.datatables.net/1.11.3/css/jquery.dataTables.min.css">'); $ui->assign('d', $d); $ui->assign('statuses', ORM::for_table('tbl_customers')->getEnum("status")); $ui->assign('filter', $filter); diff --git a/system/controllers/routers.php b/system/controllers/routers.php index a507207a..23bbbfb7 100644 --- a/system/controllers/routers.php +++ b/system/controllers/routers.php @@ -18,24 +18,26 @@ if (!in_array($admin['user_type'], ['SuperAdmin', 'Admin'])) { _alert(Lang::T('You do not have permission to access this page'), 'danger', "dashboard"); } +$leafletpickerHeader = <<<EOT +<link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.3/dist/leaflet.css"> +EOT; + switch ($action) { - case 'list': - $ui->assign('xfooter', '<script type="text/javascript" src="ui/lib/c/routers.js"></script>'); - + case 'maps': $name = _post('name'); + $query = ORM::for_table('tbl_routers')->where_not_equal('coordinates', '')->order_by_desc('id'); + $query->selects(['id', 'name', 'coordinates', 'description', 'coverage', 'enabled']); if ($name != '') { - $query = ORM::for_table('tbl_routers')->where_like('name', '%' . $name . '%')->order_by_desc('id'); - $d = Paginator::findMany($query, ['name' => $name]); - } else { - $query = ORM::for_table('tbl_routers')->order_by_desc('id'); - $d = Paginator::findMany($query); + $query->where_like('name', '%' . $name . '%'); } - + $d = Paginator::findMany($query, ['name' => $name], '20', '', true); + $ui->assign('name', $name); $ui->assign('d', $d); - run_hook('view_list_routers'); #HOOK - $ui->display('routers.tpl'); + $ui->assign('_title', Lang::T('Routers Geo Location Information')); + $ui->assign('xheader', $leafletpickerHeader); + $ui->assign('xfooter', '<script src="https://unpkg.com/leaflet@1.9.3/dist/leaflet.js"></script>'); + $ui->display('routers-maps.tpl'); break; - case 'add': run_hook('view_add_routers'); #HOOK $ui->display('routers-add.tpl'); @@ -47,6 +49,7 @@ switch ($action) { if (!$d) { $d = ORM::for_table('tbl_routers')->where_equal('name', _get('name'))->find_one(); } + $ui->assign('xheader', $leafletpickerHeader); if ($d) { $ui->assign('d', $d); run_hook('view_router_edit'); #HOOK @@ -75,16 +78,18 @@ switch ($action) { $enabled = _post('enabled'); $msg = ''; - if (Validator::Length($name, 30, 4) == false) { - $msg .= 'Name should be between 5 to 30 characters' . '<br>'; - } - if ($ip_address == '' or $username == '') { - $msg .= Lang::T('All field is required') . '<br>'; + if (Validator::Length($name, 30, 1) == false) { + $msg .= 'Name should be between 1 to 30 characters' . '<br>'; } + if($enabled || _post("testIt")){ + if ($ip_address == '' or $username == '') { + $msg .= Lang::T('All field is required') . '<br>'; + } - $d = ORM::for_table('tbl_routers')->where('ip_address', $ip_address)->find_one(); - if ($d) { - $msg .= Lang::T('IP Router Already Exist') . '<br>'; + $d = ORM::for_table('tbl_routers')->where('ip_address', $ip_address)->find_one(); + if ($d) { + $msg .= Lang::T('IP Router Already Exist') . '<br>'; + } } if (strtolower($name) == 'radius') { $msg .= '<b>Radius</b> name is reserved<br>'; @@ -92,7 +97,7 @@ switch ($action) { if ($msg == '') { run_hook('add_router'); #HOOK - if(_post("testIt")){ + if (_post("testIt")) { (new MikrotikHotspot())->getClient($ip_address, $username, $password); } $d = ORM::for_table('tbl_routers')->create(); @@ -104,7 +109,7 @@ switch ($action) { $d->enabled = $enabled; $d->save(); - r2(U . 'routers/list', 's', Lang::T('Data Created Successfully')); + r2(U . 'routers/edit/' . $d->id(), 's', Lang::T('Data Created Successfully')); } else { r2(U . 'routers/add', 'e', $msg); } @@ -117,13 +122,17 @@ switch ($action) { $username = _post('username'); $password = _post('password'); $description = _post('description'); + $coordinates = _post('coordinates'); + $coverage = _post('coverage'); $enabled = $_POST['enabled']; $msg = ''; if (Validator::Length($name, 30, 4) == false) { $msg .= 'Name should be between 5 to 30 characters' . '<br>'; } - if ($ip_address == '' or $username == '') { - $msg .= Lang::T('All field is required') . '<br>'; + if($enabled || _post("testIt")){ + if ($ip_address == '' or $username == '') { + $msg .= Lang::T('All field is required') . '<br>'; + } } $id = _post('id'); @@ -141,10 +150,12 @@ switch ($action) { } $oldname = $d['name']; - if ($d['ip_address'] != $ip_address) { - $c = ORM::for_table('tbl_routers')->where('ip_address', $ip_address)->where_not_equal('id', $id)->find_one(); - if ($c) { - $msg .= 'IP Already Exists<br>'; + if($enabled || _post("testIt")){ + if ($d['ip_address'] != $ip_address) { + $c = ORM::for_table('tbl_routers')->where('ip_address', $ip_address)->where_not_equal('id', $id)->find_one(); + if ($c) { + $msg .= 'IP Already Exists<br>'; + } } } @@ -154,7 +165,7 @@ switch ($action) { if ($msg == '') { run_hook('router_edit'); #HOOK - if(_post("testIt")){ + if (_post("testIt")) { (new MikrotikHotspot())->getClient($ip_address, $username, $password); } $d->name = $name; @@ -162,6 +173,8 @@ switch ($action) { $d->username = $username; $d->password = $password; $d->description = $description; + $d->coordinates = $coordinates; + $d->coverage = $coverage; $d->enabled = $enabled; $d->save(); if ($name != $oldname) { @@ -191,5 +204,17 @@ switch ($action) { break; default: - r2(U . 'routers/list/', 's', ''); + $ui->assign('xfooter', '<script type="text/javascript" src="ui/lib/c/routers.js"></script>'); + + $name = _post('name'); + $name = _post('name'); + $query = ORM::for_table('tbl_routers')->order_by_desc('id'); + if ($name != '') { + $query->where_like('name', '%' . $name . '%'); + } + $d = Paginator::findMany($query, ['name' => $name]); + $ui->assign('d', $d); + run_hook('view_list_routers'); #HOOK + $ui->display('routers.tpl'); + break; } diff --git a/system/lan/english.json b/system/lan/english.json index d2d7717a..e775eb4f 100644 --- a/system/lan/english.json +++ b/system/lan/english.json @@ -690,5 +690,9 @@ "Mail_Deleted_Successfully": "Mail Deleted Successfully", "Message_Not_Found": "Message Not Found", "Send_Welcome_Message": "Send Welcome Message", - "WA": "WA" + "WA": "WA", + "_": "-", + "Routers_Maps": "Routers Maps", + "Routers_Geo_Location_Information": "Routers Geo Location Information", + "Coverage": "Coverage" } \ No newline at end of file diff --git a/system/updates.json b/system/updates.json index 32df040d..87db6cf1 100644 --- a/system/updates.json +++ b/system/updates.json @@ -140,5 +140,9 @@ "2024.8.5" : [ "ALTER TABLE `tbl_customers` ADD `pppoe_username` VARCHAR(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT 'For PPPOE Login' AFTER `password`;", "ALTER TABLE `tbl_customers` ADD `pppoe_ip` VARCHAR(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT 'For PPPOE Login' AFTER `pppoe_password`;" + ], + "2024.8.5.1" : [ + "ALTER TABLE `tbl_routers` ADD `coordinates` VARCHAR(50) NOT NULL DEFAULT '' AFTER `description`;", + "ALTER TABLE `tbl_routers` ADD `coverage` VARCHAR(8) NOT NULL DEFAULT '0' AFTER `coordinates`;" ] } \ No newline at end of file diff --git a/ui/ui/routers-edit.tpl b/ui/ui/routers-edit.tpl index 737d74a0..df2ffba2 100644 --- a/ui/ui/routers-edit.tpl +++ b/ui/ui/routers-edit.tpl @@ -5,70 +5,147 @@ <div class="col-sm-12 col-md-12"> <div class="panel panel-primary panel-hovered panel-stacked mb30"> <div class="panel-heading">{Lang::T('Edit Router')}</div> - <div class="panel-body"> - <form class="form-horizontal" method="post" role="form" action="{$_url}routers/edit-post" > - <input type="hidden" name="id" value="{$d['id']}"> - <div class="form-group"> - <label class="col-md-2 control-label">{Lang::T('Status')}</label> - <div class="col-md-10"> - <label class="radio-inline warning"> - <input type="radio" {if $d['enabled'] == 1}checked{/if} name="enabled" value="1"> Enable - </label> - <label class="radio-inline"> - <input type="radio" {if $d['enabled'] == 0}checked{/if} name="enabled" value="0"> Disable - </label> - </div> + <div class="panel-body"> + <form class="form-horizontal" method="post" role="form" action="{$_url}routers/edit-post"> + <input type="hidden" name="id" value="{$d['id']}"> + <div class="form-group"> + <label class="col-md-2 control-label">{Lang::T('Status')}</label> + <div class="col-md-10"> + <label class="radio-inline warning"> + <input type="radio" {if $d['enabled'] == 1}checked{/if} name="enabled" value="1"> Enable + </label> + <label class="radio-inline"> + <input type="radio" {if $d['enabled'] == 0}checked{/if} name="enabled" value="0"> + Disable + </label> </div> - <div class="form-group"> - <label class="col-md-2 control-label">{Lang::T('Router Name / Location')}</label> - <div class="col-md-6"> - <input type="text" class="form-control" id="name" name="name" maxlength="32" value="{$d['name']}"> - <p class="help-block">{Lang::T('Name of Area that router operated')}</p> - </div> + </div> + <div class="form-group"> + <label class="col-md-2 control-label">{Lang::T('Router Name / Location')}</label> + <div class="col-md-6"> + <input type="text" class="form-control" id="name" name="name" maxlength="32" + value="{$d['name']}"> + <p class="help-block">{Lang::T('Name of Area that router operated')}</p> </div> - <div class="form-group"> - <label class="col-md-2 control-label">{Lang::T('IP Address')}</label> - <div class="col-md-6"> - <input type="text" placeholder="192.168.88.1:8728" class="form-control" id="ip_address" name="ip_address" value="{$d['ip_address']}"> - </div> + </div> + <div class="form-group"> + <label class="col-md-2 control-label">{Lang::T('IP Address')}</label> + <div class="col-md-6"> + <input type="text" placeholder="192.168.88.1:8728" class="form-control" id="ip_address" + name="ip_address" value="{$d['ip_address']}"> </div> - <div class="form-group"> - <label class="col-md-2 control-label">{Lang::T('Username')}</label> - <div class="col-md-6"> - <input type="text" class="form-control" id="username" name="username" value="{$d['username']}"> - </div> + </div> + <div class="form-group"> + <label class="col-md-2 control-label">{Lang::T('Username')}</label> + <div class="col-md-6"> + <input type="text" class="form-control" id="username" name="username" + value="{$d['username']}"> </div> - <div class="form-group"> - <label class="col-md-2 control-label">{Lang::T('Router Secret')}</label> - <div class="col-md-6"> - <input type="password" class="form-control" id="password" name="password" value="{$d['password']}" onmouseleave="this.type = 'password'" + </div> + <div class="form-group"> + <label class="col-md-2 control-label">{Lang::T('Router Secret')}</label> + <div class="col-md-6"> + <input type="password" class="form-control" id="password" name="password" + value="{$d['password']}" onmouseleave="this.type = 'password'" onmouseenter="this.type = 'text'"> + </div> + </div> + <div class="form-group"> + <label class="col-md-2 control-label">{Lang::T('Description')}</label> + <div class="col-md-6"> + <textarea class="form-control" id="description" + name="description">{$d['description']}</textarea> + <p class="help-block">{Lang::T('Explain Coverage of router')}</p> + </div> + </div> + <div class="form-group"> + <label class="col-md-2 control-label">{Lang::T('Coordinates')}</label> + <div class="col-md-6"> + <input name="coordinates" id="coordinates" class="form-control" value="{$d['coordinates']}" + placeholder="6.465422, 3.406448"> + <div id="map" style="width: '100%'; height: 200px; min-height: 150px;"></div> + </div> + </div> + <div class="form-group"> + <label class="col-md-2 control-label">{Lang::T('Coverage')}</label> + <div class="col-md-6"> + <div class="input-group"> + <input type="number" class="form-control" id="coverage" name="coverage" value="{$d['coverage']}" + onkeyup="updateCoverage()"> + <span class="input-group-addon">meter(s)</span> </div> </div> - <div class="form-group"> - <label class="col-md-2 control-label">{Lang::T('Description')}</label> - <div class="col-md-6"> - <textarea class="form-control" id="description" name="description">{$d['description']}</textarea> - <p class="help-block">{Lang::T('Explain Coverage of router')}</p> - </div> + </div> + <div class="form-group"> + <div class="col-lg-offset-2 col-lg-10"> + <button class="btn btn-primary" type="submit">{Lang::T('Save Changes')}</button> + Or <a href="{$_url}routers/list">{Lang::T('Cancel')}</a> </div> - <div class="form-group row"> - <label class="col-md-2 control-label"></label> - <div class="col-md-6"> - <label><input type="checkbox" checked name="testIt" value="yes"> Test Connection</label> - </div> - </div> - <div class="form-group"> - <div class="col-lg-offset-2 col-lg-10"> - <button class="btn btn-primary" type="submit">{Lang::T('Save Changes')}</button> - Or <a href="{$_url}routers/list">{Lang::T('Cancel')}</a> - </div> - </div> - </form> - </div> + </div> + </form> </div> </div> </div> </div> +</div> -{include file="sections/footer.tpl"} +{literal} + <script src="https://unpkg.com/leaflet@1.9.3/dist/leaflet.js"></script> + <script> + var circle; + function getLocation() { + if (window.location.protocol == "https:" && navigator.geolocation) { + navigator.geolocation.getCurrentPosition(showPosition); + } else { + setupMap(51.505, -0.09); + } + } + + function showPosition(position) { + setupMap(position.coords.latitude, position.coords.longitude); + } + + function updateCoverage() { + if(circle != undefined){ + circle.setRadius($("#coverage").val()); + } + } + + + function setupMap(lat, lon) { + var map = L.map('map').setView([lat, lon], 13); + L.tileLayer('https://{s}.basemaps.cartocdn.com/rastertiles/light_all/{z}/{x}/{y}.png', { + attribution: + '© <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors © <a href="https://carto.com/attributions">CARTO</a>', + subdomains: 'abcd', + maxZoom: 20 + }).addTo(map); + circle = L.circle([lat, lon], 5, { + color: 'blue', + fillOpacity: 0.1 + }).addTo(map); + var marker = L.marker([lat, lon]).addTo(map); + map.on('click', function(e) { + var coord = e.latlng; + var lat = coord.lat; + var lng = coord.lng; + newLatLng = new L.LatLng(lat, lng); + marker.setLatLng(newLatLng); + circle.setLatLng(newLatLng); + $('#coordinates').val(lat + ',' + lng); + updateCoverage(); + }); + updateCoverage(); + } + window.onload = function() { + {/literal} + {if $d['coordinates']} + setupMap({$d['coordinates']}); + {else} + getLocation(); + {/if} + {literal} + } + </script> +{/literal} +{include file="sections/footer.tpl"} \ No newline at end of file diff --git a/ui/ui/routers-maps.tpl b/ui/ui/routers-maps.tpl new file mode 100644 index 00000000..7e5b1046 --- /dev/null +++ b/ui/ui/routers-maps.tpl @@ -0,0 +1,84 @@ +{include file="sections/header.tpl"} + + +<form id="site-search" method="post" action="{$_url}routers/maps"> + <input type="hidden" name="_route" value="routers/maps"> + <div class="input-group"> + <div class="input-group-addon"> + <span class="fa fa-search"></span> + </div> + <input type="text" name="name" class="form-control" value="{$name}" + placeholder="{Lang::T('Search')}..."> + <div class="input-group-btn"> + <button class="btn btn-success" type="submit">{Lang::T('Search')}</button> + </div> + </div> +</form> + +<!-- Map container div --> +<div id="map" class="well" style="width: '100%'; height: 70vh; margin: 20px auto"></div> + +{include file="pagination.tpl"} + +{literal} + <script> + function getLocation() { + if (window.location.protocol == "https:" && navigator.geolocation) { + navigator.geolocation.getCurrentPosition(showPosition); + } else { + setupMap(51.505, -0.09); + } + } + + function showPosition(position) { + setupMap(position.coords.latitude, position.coords.longitude); + } + + function setupMap(lat, lon) { + var map = L.map('map').setView([lat, lon], 9); + var group = L.featureGroup().addTo(map); + + var routers = {/literal}{$d|json_encode}{literal}; + + L.tileLayer('https://{s}.basemaps.cartocdn.com/rastertiles/light_all/{z}/{x}/{y}.png', { + attribution: + '© <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors © <a href="https://carto.com/attributions">CARTO</a>', + subdomains: 'abcd', + maxZoom: 20 + }).addTo(map); + + routers.forEach(function(router) { + var name = router.name; + var info = router.description; + var coordinates = router.coordinates; + console.log(coordinates.split(",")) + // Create a popup for the marker + var popupContent = "<strong>Name</strong>: " + name + "<br>" + + "<strong>Info</strong>: " + info + "<br>" + "<a href='{/literal}{$_url}{literal}routers/edit/"+ router.id +"'>More Info</a> • " + + "<a href='https://www.google.com/maps/dir//" + coordinates + "' target='maps'>Get Direction</a><br>"; + + // Add marker to map + if(router.enabled == 1){ + var circle = L.circle(coordinates.split(","), router.coverage*1, { + color: 'blue', + fillOpacity: 0.1 + }).addTo(map); + }else{ + var circle = L.circle(coordinates.split(","), router.coverage*1, { + color: 'red', + fillOpacity: 0.1 + }).addTo(map); + } + var marker = L.marker(coordinates.split(",")).addTo(group); + marker.bindTooltip(name, { permanent: true }).bindPopup(popupContent); + }); + + map.fitBounds(group.getBounds()); + } + window.onload = function() { + getLocation(); + } + </script> +{/literal} +{include file="sections/footer.tpl"} \ No newline at end of file diff --git a/ui/ui/routers.tpl b/ui/ui/routers.tpl index 9aad56a3..53c2dd4d 100644 --- a/ui/ui/routers.tpl +++ b/ui/ui/routers.tpl @@ -4,7 +4,12 @@ <div class="row"> <div class="col-sm-12"> <div class="panel panel-hovered mb20 panel-primary"> - <div class="panel-heading">{Lang::T('Routers')}</div> + <div class="panel-heading">{Lang::T('Routers')} + <div class="btn-group pull-right"> + <a class="btn btn-primary btn-xs" title="save" href="{$_url}routers/maps"> + <span class="glyphicon glyphicon-map-marker"></span></a> + </div> + </div> <div class="panel-body"> <div class="md-whiteframe-z1 mb20 text-center" style="padding: 15px"> <div class="col-md-8"> @@ -43,7 +48,13 @@ <tbody> {foreach $d as $ds} <tr {if $ds['enabled'] != 1}class="danger" title="disabled" {/if}> - <td>{$ds['name']}</td> + <td> + {if $ds['coordinates']} + <a href="https://www.google.com/maps/dir//{$ds['coordinates']}/" target="_blank" + class="btn btn-default btn-xs" title="{$ds['coordinates']}"><i + class="glyphicon glyphicon-map-marker"></i></a> + {/if} + {$ds['name']}</td> <td>{$ds['ip_address']}</td> <td>{$ds['username']}</td> <td>{$ds['description']}</td> diff --git a/ui/ui/sections/header.tpl b/ui/ui/sections/header.tpl index 307df2d9..cd337197 100644 --- a/ui/ui/sections/header.tpl +++ b/ui/ui/sections/header.tpl @@ -427,10 +427,12 @@ </span> </a> <ul class="treeview-menu"> - <li {if $_routes[0] eq 'routers' and $_routes[1] eq 'list' }class="active" {/if}><a - href="{$_url}routers/list">{Lang::T('Routers')}</a></li> + <li {if $_routes[0] eq 'routers' and $_routes[1] eq '' }class="active" {/if}><a + href="{$_url}routers">{Lang::T('Routers')}</a></li> <li {if $_routes[0] eq 'pool' and $_routes[1] eq 'list' }class="active" {/if}><a href="{$_url}pool/list">{Lang::T('IP Pool')}</a></li> + <li {if $_routes[0] eq 'routers' and $_routes[1] eq 'maps' }class="active" {/if}><a + href="{$_url}routers/maps">{Lang::T('Routers Maps')}</a></li> {$_MENU_NETWORK} </ul> </li> diff --git a/version.json b/version.json index 9db2c31a..7602a143 100644 --- a/version.json +++ b/version.json @@ -1,3 +1,3 @@ { - "version": "2024.8.5" + "version": "2024.8.5.1" } \ No newline at end of file