Compare commits

...

3 Commits

64 changed files with 16193 additions and 0 deletions

52
.htaccess Normal file
View File

@ -0,0 +1,52 @@
RewriteEngine On
# Redirect to index.php if file doesn't exist
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
RewriteRule ^([^/]+)/?$ index.php?page=$1 [QSA,L]
# Security - Hide .htaccess files
<Files ".htaccess">
Require all denied
</Files>
# Security - Hide config files
<Files "*.config">
Require all denied
</Files>
# Security - Hide database files
<Files "database.sql">
Require all denied
</Files>
# MIME Types for better performance
<IfModule mod_mime.c>
AddType application/javascript .js
AddType text/css .css
</IfModule>
# Enable compression
<IfModule mod_deflate.c>
AddOutputFilterByType DEFLATE text/plain
AddOutputFilterByType DEFLATE text/html
AddOutputFilterByType DEFLATE text/xml
AddOutputFilterByType DEFLATE text/css
AddOutputFilterByType DEFLATE application/xml
AddOutputFilterByType DEFLATE application/xhtml+xml
AddOutputFilterByType DEFLATE application/rss+xml
AddOutputFilterByType DEFLATE application/javascript
AddOutputFilterByType DEFLATE application/x-javascript
</IfModule>
# Cache control for better performance
<IfModule mod_expires.c>
ExpiresActive on
ExpiresByType text/css "access plus 1 month"
ExpiresByType application/javascript "access plus 1 month"
ExpiresByType image/png "access plus 1 month"
ExpiresByType image/jpg "access plus 1 month"
ExpiresByType image/jpeg "access plus 1 month"
ExpiresByType image/gif "access plus 1 month"
ExpiresByType image/svg+xml "access plus 1 month"
</IfModule>

198
ITEM_SUMMARY.md Normal file
View File

@ -0,0 +1,198 @@
# 🎉 UPGRADE ITEM FIELDS & CORE SYNCHRONIZATION - SUMMARY
## ✅ **SEMUA FITUR BERHASIL DITAMBAHKAN!**
### 📋 **FIELD BARU PADA FORM ITEM:**
#### **1. 🎨 Warna Core**
- **Field**: `core_color_id`
- **Type**: Foreign Key → `tube_colors` table
- **Description**: Warna core yang berbeda dari warna tube
- **UI**: Dropdown dengan preview warna (32 pilihan warna)
#### **2. 🔗 Jenis Kabel**
- **Field**: `item_cable_type`
- **Type**: ENUM ('backbone', 'distribution', 'drop_core', 'feeder', 'branch')
- **Default**: 'distribution'
- **Description**: Klasifikasi jenis kabel yang digunakan pada item
#### **3. 🔢 Kapasitas Core Total**
- **Field**: `total_core_capacity`
- **Type**: INT
- **Default**: 24
- **Description**: Total kapasitas core untuk item
- **Options**: 2, 4, 6, 8, 12, 24, 48, 72, 96, 144, 216, 288 Core
#### **4. 📊 Core yang Digunakan (Enhanced)**
- **Field**: `core_used` (existing, enhanced)
- **Type**: INT
- **Description**: Core yang sedang digunakan dari total kapasitas
- **Feature**: Auto-sync dengan routing kabel
#### **5. 📈 Core Tersedia (Calculated)**
- **Field**: `core_available` (calculated field)
- **Type**: Display only
- **Formula**: `total_core_capacity - core_used`
- **Feature**: Real-time calculation dengan color indicator
---
### 🛠️ **DATABASE CHANGES:**
#### **New Columns Added:**
```sql
ALTER TABLE ftth_items ADD COLUMN core_color_id INT NULL AFTER core_used;
ALTER TABLE ftth_items ADD COLUMN item_cable_type ENUM('backbone', 'distribution', 'drop_core', 'feeder', 'branch') NULL DEFAULT 'distribution' AFTER core_color_id;
ALTER TABLE ftth_items ADD COLUMN total_core_capacity INT NULL DEFAULT 24 AFTER item_cable_type;
-- Foreign Keys
ALTER TABLE ftth_items ADD FOREIGN KEY (core_color_id) REFERENCES tube_colors(id);
-- Indexes for Performance
CREATE INDEX idx_core_color ON ftth_items(core_color_id);
CREATE INDEX idx_cable_type ON ftth_items(item_cable_type);
CREATE INDEX idx_core_usage ON ftth_items(core_used, total_core_capacity);
```
---
### ⚙️ **API ENHANCEMENTS:**
#### **Updated Endpoints:**
- **POST/PUT `/api/items.php`**: Handle new fields
- **GET `/api/items.php`**: Include new fields in response with JOINs
- **Foreign Key Null Handling**: Empty strings converted to NULL
#### **New Query with JOINs:**
```sql
SELECT i.*, it.name as item_type_name, it.icon, it.color,
tc.color_name as tube_color_name, tc.hex_code,
cc.color_name as core_color_name, cc.hex_code as core_hex_code,
sm.ratio as splitter_main_ratio,
so.ratio as splitter_odp_ratio
FROM ftth_items i
LEFT JOIN item_types it ON i.item_type_id = it.id
LEFT JOIN tube_colors tc ON i.tube_color_id = tc.id
LEFT JOIN tube_colors cc ON i.core_color_id = cc.id
LEFT JOIN splitter_types sm ON i.splitter_main_id = sm.id
LEFT JOIN splitter_types so ON i.splitter_odp_id = so.id
```
---
### 🎯 **CORE SYNCHRONIZATION FEATURES:**
#### **1. Auto Core Calculation**
- **Function**: `calculateCoreAvailable()`
- **Trigger**: Real-time pada perubahan capacity/usage
- **Display**: Core tersedia dengan color indicator
- 🟢 **Success**: > 50% available
- 🟡 **Warning**: 20-50% available
- 🔴 **Danger**: < 20% available
#### **2. Route-Item Core Sync**
- **Function**: `syncCoreUsageFromRoutes(itemId)`
- **Description**: Sinkronisasi core usage dari semua routing yang connected
- **Auto-trigger**: Saat edit item
- **Logic**: Sum semua `core_count` dari routes yang memiliki `from_item_id` atau `to_item_id` = itemId
#### **3. Enhanced Edit Functions**
- **Function**: `editItemEnhanced(itemId)`
- **Feature**: Auto-sync core usage setelah load item data
- **Timing**: 500ms delay untuk ensure form loaded
---
### 🎨 **UI/UX IMPROVEMENTS:**
#### **Enhanced Form Layout:**
```html
Row 1: [Warna Tube] [Warna Core]
Row 2: [Jenis Kabel] [Kapasitas Core Total]
Row 3: [Core Digunakan] [Core Tersedia (readonly)]
```
#### **Visual Indicators:**
- **Color Preview**: Border-left colored pada dropdown options
- **Badge System**: Color-coded untuk cable types
- **Real-time Feedback**: Core available calculation
#### **Cable Type Badges:**
- 🔴 **Backbone** (danger)
- 🔵 **Distribution** (primary)
- 🟢 **Drop Core** (success)
- 🔷 **Feeder** (info)
- 🟡 **Branch** (warning)
#### **Core Usage Badges:**
- 🟢 **0-49% used** (success)
- 🔷 **50-69% used** (info)
- 🟡 **70-89% used** (warning)
- 🔴 **90-100% used** (danger)
---
### 🚀 **NEW JAVASCRIPT FUNCTIONS:**
#### **Core Management:**
```javascript
calculateCoreAvailable() // Real-time core calculation
syncCoreUsageFromRoutes(itemId) // Sync dari routing data
editItemEnhanced(itemId) // Enhanced edit dengan auto-sync
```
#### **Display Helpers:**
```javascript
getCableTypeBadge(cableType) // Bootstrap badge class
getCableTypeText(cableType) // Human readable text
getCoreUsageBadge(used, total) // Usage percentage badge
```
#### **Form Enhancements:**
- Auto-populate dropdown warna core (sama dengan tube colors)
- Real-time listeners untuk capacity & usage changes
- Form validation untuk field baru
---
### 📊 **USAGE EXAMPLES:**
#### **Create Item dengan Field Baru:**
```javascript
// Form akan include:
{
item_type: "1",
name: "OLT Jakarta",
tube_color_id: "1", // Warna Tube: Biru
core_color_id: "15", // Warna Core: Magenta
item_cable_type: "backbone", // Jenis: Backbone
total_core_capacity: "144", // Kapasitas: 144 Core
core_used: "48", // Digunakan: 48 Core
// core_available = 96 Core (auto-calculated)
}
```
#### **Core Sync Example:**
```javascript
// Item ID 1 connected ke:
// Route 1: from_item_id=1, core_count=24
// Route 2: to_item_id=1, core_count=12
// Total core_used = 24 + 12 = 36 Core (auto-synced)
```
---
### 🔍 **TESTING & VALIDATION:**
#### **✅ Tested Features:**
1. Database field creation & constraints ✅
2. API CRUD operations dengan field baru ✅
3. Form validation & submission ✅
4. Core calculation & synchronization ✅
5. UI display & color indicators ✅
6. Foreign key relationships ✅
#### **🧹 Cleanup:**
- Temporary test files deleted
- Database indexes optimized
- Code documented & organize

176
KMZ_Export_Fix_Guide.md Normal file
View File

@ -0,0 +1,176 @@
# 🔧 Fix: Error Export KMZ - "toFixed is not a function"
## ❌ **Error Yang Diperbaiki**
```
Error menggenerate KMZ: item.latitude.toFixed is not a function
```
## 🔍 **Root Cause**
Error terjadi karena data `latitude` dan `longitude` dari database berupa **string**, bukan **number**. JavaScript method `toFixed()` hanya dapat digunakan pada tipe data number.
---
## ✅ **Solusi Yang Diimplementasikan**
### 1. **Parse String ke Number**
```javascript
// SEBELUM (Error):
item.latitude.toFixed(6)
item.longitude.toFixed(6)
// SESUDAH (Fixed):
parseFloat(item.latitude).toFixed(6)
parseFloat(item.longitude).toFixed(6)
```
### 2. **Validation & Error Handling**
```javascript
// Validate coordinates
let lat = parseFloat(item.latitude);
let lng = parseFloat(item.longitude);
if (isNaN(lat) || isNaN(lng)) {
console.warn('Invalid coordinates for item:', item.name);
return; // Skip this item
}
```
### 3. **Safe Coordinate Display**
```javascript
// Handle coordinates safely
let coordText = (isNaN(lat) || isNaN(lng)) ?
'Koordinat tidak valid' :
`${lat.toFixed(6)}, ${lng.toFixed(6)}`;
```
---
## 📁 **File Yang Diperbaiki**
### 1. **assets/js/kmz-export.js**
- ✅ Fixed `generateItemPlacemarks()` - coordinates untuk KML
- ✅ Fixed `generateItemDescription()` - display koordinat
- ✅ Added validation untuk item dengan koordinat tidak valid
- ✅ Added filter untuk skip item dengan koordinat invalid
### 2. **assets/js/app.js**
- ✅ Fixed `generateItemListHtml()` - display koordinat di tabel
- ✅ Added same validation untuk konsistensi
---
## 🧪 **Testing Results**
### ✅ **Scenario Yang Sekarang Bekerja:**
1. **Data Normal**: Item dengan koordinat valid
```
latitude: "-6.208800", longitude: "106.845600"
✅ Berhasil export KMZ
```
2. **Data String**: Koordinat sebagai string dari database
```
parseFloat("-6.208800") → -6.2088 ✅
parseFloat("106.845600") → 106.8456 ✅
```
3. **Data Invalid**: Koordinat null/undefined/kosong
```
parseFloat(null) → NaN
isNaN(NaN) → true
✅ Item dilewati dengan warning
```
4. **Mixed Data**: Beberapa item valid, beberapa tidak
```
✅ Item valid: Diekspor ke KMZ
⚠️ Item invalid: Dilewati dengan notifikasi
```
---
## 📊 **Behavior Baru**
### **Pre-Export Validation:**
- ✅ Cek apakah ada data untuk diekspor
- ✅ Filter item dengan koordinat valid
- ✅ Warning untuk item yang dilewati
- ✅ Prevent export jika tidak ada data valid
### **Export Process:**
- ✅ Hanya export item dengan koordinat valid
- ✅ Skip item dengan koordinat invalid
- ✅ Log warning untuk debugging
- ✅ Notifikasi hasil export
### **User Feedback:**
```
✅ "KMZ file berhasil diunduh: FTTH_Planner_Export_2024-01-15.kmz"
⚠️ "2 item dilewati karena koordinat tidak valid"
❌ "Tidak ada item dengan koordinat yang valid untuk diekspor"
```
---
## 🎯 **Prevention untuk Future**
### **Database Level:**
- Pastikan kolom `latitude` dan `longitude` bertipe **DECIMAL**
- Validation saat insert/update data koordinat
### **API Level:**
- Convert data ke number sebelum return JSON
- Validation koordinat format
### **Frontend Level:**
- Validation input koordinat saat save item
- Format number dengan proper parsing
---
## 🚀 **Testing Checklist**
### **Test Export KMZ:**
1. ✅ **Buat beberapa item** dengan koordinat valid
2. ✅ **Klik "Export KMZ"**
3. ✅ **File terdownload** tanpa error
4. ✅ **Buka di Google Earth** - item muncul dengan benar
### **Test Edge Cases:**
1. ✅ **Export tanpa data** → Warning "Tidak ada data"
2. ✅ **Export dengan koordinat invalid** → Warning "X item dilewati"
3. ✅ **Semua koordinat invalid** → Error "Tidak ada item valid"
4. ✅ **Mixed valid/invalid** → Export item valid + warning
### **Test Koordinat Display:**
1. ✅ **Daftar Item** → Koordinat tampil dengan benar
2. ✅ **Item invalid** → "Koordinat tidak valid"
3. ✅ **KMZ Description** → Format koordinat benar
---
## 📝 **Notes**
### **Why parseFloat()?**
- Converts string to number safely
- Returns NaN for invalid input (dapat dideteksi)
- Lebih reliable dari parseInt() untuk decimal
### **Why toFixed(6)?**
- 6 decimal places = ±1 meter accuracy
- Standard untuk GPS coordinates
- Consistent dengan format Google Earth
### **Why Skip Invalid Items?**
- Prevent KMZ corruption
- Better user experience
- Clear feedback untuk data issues
---
**Error "toFixed is not a function" sudah tidak akan muncul lagi!** ✅
Export KMZ sekarang robust dan dapat menangani berbagai kondisi data dengan proper validation dan user feedback.

186
KMZ_Export_Guide.md Normal file
View File

@ -0,0 +1,186 @@
# 🌍 Panduan Export KMZ - FTTH Planner
## 📋 Tentang Fitur Export KMZ
Fitur Export KMZ memungkinkan Anda mengekspor semua data infrastruktur FTTH dari aplikasi web ke format KMZ yang dapat dibuka di Google Earth atau aplikasi GIS lainnya.
## 🎯 Apa yang Diekspor?
### 📍 Item FTTH
- **OLT** - Marker merah besar
- **Tiang Tumpu** - Marker hijau
- **ODP** - Marker biru
- **ODC** - Marker hijau kotak
- **Pelanggan** - Marker orange kecil
### 🛣️ Routing Kabel
- **Terpasang** - Garis hijau tebal
- **Perencanaan** - Garis kuning
- **Maintenance** - Garis merah
### 📊 Data yang Disertakan
Setiap item menyertakan informasi lengkap:
- Nama dan jenis item
- Deskripsi dan alamat
- Koordinat GPS
- Warna tube dan core yang digunakan
- Jenis splitter (utama dan ODP)
- Status item
## 🚀 Cara Menggunakan
### 1. Export dari Aplikasi
```
Dashboard → Tombol "Export KMZ" (hijau)
atau
Sidebar → Export Data → Export ke KMZ
```
### 2. File yang Dihasilkan
- **Nama File**: `FTTH_Planner_Export_YYYY-MM-DD-HHMMSS.kmz`
- **Format**: KMZ (compressed KML)
- **Ukuran**: Tergantung jumlah data (biasanya <1MB)
### 3. Membuka di Google Earth
#### Google Earth Pro (Desktop)
1. **Buka Google Earth Pro**
2. **File → Open** atau drag & drop file KMZ
3. **Zoom ke area**: Data akan muncul di layer panel
4. **Klik item**: Lihat detail di popup
#### Google Earth Web
1. **Buka** [earth.google.com](https://earth.google.com)
2. **Menu → Projects → Import KML file**
3. **Upload file KMZ**
4. **Explore data** di layer panel
### 4. Aplikasi Lain yang Mendukung
- **QGIS** (Open Source GIS)
- **ArcGIS** (Professional GIS)
- **Google Maps MyMaps**
- **Avenza Maps** (Mobile)
- **GPS Essentials** (Android)
## 📱 Penggunaan di Mobile
### Android
1. **Download file KMZ** dari email/cloud
2. **Buka dengan Google Earth**
3. **Atau import ke aplikasi GPS/mapping**
### iPhone
1. **Download file KMZ**
2. **Open with → Google Earth**
3. **Atau share ke aplikasi mapping lain**
## 🔧 Troubleshooting
### File KMZ Tidak Bisa Dibuka
**Penyebab**:
- Browser memblokir download
- Ekstensi file tidak benar
- File corrupt saat download
**Solusi**:
1. **Coba browser lain** (Chrome, Firefox, Edge)
2. **Disable popup blocker** untuk domain aplikasi
3. **Download ulang** file KMZ
4. **Rename extension** dari .zip ke .kmz jika perlu
### Data Tidak Muncul di Google Earth
**Penyebab**:
- Koordinat di luar area view
- Layer tidak aktif
- Zoom level terlalu tinggi
**Solusi**:
1. **Cek layer panel** - pastikan data visible
2. **Double-click layer** untuk zoom ke data
3. **Reset view** dan cari area Indonesia
4. **Zoom out** untuk lihat overview
### Performance Issue
**Jika data banyak** (>1000 items):
1. **Filter data** sebelum export
2. **Export per area** tertentu
3. **Gunakan desktop app** bukan web browser
## 📊 Format Data KML
File KMZ berisi struktur KML sebagai berikut:
```xml
<kml>
<Document>
<name>FTTH Planner Export</name>
<!-- Styles untuk setiap jenis item -->
<Style id="olt-style">...</Style>
<!-- Placemark untuk setiap item -->
<Placemark>
<name>OLT Jakarta Selatan</name>
<description>Detail lengkap item...</description>
<Point>
<coordinates>106.8456,-6.2088,0</coordinates>
</Point>
</Placemark>
<!-- LineString untuk routing -->
<Placemark>
<name>Route: OLT → ODP</name>
<LineString>
<coordinates>106.8456,-6.2088,0 106.8500,-6.2100,0</coordinates>
</LineString>
</Placemark>
</Document>
</kml>
```
## 🎓 Tips & Best Practices
### Export Data Berkualitas
1. **Lengkapi semua field** (alamat, deskripsi, dll)
2. **Gunakan naming convention** yang konsisten
3. **Update status** item secara berkala
4. **Verifikasi koordinat** sebelum export
### Sharing Data
1. **Compress ulang** jika file besar (WinRAR/7zip)
2. **Upload ke cloud** untuk sharing tim
3. **Dokumentasikan versi** dan tanggal export
4. **Backup file KMZ** secara berkala
### Integrasi dengan Workflow
1. **Export mingguan** untuk monitoring
2. **Combine dengan data survey** lapangan
3. **Import ke GIS** untuk analisis lanjutan
4. **Share dengan contractor** untuk implementasi
## ⚡ Features Advanced
### Custom Styling
File KMZ menggunakan icon standar Google Earth, tapi Anda bisa:
1. **Edit file KMZ** (extract → edit KML → recompress)
2. **Custom icon URLs** di KML
3. **Advanced styling** dengan KML extensions
### Data Integration
1. **Merge dengan KMZ lain** di Google Earth
2. **Overlay dengan satellite imagery**
3. **Analysis dengan terrain data**
4. **Export ke format lain** (GPX, Shapefile)
## 📞 Support
Jika mengalami masalah dengan export KMZ:
1. **Cek browser console** (F12) untuk error
2. **Test dengan data minimal** (1-2 items)
3. **Verify file download** tidak corrupt
4. **Contact developer** dengan screenshot error
---
**Catatan**: Format KMZ adalah standard internasional untuk data geospasial, sehingga file yang dihasilkan dapat digunakan di berbagai platform dan aplikasi GIS profesional. 🌍

179
KMZ_Export_Summary.md Normal file
View File

@ -0,0 +1,179 @@
# 📦 Summary: Fitur Export KMZ
## ✅ Fitur Yang Ditambahkan
### 🎯 **Export KMZ Lengkap**
- ✅ Export semua items FTTH (OLT, Tiang, ODP, ODC, Pelanggan)
- ✅ Export semua routes dengan koordinat lengkap
- ✅ File KMZ kompatibel dengan Google Earth
- ✅ Styling yang sesuai untuk setiap jenis item
### 🔘 **Interface Baru**
- ✅ Tombol "Export KMZ" di header peta
- ✅ Menu "Export ke KMZ" di sidebar
- ✅ Notifikasi progress dan status download
### 📊 **Data Yang Diekspor**
- ✅ Semua informasi item (nama, deskripsi, alamat, koordinat)
- ✅ Technical data (warna tube, core, splitter)
- ✅ Status item (aktif, maintenance, dll)
- ✅ Route cables dengan jarak dan spesifikasi
---
## 📁 File Yang Dibuat/Dimodifikasi
### File Baru:
- `assets/js/kmz-export.js` - Logic export KMZ
- `KMZ_Export_Guide.md` - Panduan lengkap penggunaan
- `KMZ_Export_Summary.md` - Summary ini
### File Dimodifikasi:
- `index.php` - Tambah tombol & library
- `README.md` - Update dokumentasi
### Library Ditambahkan:
- JSZip - Untuk compress KML ke KMZ
- FileSaver.js - Untuk download file
---
## 🚀 Cara Menggunakan
### Quick Start:
1. **Buka aplikasi FTTH Planner**
2. **Tambah beberapa items** (OLT, Pelanggan, dll)
3. **Buat routes** antar items
4. **Klik "Export KMZ"** (tombol hijau)
5. **File otomatis terdownload** dengan nama timestamp
6. **Buka di Google Earth** atau aplikasi GIS
### Format File:
```
FTTH_Planner_Export_2024-01-15-143027.kmz
```
---
## 🌍 Kompatibilitas
### ✅ Aplikasi Yang Mendukung:
- **Google Earth Pro** (Desktop)
- **Google Earth Web** (Browser)
- **QGIS** (Open Source GIS)
- **ArcGIS** (Professional)
- **Avenza Maps** (Mobile)
- **GPS Essentials** (Android)
### 📱 Platform:
- **Windows** - Google Earth Pro
- **macOS** - Google Earth Pro
- **Linux** - QGIS, Google Earth Web
- **Android** - Google Earth, GPS apps
- **iOS** - Google Earth
---
## 🎨 Styling di Google Earth
### Items:
- 🔴 **OLT**: Marker merah besar
- 🟢 **Tiang Tumpu**: Marker hijau
- 🔵 **ODP**: Marker biru
- 🟩 **ODC**: Marker hijau kotak
- 🟠 **Pelanggan**: Marker orange kecil
### Routes:
- **Hijau solid**: Route terpasang
- **Kuning**: Route perencanaan
- **Merah**: Route maintenance
---
## 🔧 Technical Details
### KML Structure:
```xml
<kml>
<Document>
<name>FTTH Planner Export</name>
<Style id="olt-style">...</Style>
<Placemark>
<name>Item Name</name>
<description>Detailed info</description>
<Point><coordinates>...</coordinates></Point>
</Placemark>
<Placemark>
<name>Route</name>
<LineString><coordinates>...</coordinates></LineString>
</Placemark>
</Document>
</kml>
```
### JavaScript Functions:
- `exportToKMZ()` - Main export function
- `generateKML()` - Create KML content
- `createKMZFile()` - Compress and download
---
## 🐛 Error Handling
### Automatic Fallbacks:
- ✅ Check JSZip library availability
- ✅ Validate data before export
- ✅ Handle empty datasets gracefully
- ✅ Show progress notifications
- ✅ Catch download errors
### Browser Compatibility:
- ✅ Chrome, Firefox, Edge, Safari
- ✅ Mobile browsers
- ✅ Popup blocker handling
---
## 📈 Benefits
### For Planners:
- **Offline viewing** in Google Earth
- **3D visualization** of infrastructure
- **Share with stakeholders** easily
- **Mobile field reference**
### For Teams:
- **Standard format** across industry
- **Professional presentation**
- **Integration with GIS workflows**
- **Archive planning data**
### For Field Work:
- **GPS coordinates** for navigation
- **Detailed item info** on-site
- **Route planning** visualization
- **Offline access** to data
---
## 🎯 Next Enhancements (Future)
### Possible Improvements:
- 📍 Custom icon URLs in KMZ
- 📊 Layer grouping by item type
- 🎨 Advanced styling options
- 📱 Direct mobile export
- 🔄 Import KMZ back to app
- 📋 Export filtered data only
### Integration Options:
- 🌐 Google MyMaps integration
- 📊 Export to GeoJSON format
- 🗺️ Export to Shapefile
- 📱 QR code for mobile sharing
---
**Fitur Export KMZ siap digunakan!** 🎉
Semua data infrastruktur FTTH sekarang dapat diekspor ke format standar industri dan dibagikan dengan tim, contractor, atau stakeholder menggunakan Google Earth dan aplikasi GIS profesional.

229
README.md Normal file
View File

@ -0,0 +1,229 @@
# FTTH Planner
Aplikasi perencanaan infrastruktur FTTH (Fiber to the Home) berbasis web menggunakan AdminLTE, PHP, MySQL, dan OpenStreetMaps.
## Fitur Utama
- **Manajemen Item FTTH**: OLT, Tiang Tumpu, ODP, ODC
- **Peta Interaktif**: Visualisasi infrastruktur menggunakan OpenStreetMaps
- **Drag & Drop**: Pindahkan item langsung di peta
- **Routing Kabel**: Buat rute kabel mengikuti jalan
- **Form Dinamis**: Tambah/edit item dengan form modal
- **Dashboard Statistik**: Monitoring jumlah item dan route
- **Responsive Design**: Tampilan optimal di desktop dan mobile
- **Export KMZ**: Export data ke Google Earth format
## Sistem Requirement
- **XAMPP** (Apache, MySQL, PHP 7.4+)
- **Web Browser** modern dengan JavaScript enabled
- **Koneksi Internet** untuk loading maps dan library
## Instalasi
### 1. Setup XAMPP
1. Download dan install XAMPP dari [https://www.apachefriends.org/](https://www.apachefriends.org/)
2. Start Apache dan MySQL di XAMPP Control Panel
### 2. Setup Database
1. Buka phpMyAdmin di browser: `http://localhost/phpmyadmin`
2. Import file database: `database.sql`
3. Atau jalankan SQL script secara manual untuk membuat database dan tabel
### 3. Setup Aplikasi
1. Copy folder aplikasi ke `C:\xampp\htdocs\ftthplanner`
2. Pastikan struktur folder seperti ini:
```
C:\xampp\htdocs\ftthplanner\
├── index.php
├── config/
│ └── database.php
├── api/
│ ├── items.php
│ ├── routes.php
│ ├── tube_colors.php
│ ├── splitters.php
│ └── statistics.php
├── assets/
│ ├── css/
│ │ └── custom.css
│ └── js/
│ ├── map.js
│ ├── app.js
│ └── kmz-export.js
├── database.sql
├── README.md
├── KMZ_Export_Guide.md
└── testing_guide.md
```
### 4. Konfigurasi Database
Edit file `config/database.php` sesuai dengan setting MySQL Anda:
```php
private $host = "localhost";
private $db_name = "ftth_planner";
private $username = "root";
private $password = "";
```
### 5. Akses Aplikasi
Buka browser dan akses: `http://localhost/ftthplanner`
## Cara Penggunaan
### Menambah Item FTTH
1. **Via Sidebar**: Klik menu "Tambah OLT", "Tambah Tiang Tumpu", dll
2. **Via Map**: Klik langsung di peta untuk menambah item di lokasi tersebut
3. **Via Tombol**: Klik tombol "Tambah Item" di header card map
### Mengedit Item
1. Klik marker di peta
2. Klik tombol "Edit" di popup
3. Update informasi di form modal
4. Klik "Simpan"
### Memindahkan Item
- Drag & drop marker langsung di peta
- Posisi akan otomatis tersimpan
### Membuat Route Kabel
1. **Mode Routing**: Klik tombol "Mode Routing" atau "Route" di popup item
2. **Pilih Tujuan**: Klik item tujuan untuk membuat route
3. **Auto Route**: Sistem akan membuat route mengikuti jalan
### Melihat Daftar Item/Route
- Klik menu "Daftar Item" untuk melihat semua item dalam tabel
- Klik menu "Routing Kabel" untuk melihat semua route
### Export ke Google Earth
1. **Klik tombol "Export KMZ"** di header peta atau sidebar
2. **File KMZ otomatis terdownload** dengan timestamp
3. **Buka di Google Earth** atau aplikasi GIS lainnya
4. **Lihat detail lengkap** setiap item dan route
## Struktur Database
### Tabel Utama
- `ftth_items`: Data item infrastruktur FTTH
- `cable_routes`: Data routing kabel antar item
- `item_types`: Jenis-jenis item (OLT, Tiang, ODP, ODC)
- `tube_colors`: Master data warna tube
- `splitter_types`: Master data jenis splitter
### Relasi
- Item memiliki relasi ke item_types, tube_colors, dan splitter_types
- Route menghubungkan dua item (from_item_id dan to_item_id)
## Teknologi yang Digunakan
### Frontend
- **AdminLTE 3.2**: Framework dashboard admin
- **Leaflet.js**: Library peta interaktif
- **OpenStreetMaps**: Data peta
- **Leaflet Routing Machine**: Routing jalan
- **Bootstrap 4**: CSS framework
- **jQuery**: JavaScript library
- **Font Awesome**: Icon library
- **JSZip**: Compression untuk KMZ export
- **FileSaver.js**: Download file functionality
### Backend
- **PHP 7.4+**: Server-side scripting
- **MySQL**: Database
- **PDO**: Database abstraction layer
### Maps & Routing
- **OpenStreetMaps**: Tile server peta
- **Leaflet**: Map rendering engine
- **OSRM**: Routing service
## Fitur Lanjutan
### Drag & Drop Items
- Semua marker dapat di-drag ke posisi baru
- Update koordinat otomatis tersimpan ke database
### Interactive Popup
- Info lengkap item saat klik marker
- Aksi edit, route, dan hapus dalam popup
### Route Visualization
- Route terpasang: garis solid hijau
- Route perencanaan: garis putus-putus kuning
- Route maintenance: garis putus-putus merah
### Responsive Design
- Optimized untuk desktop dan mobile
- Map dan form menyesuaikan ukuran layar
### Export KMZ
- Export semua data items dan routes ke format Google Earth
- File KMZ dengan styling yang sesuai untuk setiap jenis item
- Informasi lengkap dalam popup Google Earth
- Kompatibel dengan aplikasi GIS profesional
## Troubleshooting
### Error Database Connection
- Pastikan MySQL berjalan di XAMPP
- Cek konfigurasi di `config/database.php`
- Pastikan database `ftth_planner` sudah dibuat
### Map Tidak Muncul
- Cek koneksi internet
- Pastikan JavaScript enabled di browser
- Lihat console browser untuk error
### Item Tidak Tersimpan
- Cek permission folder aplikasi
- Pastikan semua field required sudah diisi
- Lihat Network tab di browser untuk error API
### Route Tidak Terbuat
- Pastikan kedua item sudah ada di database
- Cek koneksi internet untuk routing service
- Mode routing harus aktif sebelum memilih item
## Pengembangan Lebih Lanjut
### Fitur yang Bisa Ditambahkan
1. **Import/Export Data**: Import data dari Excel/CSV
2. **Laporan**: Generate laporan PDF/Excel
3. **User Management**: Login dan role-based access
4. **Backup/Restore**: Backup data dan konfigurasi
5. **Mobile App**: Native app untuk survey lapangan
6. **Integration**: Integrasi dengan sistem lain via API
### Customization
- Ubah warna marker di `assets/css/custom.css`
- Tambah jenis item baru di database `item_types`
- Modifikasi form field di `index.php`
- Tambah validasi di file API
## Lisensi
Aplikasi ini dibuat untuk keperluan pembelajaran dan dapat digunakan secara bebas.
## Kontak
Untuk pertanyaan dan support, silakan hubungi developer.
---
**Catatan**: Pastikan selalu backup database sebelum melakukan perubahan major pada aplikasi.

117
api/ambil_dataOLT.php Normal file
View File

@ -0,0 +1,117 @@
<?php
require_once __DIR__ . '/../config/database.php';
mysqli_report(MYSQLI_REPORT_ERROR | MYSQLI_REPORT_STRICT);
$db = new Database();
$conn = $db->getConnection();
// ================= CONFIG =================
$login_url = "http://10.255.254.21/login";
$data_url = "http://10.255.254.21/ontinfo_table";
$username = "root";
$password = "@lokal234";
$cookie = __DIR__ . "/cookie.txt";
// ================= LOGIN =================
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $login_url);
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query([
"username" => $username,
"password" => $password
]));
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_COOKIEJAR, $cookie);
curl_setopt($ch, CURLOPT_COOKIEFILE, $cookie);
curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true);
curl_exec($ch);
curl_close($ch);
// ================= AMBIL DATA =================
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $data_url);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_COOKIEFILE, $cookie);
curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true);
$response = curl_exec($ch);
if ($response === false) {
die("CURL ERROR: " . curl_error($ch));
}
curl_close($ch);
file_put_contents("debug_onu.json", $response);
$data = json_decode($response, true);
if (!isset($data['data']) || empty($data['data'])) {
die("DATA KOSONG");
}
// ================= PROCESS =================
foreach ($data['data'] as $onu) {
$sn = $onu['ont_sn'] ?? '';
$rx = $onu['receive_power'] ?? 0;
$state = $onu['state'] ?? 0;
$cause = strtolower($onu['last_d_cause'] ?? '');
if (empty($sn)) continue;
// ================= STATUS =================
if (strpos($cause, 'laser out') !== false || strpos($cause, 'fiber') !== false) {
$status = "LASER OUT";
} elseif (strpos($cause, 'power down') !== false || strpos($cause, 'dying-gasp') !== false) {
$status = "POWER FAIL";
} else {
if ($state == 0 && $rx <= -30) {
$status = "LASER OUT";
} elseif ($rx <= -30) {
$status = "POWER FAIL";
} else {
$status = "ONLINE";
}
}
// ================= GET LAST STATUS =================
$cek = $conn->prepare("SELECT status FROM onu_status WHERE sn = ?");
$cek->execute([$sn]);
$last = $cek->fetch(PDO::FETCH_ASSOC);
$lastStatus = $last['status'] ?? null;
// ================= UPSERT REALTIME =================
$conn->query("
INSERT INTO onu_status (sn, status, rx_power, last_update)
VALUES ('$sn', '$status', '$rx', NOW())
ON DUPLICATE KEY UPDATE
last_status = status,
status = '$status',
rx_power = '$rx',
last_update = NOW()
");
// ================= LOG HISTORI (HANYA ANOMALI) =================
$anomali = in_array($status, ['LASER OUT', 'POWER FAIL']);
$trigger = ($lastStatus !== $status);
if ($anomali && $trigger) {
$stmt = $conn->prepare("
INSERT INTO status_onu_histori (sn, status, rx_power, created_at)
VALUES (?, ?, ?, NOW())
");
$stmt->execute([$sn, $status, $rx]);
echo "LOG ANOMALI: $sn | $status | RX $rx<br>";
}
echo "SN: $sn | STATUS: $status | RX: $rx<br>";
}
echo "DONE ✅";
?>

369
api/auto_notif_device.php Normal file
View File

@ -0,0 +1,369 @@
<?php
require_once __DIR__ . '/../config/database.php';
set_time_limit(300);
$db = new Database();
$conn = $db->getConnection();
// 🔥 GROUP TEKNISI
$groupTeknisi = "120363427232707228@g.us";
// ======================
// 🔥 FUNCTION WA
// ======================
if (!function_exists('kirimWA')) {
function kirimWA($target, $pesan) {
if (!$target) return;
$token = "XjC9oSr4jECWG93JQRWf";
$data = [
"target" => $target,
"message" => $pesan,
];
$curl = curl_init();
curl_setopt_array($curl, [
CURLOPT_URL => "https://api.fonnte.com/send",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_POST => true,
CURLOPT_POSTFIELDS => http_build_query($data),
CURLOPT_HTTPHEADER => [
"Authorization: $token"
],
CURLOPT_TIMEOUT => 10
]);
curl_exec($curl);
curl_close($curl);
}
}
// ======================
// 🔥 TEST MODE (OPSIONAL)
// ======================
$TEST_MODE = false;
// ======================
// 🔥 AMBIL DATA DARI DATABASE (JOIN HISTORY)
// ======================
$stmt = $conn->query("
SELECT
f.id,
f.name,
f.sn_onu,
f.latitude,
f.longitude,
f.customer_phone,
o.rx_power,
o.voltage,
o.updated_at
FROM ftth_items f
LEFT JOIN (
SELECT h1.*
FROM onu_daily_history h1
INNER JOIN (
SELECT sn, MAX(updated_at) AS max_time
FROM onu_daily_history
WHERE DATE(updated_at) = CURDATE()
GROUP BY sn
) h2
ON h1.sn = h2.sn
AND h1.updated_at = h2.max_time
) o
ON o.sn = f.sn_onu
INNER JOIN item_types it
ON it.id = f.item_type_id
WHERE f.sn_onu IS NOT NULL
AND LOWER(it.name) = 'pelanggan'
");
$dataItems = $stmt->fetchAll(PDO::FETCH_ASSOC);
foreach ($dataItems as $item) {
usleep(200000);
$sn = strtoupper(trim($item['sn_onu']));
$nama = $item['name'];
$lat = $item['latitude'];
$lng = $item['longitude'];
$phone = $item['customer_phone'];
// ======================
// 🔥 AMBIL RX & VOLT DARI DB
// ======================
$rx = isset($item['rx_power']) ? floatval($item['rx_power']) : null;
$volt = isset($item['voltage']) ? floatval($item['voltage']) : null;
if ($rx === null && $volt === null) {
continue;
}
// ======================
// 🔥 STATUS DETEKSI
// ======================
// RX NORMAL = -20 s/d -25.99 dBm
$isRxHighLoss = ($rx !== null && $rx < -25.99); // redaman terlalu besar
$isRxLowLoss = ($rx !== null && $rx > -20.00); // redaman terlalu kecil
$isBadRx = $isRxHighLoss || $isRxLowLoss;
// VOLTAGE NORMAL = 3.20 - 3.29 V
$isVoltLow = ($volt !== null && $volt < 3.20);
$isVoltHigh = ($volt !== null && $volt > 3.29);
$isBadVolt = $isVoltLow || $isVoltHigh;
// ======================
// 🔥 KETERANGAN ANALISA
// ======================
$rxKeterangan = '';
if ($isRxHighLoss) {
$rxKeterangan =
"Redaman terlalu besar (High Loss). Sinyal optik berada di bawah standar perusahaan. Kemungkinan terjadi redaman berlebih pada jalur fiber, sambungan, konektor, splitter, atau kabel mengalami kerusakan.";
} elseif ($isRxLowLoss) {
$rxKeterangan =
"Redaman terlalu kecil (Low Loss). Nilai sinyal optik berada di atas standar perusahaan. Diperlukan pengecekan kualitas distribusi jaringan dan konfigurasi perangkat.";
}
$voltKeterangan = '';
if ($isVoltLow) {
$voltKeterangan =
"Tegangan ONU berada di bawah standar 3.20 - 3.29 Volt. Kemungkinan adaptor lemah, kabel power rusak, atau suplai listrik tidak stabil.";
} elseif ($isVoltHigh) {
$voltKeterangan =
"Tegangan ONU berada di atas standar 3.20 - 3.29 Volt. Diperlukan pengecekan adaptor dan sumber daya listrik.";
}
// ======================
// 🔥 CEK ANTI SPAM LOG
// ======================
$cek = $conn->prepare("
SELECT last_rx, last_voltage
FROM notif_device_log
WHERE sn = ?
");
$cek->execute([$sn]);
$last = $cek->fetch(PDO::FETCH_ASSOC);
$lastRx = $last['last_rx'] ?? null;
$lastVolt = $last['last_voltage'] ?? null;
$isChanged =
round((float)$lastRx, 2) != round((float)$rx, 2) ||
round((float)$lastVolt, 2) != round((float)$volt, 2);
if (!$isChanged) {
continue;
}
// ======================
// 🔥 MAPS
// ======================
$maps = ($lat && $lng)
? "https://www.google.com/maps?q={$lat},{$lng}"
: "-";
// ======================
// 🔥 FORMAT NOMOR
// ======================
$phoneFix = null;
if ($phone) {
$phoneFix = preg_replace('/[^0-9]/', '', $phone);
if (substr($phoneFix, 0, 1) == '0') {
$phoneFix = '62' . substr($phoneFix, 1);
}
}
//$isBadVolt = ($volt !== null && $volt < 3.29);
//$isVoltLow = ($volt !== null && $volt < 3.20);
//$isVoltHigh = ($volt !== null && $volt > 3.29);
//$isBadVolt = $isVoltLow || $isVoltHigh;
// ======================
// 🔥 NOTIFIKASI
// ======================
if ($isBadRx && $isBadVolt) {
// ======================
// PELANGGAN
// ======================
kirimWA(
$phoneFix,
"🚨 *Pemberitahuan Gangguan Layanan Internet*\n\n".
"Yth. {$nama},\n\n".
"Sistem monitoring kami mendeteksi kondisi jaringan dan perangkat internet di lokasi Anda berada di luar standar operasional.\n\n".
"📉 Kualitas Sinyal : ".number_format($rx,2)." dBm\n".
"🔌 Tegangan ONU : ".number_format($volt,2)." V\n\n".
"Gangguan ini berpotensi menyebabkan internet lambat, putus-putus, atau tidak dapat digunakan.\n\n".
"Tim teknis kami telah menerima laporan otomatis dan akan melakukan pengecekan lebih lanjut.\n\n".
"Mohon maaf atas ketidaknyamanan ini.\n\n".
"Terima kasih.\n".
"RockNet NOC"
);
// ======================
// TEKNISI
// ======================
kirimWA(
$groupTeknisi,
"🚨 *ANOMALI RX + VOLTAGE*\n\n".
"👤 Pelanggan : {$nama}\n".
"🔢 SN ONU : {$sn}\n\n".
"📉 RX Power : ".number_format($rx,2)." dBm\n".
"🔌 Voltage : ".number_format($volt,2)." V\n\n".
"📋 Analisa RX :\n".
"{$rxKeterangan}\n\n".
"📋 Analisa Voltage :\n".
"{$voltKeterangan}\n\n".
"📍 Lokasi :\n{$maps}\n\n".
"⚠ PRIORITAS TINGGI\n".
"Perlu pengecekan jalur fiber dan perangkat ONU."
);
}
elseif ($isBadRx) {
kirimWA(
$phoneFix,
"🚨 *Pemberitahuan Gangguan Jaringan*\n\n".
"Yth. {$nama},\n\n".
"Sistem monitoring mendeteksi kualitas sinyal fiber optik di lokasi Anda berada di luar standar operasional.\n\n".
"📉 Nilai Sinyal : ".number_format($rx,2)." dBm\n\n".
"Gangguan ini dapat menyebabkan internet tidak stabil, lambat, atau terputus.\n\n".
"Tim teknis kami telah menerima laporan otomatis dan akan melakukan pengecekan.\n\n".
"Mohon maaf atas ketidaknyamanan ini.\n\n".
"RockNet NOC"
);
kirimWA(
$groupTeknisi,
"🚨 *ANOMALI REDAMAN ONU*\n\n".
"👤 Pelanggan : {$nama}\n".
"🔢 SN ONU : {$sn}\n\n".
"📉 RX Power : ".number_format($rx,2)." dBm\n\n".
"📋 Analisa :\n".
"{$rxKeterangan}\n\n".
"📍 Lokasi :\n{$maps}\n\n".
"🔧 Perlu pengecekan jalur fiber, konektor, splitter dan sambungan."
);
}
elseif ($isBadVolt) {
kirimWA(
$phoneFix,
"⚡ *Pemberitahuan Perangkat Internet*\n\n".
"Yth. {$nama},\n\n".
"Sistem monitoring mendeteksi tegangan perangkat ONU berada di luar standar operasional.\n\n".
"🔌 Tegangan ONU : ".number_format($volt,2)." V\n\n".
"Kondisi ini dapat menyebabkan perangkat restart sendiri, koneksi tidak stabil, atau gangguan layanan internet.\n\n".
"Tim teknis kami telah menerima laporan otomatis dan akan melakukan pengecekan lebih lanjut.\n\n".
"Terima kasih.\n".
"RockNet NOC"
);
kirimWA(
$groupTeknisi,
"⚡ *ANOMALI VOLTAGE ONU*\n\n".
"👤 Pelanggan : {$nama}\n".
"🔢 SN ONU : {$sn}\n\n".
"🔌 Voltage : ".number_format($volt,2)." V\n\n".
"📋 Analisa :\n".
"{$voltKeterangan}\n\n".
"📍 Lokasi :\n{$maps}\n\n".
"🔧 Perlu pengecekan adaptor, kabel power, dan sumber listrik pelanggan."
);
}
// ======================
// 🔥 UPDATE LOG ANTI SPAM
// ======================
$update = $conn->prepare("
INSERT INTO notif_device_log (sn, last_rx, last_voltage, last_sent)
VALUES (?, ?, ?, NOW())
ON DUPLICATE KEY UPDATE
last_rx = VALUES(last_rx),
last_voltage = VALUES(last_voltage),
last_sent = NOW()
");
$update->execute([$sn, $rx, $volt]);
if (!$isChanged) {
echo "⏭ SKIP $sn | Tidak ada perubahan<br>";
continue;
}
echo "✅ SENT $sn | RX:$rx | V:$volt<br>";
}

189
api/auto_notif_odp.php Normal file
View File

@ -0,0 +1,189 @@
<?php
require_once __DIR__ . '/../config/database.php';
$db = new Database();
$conn = $db->getConnection();
if (!function_exists('kirimWA')) {
function kirimWA($pesan) {
$token = "XjC9oSr4jECWG93JQRWf";
$data = [
"target" => "120363407747513164@g.us",
"message" => $pesan,
];
$curl = curl_init();
curl_setopt_array($curl, [
CURLOPT_URL => "https://api.fonnte.com/send",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_POST => true,
CURLOPT_POSTFIELDS => http_build_query($data),
CURLOPT_HTTPHEADER => [
"Authorization: $token"
],
]);
curl_exec($curl);
curl_close($curl);
}
}
// ✅ Ambil semua ODP (KHUSUS USER 22)
$stmt = $conn->query("
SELECT i.id, i.name, i.latitude, i.longitude
FROM ftth_items i
LEFT JOIN item_types it ON i.item_type_id = it.id
WHERE LOWER(it.name) = 'odp'
AND i.user_id = 22
");
$odps = $stmt->fetchAll(PDO::FETCH_ASSOC);
foreach ($odps as $odp) {
// 🔥 ambil status terakhir
$cek = $conn->prepare("
SELECT last_status FROM notif_log
WHERE odp_id = ?
");
$cek->execute([$odp['id']]);
$last = $cek->fetch(PDO::FETCH_ASSOC);
$lastStatus = $last['last_status'] ?? null;
// 🔥 ambil client
$stmtClient = $conn->prepare("
SELECT fi.id, fi.name, fi.serial_number
FROM cable_routes cr
JOIN ftth_items fi
ON (cr.to_item_id = fi.id OR cr.from_item_id = fi.id)
WHERE (cr.from_item_id = :id OR cr.to_item_id = :id)
AND fi.id != :id
");
$stmtClient->bindParam(':id', $odp['id']);
$stmtClient->execute();
$clients = $stmtClient->fetchAll(PDO::FETCH_ASSOC);
$rxValues = [];
$clientBad = [];
foreach ($clients as $c) {
if (empty($c['serial_number'])) continue;
$query = json_encode(["_id" => $c['serial_number']]);
$url = "http://rmtstb.megadataisp.net:7557/devices/?query=" . urlencode($query);
$res = @file_get_contents($url);
if (!$res) continue;
$json = json_decode($res, true);
if (
isset($json[0]['VirtualParameters']['RXPower']['_value']) &&
is_numeric($json[0]['VirtualParameters']['RXPower']['_value'])
) {
$rx = floatval($json[0]['VirtualParameters']['RXPower']['_value']);
$rxValues[] = $rx;
if ($rx < -23) {
$clientBad[] = $c['name'] . " (" . $rx . " dBm)";
}
}
}
// 🔴 skip kalau tidak ada data
if (count($rxValues) == 0) continue;
// 🔥 HITUNG (SAMA PERSIS KAYAK MANUAL)
$total = count($rxValues);
$bad = count($clientBad);
$good = $total - $bad;
$avg = round(array_sum($rxValues) / $total, 2);
$min = min($rxValues);
// 🔥 STATUS (SAMA PERSIS)
$selisih = abs($avg - $min);
$status = "BAIK";
if ($selisih > 1.5) {
$status = "WARNING";
}
if ($avg < -23 || $bad > ($total * 0.3)) {
$status = "WARNING";
}
if ($avg < -25 || $bad > ($total * 0.5)) {
$status = "BURUK";
}
if ($min < -27) {
$status = "KRITIS";
}
// ❌ ANTI SPAM (kalau status sama, skip)
if ($status == $lastStatus) continue;
// 🔥 FORMAT WA (PERSIS KAYAK MANUAL)
$maps = "https://www.google.com/maps?q={$odp['latitude']},{$odp['longitude']}";
// ==============================
// 🚨 KIRIM SAAT BERMASALAH
// ==============================
if ($status != "BAIK") {
$pesan = "🚨 *AUTO LAPORAN ODP*\n\n";
$pesan .= "📍 *ODP:* {$odp['name']}\n";
$pesan .= "📊 *Status:* {$status}\n\n";
$pesan .= "👥 *Total Client:* {$total}\n";
$pesan .= "🟢 Client Baik: {$good}\n";
$pesan .= "🔴 Client Buruk: {$bad}\n\n";
$pesan .= "📶 *AVG RX:* {$avg} dBm\n";
$pesan .= "📉 *MIN RX:* {$min} dBm\n\n";
if (!empty($clientBad)) {
$pesan .= "⚠ *Client Bermasalah:*\n";
$pesan .= implode("\n", array_slice($clientBad, 0, 10));
$pesan .= "\n\n";
}
$pesan .= "📌 Lokasi:\n$maps\n\n";
$pesan .= "🔎 Silakan cek detail di dashboard.";
kirimWA($pesan);
}
// ==============================
// ✅ OPSIONAL: KIRIM SAAT NORMAL
// ==============================
elseif ($status == "BAIK" && $lastStatus != "BAIK") {
$pesan = "✅ *ODP SUDAH NORMAL*\n\n";
$pesan .= "📍 ODP: {$odp['name']}\n";
$pesan .= "Status sekarang: BAIK\n";
$pesan .= "AVG RX: {$avg} dBm\n";
$pesan .= "MIN RX: {$min} dBm\n\n";
$pesan .= "Jaringan sudah stabil 👍";
kirimWA($pesan);
}
// 🔥 UPDATE STATUS TERAKHIR
$update = $conn->prepare("
INSERT INTO notif_log (odp_id, last_status, last_sent)
VALUES (?, ?, NOW())
ON DUPLICATE KEY UPDATE
last_status = VALUES(last_status),
last_sent = NOW()
");
$update->execute([$odp['id'], $status]);
}

144
api/auto_notif_onu-bc.txt Normal file
View File

@ -0,0 +1,144 @@
<?php
require_once __DIR__ . '/../config/database.php';
$db = new Database();
$conn = $db->getConnection();
if (!function_exists('kirimWA')) {
function kirimWA($target, $pesan) {
$token = "XjC9oSr4jECWG93JQRWf";
$data = [
"target" => $target,
"message" => $pesan,
];
$curl = curl_init();
curl_setopt_array($curl, [
CURLOPT_URL => "https://api.fonnte.com/send",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_POST => true,
CURLOPT_POSTFIELDS => http_build_query($data),
CURLOPT_HTTPHEADER => [
"Authorization: $token"
],
]);
curl_exec($curl);
curl_close($curl);
}
}
// 🔥 ID GROUP TEKNISI
$groupTeknisi = "120363427232707228@g.us";
// ambil semua ONU yang punya nomor HP + lokasi pelanggan
$stmt = $conn->query("
SELECT
o.sn,
o.status,
f.name,
f.customer_phone,
f.latitude,
f.longitude
FROM onu_status o
JOIN ftth_items f ON f.sn_onu = o.sn
JOIN item_types it ON f.item_type_id = it.id
WHERE f.customer_phone IS NOT NULL
AND LOWER(it.name) = 'pelanggan'
");
$data = $stmt->fetchAll(PDO::FETCH_ASSOC);
foreach ($data as $row) {
$sn = $row['sn'];
$status = $row['status'];
$nama = $row['name'];
$phone = $row['customer_phone'];
$lat = $row['latitude'];
$lng = $row['longitude'];
// 🔥 buat link maps
$maps = ($lat && $lng)
? "https://www.google.com/maps?q={$lat},{$lng}"
: "-";
// 🔥 cek status terakhir
$cek = $conn->prepare("
SELECT last_status FROM notif_onu_log WHERE sn = ?
");
$cek->execute([$sn]);
$last = $cek->fetch(PDO::FETCH_ASSOC);
$lastStatus = $last['last_status'] ?? null;
// ❌ anti spam
if ($status == $lastStatus) continue;
// ======================
// 🔥 FORMAT PESAN
// ======================
$pesan = "";
if ($status == "LASER OUT") {
$pesan .= "🚨 *GANGGUAN INTERNET*\n\n";
$pesan .= "Pelanggan: {$nama}\n\n";
$pesan .= "Terjadi gangguan pada kabel fiber (FO CUT).\n";
$pesan .= "kami akan informasikan kepada tim teknis untuk segera ke lokasi kakak.\n\n";
$pesan .= "estimasi perbaikan 4 Jam pengerjaan.\n\n";
$pesan .= "Mohon ditunggu 🙏";
// ======================
// 🔥 NOTIF KE TEKNISI (KHUSUS LOS)
// ======================
$pesanTeknisi = "🚨 *FO CUT TERDETEKSI*\n\n";
$pesanTeknisi .= "👤 Pelanggan: {$nama}\n";
$pesanTeknisi .= "📡 SN: {$sn}\n\n";
$pesanTeknisi .= "Status: LOS\n";
$pesanTeknisi .= "Estimasi: 4 Jam\n\n";
$pesanTeknisi .= "📍 Lokasi:\n{$maps}\n\n";
$pesanTeknisi .= "⚠ Segera ke lokasi";
// kirim ke group teknisi
kirimWA($groupTeknisi, $pesanTeknisi);
}
elseif ($status == "POWER FAIL") {
$pesan .= "⚡ *PERANGKAT MATI*\n\n";
$pesan .= "Pelanggan: {$nama}\n\n";
$pesan .= "Perangkat ONU terdeteksi mati / tidak mendapat listrik.\n";
$pesan .= "Silakan cek adaptor atau listrik di lokasi.\n\n";
$pesan .= "Jika terjadi kendala dilokasi mohon konfirmasinya kakak\n\n";
$pesan .= "bisa di bantu videokan alat atau foto alat dilokasi kakak🙏\n\n";
$pesan .= "Terima kasih 🙏";
}
elseif ($status == "ONLINE" && $lastStatus != "ONLINE") {
$pesan .= "✅ *INTERNET NORMAL*\n\n";
$pesan .= "Pelanggan: {$nama}\n\n";
$pesan .= "Koneksi internet sudah kembali normal.\n\n";
$pesan .= "Internet saat ini bisa dicoba kembali\n\n";
$pesan .= "Terima kasih 🙏";
}
// kirim kalau ada pesan
if ($pesan != "") {
// 🔥 kirim WA ke pelanggan (TIDAK DIUBAH)
kirimWA($phone, $pesan);
// update log
$update = $conn->prepare("
INSERT INTO notif_onu_log (sn, last_status, last_sent)
VALUES (?, ?, NOW())
ON DUPLICATE KEY UPDATE
last_status = VALUES(last_status),
last_sent = NOW()
");
$update->execute([$sn, $status]);
echo "Kirim ke $phone | $status <br>";
}
}

161
api/auto_notif_onu.php Normal file
View File

@ -0,0 +1,161 @@
<?php
require_once __DIR__ . '/../config/database.php';
$db = new Database();
$conn = $db->getConnection();
if (!function_exists('kirimWA')) {
function kirimWA($target, $pesan) {
$token = "XjC9oSr4jECWG93JQRWf";
$data = [
"target" => $target,
"message" => $pesan,
];
$curl = curl_init();
curl_setopt_array($curl, [
CURLOPT_URL => "https://api.fonnte.com/send",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_POST => true,
CURLOPT_POSTFIELDS => http_build_query($data),
CURLOPT_HTTPHEADER => [
"Authorization: $token"
],
]);
curl_exec($curl);
curl_close($curl);
}
}
// 🔥 ID GROUP TEKNISI
$groupTeknisi = "120363427232707228@g.us";
// ambil semua ONU yang punya nomor HP + lokasi pelanggan
$stmt = $conn->query("
SELECT
o.sn,
o.status,
f.name,
f.customer_phone,
f.latitude,
f.longitude
FROM onu_status o
JOIN ftth_items f ON f.sn_onu = o.sn
JOIN item_types it ON f.item_type_id = it.id
WHERE f.customer_phone IS NOT NULL
AND LOWER(it.name) = 'pelanggan'
");
$data = $stmt->fetchAll(PDO::FETCH_ASSOC);
foreach ($data as $row) {
$sn = $row['sn'];
$status = $row['status'];
$nama = $row['name'];
$phone = $row['customer_phone'];
$lat = $row['latitude'];
$lng = $row['longitude'];
// 🔥 buat link maps
$maps = ($lat && $lng)
? "https://www.google.com/maps?q={$lat},{$lng}"
: "-";
// 🔥 cek status terakhir
$cek = $conn->prepare("
SELECT last_status FROM notif_onu_log WHERE sn = ?
");
$cek->execute([$sn]);
$last = $cek->fetch(PDO::FETCH_ASSOC);
$lastStatus = $last['last_status'] ?? null;
// ❌ anti spam
if ($status == $lastStatus) continue;
// ======================
// 🔥 FORMAT PESAN
// ======================
$pesan = "";
if ($status == "LASER OUT") {
$pesan .= "🚨 *GANGGUAN INTERNET*\n\n";
$pesan .= "Pelanggan: {$nama}\n\n";
$pesan .= "Terjadi gangguan pada kabel fiber (FO CUT).\n";
$pesan .= "kami akan informasikan kepada tim teknis untuk segera ke lokasi kakak.\n\n";
$pesan .= "estimasi perbaikan 4 Jam pengerjaan.\n\n";
$pesan .= "Mohon ditunggu 🙏";
// ======================
// 🔥 NOTIF KE TEKNISI (KHUSUS LOS)
// ======================
$pesanTeknisi = "🚨 *FO CUT TERDETEKSI*\n\n";
$pesanTeknisi .= "👤 Pelanggan: {$nama}\n";
$pesanTeknisi .= "📡 SN: {$sn}\n\n";
$pesanTeknisi .= "Status: LOS\n";
$pesanTeknisi .= "Estimasi: 4 Jam\n\n";
$pesanTeknisi .= "📍 Lokasi:\n{$maps}\n\n";
$pesanTeknisi .= "⚠ Segera ke lokasi";
// kirim ke group teknisi
kirimWA($groupTeknisi, $pesanTeknisi);
}
elseif ($status == "POWER FAIL") {
// 🔥 NOTIF KE PELANGGAN
$pesan .= "⚡ *PERANGKAT MATI*\n\n";
$pesan .= "Pelanggan: {$nama}\n\n";
$pesan .= "Perangkat ONU terdeteksi mati / tidak mendapat listrik.\n";
$pesan .= "Silakan cek adaptor atau listrik di lokasi.\n\n";
$pesan .= "Jika terjadi kendala dilokasi mohon konfirmasinya kakak\n\n";
$pesan .= "Bisa dibantu videokan atau foto kondisi alat 🙏\n\n";
$pesan .= "Terima kasih 🙏";
// ======================
// 🔥 NOTIF KE TEKNISI (POWER FAIL)
// ======================
$pesanTeknisi = "⚡ *POWER FAIL TERDETEKSI*\n\n";
$pesanTeknisi .= "👤 Pelanggan: {$nama}\n";
$pesanTeknisi .= "📡 SN: {$sn}\n\n";
$pesanTeknisi .= "Status: POWER FAIL\n\n";
$pesanTeknisi .= "📍 Lokasi:\n{$maps}\n\n";
$pesanTeknisi .= " Saat ini sedang konfirmasi ke pelanggan terkait kondisi alat di lokasi.\n";
$pesanTeknisi .= "📸 Jika sudah ada info, akan dikirim foto / video alat di grup ini.\n\n";
$pesanTeknisi .= "Mohon standby 🙏";
// kirim ke group teknisi
kirimWA($groupTeknisi, $pesanTeknisi);
}
elseif ($status == "ONLINE" && $lastStatus != "ONLINE") {
$pesan .= "✅ *INTERNET NORMAL*\n\n";
$pesan .= "Pelanggan: {$nama}\n\n";
$pesan .= "Koneksi internet sudah kembali normal.\n\n";
$pesan .= "Internet saat ini bisa dicoba kembali\n\n";
$pesan .= "Terima kasih 🙏";
}
// kirim kalau ada pesan
if ($pesan != "") {
// 🔥 kirim WA ke pelanggan (TIDAK DIUBAH)
kirimWA($phone, $pesan);
// update log
$update = $conn->prepare("
INSERT INTO notif_onu_log (sn, last_status, last_sent)
VALUES (?, ?, NOW())
ON DUPLICATE KEY UPDATE
last_status = VALUES(last_status),
last_sent = NOW()
");
$update->execute([$sn, $status]);
echo "Kirim ke $phone | $status <br>";
}
}

55
api/auto_runner.php Normal file
View File

@ -0,0 +1,55 @@
<?php
// ⏱ interval (detik)
$interval_odp = 60; // 1 menit
$interval_onu = 60; // 1 menit
$interval_olt = 180; // 3 menit
// ⏱ last run (awal = 0 biar langsung jalan)
$last_odp = 0;
$last_onu = 0;
$last_olt = 0;
echo "🚀 Auto Runner Started...\n";
while (true) {
$now = time();
// ===============================
// 🔥 CEK ODP (1 menit)
// ===============================
if (($now - $last_odp) >= $interval_odp) {
echo "📡 Cek ODP: " . date("H:i:s") . PHP_EOL;
include __DIR__ . "/auto_notif_odp.php";
$last_odp = $now;
}
// ===============================
// 🔥 CEK ONU + NOTIF (1 menit)
// ===============================
if (($now - $last_onu) >= $interval_onu) {
echo "📶 Cek ONU: " . date("H:i:s") . PHP_EOL;
include __DIR__ . "/auto_notif_onu.php"; // kirim notif WA
$last_onu = $now;
}
// ===============================
// 🔥 CEK OLT (3 menit)
// ===============================
//if (($now - $last_olt) >= $interval_olt) {
// echo "🛰️ Cek OLT: " . date("H:i:s") . PHP_EOL;
// include __DIR__ . "/ambil_dataOLT.php"; // update status ONU
// $last_olt = $now;
// }
// ⏳ delay kecil biar CPU gak 100%
sleep(5);
}

51
api/backup.txt Normal file
View File

@ -0,0 +1,51 @@
<?php
// waktu terakhir dijalankan
$lastODP = 0;
$lastONU = 0;
$lastOLT = 0;
while (true) {
$now = time();
echo "Loop jalan: " . date("H:i:s") . PHP_EOL;
// ======================
// CEK ODP (tiap 60 detik)
// ======================
if ($now - $lastODP >= 60) {
echo "Cek ODP: " . date("H:i:s") . PHP_EOL;
require_once __DIR__ . "/auto_notif_odp.php";
$lastODP = $now;
}
// ======================
// CEK ONU (tiap 60 detik)
// ======================
if ($now - $lastONU >= 60) {
echo "Cek ONU: " . date("H:i:s") . PHP_EOL;
require_once __DIR__ . "/auto_notif_onu.php";
$lastONU = $now;
}
// ==========================
// AMBIL DATA OLT (tiap 5 menit)
// ==========================
if ($now - $lastOLT >= 300) {
echo "Ambil Data OLT: " . date("H:i:s") . PHP_EOL;
require_once __DIR__ . "/ambil_dataOLT.php";
$lastOLT = $now;
}
// jeda kecil biar CPU nggak full
sleep(5);
}
rmtstb.megadataisp.net
📡 Cek ODP: 20:16:25
📶 Cek ONU: 20:16:33
Kirim ke 6282338451677 | ONLINE <br>Kirim ke 6281252188305 | ONLINE <br>📡 Cek ODP: 20:17:29

119
api/bc-ambildata_olt.txt Normal file
View File

@ -0,0 +1,119 @@
<?php
require_once __DIR__ . '/../config/database.php';
mysqli_report(MYSQLI_REPORT_ERROR | MYSQLI_REPORT_STRICT);
$db = new Database();
$conn = $db->getConnection();
// ================= CONFIG =================
$login_url = "http://10.255.254.21/login";
$data_url = "http://10.255.254.21/ontinfo_table";
$username = "root";
$password = "@lokal234";
$cookie = __DIR__ . "/cookie.txt";
// ================= LOGIN =================
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $login_url);
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query([
"username" => $username,
"password" => $password
]));
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_COOKIEJAR, $cookie);
curl_setopt($ch, CURLOPT_COOKIEFILE, $cookie);
curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true);
curl_exec($ch);
curl_close($ch);
// ================= AMBIL DATA (JSON) =================
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $data_url);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_COOKIEFILE, $cookie);
curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true);
$response = curl_exec($ch);
if ($response === false) {
die("CURL ERROR: " . curl_error($ch));
}
curl_close($ch);
// simpan debug
file_put_contents("debug_onu.json", $response);
// ================= PARSING JSON =================
$data = json_decode($response, true);
if (!isset($data['data']) || empty($data['data'])) {
die("DATA KOSONG / LOGIN GAGAL / ENDPOINT SALAH");
}
foreach ($data['data'] as $onu) {
$sn = $onu['ont_sn'] ?? '';
$rx = $onu['receive_power'] ?? 0;
$state = $onu['state'] ?? 0;
$cause = $onu['last_d_cause'] ?? '';
// skip kalau SN kosong
if (empty($sn)) {
continue;
}
// ================= STATUS =================
$cause = strtolower($cause);
// 🔥 PRIORITAS 1: dari cause
if (strpos($cause, 'laser out') !== false || strpos($cause, 'fiber') !== false) {
$status = "LASER OUT";
}
elseif (strpos($cause, 'power down') !== false || strpos($cause, 'dying-gasp') !== false) {
$status = "POWER FAIL";
}
// 🔥 PRIORITAS 2: fallback dari state + RX
else {
if ($state == 0 && $rx <= -30) {
$status = "LASER OUT"; // default ke LOS kalau tidak jelas
} elseif ($rx <= -30) {
$status = "POWER FAIL";
} else {
$status = "ONLINE";
}
}
// ================= SIMPAN DB =================
$conn->query("
INSERT INTO onu_status (sn, status, rx_power, last_update)
VALUES ('$sn', '$status', '$rx', NOW())
ON DUPLICATE KEY UPDATE
last_status = status,
status = '$status',
rx_power = '$rx',
last_update = NOW()
");
$conn->query("
INSERT INTO status_onu_histori (sn, status, rx_power, cause, created_at)
VALUES (
'$sn',
'$status',
'$rx',
'$cause',
NOW()
)
");
echo "SN: $sn | STATUS: $status | RX: $rx <br>";
}
echo "<br>DONE ✅";
?>

View File

@ -0,0 +1,224 @@
<?php
require_once __DIR__ . '/../config/database.php';
set_time_limit(300);
$db = new Database();
$conn = $db->getConnection();
// 🔥 GROUP TEKNISI
$groupTeknisi = "120363427232707228@g.us";
// ======================
// 🔥 FUNCTION WA
// ======================
if (!function_exists('kirimWA')) {
function kirimWA($target, $pesan) {
if (!$target) return;
$token = "XjC9oSr4jECWG93JQRWf";
$data = [
"target" => $target,
"message" => $pesan,
];
$curl = curl_init();
curl_setopt_array($curl, [
CURLOPT_URL => "https://api.fonnte.com/send",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_POST => true,
CURLOPT_POSTFIELDS => http_build_query($data),
CURLOPT_HTTPHEADER => [
"Authorization: $token"
],
CURLOPT_TIMEOUT => 10
]);
curl_exec($curl);
curl_close($curl);
}
}
// ======================
// 🔥 TEST MODE (OPSIONAL)
// ======================
$TEST_MODE = false;
// ======================
// 🔥 AMBIL DATA DARI DATABASE (JOIN HISTORY)
// ======================
$stmt = $conn->query("
SELECT
f.id,
f.name,
f.sn_onu,
f.latitude,
f.longitude,
f.customer_phone,
o.rx_power,
o.voltage,
o.updated_at
FROM ftth_items f
LEFT JOIN onu_daily_history o
ON o.sn = f.sn_onu
INNER JOIN item_types it
ON it.id = f.item_type_id
WHERE f.sn_onu IS NOT NULL
AND LOWER(it.name) = 'pelanggan'
");
$dataItems = $stmt->fetchAll(PDO::FETCH_ASSOC);
foreach ($dataItems as $item) {
usleep(200000);
$sn = strtoupper(trim($item['sn_onu']));
$nama = $item['name'];
$lat = $item['latitude'];
$lng = $item['longitude'];
$phone = $item['customer_phone'];
// ======================
// 🔥 AMBIL RX & VOLT DARI DB
// ======================
$rx = isset($item['rx_power']) ? floatval($item['rx_power']) : null;
$volt = isset($item['voltage']) ? floatval($item['voltage']) : null;
if ($rx === null && $volt === null) {
continue;
}
// ======================
// 🔥 STATUS DETEKSI
// ======================
$isBadRx = ($rx !== null && $rx <= -25.99);
$isBadVolt = ($volt !== null && $volt < 3.29);
echo "DEBUG $sn | RX:$rx | Volt:$volt<br>";
if (!$isBadRx && !$isBadVolt) {
continue;
}
// ======================
// 🔥 CEK ANTI SPAM LOG
// ======================
$cek = $conn->prepare("
SELECT last_rx, last_voltage
FROM notif_device_log
WHERE sn = ?
");
$cek->execute([$sn]);
$last = $cek->fetch(PDO::FETCH_ASSOC);
$lastRx = $last['last_rx'] ?? null;
$lastVolt = $last['last_voltage'] ?? null;
$isChanged = ($lastRx != $rx || $lastVolt != $volt);
if (!$isChanged) {
continue;
}
// ======================
// 🔥 MAPS
// ======================
$maps = ($lat && $lng)
? "https://www.google.com/maps?q={$lat},{$lng}"
: "-";
// ======================
// 🔥 FORMAT NOMOR
// ======================
$phoneFix = null;
if ($phone) {
$phoneFix = preg_replace('/[^0-9]/', '', $phone);
if (substr($phoneFix, 0, 1) == '0') {
$phoneFix = '62' . substr($phoneFix, 1);
}
}
// ======================
// 🔥 NOTIFIKASI
// ======================
if ($isBadRx && $isBadVolt) {
kirimWA($phoneFix,
"🚨 *Gangguan Jaringan & Perangkat*\n\n" .
"Yth. {$nama},\n" .
"📉 RX: " . number_format($rx,2) . " dBm\n" .
"🔌 Volt: " . number_format($volt,2) . " V\n\n" .
"Gangguan pada jaringan & perangkat terdeteksi.\n" .
"Estimasi perbaikan ±2 jam.\n\n" .
"Mohon maaf 🙏"
);
kirimWA($groupTeknisi,
"🚨 *DOUBLE ISSUE*\n\n" .
"👤 {$nama}\n" .
"📉 RX: {$rx} dBm\n" .
"🔌 Volt: " . number_format($volt,2) . " V\n\n" .
"📡 Gangguan IKR + ONU\n" .
"📍 {$maps}\n\n" .
"⚠ PRIORITAS TINGGI"
);
} elseif ($isBadRx) {
kirimWA($phoneFix,
"🚨 *Gangguan Jaringan*\n\n" .
"Yth. {$nama},\n" .
"📉 RX: {$rx} dBm\n\n" .
"Terjadi gangguan pada jalur fiber.\n" .
"Tim akan segera ke lokasi.\n"
);
kirimWA($groupTeknisi,
"🚨 *REDAMAN TIDAK NORMAL*\n\n" .
"👤 {$nama}\n" .
"📉 RX: {$rx} dBm\n" .
"📍 {$maps}"
);
} elseif ($isBadVolt) {
kirimWA($phoneFix,
"⚡ *Perangkat Bermasalah*\n\n" .
"Yth. {$nama},\n" .
"🔌 Volt: " . number_format($volt,2) . " V\n\n" .
"Indikasi power tidak stabil.\n"
);
kirimWA($groupTeknisi,
"⚡ *VOLTAGE TIDAK NORMAL*\n\n" .
"👤 {$nama}\n" .
"🔌 Volt: " . number_format($volt,2) . " V\n" .
"📍 {$maps}"
);
}
// ======================
// 🔥 UPDATE LOG ANTI SPAM
// ======================
$update = $conn->prepare("
INSERT INTO notif_device_log (sn, last_rx, last_voltage, last_sent)
VALUES (?, ?, ?, NOW())
ON DUPLICATE KEY UPDATE
last_rx = VALUES(last_rx),
last_voltage = VALUES(last_voltage),
last_sent = NOW()
");
$update->execute([$sn, $rx, $volt]);
echo "✅ SENT $sn | RX:$rx | V:$volt<br>";
}

4
api/cookie.txt Normal file
View File

@ -0,0 +1,4 @@
# Netscape HTTP Cookie File
# https://curl.se/docs/http-cookies.html
# This file was generated by libcurl! Edit at your own risk.

87
api/data_log_hsgq.php Normal file
View File

@ -0,0 +1,87 @@
<?php
// ================= CONFIG =================
$login_url = "http://10.255.254.21/login";
$data_url = "http://10.255.254.21/ontinfo_table";
$username = "root";
$password = "@lokal234";
$cookie = __DIR__ . "/cookie.txt";
// ================= LOGIN =================
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $login_url);
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query([
"username" => $username,
"password" => $password
]));
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_COOKIEJAR, $cookie);
curl_setopt($ch, CURLOPT_COOKIEFILE, $cookie);
curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true);
$result = curl_exec($ch);
if (curl_errno($ch)) {
die("LOGIN ERROR : " . curl_error($ch));
}
curl_close($ch);
// ================= AMBIL DATA =================
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $data_url);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_COOKIEFILE, $cookie);
curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true);
$response = curl_exec($ch);
if (curl_errno($ch)) {
die("DATA ERROR : " . curl_error($ch));
}
curl_close($ch);
// simpan raw response
file_put_contents("debug_hsgq.json", $response);
// ================= TAMPILKAN =================
$data = json_decode($response, true);
echo "<h3>RAW RESPONSE</h3>";
if ($data === null) {
echo "<pre>";
echo htmlspecialchars(substr($response, 0, 5000));
echo "</pre>";
exit;
}
echo "<h3>FIELD YANG TERSEDIA</h3>";
echo "<pre>";
if (isset($data['data'][0])) {
echo "=== DATA ONU PERTAMA ===\n\n";
print_r($data['data'][0]);
echo "\n\n=== NAMA FIELD ===\n\n";
foreach ($data['data'][0] as $key => $value) {
echo $key . "\n";
}
} else {
print_r($data);
}
echo "</pre>";

1
api/debug_hsgq.json Normal file
View File

@ -0,0 +1 @@
Not found

1176
api/debug_onu.html Normal file

File diff suppressed because it is too large Load Diff

1426
api/debug_onu.json Normal file

File diff suppressed because it is too large Load Diff

95
api/detail_redaman-bc.txt Normal file
View File

@ -0,0 +1,95 @@
<?php
require_once __DIR__ . '/../config/database.php';
header('Content-Type: application/json');
header('Access-Control-Allow-Origin: *');
$database = new Database();
$conn = $database->getConnection();
$id = intval($_GET['id'] ?? 0);
if ($id <= 0) {
echo json_encode(['success'=>false,'message'=>'ID tidak valid']);
exit;
}
$stmt = $conn->prepare("SELECT serial_number FROM ftth_items WHERE id=:id");
$stmt->bindParam(':id', $id, PDO::PARAM_INT);
$stmt->execute();
$row = $stmt->fetch(PDO::FETCH_ASSOC);
if (!$row) {
echo json_encode(['success'=>false,'message'=>'Data tidak ditemukan']);
exit;
}
$serialNumber = $row['serial_number'];
// Default nilai
$rxPower = null;
$pppoeIP = null;
$voltage = null;
$uptime = null;
$inform = null;
if ($serialNumber) {
$query = json_encode(["_id" => $serialNumber]);
$url = "http://rmtstb.megadataisp.net:7557/devices/?query=" . urlencode($query);
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $url);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
$response = curl_exec($ch);
curl_close($ch);
if ($response) {
$data = json_decode($response, true);
if (isset($data[0])) {
// RX Power
if (isset($data[0]['VirtualParameters']['RXPower']['_value'])) {
$rxPower = $data[0]['VirtualParameters']['RXPower']['_value'] . " dBm";
}
// PPPoE IP
if (isset($data[0]['VirtualParameters']['pppoeIP']['_value'])) {
$pppoeIP = $data[0]['VirtualParameters']['pppoeIP']['_value'];
}
// Voltage (VP)
if (isset($data[0]['VirtualParameters']['Voltage']['_value'])) {
$rawVoltage = $data[0]['VirtualParameters']['Voltage']['_value'];
// konversi ke Volt (dibagi 10000)
$voltageConvert = $rawVoltage / 10000;
// ambil 2 angka belakang
$voltage = number_format($voltageConvert, 2) . " V";
}
// Uptime (detik → HH:MM:SS)
if (isset($data[0]['InternetGatewayDevice']['DeviceInfo']['UpTime']['_value'])) {
$seconds = (int)$data[0]['InternetGatewayDevice']['DeviceInfo']['UpTime']['_value'];
$uptime = gmdate("H:i:s", $seconds);
}
// Inform terakhir
if (isset($data[0]['Events']['Inform'])) {
$inform = $data[0]['Events']['Inform'];
}
}
}
}
// Output JSON
echo json_encode([
'success' => true,
'data' => [
'serial_number' => $serialNumber,
'rx_power' => $rxPower ?? 'Tidak tersedia',
'pppoe_ip' => $pppoeIP ?? 'Tidak tersedia',
'voltage' => $voltage ?? 'Tidak tersedia',
'uptime' => $uptime ?? 'Tidak tersedia',
'inform' => $inform ?? 'Tidak tersedia'
]
]);

151
api/detail_redaman.php Normal file
View File

@ -0,0 +1,151 @@
<?php
require_once __DIR__ . '/../config/database.php';
header('Content-Type: application/json');
header('Access-Control-Allow-Origin: *');
$database = new Database();
$conn = $database->getConnection();
// ======================
// 🔥 TEST MODE
// ======================
// url test : http://localhost/ftthplanner/api/detail_redaman.php?id=859
$TEST_MODE = true;
$TEST_DATA = [
"08A5AE-F609-ZXIC6A7451C337D" => [
"rx" => -23,
"volt" => 2.9
],
//id 836 Client putri 081252188305 sn : GGCL298413e0
"08A5AE-F609-ZXIC03C33C4B433" => [
"rx" => -32,
"volt" => 3.2
],
//id : 859 difta dwi RT via kantor sn : CDTCaf8ee4ed
"08A5AE-F609-ZXIC1D6EF05BDD7" => [
"rx" => -28,
"volt" => 3.2
],
];
// ======================
// 🔥 VALIDASI ID
// ======================
$id = intval($_GET['id'] ?? 0);
if ($id <= 0) {
echo json_encode(['success'=>false,'message'=>'ID tidak valid']);
exit;
}
// ======================
// 🔥 AMBIL SN
// ======================
$stmt = $conn->prepare("SELECT serial_number FROM ftth_items WHERE id=:id");
$stmt->bindParam(':id', $id, PDO::PARAM_INT);
$stmt->execute();
$row = $stmt->fetch(PDO::FETCH_ASSOC);
if (!$row) {
echo json_encode(['success'=>false,'message'=>'Data tidak ditemukan']);
exit;
}
// 🔥 NORMALISASI SN
$serialNumber = strtoupper(trim($row['serial_number']));
// ======================
// 🔥 DEFAULT
// ======================
$rxPower = null;
$pppoeIP = null;
$voltage = null;
$uptime = null;
$inform = null;
// ======================
// 🔥 CEK: MANUAL ATAU AUTO
// ======================
$useManual = ($TEST_MODE && isset($TEST_DATA[$serialNumber]));
if ($useManual) {
// ======================
// 🔥 MODE MANUAL (HANYA SN YANG ADA DI TEST_DATA)
// ======================
$rxPower = $TEST_DATA[$serialNumber]['rx'] . " dBm";
$voltage = number_format($TEST_DATA[$serialNumber]['volt'], 2) . " V";
$pppoeIP = "TEST MODE";
$uptime = "-";
$inform = "MANUAL";
} else {
// ======================
// 🔥 MODE AUTO (GENIEACS)
// ======================
if ($serialNumber) {
$query = json_encode(["_id" => $serialNumber]);
$url = "http://rmtstb.megadataisp.net:7557/devices/?query=" . urlencode($query);
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $url);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_TIMEOUT, 5);
$response = curl_exec($ch);
curl_close($ch);
if ($response) {
$data = json_decode($response, true);
if (isset($data[0])) {
// RX
if (isset($data[0]['VirtualParameters']['RXPower']['_value'])) {
$rxPower = $data[0]['VirtualParameters']['RXPower']['_value'] . " dBm";
}
// PPPoE
if (isset($data[0]['VirtualParameters']['pppoeIP']['_value'])) {
$pppoeIP = $data[0]['VirtualParameters']['pppoeIP']['_value'];
}
// Voltage
if (isset($data[0]['VirtualParameters']['Voltage']['_value'])) {
$rawVoltage = $data[0]['VirtualParameters']['Voltage']['_value'];
$voltageConvert = $rawVoltage / 10000;
$voltage = number_format($voltageConvert, 2) . " V";
}
// Uptime
if (isset($data[0]['InternetGatewayDevice']['DeviceInfo']['UpTime']['_value'])) {
$seconds = (int)$data[0]['InternetGatewayDevice']['DeviceInfo']['UpTime']['_value'];
$uptime = gmdate("H:i:s", $seconds);
}
// Inform
if (isset($data[0]['Events']['Inform'])) {
$inform = $data[0]['Events']['Inform'];
}
}
}
}
}
// ======================
// 🔥 OUTPUT
// ======================
echo json_encode([
'success' => true,
'data' => [
'serial_number' => $serialNumber,
'rx_power' => $rxPower ?? 'Tidak tersedia',
'pppoe_ip' => $pppoeIP ?? 'Tidak tersedia',
'voltage' => $voltage ?? 'Tidak tersedia',
'uptime' => $uptime ?? 'Tidak tersedia',
'inform' => $inform ?? 'Tidak tersedia'
]
]);

0
api/deteksi loss Normal file
View File

62
api/generate.php Normal file
View File

@ -0,0 +1,62 @@
<?php
$baseData = [
['sn' => 'VSOL00addd35', 'name' => 'ARTINI', 'rx' => -25.5300, 'volt' => 3.24],
['sn' => 'VSOL00ad1105', 'name' => 'SATRIA PERUM PANGGUNG', 'rx' => -24.3180, 'volt' => 3.26],
['sn' => 'HWTCa90adda0', 'name' => 'RIDA AGUS', 'rx' => -21.2500, 'volt' => 3.24],
['sn' => 'HWTCa90add88', 'name' => 'SMP 6 SITUBONDO', 'rx' => -21.4280, 'volt' => 3.26],
['sn' => 'HWTCa90aaf00', 'name' => 'BASRIYADI', 'rx' => -21.1360, 'volt' => 3.26],
['sn' => 'HWTCd1b3aa28', 'name' => 'IRWAN', 'rx' => -22.0780, 'volt' => 3.28],
['sn' => 'HWTCa90a4760', 'name' => 'TEGUH ADI', 'rx' => -21.0240, 'volt' => 3.24],
['sn' => 'CDTCaf8e7bf1', 'name' => 'MALD/H1/1/3/13', 'rx' => -22.4420, 'volt' => 3.30],
['sn' => 'HWTCa90aaed8', 'name' => 'FENTI', 'rx' => -21.4280, 'volt' => 3.28],
['sn' => 'HWTCa90aabf8', 'name' => 'DESY/SUPARJO', 'rx' => -22.2920, 'volt' => 3.28],
['sn' => 'HWTCa90addb0', 'name' => 'AYANG', 'rx' => -20.6060, 'volt' => 3.22],
];
$dates = [];
for ($d = 12; $d <= 19; $d++) {
$dates[] = "2026-06-" . str_pad($d, 2, "0", STR_PAD_LEFT);
}
$sql = "";
foreach ($dates as $date) {
foreach ($baseData as $row) {
// 🔥 variasi biar realistis (fluktuasi kecil)
$rx = $row['rx'] + rand(-80, 80) / 1000; // ±0.08 dBm
$volt = $row['volt'] + rand(-5, 5) / 1000; // ±0.005 V
$sample = rand(6, 12);
$min_rx = $rx - rand(5, 20) / 1000;
$max_rx = $rx + rand(5, 20) / 1000;
$min_v = $volt - rand(1, 5) / 1000;
$max_v = $volt + rand(1, 5) / 1000;
$sql .= "INSERT INTO onu_daily_history
(sn, ont_name, rx_power, voltage, min_rx, max_rx, min_voltage, max_voltage, last_cause, sample_count, log_date, created_at, updated_at)
VALUES
(
'{$row['sn']}',
'{$row['name']}',
" . round($rx, 4) . ",
" . round($volt, 3) . ",
" . round($min_rx, 4) . ",
" . round($max_rx, 4) . ",
" . round($min_v, 3) . ",
" . round($max_v, 3) . ",
NULL,
{$sample},
'{$date}',
'{$date} 08:00:00',
'{$date} 17:00:00'
);\n\n";
}
}
file_put_contents("onu_history_12_19_june.sql", $sql);
echo "DONE: file SQL berhasil dibuat";

50
api/get_onu_by_item.php Normal file
View File

@ -0,0 +1,50 @@
<?php
header('Content-Type: application/json');
require_once __DIR__ . '/../config/database.php';
$db = (new Database())->getConnection();
$id = $_GET['id'] ?? null;
if (!$id) {
echo json_encode(['success' => false, 'message' => 'ID kosong']);
exit;
}
// 1. ambil SN dari ftth_items
$query = "SELECT sn_onu FROM ftth_items WHERE id = :id";
$stmt = $db->prepare($query);
$stmt->bindParam(':id', $id);
$stmt->execute();
$item = $stmt->fetch(PDO::FETCH_ASSOC);
if (!$item || empty($item['sn_onu'])) {
echo json_encode(['success' => false, 'message' => 'SN tidak ada']);
exit;
}
// 2. ambil dari onu_status
$query = "SELECT status, rx_power, last_update
FROM onu_status
WHERE sn = :sn
LIMIT 1";
$stmt = $db->prepare($query);
$stmt->bindParam(':sn', $item['sn_onu']);
$stmt->execute();
$data = $stmt->fetch(PDO::FETCH_ASSOC);
if ($data) {
echo json_encode([
'success' => true,
'data' => $data
]);
} else {
echo json_encode([
'success' => false,
'message' => 'Data ONU tidak ditemukan'
]);
}

105
api/get_onu_volt.php Normal file
View File

@ -0,0 +1,105 @@
<?php
header('Content-Type: application/json');
require_once __DIR__ . '/../config/database.php';
$db = new Database();
$conn = $db->getConnection();
$item_id = intval($_GET['id'] ?? 0);
if (!$item_id) {
echo json_encode([
'success' => false,
'message' => 'ID tidak valid'
]);
exit;
}
/*
|--------------------------------------------------------------------------
| Ambil SN dari ftth_items
|--------------------------------------------------------------------------
*/
$stmt = $conn->prepare("
SELECT sn_onu
FROM ftth_items
WHERE id = ?
");
$stmt->execute([$item_id]);
$item = $stmt->fetch(PDO::FETCH_ASSOC);
if (!$item || empty($item['sn_onu'])) {
echo json_encode([
'success' => false,
'message' => 'SN tidak ada'
]);
exit;
}
$sn = trim($item['sn_onu']);
/*
|--------------------------------------------------------------------------
| Ambil optical terakhir
|--------------------------------------------------------------------------
*/
$stmt = $conn->prepare("
SELECT
sn,
ont_name,
rx_power,
voltage,
min_rx,
max_rx,
min_voltage,
max_voltage,
updated_at
FROM onu_daily_history
WHERE sn = ?
ORDER BY updated_at DESC
LIMIT 1
");
$stmt->execute([$sn]);
$data = $stmt->fetch(PDO::FETCH_ASSOC);
if (!$data) {
echo json_encode([
'success' => false,
'message' => 'Data optical tidak ditemukan'
]);
exit;
}
/*
|--------------------------------------------------------------------------
| Format Data
|--------------------------------------------------------------------------
*/
$data['rx_power'] = number_format((float)$data['rx_power'], 2) . ' dBm';
$data['voltage'] = number_format((float)$data['voltage'], 2) . ' V';
$data['min_rx'] = number_format((float)$data['min_rx'], 2) . ' dBm';
$data['max_rx'] = number_format((float)$data['max_rx'], 2) . ' dBm';
$data['min_voltage'] = number_format((float)$data['min_voltage'], 2) . ' V';
$data['max_voltage'] = number_format((float)$data['max_voltage'], 2) . ' V';
echo json_encode([
'success' => true,
'data' => $data
]);

42
api/histori_onu.php Normal file
View File

@ -0,0 +1,42 @@
<?php
require_once __DIR__ . '/../config/database.php';
$db = new Database();
$conn = $db->getConnection();
$id = $_GET['id'] ?? null;
if (!$id) {
echo json_encode(['success' => false]);
exit;
}
// ambil SN dari item
$stmt = $conn->prepare("SELECT sn_onu FROM ftth_items WHERE id = ?");
$stmt->execute([$id]);
$item = $stmt->fetch(PDO::FETCH_ASSOC);
if (!$item) {
echo json_encode(['success' => false]);
exit;
}
$sn = $item['sn_onu'];
// ambil histori anomali saja
$stmt = $conn->prepare("
SELECT *
FROM status_onu_histori
WHERE sn = ?
ORDER BY created_at DESC
LIMIT 100
");
$stmt->execute([$sn]);
$data = $stmt->fetchAll(PDO::FETCH_ASSOC);
echo json_encode([
'success' => true,
'sn' => $sn,
'data' => $data
]);

94
api/histori_redaman.php Normal file
View File

@ -0,0 +1,94 @@
<?php
header('Content-Type: application/json');
require_once __DIR__ . '/../config/database.php';
$db = new Database();
$conn = $db->getConnection();
$item_id = intval($_GET['id'] ?? 0);
if (!$item_id) {
echo json_encode([
'success' => false,
'message' => 'ID tidak valid'
]);
exit;
}
/*
|--------------------------------------------------------------------------
| Ambil SN dari ftth_items
|--------------------------------------------------------------------------
*/
$stmt = $conn->prepare("
SELECT sn_onu, name
FROM ftth_items
WHERE id = ?
");
$stmt->execute([$item_id]);
$item = $stmt->fetch(PDO::FETCH_ASSOC);
if (!$item || empty($item['sn_onu'])) {
echo json_encode([
'success' => false,
'message' => 'SN tidak ditemukan'
]);
exit;
}
$sn = trim($item['sn_onu']);
/*
|--------------------------------------------------------------------------
| Ambil histori redaman & voltage
|--------------------------------------------------------------------------
*/
$stmt = $conn->prepare("
SELECT
rx_power,
voltage,
updated_at
FROM onu_daily_history
WHERE sn = ?
ORDER BY updated_at DESC
LIMIT 30
");
$stmt->execute([$sn]);
$data = $stmt->fetchAll(PDO::FETCH_ASSOC);
if (!$data) {
echo json_encode([
'success' => false,
'message' => 'Histori kosong'
]);
exit;
}
/*
|--------------------------------------------------------------------------
| Format biar rapi (opsional)
|--------------------------------------------------------------------------
*/
foreach ($data as &$row) {
// format RX
if ($row['rx_power'] !== null) {
$row['rx_power'] = number_format((float)$row['rx_power'], 2) . ' dBm';
}
// format voltage
if ($row['voltage'] !== null) {
$row['voltage'] = number_format((float)$row['voltage'], 2) . ' V';
}
// tanggal lebih enak dibaca
$row['updated_at'] = date('d-m-Y H:i:s', strtotime($row['updated_at']));
}
echo json_encode([
'success' => true,
'sn' => $sn,
'data' => $data
]);

343
api/items.php Normal file
View File

@ -0,0 +1,343 @@
<?php
header('Content-Type: application/json');
header('Access-Control-Allow-Origin: *');
header('Access-Control-Allow-Methods: GET, POST, PUT, DELETE, OPTIONS');
header('Access-Control-Allow-Headers: Content-Type, X-Requested-With, Authorization');
header('Access-Control-Max-Age: 3600');
// Handle preflight OPTIONS request
if ($_SERVER['REQUEST_METHOD'] == 'OPTIONS') {
http_response_code(200);
exit();
}
session_start(); // <-- Tambahan penting
require_once __DIR__ . '/../config/database.php';
// Ambil user info dari session
$user_id = $_SESSION['user_id'] ?? null;
$role = $_SESSION['role'] ?? 'teknisi1';
// Function to manually parse multipart form data
function parseMultipartFormData($input, $contentType) {
$data = array();
if (preg_match('/boundary=(.+)$/', $contentType, $matches)) {
$boundary = $matches[1];
$parts = array_slice(explode('--' . $boundary, $input), 1);
foreach ($parts as $part) {
if (trim($part) == '--' || empty(trim($part))) continue;
$sections = explode("\r\n\r\n", $part, 2);
if (count($sections) != 2) continue;
$headers = $sections[0];
$body = rtrim($sections[1], "\r\n");
if (preg_match('/name="([^"]*)"/', $headers, $matches)) {
$fieldName = $matches[1];
$data[$fieldName] = $body;
}
}
}
return $data;
}
$database = new Database();
$db = $database->getConnection();
$method = $_SERVER['REQUEST_METHOD'];
$parsed_data = array();
if (($method === 'PUT' || $method === 'PATCH') && empty($_POST) &&
isset($_SERVER['CONTENT_TYPE']) && strpos($_SERVER['CONTENT_TYPE'], 'multipart/form-data') !== false) {
$raw_input = file_get_contents('php://input');
$parsed_data = parseMultipartFormData($raw_input, $_SERVER['CONTENT_TYPE']);
if (!empty($parsed_data)) {
if (isset($parsed_data['_method'])) {
$method = strtoupper($parsed_data['_method']);
unset($parsed_data['_method']);
}
$_POST = $parsed_data;
}
}
if (isset($_SERVER['HTTP_X_HTTP_METHOD_OVERRIDE'])) {
$method = $_SERVER['HTTP_X_HTTP_METHOD_OVERRIDE'];
}
if (isset($_POST['_method'])) {
$method = strtoupper($_POST['_method']);
unset($_POST['_method']);
}
$response = array('success' => false, 'message' => '', 'data' => null);
if ($method === 'PUT' || $method === 'DELETE') {
error_log("FTTH API - " . $method . " request, ID: " . (isset($_POST['id']) ? $_POST['id'] : 'N/A'));
}
try {
switch($method) {
case 'GET':
if (isset($_GET['id'])) {
$query = "SELECT i.*, i.serial_number,
it.name as item_type_name, it.icon, it.color,
tc.color_name as tube_color_name, tc.hex_code,
cc.color_name as core_color_name, cc.hex_code as core_hex_code,
sm.ratio as splitter_main_ratio,
so.ratio as splitter_odp_ratio
FROM ftth_items i
LEFT JOIN item_types it ON i.item_type_id = it.id
LEFT JOIN tube_colors tc ON i.tube_color_id = tc.id
LEFT JOIN tube_colors cc ON i.core_color_id = cc.id
LEFT JOIN splitter_types sm ON i.splitter_main_id = sm.id
LEFT JOIN splitter_types so ON i.splitter_odp_id = so.id
WHERE i.id = :id";
// kalau teknisi, batasi ke user_id
if ($role !== 'engineer') {
$query .= " AND i.user_id = :user_id";
}
$stmt = $db->prepare($query);
$stmt->bindParam(':id', $_GET['id']);
if ($role !== 'engineer') {
$stmt->bindParam(':user_id', $user_id);
}
$stmt->execute();
$item = $stmt->fetch(PDO::FETCH_ASSOC);
if ($item) {
$response['success'] = true;
$response['data'] = $item;
} else {
$response['message'] = 'Item not found';
}
} else {
$query = "SELECT i.*, i.serial_number,
it.name as item_type_name, it.icon, it.color,
tc.color_name as tube_color_name, tc.hex_code,
cc.color_name as core_color_name, cc.hex_code as core_hex_code,
sm.ratio as splitter_main_ratio,
so.ratio as splitter_odp_ratio
FROM ftth_items i
LEFT JOIN item_types it ON i.item_type_id = it.id
LEFT JOIN tube_colors tc ON i.tube_color_id = tc.id
LEFT JOIN tube_colors cc ON i.core_color_id = cc.id
LEFT JOIN splitter_types sm ON i.splitter_main_id = sm.id
LEFT JOIN splitter_types so ON i.splitter_odp_id = so.id";
if ($role !== 'engineer') {
$query .= " WHERE i.user_id = :user_id";
}
$query .= " ORDER BY i.created_at DESC";
$stmt = $db->prepare($query);
if ($role !== 'engineer') {
$stmt->bindParam(':user_id', $user_id);
}
$stmt->execute();
$items = $stmt->fetchAll(PDO::FETCH_ASSOC);
$response['success'] = true;
$response['data'] = $items;
}
break;
case 'POST':
$item_type_id = $_POST['item_type'] ?? null;
$name = $_POST['name'] ?? null;
$description = $_POST['description'] ?? null;
$latitude = $_POST['latitude'] ?? null;
$longitude = $_POST['longitude'] ?? null;
$address = $_POST['address'] ?? null;
$serial_number = $_POST['serial_number'] ?? null;
$sn_onu = $_POST['sn_onu'] ?? null;
$customer_phone = $_POST['customer_phone'] ?? null;
$tube_color_id = (!empty($_POST['tube_color_id']) && $_POST['tube_color_id'] !== '0') ? $_POST['tube_color_id'] : null;
$core_used = (!empty($_POST['core_used']) && $_POST['core_used'] !== '0') ? $_POST['core_used'] : null;
$core_color_id = (!empty($_POST['core_color_id']) && $_POST['core_color_id'] !== '0') ? $_POST['core_color_id'] : null;
$item_cable_type = (!empty($_POST['item_cable_type'])) ? $_POST['item_cable_type'] : null;
$total_core_capacity = $_POST['total_core_capacity'] ?? 24;
$splitter_main_id = (!empty($_POST['splitter_main_id']) && $_POST['splitter_main_id'] !== '0') ? $_POST['splitter_main_id'] : null;
$splitter_odp_id = (!empty($_POST['splitter_odp_id']) && $_POST['splitter_odp_id'] !== '0') ? $_POST['splitter_odp_id'] : null;
$status = $_POST['status'] ?? 'active';
if (!$item_type_id || !$name || !$latitude || !$longitude) {
$response['message'] = 'Required fields missing';
break;
}
if ($role === 'engineer') {
$query = "INSERT INTO ftth_items (
item_type_id, name, description, latitude, longitude, address, serial_number, sn_onu, customer_phone,
tube_color_id, core_used, core_color_id, item_cable_type, total_core_capacity,
splitter_main_id, splitter_odp_id, status
) VALUES (
:item_type_id, :name, :description, :latitude, :longitude, :address, :serial_number, :sn_onu, :customer_phone,
:tube_color_id, :core_used, :core_color_id, :item_cable_type, :total_core_capacity,
:splitter_main_id, :splitter_odp_id, :status
)";
$stmt = $db->prepare($query);
} else {
$query = "INSERT INTO ftth_items (
user_id, item_type_id, name, description, latitude, longitude, address, serial_number, sn_onu, customer_phone,
tube_color_id, core_used, core_color_id, item_cable_type, total_core_capacity,
splitter_main_id, splitter_odp_id, status
) VALUES (
:user_id, :item_type_id, :name, :description, :latitude, :longitude, :address, :serial_number, :sn_onu, :customer_phone,
:tube_color_id, :core_used, :core_color_id, :item_cable_type, :total_core_capacity,
:splitter_main_id, :splitter_odp_id, :status
)";
$stmt = $db->prepare($query);
$stmt->bindParam(':user_id', $user_id);
}
$stmt->bindParam(':item_type_id', $item_type_id);
$stmt->bindParam(':name', $name);
$stmt->bindParam(':description', $description);
$stmt->bindParam(':latitude', $latitude);
$stmt->bindParam(':longitude', $longitude);
$stmt->bindParam(':address', $address);
$stmt->bindParam(':serial_number', $serial_number);
$stmt->bindParam(':tube_color_id', $tube_color_id);
$stmt->bindParam(':core_used', $core_used);
$stmt->bindParam(':core_color_id', $core_color_id);
$stmt->bindParam(':item_cable_type', $item_cable_type);
$stmt->bindParam(':total_core_capacity', $total_core_capacity);
$stmt->bindParam(':splitter_main_id', $splitter_main_id);
$stmt->bindParam(':splitter_odp_id', $splitter_odp_id);
$stmt->bindParam(':status', $status);
$stmt->bindParam(':sn_onu', $sn_onu);
$stmt->bindParam(':customer_phone', $customer_phone);
if ($stmt->execute()) {
$item_id = $db->lastInsertId();
$query = "SELECT i.*, i.serial_number,
it.name as item_type_name, it.icon, it.color,
tc.color_name as tube_color_name, tc.hex_code,
cc.color_name as core_color_name, cc.hex_code as core_hex_code,
sm.ratio as splitter_main_ratio,
so.ratio as splitter_odp_ratio
FROM ftth_items i
LEFT JOIN item_types it ON i.item_type_id = it.id
LEFT JOIN tube_colors tc ON i.tube_color_id = tc.id
LEFT JOIN tube_colors cc ON i.core_color_id = cc.id
LEFT JOIN splitter_types sm ON i.splitter_main_id = sm.id
LEFT JOIN splitter_types so ON i.splitter_odp_id = so.id
WHERE i.id = :id";
$stmt = $db->prepare($query);
$stmt->bindParam(':id', $item_id);
$stmt->execute();
$response['success'] = true;
$response['message'] = 'Item created successfully';
$response['data'] = $stmt->fetch(PDO::FETCH_ASSOC);
} else {
$response['message'] = 'Failed to create item';
}
break;
case 'PUT':
$put_data = $_POST;
$id = $put_data['id'] ?? null;
if (!$id) {
$response['message'] = 'ID required for update';
break;
}
$update_fields = array();
$params = array(':id' => $id);
$allowed_fields = ['item_type', 'name', 'description', 'latitude', 'longitude', 'address', 'serial_number','tube_color_id', 'core_used', 'core_color_id', 'item_cable_type', 'total_core_capacity', 'splitter_main_id', 'splitter_odp_id', 'sn_onu', 'customer_phone','status'];
foreach ($allowed_fields as $field) {
if (isset($put_data[$field])) {
$db_field = $field === 'item_type' ? 'item_type_id' : $field;
$update_fields[] = "$db_field = :$field";
$value = $put_data[$field];
if (in_array($field, ['tube_color_id', 'core_color_id', 'splitter_main_id', 'splitter_odp_id']) && ($value === '' || $value === '0' || $value === 0)) {
$value = null;
}
$params[":$field"] = $value;
}
}
if (empty($update_fields)) {
$response['message'] = 'No fields to update';
break;
}
$query = "UPDATE ftth_items SET " . implode(', ', $update_fields) . " WHERE id = :id";
if ($role !== 'engineer') {
$query .= " AND user_id = :user_id";
$params[':user_id'] = $user_id;
}
$stmt = $db->prepare($query);
if ($stmt->execute($params)) {
$query = "SELECT i.*, i.serial_number, i.sn_onu, i.customer_phone,
it.name as item_type_name, it.icon, it.color,
tc.color_name as tube_color_name, tc.hex_code,
cc.color_name as core_color_name, cc.hex_code as core_hex_code,
sm.ratio as splitter_main_ratio,
so.ratio as splitter_odp_ratio
FROM ftth_items i
LEFT JOIN item_types it ON i.item_type_id = it.id
LEFT JOIN tube_colors tc ON i.tube_color_id = tc.id
LEFT JOIN tube_colors cc ON i.core_color_id = cc.id
LEFT JOIN splitter_types sm ON i.splitter_main_id = sm.id
LEFT JOIN splitter_types so ON i.splitter_odp_id = so.id
WHERE i.id = :id";
$stmt = $db->prepare($query);
$stmt->bindParam(':id', $id);
$stmt->execute();
$response['success'] = true;
$response['message'] = 'Item updated successfully';
$response['data'] = $stmt->fetch(PDO::FETCH_ASSOC);
} else {
$response['message'] = 'Failed to update item';
}
break;
case 'DELETE':
$input = file_get_contents("php://input");
$delete_data = array();
$json_data = json_decode($input, true);
if ($json_data) {
$delete_data = $json_data;
} else {
parse_str($input, $delete_data);
}
if (empty($delete_data)) {
$delete_data = $_POST;
}
$id = $delete_data['id'] ?? null;
if (!$id) {
$response['message'] = 'ID required for deletion';
break;
}
$query = "DELETE FROM cable_routes WHERE from_item_id = :id OR to_item_id = :id";
$stmt = $db->prepare($query);
$stmt->bindParam(':id', $id);
$stmt->execute();
$query = "DELETE FROM ftth_items WHERE id = :id";
if ($role !== 'engineer') {
$query .= " AND user_id = :user_id";
}
$stmt = $db->prepare($query);
$stmt->bindParam(':id', $id);
if ($role !== 'engineer') {
$stmt->bindParam(':user_id', $user_id);
}
if ($stmt->execute()) {
$response['success'] = true;
$response['message'] = 'Item deleted successfully';
} else {
$response['message'] = 'Failed to delete item';
}
break;
default:
$response['message'] = 'Method not allowed';
break;
}
} catch (Exception $e) {
$response['message'] = 'Database error: ' . $e->getMessage();
}
echo json_encode($response);
?>

314
api/laporan_odp.php Normal file
View File

@ -0,0 +1,314 @@
<?php
require_once __DIR__ . '/../config/database.php';
header('Content-Type: application/json');
$db = new Database();
$conn = $db->getConnection();
if (isset($_GET['kirim']) && isset($_GET['id'])) {
function kirimWA($pesan) {
$token = "XjC9oSr4jECWG93JQRWf";
$data = [
"target" => "120363407747513164@g.us",
"message" => $pesan,
];
$curl = curl_init();
curl_setopt_array($curl, [
CURLOPT_URL => "https://api.fonnte.com/send",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_POST => true,
CURLOPT_POSTFIELDS => http_build_query($data),
CURLOPT_HTTPHEADER => [
"Authorization: $token"
],
]);
curl_exec($curl);
curl_close($curl);
}
$id = $_GET['id'];
// 🔥 ambil ODP
$stmt = $conn->prepare("
SELECT i.id, i.name, i.latitude, i.longitude
FROM ftth_items i
WHERE i.id = ?
");
$stmt->execute([$id]);
$odp = $stmt->fetch(PDO::FETCH_ASSOC);
if (!$odp) {
echo json_encode(['success' => false]);
exit;
}
// 🔥 ambil client
$stmtClient = $conn->prepare("
SELECT fi.id, fi.name, fi.serial_number
FROM cable_routes cr
JOIN ftth_items fi
ON (cr.to_item_id = fi.id OR cr.from_item_id = fi.id)
WHERE (cr.from_item_id = :id OR cr.to_item_id = :id)
AND fi.id != :id
");
$stmtClient->bindParam(':id', $id);
$stmtClient->execute();
$clients = $stmtClient->fetchAll(PDO::FETCH_ASSOC);
$rxValues = [];
$clientBad = [];
foreach ($clients as $c) {
if (empty($c['serial_number'])) continue;
$query = json_encode(["_id" => $c['serial_number']]);
$url = "http://rmtstb.megadataisp.net:7557/devices/?query=" . urlencode($query);
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $url);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_TIMEOUT, 5);
$res = curl_exec($ch);
curl_close($ch);
if (!$res) continue;
$json = json_decode($res, true);
if (
isset($json[0]['VirtualParameters']['RXPower']['_value']) &&
is_numeric($json[0]['VirtualParameters']['RXPower']['_value'])
) {
$rx = floatval($json[0]['VirtualParameters']['RXPower']['_value']);
$rxValues[] = $rx;
if ($rx < -23) {
$clientBad[] = $c['name'] . " (" . $rx . " dBm)";
}
}
}
// 🔥 HITUNG
$total = count($rxValues);
$bad = count($clientBad);
$good = $total - $bad;
$avg = $total > 0 ? round(array_sum($rxValues) / $total, 2) : '-';
$min = $total > 0 ? min($rxValues) : '-';
// 🔥 STATUS
$status = "BAIK";
if ($total == 0) {
$status = "NO DATA";
} elseif ($min < -27) {
$status = "KRITIS";
} elseif ($avg < -25 || $bad > ($total * 0.5)) {
$status = "BURUK";
} elseif ($avg < -23 || $bad > ($total * 0.3)) {
$status = "WARNING";
}
// 🔥 FORMAT WA
$maps = "https://www.google.com/maps?q={$odp['latitude']},{$odp['longitude']}";
$pesan = "🚨 *LAPORAN ODP*\n\n";
$pesan .= "📍 *ODP:* {$odp['name']}\n";
$pesan .= "📊 *Status:* {$status}\n\n";
$pesan .= "👥 *Total Client:* {$total}\n";
$pesan .= "🟢 Client Baik: {$good}\n";
$pesan .= "🔴 Client Buruk: {$bad}\n\n";
$pesan .= "📶 *AVG RX:* {$avg} dBm\n";
$pesan .= "📉 *MIN RX:* {$min} dBm\n\n";
if (!empty($clientBad)) {
$pesan .= "⚠ *Client Bermasalah:*\n";
$pesan .= implode("\n", array_slice($clientBad, 0, 10));
$pesan .= "\n\n";
}
$pesan .= "📌 Lokasi:\n$maps\n\n";
$pesan .= "🔎 Silakan cek detail di dashboard.";
kirimWA($pesan);
echo json_encode(['success' => true]);
exit;
}
// ✅ Ambil semua ODP
$stmt = $conn->query("
SELECT i.id, i.name, i.latitude, i.longitude
FROM ftth_items i
LEFT JOIN item_types it ON i.item_type_id = it.id
WHERE LOWER(it.name) = 'odp'
AND i.user_id = 22
");
$odps = $stmt->fetchAll(PDO::FETCH_ASSOC);
$result = [];
foreach ($odps as $odp) {
// ✅ ambil client
$stmtClient = $conn->prepare("
SELECT fi.id, fi.name, fi.serial_number
FROM cable_routes cr
JOIN ftth_items fi
ON (cr.to_item_id = fi.id OR cr.from_item_id = fi.id)
WHERE (cr.from_item_id = :id OR cr.to_item_id = :id)
AND fi.id != :id
");
$stmtClient->bindParam(':id', $odp['id']);
$stmtClient->execute();
$clients = $stmtClient->fetchAll(PDO::FETCH_ASSOC);
$rxValues = [];
$rxList = [];
$rxBadList = [];
$clientBad = [];
foreach ($clients as $c) {
if (empty($c['serial_number'])) continue;
$query = json_encode(["_id" => $c['serial_number']]);
$url = "http://rmtstb.megadataisp.net:7557/devices/?query=" . urlencode($query);
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $url);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_TIMEOUT, 5);
$res = curl_exec($ch);
curl_close($ch);
if (!$res) continue;
$json = json_decode($res, true);
if (
isset($json[0]['VirtualParameters']['RXPower']['_value']) &&
is_numeric($json[0]['VirtualParameters']['RXPower']['_value'])
) {
$rx = floatval($json[0]['VirtualParameters']['RXPower']['_value']);
$rxValues[] = $rx;
$rxList[] = $rx;
if ($rx < -23) {
$rxBadList[] = $rx;
$clientBad[] = $c['name'] . " (" . $rx . " dBm)";
}
}
}
// 🔴 Tidak ada data
if (count($rxValues) == 0) {
$result[] = [
'id' => $odp['id'],
'nama' => $odp['name'],
'lat' => $odp['latitude'],
'lng' => $odp['longitude'],
'total' => 0,
'avg_rx' => null,
'bad_count' => 0,
'min_rx' => null,
'status' => 'NO DATA',
'status_color' => 'secondary',
'analisa' => 'Tidak ada data pelanggan / ONU offline semua'
];
continue;
}
$total = count($rxValues);
$avg = array_sum($rxValues) / $total;
$min = min($rxValues);
$bad = count($rxBadList);
// 🔥 DETEKSI SELISIH
$selisih = abs($avg - $min);
// 🔥 STATUS
$status = "BAIK";
$color = "success";
if ($selisih > 1.5) {
$status = "WARNING";
$color = "warning";
}
if ($avg < -23 || $bad > ($total * 0.3)) {
$status = "WARNING";
$color = "warning";
}
if ($avg < -25 || $bad > ($total * 0.5)) {
$status = "BURUK";
$color = "danger";
}
if ($min < -27) {
$status = "KRITIS";
$color = "dark";
}
// 🔥 ANALISA + TAMBAHAN INFORMASI CLIENT
if ($min < ($avg - 1.5)) {
$analisa = "Ada client lebih jelek dari yang lain → kemungkinan IKR/dropcore bermasalah";
}
elseif ($status === "KRITIS") {
$analisa = "Banyak RX < -27 dBm → kemungkinan feeder/ODP bermasalah";
}
elseif ($status === "BURUK") {
$analisa = "Lebih dari 50% client jelek → indikasi splitter ODP / konektor kotor";
}
elseif ($status === "WARNING") {
$analisa = "Sebagian client jelek → kemungkinan jalur ke rumah";
}
else {
$analisa = "Semua pelanggan normal";
}
// 🔥 TAMBAHKAN DETAIL RX & CLIENT BURUK KE ANALISA
$analisa .= " | RX: " . implode(", ", $rxList);
if (!empty($clientBad)) {
$analisa .= " | Client bermasalah: " . implode(", ", $clientBad);
}
$result[] = [
'id' => $odp['id'],
'nama' => $odp['name'],
'lat' => $odp['latitude'],
'lng' => $odp['longitude'],
'total' => $total,
'avg_rx' => round($avg, 2),
'bad_count' => $bad,
'min_rx' => $min,
'status' => $status,
'status_color' => $color,
'analisa' => $analisa
];
}
echo json_encode([
'success' => true,
'data' => $result
]);

167
api/log.php Normal file
View File

@ -0,0 +1,167 @@
<?php
require_once __DIR__ . '/../config/database.php';
$db = new Database();
$conn = $db->getConnection();
$login_url = "http://10.255.254.21/login";
$cookie = __DIR__ . "/cookie.txt";
/*
|--------------------------------------------------------------------------
| LOGIN
|--------------------------------------------------------------------------
*/
$ch = curl_init();
curl_setopt_array($ch, [
CURLOPT_URL => $login_url,
CURLOPT_POST => true,
CURLOPT_POSTFIELDS => http_build_query([
"username" => "root",
"password" => "@lokal234"
]),
CURLOPT_RETURNTRANSFER => true,
CURLOPT_COOKIEJAR => $cookie,
CURLOPT_COOKIEFILE => $cookie,
]);
$login = curl_exec($ch);
echo "<h3>LOGIN</h3>";
echo htmlspecialchars($login);
curl_close($ch);
/*
|--------------------------------------------------------------------------
| PREPARE INSERT
|--------------------------------------------------------------------------
*/
$stmt = $conn->prepare("
INSERT INTO onu_daily_history
(
sn,
ont_name,
rx_power,
voltage,
min_rx,
max_rx,
min_voltage,
max_voltage,
sample_count,
log_date
)
VALUES
(
:sn,
:ont_name,
:rx_power,
:voltage,
:min_rx,
:max_rx,
:min_voltage,
:max_voltage,
1,
CURDATE()
)
ON DUPLICATE KEY UPDATE
ont_name = VALUES(ont_name),
rx_power = VALUES(rx_power),
voltage = VALUES(voltage),
min_rx = LEAST(min_rx, VALUES(rx_power)),
max_rx = GREATEST(max_rx, VALUES(rx_power)),
min_voltage = LEAST(min_voltage, VALUES(voltage)),
max_voltage = GREATEST(max_voltage, VALUES(voltage)),
sample_count = sample_count + 1,
updated_at = NOW()
");
/*
|--------------------------------------------------------------------------
| PORT GPON
|--------------------------------------------------------------------------
*/
$ports = [1, 2];
foreach ($ports as $port_id) {
$url = "http://10.255.254.21/gponmgmt?form=optical_onu&port_id=" . $port_id;
$ch = curl_init();
curl_setopt_array($ch, [
CURLOPT_URL => $url,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => [
//ini token akan selalu berubah kalo beda web
"x-token: eb5d9d8f66242c63dc353342d3896a95"
]
]);
$response = curl_exec($ch);
curl_close($ch);
$json = json_decode($response, true);
echo "<h3>PON {$port_id}</h3>";
if (!isset($json['data']) || empty($json['data'])) {
echo "Tidak ada ONU ditemukan<br>";
continue;
}
foreach ($json['data'] as $onu) {
$sn = trim($onu['ont_sn'] ?? '');
if (empty($sn)) {
continue;
}
$name = trim($onu['ont_name'] ?? '');
$rx = (float) preg_replace(
'/[^0-9\.\-]/',
'',
$onu['receive_power'] ?? '0'
);
$voltage = (float) preg_replace(
'/[^0-9\.]/',
'',
$onu['work_voltage'] ?? '0'
);
try {
$stmt->execute([
':sn' => $sn,
':ont_name' => $name,
':rx_power' => $rx,
':voltage' => $voltage,
':min_rx' => $rx,
':max_rx' => $rx,
':min_voltage' => $voltage,
':max_voltage' => $voltage
]);
echo "SAVE : {$sn} | RX={$rx} | V={$voltage}<br>";
} catch (PDOException $e) {
echo "ERROR {$sn} : " . $e->getMessage() . "<br>";
}
}
}
echo "<hr>";
echo "<b>SELESAI</b>";

File diff suppressed because it is too large Load Diff

314
api/routes.php Normal file
View File

@ -0,0 +1,314 @@
<?php
session_start(); // ✅ ambil session login user
header('Content-Type: application/json');
header('Access-Control-Allow-Origin: *');
header('Access-Control-Allow-Methods: GET, POST, PUT, DELETE, OPTIONS');
header('Access-Control-Allow-Headers: Content-Type, X-Requested-With, Authorization');
header('Access-Control-Max-Age: 3600');
// Handle preflight OPTIONS request
if ($_SERVER['REQUEST_METHOD'] == 'OPTIONS') {
http_response_code(200);
exit();
}
require_once '../config/database.php';
$database = new Database();
$db = $database->getConnection();
// Ambil user login
$user_id = $_SESSION['user_id'] ?? null;
$user_role = $_SESSION['role'] ?? null;
// Function to parse multipart/form-data for PUT requests
function parseMultipartFormData($input, $contentType) {
$data = array();
if (preg_match('/boundary=(.+)$/', $contentType, $matches)) {
$boundary = $matches[1];
$parts = array_slice(explode('--' . $boundary, $input), 1);
foreach ($parts as $part) {
if (trim($part) == '--' || empty(trim($part))) continue;
$sections = explode("\r\n\r\n", $part, 2);
if (count($sections) != 2) continue;
$headers = $sections[0];
$body = rtrim($sections[1], "\r\n");
if (preg_match('/name="([^"]*)"/', $headers, $matches)) {
$fieldName = $matches[1];
$data[$fieldName] = $body;
}
}
}
return $data;
}
// Handle method override and multipart data parsing
$method = $_SERVER['REQUEST_METHOD'];
if (($method === 'PUT' || $method === 'PATCH') && empty($_POST) &&
isset($_SERVER['CONTENT_TYPE']) && strpos($_SERVER['CONTENT_TYPE'], 'multipart/form-data') !== false) {
$raw_input = file_get_contents('php://input');
$parsed_data = parseMultipartFormData($raw_input, $_SERVER['CONTENT_TYPE']);
if (!empty($parsed_data)) {
if (isset($parsed_data['_method'])) {
$method = strtoupper($parsed_data['_method']);
unset($parsed_data['_method']);
}
$_POST = $parsed_data;
}
}
// Check for X-HTTP-Method-Override header
if (isset($_SERVER['HTTP_X_HTTP_METHOD_OVERRIDE'])) {
$method = $_SERVER['HTTP_X_HTTP_METHOD_OVERRIDE'];
}
// Check for _method parameter (Laravel style)
if (isset($_POST['_method'])) {
$method = strtoupper($_POST['_method']);
unset($_POST['_method']); // Clean it up
}
$response = array('success' => false, 'message' => '', 'data' => null);
try {
switch($method) {
case 'GET':
if (isset($_GET['id'])) {
// Get single route
$query = "SELECT r.*,
fi.name as from_item_name, fi.latitude as from_lat, fi.longitude as from_lng,
ti.name as to_item_name, ti.latitude as to_lat, ti.longitude as to_lng
FROM cable_routes r
LEFT JOIN ftth_items fi ON r.from_item_id = fi.id
LEFT JOIN ftth_items ti ON r.to_item_id = ti.id
WHERE r.id = :id";
if ($user_role !== 'engineer') {
$query .= " AND r.user_id = :user_id";
}
$stmt = $db->prepare($query);
$stmt->bindParam(':id', $_GET['id']);
if ($user_role !== 'engineer') {
$stmt->bindParam(':user_id', $user_id);
}
$stmt->execute();
$route = $stmt->fetch(PDO::FETCH_ASSOC);
if ($route) {
$response['success'] = true;
$response['data'] = $route;
} else {
$response['message'] = 'Route not found';
}
} else {
// Get all routes
$query = "SELECT r.*,
fi.name as from_item_name, fi.latitude as from_lat, fi.longitude as from_lng,
ti.name as to_item_name, ti.latitude as to_lat, ti.longitude as to_lng
FROM cable_routes r
LEFT JOIN ftth_items fi ON r.from_item_id = fi.id
LEFT JOIN ftth_items ti ON r.to_item_id = ti.id";
if ($user_role !== 'engineer') {
$query .= " WHERE r.user_id = :user_id";
}
$query .= " ORDER BY r.created_at DESC";
$stmt = $db->prepare($query);
if ($user_role !== 'engineer') {
$stmt->bindParam(':user_id', $user_id);
}
$stmt->execute();
$routes = $stmt->fetchAll(PDO::FETCH_ASSOC);
$response['success'] = true;
$response['data'] = $routes;
}
break;
case 'POST':
// Create new route
$from_item_id = $_POST['from_item_id'] ?? null;
$to_item_id = $_POST['to_item_id'] ?? null;
$route_coordinates = $_POST['route_coordinates'] ?? null;
$distance = $_POST['distance'] ?? null;
$cable_type = $_POST['cable_type'] ?? 'Fiber Optic';
$core_count = $_POST['core_count'] ?? 24;
$status = $_POST['status'] ?? 'planned';
if (!$from_item_id || !$to_item_id) {
$response['message'] = 'From and To items are required';
break;
}
// Check if route already exists
$query = "SELECT id FROM cable_routes WHERE
(from_item_id = :from_id AND to_item_id = :to_id) OR
(from_item_id = :to_id AND to_item_id = :from_id)";
$stmt = $db->prepare($query);
$stmt->bindParam(':from_id', $from_item_id);
$stmt->bindParam(':to_id', $to_item_id);
$stmt->execute();
if ($stmt->fetch()) {
$response['message'] = 'Route already exists between these items';
break;
}
$query = "INSERT INTO cable_routes
(from_item_id, to_item_id, route_coordinates, distance, cable_type, core_count, status, user_id)
VALUES
(:from_item_id, :to_item_id, :route_coordinates, :distance, :cable_type, :core_count, :status, :user_id)";
$stmt = $db->prepare($query);
$stmt->bindParam(':from_item_id', $from_item_id);
$stmt->bindParam(':to_item_id', $to_item_id);
$stmt->bindParam(':route_coordinates', $route_coordinates);
$stmt->bindParam(':distance', $distance);
$stmt->bindParam(':cable_type', $cable_type);
$stmt->bindParam(':core_count', $core_count);
$stmt->bindParam(':status', $status);
$stmt->bindParam(':user_id', $user_id);
if ($stmt->execute()) {
$route_id = $db->lastInsertId();
$response['success'] = true;
$response['message'] = 'Route created successfully';
$response['route_id'] = $route_id;
} else {
$response['message'] = 'Failed to create route';
}
break;
case 'PUT':
$put_data = $_POST;
$id = $put_data['id'] ?? null;
if (!$id) {
$response['message'] = 'ID required for update';
break;
}
// Pastikan teknisi hanya update kabel miliknya
if ($user_role !== 'engineer') {
$check = $db->prepare("SELECT id FROM cable_routes WHERE id = :id AND user_id = :user_id");
$check->execute([':id'=>$id, ':user_id'=>$user_id]);
if (!$check->fetch()) {
$response['message'] = 'Not authorized to update this route';
break;
}
}
$update_fields = [];
$params = [':id' => $id];
$allowed_fields = ['cable_type', 'core_count', 'status', 'distance', 'route_coordinates'];
foreach ($allowed_fields as $field) {
if (isset($put_data[$field])) {
$update_fields[] = "$field = :$field";
$params[":$field"] = $put_data[$field];
}
}
if (empty($update_fields)) {
$response['message'] = 'No fields to update';
break;
}
$query = "UPDATE cable_routes SET " . implode(', ', $update_fields) . " WHERE id = :id";
$stmt = $db->prepare($query);
if ($stmt->execute($params)) {
$response['success'] = true;
$response['message'] = 'Route updated successfully';
} else {
$response['message'] = 'Failed to update route';
}
break;
case 'DELETE':
$input = file_get_contents("php://input");
$delete_data = [];
$json_data = json_decode($input, true);
if ($json_data) {
$delete_data = $json_data;
} else {
parse_str($input, $delete_data);
}
if (empty($delete_data)) {
$delete_data = $_POST;
}
if (isset($delete_data['id'])) {
$id = $delete_data['id'];
// Pastikan teknisi hanya hapus kabel miliknya
if ($user_role !== 'engineer') {
$check = $db->prepare("SELECT id FROM cable_routes WHERE id = :id AND user_id = :user_id");
$check->execute([':id'=>$id, ':user_id'=>$user_id]);
if (!$check->fetch()) {
$response['message'] = 'Not authorized to delete this route';
break;
}
}
$query = "DELETE FROM cable_routes WHERE id = :id";
$stmt = $db->prepare($query);
$stmt->bindParam(':id', $id);
if ($stmt->execute()) {
$response['success'] = true;
$response['message'] = 'Route deleted successfully';
} else {
$response['message'] = 'Failed to delete route';
}
} else if (isset($delete_data['item_id'])) {
$item_id = $delete_data['item_id'];
// Pastikan teknisi hanya hapus rute yg terkait item miliknya
if ($user_role !== 'engineer') {
$queryCheck = "SELECT id FROM cable_routes WHERE (from_item_id = :item_id OR to_item_id = :item_id) AND user_id = :user_id";
$stmtCheck = $db->prepare($queryCheck);
$stmtCheck->execute([':item_id'=>$item_id, ':user_id'=>$user_id]);
$route_ids = $stmtCheck->fetchAll(PDO::FETCH_COLUMN);
if (empty($route_ids)) {
$response['message'] = 'Not authorized to delete these routes';
break;
}
}
$query = "DELETE FROM cable_routes WHERE from_item_id = :item_id OR to_item_id = :item_id";
if ($user_role !== 'engineer') {
$query .= " AND user_id = :user_id";
}
$stmt = $db->prepare($query);
$stmt->bindParam(':item_id', $item_id);
if ($user_role !== 'engineer') {
$stmt->bindParam(':user_id', $user_id);
}
if ($stmt->execute()) {
$response['success'] = true;
$response['message'] = 'Routes deleted successfully';
} else {
$response['message'] = 'Failed to delete routes';
}
} else {
$response['message'] = 'ID or item_id required for deletion';
}
break;
default:
$response['message'] = 'Method not allowed';
break;
}
} catch (Exception $e) {
$response['message'] = 'Database error: ' . $e->getMessage();
}
echo json_encode($response);

26
api/splitters.php Normal file
View File

@ -0,0 +1,26 @@
<?php
header('Content-Type: application/json');
header('Access-Control-Allow-Origin: *');
require_once __DIR__ . '/../config/database.php';
$database = new Database();
$db = $database->getConnection();
$response = array('success' => false, 'message' => '', 'data' => null);
try {
$query = "SELECT * FROM splitter_types ORDER BY type, ratio";
$stmt = $db->prepare($query);
$stmt->execute();
$splitters = $stmt->fetchAll(PDO::FETCH_ASSOC);
$response['success'] = true;
$response['data'] = $splitters;
} catch (Exception $e) {
$response['message'] = 'Database error: ' . $e->getMessage();
}
echo json_encode($response);
?>

59
api/statistics.php Normal file
View File

@ -0,0 +1,59 @@
<?php
header('Content-Type: application/json');
header('Access-Control-Allow-Origin: *');
require_once __DIR__ . '/../config/database.php';
$database = new Database();
$db = $database->getConnection();
$response = array('success' => false, 'message' => '', 'data' => null);
try {
// Get item counts by type
$query = "SELECT it.name as item_type, COUNT(i.id) as count
FROM item_types it
LEFT JOIN ftth_items i ON it.id = i.item_type_id
GROUP BY it.id, it.name
ORDER BY it.id";
$stmt = $db->prepare($query);
$stmt->execute();
$statistics = array();
while ($row = $stmt->fetch(PDO::FETCH_ASSOC)) {
$key = strtolower(str_replace(' ', '_', $row['item_type']));
$statistics[$key] = (int)$row['count'];
}
// Get total routes
$query = "SELECT COUNT(*) as total_routes FROM cable_routes";
$stmt = $db->prepare($query);
$stmt->execute();
$result = $stmt->fetch(PDO::FETCH_ASSOC);
$statistics['total_routes'] = (int)$result['total_routes'];
// Get route status counts
$query = "SELECT status, COUNT(*) as count FROM cable_routes GROUP BY status";
$stmt = $db->prepare($query);
$stmt->execute();
while ($row = $stmt->fetch(PDO::FETCH_ASSOC)) {
$statistics['routes_' . $row['status']] = (int)$row['count'];
}
// Calculate total distance
$query = "SELECT SUM(distance) as total_distance FROM cable_routes WHERE distance IS NOT NULL";
$stmt = $db->prepare($query);
$stmt->execute();
$result = $stmt->fetch(PDO::FETCH_ASSOC);
$statistics['total_distance_km'] = round((float)$result['total_distance'] / 1000, 2);
$response['success'] = true;
$response['data'] = $statistics;
} catch (Exception $e) {
$response['message'] = 'Database error: ' . $e->getMessage();
}
echo json_encode($response);
?>

26
api/tube_colors.php Normal file
View File

@ -0,0 +1,26 @@
<?php
header('Content-Type: application/json');
header('Access-Control-Allow-Origin: *');
require_once __DIR__ . '/../config/database.php';
$database = new Database();
$db = $database->getConnection();
$response = array('success' => false, 'message' => '', 'data' => null);
try {
$query = "SELECT * FROM tube_colors ORDER BY color_name";
$stmt = $db->prepare($query);
$stmt->execute();
$colors = $stmt->fetchAll(PDO::FETCH_ASSOC);
$response['success'] = true;
$response['data'] = $colors;
} catch (Exception $e) {
$response['message'] = 'Database error: ' . $e->getMessage();
}
echo json_encode($response);
?>

547
assets/css/custom.css Normal file
View File

@ -0,0 +1,547 @@
/* Custom CSS untuk FTTH Planner */
/* Map Styles */
#map {
border-radius: 0;
border: none;
}
.leaflet-popup-content {
margin: 8px 15px;
line-height: 1.4;
}
.leaflet-popup-content h5 {
margin: 0 0 5px 0;
color: #007bff;
}
.popup-info {
font-size: 13px;
}
.popup-info .info-row {
margin-bottom: 5px;
}
.popup-info .info-label {
font-weight: bold;
color: #333;
}
.popup-actions {
margin-top: 10px;
text-align: center;
}
.popup-actions .btn {
margin: 2px;
padding: 5px 10px;
font-size: 12px;
}
/* Custom marker icons */
.custom-marker {
border-radius: 50%;
border: 3px solid white;
box-shadow: 0 2px 5px rgba(0,0,0,0.3);
}
.marker-olt {
background-color: #FF6B6B;
}
.marker-tiang {
background-color: #4ECDC4;
}
.marker-odp {
background-color: #45B7D1;
}
.marker-odc {
background-color: #96CEB4;
}
.marker-pelanggan {
background-color: #FFA500;
}
/* Modal styles */
.modal-header {
background-color: #007bff;
color: white;
}
.modal-header .close {
color: white;
opacity: 0.8;
}
.modal-header .close:hover {
opacity: 1;
}
/* Form styles */
.form-group label {
font-weight: 600;
color: #333;
}
.tube-color-option {
display: inline-block;
width: 20px;
height: 20px;
margin-right: 8px;
vertical-align: middle;
border-radius: 3px;
border: 1px solid #ddd;
}
/* Routing styles */
.routing-mode {
cursor: crosshair !important;
}
.route-line {
stroke-width: 4;
stroke-opacity: 0.8;
}
.route-installed {
stroke: #28a745;
}
.route-planned {
stroke: #ffc107;
stroke-dasharray: 10, 5;
}
.route-maintenance {
stroke: #dc3545;
stroke-dasharray: 5, 5;
}
/* Statistics cards */
.small-box {
border-radius: 10px;
}
.small-box .icon {
font-size: 60px;
}
/* Sidebar brand */
.brand-link {
border-bottom: 1px solid #4f5962;
}
.brand-link:hover {
text-decoration: none;
}
/* Loading spinner */
.loading-spinner {
display: inline-block;
width: 20px;
height: 20px;
border: 3px solid rgba(255,255,255,.3);
border-radius: 50%;
border-top-color: #fff;
animation: spin 1s ease-in-out infinite;
}
@keyframes spin {
to { transform: rotate(360deg); }
}
/* Responsive adjustments */
@media (max-width: 768px) {
#map {
height: 400px !important;
}
.modal-dialog {
margin: 10px;
}
.popup-actions .btn {
display: block;
width: 100%;
margin: 3px 0;
}
}
/* Drag and drop indicators */
.item-dragging {
opacity: 0.7;
transform: scale(1.1);
z-index: 1000;
}
.drop-zone {
border: 2px dashed #007bff;
background-color: rgba(0, 123, 255, 0.1);
}
/* Route drawing mode */
.route-drawing .leaflet-container {
cursor: crosshair;
}
.route-point {
background-color: #ff6b6b;
border: 2px solid white;
border-radius: 50%;
width: 12px;
height: 12px;
}
/* Notifications */
.toast {
min-width: 300px;
}
.toast-header {
background-color: #007bff;
color: white;
}
/* Map controls */
.map-controls {
position: absolute;
top: 10px;
right: 10px;
z-index: 1000;
}
.map-controls .btn-group-vertical .btn {
margin-bottom: 5px;
}
/* Legend */
.map-legend {
background: white;
padding: 10px;
border-radius: 5px;
box-shadow: 0 2px 5px rgba(0,0,0,0.2);
position: absolute;
bottom: 10px;
left: 10px;
z-index: 1000;
max-width: 200px;
}
.legend-item {
display: flex;
align-items: center;
margin-bottom: 5px;
}
.legend-icon {
width: 20px;
height: 20px;
margin-right: 8px;
border-radius: 50%;
border: 2px solid white;
box-shadow: 0 1px 3px rgba(0,0,0,0.3);
}
/* Enhanced Map Controls */
.leaflet-control-zoom {
border: none !important;
border-radius: 5px !important;
box-shadow: 0 2px 10px rgba(0,0,0,0.2) !important;
}
.leaflet-control-zoom a {
background-color: #fff !important;
border: none !important;
color: #333 !important;
font-weight: bold;
transition: all 0.3s ease;
}
.leaflet-control-zoom a:hover {
background-color: #007bff !important;
color: white !important;
transform: scale(1.1);
}
.leaflet-control-custom {
border-radius: 3px !important;
transition: all 0.3s ease;
}
.leaflet-control-custom:hover {
background-color: #007bff !important;
color: white !important;
transform: scale(1.1);
}
.leaflet-control-custom i {
color: #333;
transition: color 0.3s ease;
}
.leaflet-control-custom:hover i {
color: white !important;
}
/* Layer Control Styling */
.leaflet-control-layers {
border-radius: 8px !important;
box-shadow: 0 2px 15px rgba(0,0,0,0.2) !important;
background: white !important;
padding: 8px !important;
}
.leaflet-control-layers-toggle {
background-image: none !important;
color: #333 !important;
font-size: 18px;
}
.leaflet-control-layers-expanded {
padding: 12px !important;
}
.leaflet-control-layers label {
font-weight: 500;
margin: 5px 0;
display: flex;
align-items: center;
}
.leaflet-control-layers input[type="radio"] {
margin-right: 8px;
}
/* Coordinates Control */
.leaflet-control-coords {
font-family: 'Courier New', monospace;
border-radius: 3px;
border: 1px solid #ccc;
box-shadow: 0 1px 5px rgba(0,0,0,0.2);
}
/* Scale Control */
.leaflet-control-scale {
margin-bottom: 40px !important;
}
/* Help Control */
.leaflet-control-help {
border: 1px solid #ccc;
box-shadow: 0 1px 5px rgba(0,0,0,0.2);
}
.leaflet-control-help i {
color: #007bff;
font-size: 16px;
}
/* Fullscreen Control */
.leaflet-control-fullscreen a {
background-color: white !important;
border-radius: 3px;
color: #333 !important;
transition: all 0.3s ease;
}
.leaflet-control-fullscreen a:hover {
background-color: #007bff !important;
color: white !important;
}
/* Map Zoom Buttons in Header */
.map-zoom-controls {
display: inline-flex;
gap: 5px;
margin-left: 10px;
}
.map-zoom-controls .btn {
padding: 4px 8px;
font-size: 12px;
line-height: 1.2;
}
/* Enhanced Map Container */
#map {
border: 2px solid #e3f2fd;
border-radius: 8px;
overflow: hidden;
position: relative;
}
/* Map Loading Indicator */
.map-loading {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
background: rgba(255,255,255,0.9);
padding: 20px;
border-radius: 8px;
z-index: 10000;
display: none;
}
.map-loading.active {
display: block;
}
/* Responsive Map Controls */
@media (max-width: 768px) {
.leaflet-control-layers {
max-width: 200px;
}
.leaflet-control-coords {
font-size: 10px;
padding: 3px;
}
.map-zoom-controls {
display: none; /* Hide header zoom controls on mobile */
}
}
/* Map Tooltips */
.map-tooltip {
background: rgba(0,0,0,0.8);
color: white;
padding: 5px 10px;
border-radius: 4px;
font-size: 12px;
border: none;
box-shadow: 0 2px 4px rgba(0,0,0,0.2);
}
.map-tooltip:before {
border-top-color: rgba(0,0,0,0.8);
}
/* Item Detail Modal Styles */
.modal-xl {
max-width: 1140px;
}
.color-box {
border-radius: 3px;
vertical-align: middle;
}
.popup-actions {
margin-top: 10px;
text-align: center;
}
.popup-actions .btn {
margin: 2px;
font-size: 11px;
}
.info-row {
margin-bottom: 5px;
font-size: 13px;
}
.info-label {
font-weight: bold;
color: #666;
}
/* ==== Animasi kelap-kelip redaman ==== */
@keyframes blink {
0%, 50%, 100% { opacity: 1; }
25%, 75% { opacity: 0; }
}
.red-blink {
color: red;
animation: blink 1s infinite;
}
.green-stable {
color: green;
}
/* Enhanced table styling for detail modal */
.modal-body .table td {
border-top: none;
padding: 8px 12px;
vertical-align: middle;
}
.modal-body .table tr:nth-child(even) {
background-color: #f8f9fa;
}
.modal-body .table td:first-child {
width: 35%;
font-weight: 500;
color: #495057;
}
.modal-body .card {
box-shadow: none;
border: 1px solid #dee2e6;
}
.modal-body .card-header {
border-bottom: 1px solid rgba(255,255,255,0.2);
}
/* Badge improvements */
.badge {
font-size: 0.85em;
padding: 0.4em 0.6em;
}
/* untuk animasi kabel*/
.leaflet-interactive.animated-route {
stroke-dasharray: 15, 10;
stroke-dashoffset: 1000;
animation: move-dash 20s linear infinite;
}
@keyframes move-dash {
to {
stroke-dashoffset: 0;
}
}
/* ONLINE (normal kuning) */
.status-online {
filter: none;
}
/* POWER FAIL (abu) */
.status-power {
filter: grayscale(100%) brightness(0.7);
}
/* LOS → MERAH + BLINK */
.status-los {
/* Mengubah kuning ke merah, menurunkan kecerahan untuk efek 'merah hati' */
filter: brightness(0.5) sepia(1) saturate(500%) hue-rotate(330deg) contrast(120%);
animation: blink 1s infinite;
}
/*
.status-los {
filter: brightness(0) saturate(100%) invert(19%) sepia(96%) saturate(7487%) hue-rotate(357deg) brightness(97%) contrast(119%);
animation: blink 1s infinite;
}
*/
@keyframes blink {
0% { opacity: 1; }
50% { opacity: 0.3; }
100% { opacity: 1; }
}

BIN
assets/img/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

1855
assets/js/app.js Normal file

File diff suppressed because it is too large Load Diff

392
assets/js/kmz-export.js Normal file
View File

@ -0,0 +1,392 @@
// KMZ Export functionality for FTTH Planner
// Main export function
function exportToKMZ() {
showNotification('Menggenerate KMZ file...', 'info');
// Get all items and routes data
Promise.all([
fetchAllItems(),
fetchAllRoutes()
]).then(function(results) {
let items = results[0];
let routes = results[1];
// Validate data
if (!items || items.length === 0) {
showNotification('Tidak ada data item untuk diekspor', 'warning');
return;
}
// Filter items with valid coordinates
let validItems = items.filter(function(item) {
let lat = parseFloat(item.latitude);
let lng = parseFloat(item.longitude);
return !isNaN(lat) && !isNaN(lng);
});
if (validItems.length === 0) {
showNotification('Tidak ada item dengan koordinat yang valid untuk diekspor', 'warning');
return;
}
if (validItems.length < items.length) {
let skipped = items.length - validItems.length;
showNotification(`${skipped} item dilewati karena koordinat tidak valid`, 'warning');
}
// Generate KML content
let kmlContent = generateKML(validItems, routes);
// Create KMZ file and download
createKMZFile(kmlContent);
}).catch(function(error) {
console.error('Error exporting KMZ:', error);
showNotification('Error menggenerate KMZ: ' + error.message, 'error');
});
}
// Fetch all items from API
function fetchAllItems() {
return new Promise(function(resolve, reject) {
$.ajax({
url: 'api/items.php',
method: 'GET',
success: function(response) {
if (response.success) {
resolve(response.data);
} else {
reject(new Error(response.message || 'Failed to fetch items'));
}
},
error: function(xhr, status, error) {
reject(new Error('API error: ' + error));
}
});
});
}
// Fetch all routes from API
function fetchAllRoutes() {
return new Promise(function(resolve, reject) {
$.ajax({
url: 'api/routes.php',
method: 'GET',
success: function(response) {
if (response.success) {
resolve(response.data);
} else {
reject(new Error(response.message || 'Failed to fetch routes'));
}
},
error: function(xhr, status, error) {
reject(new Error('API error: ' + error));
}
});
});
}
// Generate KML content
function generateKML(items, routes) {
let kml = `<?xml version="1.0" encoding="UTF-8"?>
<kml xmlns="http://www.opengis.net/kml/2.2">
<Document>
<name>FTTH Planner Export</name>
<description>Export data infrastruktur FTTH dari FTTH Planner</description>
${generateStyles()}
${generateItemPlacemarks(items)}
${generateRoutePlacemarks(routes)}
</Document>
</kml>`;
return kml;
}
// Generate KML styles for different item types
function generateStyles() {
return `
<!-- Styles for OLT -->
<Style id="olt-style">
<IconStyle>
<Icon>
<href>http://maps.google.com/mapfiles/kml/paddle/red-circle.png</href>
</Icon>
<scale>1.2</scale>
</IconStyle>
<LabelStyle>
<color>ffffffff</color>
<scale>0.8</scale>
</LabelStyle>
</Style>
<!-- Styles for Tiang Tumpu -->
<Style id="tiang-style">
<IconStyle>
<Icon>
<href>http://maps.google.com/mapfiles/kml/paddle/grn-circle.png</href>
</Icon>
<scale>1.0</scale>
</IconStyle>
<LabelStyle>
<color>ffffffff</color>
<scale>0.8</scale>
</LabelStyle>
</Style>
<!-- Styles for ODP -->
<Style id="odp-style">
<IconStyle>
<Icon>
<href>http://maps.google.com/mapfiles/kml/paddle/blu-circle.png</href>
</Icon>
<scale>1.0</scale>
</IconStyle>
<LabelStyle>
<color>ffffffff</color>
<scale>0.8</scale>
</LabelStyle>
</Style>
<!-- Styles for ODC -->
<Style id="odc-style">
<IconStyle>
<Icon>
<href>http://maps.google.com/mapfiles/kml/paddle/grn-square.png</href>
</Icon>
<scale>1.0</scale>
</IconStyle>
<LabelStyle>
<color>ffffffff</color>
<scale>0.8</scale>
</LabelStyle>
</Style>
<!-- Styles for Pelanggan -->
<Style id="pelanggan-style">
<IconStyle>
<Icon>
<href>http://maps.google.com/mapfiles/kml/paddle/orange-circle.png</href>
</Icon>
<scale>0.8</scale>
</IconStyle>
<LabelStyle>
<color>ffffffff</color>
<scale>0.8</scale>
</LabelStyle>
</Style>
<!-- Styles for Routes -->
<Style id="route-planned">
<LineStyle>
<color>ff00ffff</color>
<width>3</width>
</LineStyle>
</Style>
<Style id="route-installed">
<LineStyle>
<color>ff00ff00</color>
<width>4</width>
</LineStyle>
</Style>
<Style id="route-maintenance">
<LineStyle>
<color>ff0000ff</color>
<width>3</width>
</LineStyle>
</Style>
`;
}
// Generate placemarks for items
function generateItemPlacemarks(items) {
let placemarks = '';
items.forEach(function(item) {
// Validate coordinates
let lat = parseFloat(item.latitude);
let lng = parseFloat(item.longitude);
if (isNaN(lat) || isNaN(lng)) {
console.warn('Invalid coordinates for item:', item.name, 'lat:', item.latitude, 'lng:', item.longitude);
return; // Skip this item
}
let styleId = getStyleId(item.item_type_name);
let description = generateItemDescription(item);
placemarks += `
<Placemark>
<name>${escapeXML(item.name)}</name>
<description><![CDATA[${description}]]></description>
<styleUrl>#${styleId}</styleUrl>
<Point>
<coordinates>${lng},${lat},0</coordinates>
</Point>
</Placemark>`;
});
return placemarks;
}
// Generate placemarks for routes
function generateRoutePlacemarks(routes) {
let placemarks = '';
routes.forEach(function(route) {
if (route.route_coordinates) {
let coordinates = '';
try {
let coordArray = JSON.parse(route.route_coordinates);
coordinates = coordArray.map(coord => `${coord.lng || coord[1]},${coord.lat || coord[0]},0`).join(' ');
} catch (e) {
console.warn('Invalid route coordinates for route', route.id);
return;
}
let styleId = 'route-' + route.status;
let description = generateRouteDescription(route);
placemarks += `
<Placemark>
<name>Route: ${escapeXML(route.from_item_name)} ${escapeXML(route.to_item_name)}</name>
<description><![CDATA[${description}]]></description>
<styleUrl>#${styleId}</styleUrl>
<LineString>
<tessellate>1</tessellate>
<coordinates>${coordinates}</coordinates>
</LineString>
</Placemark>`;
}
});
return placemarks;
}
// Get style ID based on item type
function getStyleId(itemType) {
switch(itemType) {
case 'OLT': return 'olt-style';
case 'Tiang Tumpu': return 'tiang-style';
case 'ODP': return 'odp-style';
case 'ODC': return 'odc-style';
case 'Pelanggan': return 'pelanggan-style';
default: return 'odp-style';
}
}
// Generate item description HTML
function generateItemDescription(item) {
let description = `
<table border="1" cellpadding="5">
<tr><td><b>Jenis:</b></td><td>${item.item_type_name}</td></tr>
<tr><td><b>Nama:</b></td><td>${escapeXML(item.name)}</td></tr>`;
if (item.description) {
description += `<tr><td><b>Deskripsi:</b></td><td>${escapeXML(item.description)}</td></tr>`;
}
if (item.address) {
description += `<tr><td><b>Alamat:</b></td><td>${escapeXML(item.address)}</td></tr>`;
}
// Handle coordinates safely
let lat = parseFloat(item.latitude);
let lng = parseFloat(item.longitude);
let coordText = (isNaN(lat) || isNaN(lng)) ? 'Koordinat tidak valid' : `${lat.toFixed(6)}, ${lng.toFixed(6)}`;
description += `<tr><td><b>Koordinat:</b></td><td>${coordText}</td></tr>`;
if (item.tube_color_name) {
description += `<tr><td><b>Warna Tube:</b></td><td>${item.tube_color_name}</td></tr>`;
}
if (item.core_used) {
description += `<tr><td><b>Core Digunakan:</b></td><td>${item.core_used}</td></tr>`;
}
if (item.splitter_main_ratio) {
description += `<tr><td><b>Splitter Utama:</b></td><td>${item.splitter_main_ratio}</td></tr>`;
}
if (item.splitter_odp_ratio) {
description += `<tr><td><b>Splitter ODP:</b></td><td>${item.splitter_odp_ratio}</td></tr>`;
}
description += `<tr><td><b>Status:</b></td><td>${getStatusText(item.status)}</td></tr>`;
description += `</table>`;
return description;
}
// Generate route description HTML
function generateRouteDescription(route) {
let distance = route.distance ? (route.distance / 1000).toFixed(2) + ' km' : 'Unknown';
return `
<table border="1" cellpadding="5">
<tr><td><b>Dari:</b></td><td>${escapeXML(route.from_item_name || 'Unknown')}</td></tr>
<tr><td><b>Ke:</b></td><td>${escapeXML(route.to_item_name || 'Unknown')}</td></tr>
<tr><td><b>Jarak:</b></td><td>${distance}</td></tr>
<tr><td><b>Tipe Kabel:</b></td><td>${escapeXML(route.cable_type || 'Fiber Optic')}</td></tr>
<tr><td><b>Jumlah Core:</b></td><td>${route.core_count || 24}</td></tr>
<tr><td><b>Status:</b></td><td>${getStatusText(route.status)}</td></tr>
</table>
`;
}
// Escape XML special characters
function escapeXML(text) {
if (!text) return '';
return text.toString()
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&#39;');
}
// Get status text in Indonesian
function getStatusText(status) {
switch(status) {
case 'active': return 'Aktif';
case 'inactive': return 'Tidak Aktif';
case 'maintenance': return 'Maintenance';
case 'planned': return 'Perencanaan';
case 'installed': return 'Terpasang';
default: return status || 'Unknown';
}
}
// Create KMZ file and trigger download
function createKMZFile(kmlContent) {
try {
// Create ZIP file containing the KML
let zip = new JSZip();
zip.file("doc.kml", kmlContent);
// Generate KMZ file
zip.generateAsync({type:"blob"}).then(function(content) {
// Create filename with timestamp
let timestamp = new Date().toISOString().slice(0,19).replace(/:/g,'-');
let filename = `FTTH_Planner_Export_${timestamp}.kmz`;
// Save file
saveAs(content, filename);
showNotification(`KMZ file berhasil diunduh: ${filename}`, 'success');
}).catch(function(error) {
console.error('Error creating KMZ:', error);
showNotification('Error membuat file KMZ: ' + error.message, 'error');
});
} catch (error) {
console.error('Error in createKMZFile:', error);
showNotification('Error membuat KMZ file: ' + error.message, 'error');
}
}
// Add export button to global scope
window.exportToKMZ = exportToKMZ;

1572
assets/js/map.js Normal file

File diff suppressed because it is too large Load Diff

39
auto_login_genieacs.php Normal file
View File

@ -0,0 +1,39 @@
<?php
// auto_login_genieacs.php
// Konfigurasi login GenieACS
$genieacs_host = 'http://rmtstb.megadataisp.net:3000';
$username = 'dwi';
$password = 'jalo';
// URL target setelah login
$target_path = '/#!/devices';
// Mulai cURL untuk login
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $genieacs_host . '/login'); // endpoint login GenieACS
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query([
'username' => $username,
'password' => $password
]));
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true);
curl_setopt($ch, CURLOPT_COOKIEJAR, __DIR__ . '/genieacs_cookie.txt'); // simpan cookie
curl_setopt($ch, CURLOPT_COOKIEFILE, __DIR__ . '/genieacs_cookie.txt');
curl_setopt($ch, CURLOPT_HEADER, false);
// Jalankan login
$response = curl_exec($ch);
if (curl_errno($ch)) {
die('Login error: ' . curl_error($ch));
}
// Redirect user ke halaman target dengan session cookie
curl_close($ch);
// Gunakan header untuk redirect
header('Location: ' . $genieacs_host . $target_path);
exit();
?>

36
auto_login_observium.php Normal file
View File

@ -0,0 +1,36 @@
<?php
// auto_login_observium.php
// Konfigurasi login Observium
$observium_host = 'http://rmtstb.megadataisp.net:8131'; // ganti sesuai IP Observium
$username = 'megadata';
$password = 'megadata';
// Mulai cURL untuk login
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $observium_host . '/login.php'); // endpoint login Observium
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query([
'username' => $username,
'password' => $password,
'login' => 'Login'
]));
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true);
curl_setopt($ch, CURLOPT_COOKIEJAR, __DIR__ . '/observium_cookie.txt'); // simpan cookie
curl_setopt($ch, CURLOPT_COOKIEFILE, __DIR__ . '/observium_cookie.txt');
curl_setopt($ch, CURLOPT_HEADER, false);
// Jalankan login
$response = curl_exec($ch);
if (curl_errno($ch)) {
die('Login error: ' . curl_error($ch));
}
// Redirect user ke halaman dashboard Observium
curl_close($ch);
header('Location: ' . $observium_host . '/'); // ganti path kalau mau ke halaman tertentu
exit();
?>

35
auto_olt.py Normal file
View File

@ -0,0 +1,35 @@
import requests
import threading
import time
from datetime import datetime
URLS = [
"http://192.168.1.13/ftthplanner/api/ambil_dataOLT.php",
"http://10.255.254.21/#/status"
]
def hit_url(url):
try:
r = requests.get(url, timeout=300)
print(
f"[{datetime.now().strftime('%Y-%m-%d %H:%M:%S')}] "
f"{url} -> Status: {r.status_code}"
)
except Exception as e:
print(
f"[{datetime.now().strftime('%Y-%m-%d %H:%M:%S')}] "
f"{url} -> ERROR: {e}"
)
while True:
threads = []
for url in URLS:
t = threading.Thread(target=hit_url, args=(url,))
t.start()
threads.append(t)
for t in threads:
t.join()
time.sleep(30)

35
auto_refresh.py Normal file
View File

@ -0,0 +1,35 @@
import requests
import threading
import time
from datetime import datetime
URLS = [
"http://192.168.1.13/ftthplanner/api/auto_notif_device.php",
"http://192.168.1.13/ftthplanner/api/auto_notif_onu.php"
]
def hit_url(url):
try:
r = requests.get(url, timeout=300)
print(
f"[{datetime.now().strftime('%Y-%m-%d %H:%M:%S')}] "
f"{url} -> Status: {r.status_code}"
)
except Exception as e:
print(
f"[{datetime.now().strftime('%Y-%m-%d %H:%M:%S')}] "
f"{url} -> ERROR: {e}"
)
while True:
threads = []
for url in URLS:
t = threading.Thread(target=hit_url, args=(url,))
t.start()
threads.append(t)
for t in threads:
t.join()
time.sleep(30)

40
bc html.txt Normal file
View File

@ -0,0 +1,40 @@
let html = `
<div>
<h5><i class="${getItemIcon(item.item_type_name)}"></i> ${item.name}</h5>
<div class="popup-info">
<div class="info-row"><span class="info-label">Jenis:</span> ${item.item_type_name}</div>
${item.description ? `<div class="info-row"><span class="info-label">Deskripsi:</span> ${item.description}</div>` : ''}
${item.address ? `<div class="info-row"><span class="info-label">Alamat:</span> ${item.address}</div>` : ''}
<div class="info-row"><span class="info-label">Warna Tube:</span> ${tubeColorName}</div>
${item.core_used ? `<div class="info-row"><span class="info-label">Core Digunakan:</span> ${item.core_used}</div>` : ''}
<div class="info-row"><span class="info-label">Splitter Utama:</span> ${splitterMain}</div>
<div class="info-row"><span class="info-label">Splitter ODP:</span> ${splitterOdp}</div>
<div class="info-row"><span class="info-label">Status:</span>
<span class="badge badge-${getStatusBadgeClass(item.status)}">${getStatusText(item.status)}</span>
</div>
<!-- Redaman hanya untuk item selain ODP, ODC, Tiang Tumpu, OLT -->
${!['odp', 'odc', 'tiang tumpu', 'olt'].includes(item.item_type_name.toLowerCase())
? `
<div class="info-row" id="redaman-${item.id}">
<span class="info-label">Redaman :</span> <em>Memuat...</em>
</div>
<div class="info-row" id="voltage-${item.id}">
<span class="info-label">Voltage :</span> <em>Memuat...</em>
</div>
<div class="info-row" id="onu-status-${item.id}">
<span class="info-label">ONU Status :</span> <em>Memuat...</em>
</div>
<div class="info-row" id="last-update-${item.id}">
<span class="info-label">Last Update :</span> <em>Memuat...</em>
</div>
<div class="info-row">
<span class="info-label">Customer Phone :</span>
<span>${item.customer_phone ? item.customer_phone : '-'}</span>
</div>
`
: ''}

BIN
bg.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 422 KiB

22
config/database.php Normal file
View File

@ -0,0 +1,22 @@
<?php
// Konfigurasi Database untuk FTTH Planner
class Database {
private $host = "localhost";
private $db_name = "ftth_planner";
private $username = "root";
private $password = "megadata";
public $conn;
public function getConnection() {
$this->conn = null;
try {
$this->conn = new PDO("mysql:host=" . $this->host . ";dbname=" . $this->db_name, $this->username, $this->password);
$this->conn->exec("set names utf8");
$this->conn->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
} catch(PDOException $exception) {
echo "Connection error: " . $exception->getMessage();
}
return $this->conn;
}
}
?>

142
database.sql Normal file
View File

@ -0,0 +1,142 @@
-- Database untuk FTTH Planner
CREATE DATABASE IF NOT EXISTS ftth_planner;
USE ftth_planner;
-- Tabel untuk menyimpan jenis item FTTH
CREATE TABLE item_types (
id INT PRIMARY KEY AUTO_INCREMENT,
name VARCHAR(50) NOT NULL,
icon VARCHAR(100),
color VARCHAR(20),
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
-- Tabel untuk menyimpan warna tube
CREATE TABLE tube_colors (
id INT PRIMARY KEY AUTO_INCREMENT,
color_name VARCHAR(30) NOT NULL,
hex_code VARCHAR(7),
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
-- Tabel untuk menyimpan jenis splitter
CREATE TABLE splitter_types (
id INT PRIMARY KEY AUTO_INCREMENT,
type VARCHAR(20) NOT NULL, -- 'main' untuk jaringan utama, 'odp' untuk ODP
ratio VARCHAR(10) NOT NULL, -- 1:2, 1:3, 1:4, 1:8, 1:16
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
-- Tabel utama untuk menyimpan item-item FTTH di maps
CREATE TABLE ftth_items (
id INT PRIMARY KEY AUTO_INCREMENT,
item_type_id INT,
name VARCHAR(100) NOT NULL,
description TEXT,
latitude DECIMAL(10, 8) NOT NULL,
longitude DECIMAL(11, 8) NOT NULL,
address TEXT,
tube_color_id INT,
core_used INT COMMENT 'Core yang sedang digunakan dari total kapasitas',
core_color_id INT NULL COMMENT 'Warna core yang digunakan (referensi ke tube_colors)',
item_cable_type ENUM('backbone', 'distribution', 'drop_core', 'feeder', 'branch') NULL DEFAULT 'distribution' COMMENT 'Jenis kabel yang digunakan pada item ini',
total_core_capacity INT NULL DEFAULT 24 COMMENT 'Total kapasitas core untuk item ini',
splitter_main_id INT,
splitter_odp_id INT,
status ENUM('active', 'inactive', 'maintenance') DEFAULT 'active',
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
FOREIGN KEY (item_type_id) REFERENCES item_types(id),
FOREIGN KEY (tube_color_id) REFERENCES tube_colors(id),
FOREIGN KEY (core_color_id) REFERENCES tube_colors(id),
FOREIGN KEY (splitter_main_id) REFERENCES splitter_types(id),
FOREIGN KEY (splitter_odp_id) REFERENCES splitter_types(id)
);
-- Tabel untuk menyimpan routing kabel
CREATE TABLE cable_routes (
id INT PRIMARY KEY AUTO_INCREMENT,
from_item_id INT,
to_item_id INT,
route_coordinates TEXT, -- JSON array of lat,lng coordinates
distance DECIMAL(8,2), -- dalam meter
cable_type VARCHAR(50),
core_count INT,
status ENUM('planned', 'installed', 'maintenance') DEFAULT 'planned',
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
FOREIGN KEY (from_item_id) REFERENCES ftth_items(id),
FOREIGN KEY (to_item_id) REFERENCES ftth_items(id)
);
-- Insert data default untuk item types
INSERT INTO item_types (name, icon, color) VALUES
('OLT', 'fas fa-server', '#FF6B6B'),
('Tiang Tumpu', 'fas fa-tower-broadcast', '#4ECDC4'),
('ODP', 'fas fa-project-diagram', '#45B7D1'),
('ODC', 'fas fa-network-wired', '#96CEB4'),
('Pelanggan', 'fas fa-home', '#FFA500');
-- Insert data default untuk warna tube (32 warna total)
INSERT INTO tube_colors (color_name, hex_code) VALUES
('Biru', '#0066CC'),
('Orange', '#FF6600'),
('Hijau', '#00CC66'),
('Coklat', '#996633'),
('Abu-abu', '#666666'),
('Putih', '#FFFFFF'),
('Merah', '#CC0000'),
('Hitam', '#000000'),
('Kuning', '#FFCC00'),
('Violet', '#9900CC'),
('Pink', '#FF6699'),
('Aqua', '#00CCCC'),
('Turquoise', '#40E0D0'),
('Lime', '#32CD32'),
('Magenta', '#FF00FF'),
('Cyan', '#00FFFF'),
('Indigo', '#4B0082'),
('Crimson', '#DC143C'),
('Gold', '#FFD700'),
('Silver', '#C0C0C0'),
('Teal', '#008080'),
('Navy', '#000080'),
('Coral', '#FF7F50'),
('Salmon', '#FA8072'),
('Lavender', '#E6E6FA'),
('Beige', '#F5F5DC'),
('Olive', '#808000'),
('Maroon', '#800000'),
('Khaki', '#F0E68C'),
('Plum', '#DDA0DD'),
('Bronze', '#CD7F32'),
('Emerald', '#50C878');
-- Insert data default untuk splitter types
INSERT INTO splitter_types (type, ratio) VALUES
('main', '1:2'),
('main', '1:3'),
('main', '1:4'),
('odp', '1:2'),
('odp', '1:4'),
('odp', '1:8'),
('odp', '1:16');
-- Create indexes for performance optimization
CREATE INDEX idx_core_color ON ftth_items(core_color_id);
CREATE INDEX idx_cable_type ON ftth_items(item_cable_type);
CREATE INDEX idx_core_usage ON ftth_items(core_used, total_core_capacity);
-- Tabel untuk menyimpan data user
CREATE TABLE users (
id INT PRIMARY KEY AUTO_INCREMENT,
username VARCHAR(50) NOT NULL UNIQUE,
password VARCHAR(255) NOT NULL,
role ENUM('engineer', 'teknisi') NOT NULL DEFAULT 'teknisi',
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
-- Contoh user default (password: admin123 untuk engineer, teknisi123 untuk teknisi)
INSERT INTO users (username, password, role) VALUES
('admin', MD5('admin123'), 'engineer'),
('teknisi', MD5('teknisi123'), 'teknisi');

0
debug_onu.html Normal file
View File

1194
debug_onu.json Normal file

File diff suppressed because it is too large Load Diff

37
get_clients.php Normal file
View File

@ -0,0 +1,37 @@
<?php
session_start();
require 'config/database.php'; // koneksi database
header('Content-Type: application/json');
try {
$database = new Database();
$db = $database->getConnection();
// Ambil hanya data yang punya koordinat valid
$query = "SELECT
id,
name,
description,
latitude,
longitude,
address,
serial_number,
status
FROM ftth_items
WHERE latitude IS NOT NULL AND longitude IS NOT NULL";
$stmt = $db->prepare($query);
$stmt->execute();
$clients = $stmt->fetchAll(PDO::FETCH_ASSOC);
echo json_encode([
"success" => true,
"data" => $clients
]);
} catch (Exception $e) {
echo json_encode([
"success" => false,
"message" => "Gagal mengambil data client: " . $e->getMessage()
]);
}

43
get_rx_power.php Normal file
View File

@ -0,0 +1,43 @@
<?php
error_reporting(E_ALL);
ini_set('display_errors', 1);
header('Content-Type: application/json');
$serialNumber = $_GET['sn'] ?? '';
if (empty($serialNumber)) {
echo json_encode(['rx_power' => null, 'error' => 'Serial number kosong']);
exit;
}
function getRxPowerFromGenieACS($serialNumber) {
$query = json_encode(["_id" => $serialNumber]);
$encodedQuery = urlencode($query); // penting untuk URL-safe
$url = "http://rmtstb.megadataisp.net:7557/devices/?query={$encodedQuery}";
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $url);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
$response = curl_exec($ch);
if ($response === false) {
return ['rx_power' => null, 'error' => curl_error($ch)];
}
curl_close($ch);
$data = json_decode($response, true);
if (!isset($data[0]['VirtualParameters']['RXPower']['_value'])) {
return ['rx_power' => null, 'error' => 'RX Power tidak ditemukan'];
}
$rxPowerValue = $data[0]['VirtualParameters']['RXPower']['_value'];
return ['rx_power' => $rxPowerValue];
}
$result = getRxPowerFromGenieACS($serialNumber);
echo json_encode($result);

15
hapus.php Normal file
View File

@ -0,0 +1,15 @@
<?php
require 'config/database.php'; // koneksi PDO
if (isset($_GET['delete'])) {
$id = intval($_GET['delete']); // pastikan integer
if ($id > 0) {
$stmt = $pdo->prepare("DELETE FROM items WHERE id = :id");
$stmt->execute(['id' => $id]);
}
header("Location: index.php");
exit;
}
?>

781
index.php Normal file
View File

@ -0,0 +1,781 @@
<?php
session_start();
if (!isset($_SESSION['user'])) {
header("Location: login.php");
exit;
}
require_once __DIR__ . '/config/database.php';
$database = new Database();
$db = $database->getConnection();
$role = $_SESSION['role'] ?? 'teknisi1';
$user_id = $_SESSION['user_id'] ?? null;
// Helper function buat hitung statistik
function getCount($db, $item_type_id, $role, $user_id) {
$query = "SELECT COUNT(*) as total FROM ftth_items WHERE item_type_id = :item_type_id";
if ($role !== 'engineer') {
$query .= " AND user_id = :user_id";
}
$stmt = $db->prepare($query);
$stmt->bindParam(':item_type_id', $item_type_id, PDO::PARAM_INT);
if ($role !== 'engineer') {
$stmt->bindParam(':user_id', $user_id, PDO::PARAM_INT);
}
$stmt->execute();
return $stmt->fetch(PDO::FETCH_ASSOC)['total'] ?? 0;
}
// Mapping item_type_id sesuai database kamu
$olt = getCount($db, 1, $role, $user_id);
$tiang = getCount($db, 2, $role, $user_id);
$odp = getCount($db, 3, $role, $user_id);
$odc = getCount($db, 4, $role, $user_id);
$pelanggan = getCount($db, 5, $role, $user_id);
// Routes (butuh join ke ftth_items)
$query = "SELECT COUNT(*) as total
FROM cable_routes r
JOIN ftth_items i ON r.from_item_id = i.id
WHERE 1=1";
if ($role !== 'engineer') {
$query .= " AND i.user_id = :user_id";
}
$stmt = $db->prepare($query);
if ($role !== 'engineer') {
$stmt->bindParam(':user_id', $user_id, PDO::PARAM_INT);
}
$stmt->execute();
$routes = $stmt->fetch(PDO::FETCH_ASSOC)['total'] ?? 0;
?>
<!DOCTYPE html>
<html lang="id">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>FTTH Megadata | Dashboard</title>
<!-- Google Font: Source Sans Pro -->
<link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Source+Sans+Pro:300,400,400i,700&display=fallback">
<!-- Font Awesome -->
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css">
<!-- AdminLTE -->
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/admin-lte@3.2/dist/css/adminlte.min.css">
<!-- Leaflet CSS (Latest Version) -->
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css" />
<!-- Leaflet Fullscreen Plugin -->
<link rel="stylesheet" href="https://unpkg.com/leaflet-fullscreen@1.0.1/dist/leaflet.fullscreen.css" />
<!-- Leaflet Routing Machine -->
<link rel="stylesheet" href="https://unpkg.com/leaflet-routing-machine@3.2.12/dist/leaflet-routing-machine.css" />
<!-- Custom CSS -->
<link rel="stylesheet" href="assets/css/custom.css">
</head>
<body class="hold-transition sidebar-mini layout-fixed">
<div class="wrapper">
<!-- Preloader -->
<div class="preloader flex-column justify-content-center align-items-center">
<i class="fas fa-network-wired fa-3x text-primary"></i>
<h4 class="mt-3">FTTH Megadata</h4>
</div>
<!-- Navbar -->
<nav class="main-header navbar navbar-expand navbar-white navbar-light">
<!-- Left navbar links -->
<ul class="navbar-nav">
<li class="nav-item">
<a class="nav-link" data-widget="pushmenu" href="#" role="button"><i class="fas fa-bars"></i></a>
</li>
<li class="nav-item d-none d-sm-inline-block">
<a href="index.php" class="nav-link">Dashboard</a>
</li>
</ul>
<!-- Right navbar links -->
<ul class="navbar-nav ml-auto">
<li class="nav-item">
<a class="nav-link" href="logout.php" role="button">
<i class="fas fa-sign-out-alt"></i> Logout
</a>
</li>
</ul>
</nav>
<!-- Main Sidebar Container -->
<aside class="main-sidebar sidebar-dark-primary elevation-4">
<!-- Brand Logo -->
<a href="index.php" class="brand-link">
<i class="fas fa-network-wired brand-image img-circle elevation-3" style="opacity: .8; margin-left: 10px; color: white;"></i>
<span class="brand-text font-weight-light">FTTH Megadata</span>
</a>
<!-- Sidebar -->
<div class="sidebar">
<!-- Sidebar Menu -->
<nav class="mt-2">
<ul class="nav nav-pills nav-sidebar flex-column" data-widget="treeview" role="menu" data-accordion="false">
<li class="nav-item">
<a href="#" class="nav-link active">
<i class="nav-icon fas fa-map"></i>
<p>Peta FTTH</p>
</a>
</li>
<!-- //ini adalah tombol genieacs lama
<li class="nav-item">
<a href="http://172.16.96.10:3000/" class="nav-link" target="_blank">
<i class="nav-icon fas fa-server"></i>
<p>GenieACS</p>
</a>
</li>
<li class="nav-item">
<a href="url_observium.html" class="nav-link" target="_blank">
<i class="nav-icon fas fa-chart-line"></i>
<p>Observium</p>
</a>
</li>
!-->
<!-- Tombol Manajemen Users -->
<li class="nav-item">
<a href="manajemen_users.php" class="nav-link">
<i class="nav-icon fas fa-users"></i>
<p>Users</p>
</a>
</li>
<li class="nav-item">
<a href="#" class="nav-link" onclick="showItemList()">
<i class="nav-icon fas fa-list"></i>
<p>Daftar Item</p>
</a>
</li>
<li class="nav-item">
<a href="#" class="nav-link" onclick="showRouteList()">
<i class="nav-icon fas fa-route"></i>
<p>Routing Kabel</p>
</a>
</li>
<li class="nav-item">
<a href="#" class="nav-link" onclick="showLaporanOdp()">
<i class="nav-icon fas fa-chart-bar"></i>
<p>Laporan ODP</p>
</a>
</li>
<li class="nav-header">NAVIGASI PETA</li>
<li class="nav-item">
<a href="#" class="nav-link" onclick="zoomToItems()">
<i class="nav-icon fas fa-expand-arrows-alt" style="color: #17a2b8;"></i>
<p>Zoom Semua Item</p>
</a>
</li>
<li class="nav-item has-treeview">
<a href="#" class="nav-link">
<i class="nav-icon fas fa-search-location" style="color: #6f42c1;"></i>
<p>
Zoom ke Item
<i class="right fas fa-angle-left"></i>
</p>
</a>
<ul class="nav nav-treeview">
<li class="nav-item">
<a href="#" class="nav-link" onclick="zoomToItemType('OLT')">
<i class="fas fa-cloud nav-icon" style="color: #FF6B6B;"></i>
<p>Zoom ke OLT</p>
</a>
</li>
<li class="nav-item">
<a href="#" class="nav-link" onclick="zoomToItemType('Tiang Tumpu')">
<i class="fas fa-tower-broadcast nav-icon" style="color: #4ECDC4;"></i>
<p>Zoom ke Tiang</p>
</a>
</li>
<li class="nav-item">
<a href="#" class="nav-link" onclick="zoomToItemType('ODP')">
<i class="fas fa-project-diagram nav-icon" style="color: #45B7D1;"></i>
<p>Zoom ke ODP</p>
</a>
</li>
<li class="nav-item">
<a href="#" class="nav-link" onclick="zoomToItemType('ODC')">
<i class="fas fa-network-wired nav-icon" style="color: #96CEB4;"></i>
<p>Zoom ke ODC</p>
</a>
</li>
<li class="nav-item">
<a href="#" class="nav-link" onclick="zoomToItemType('Pelanggan')">
<i class="fas fa-home nav-icon" style="color: #FFA500;"></i>
<p>Zoom ke Pelanggan</p>
</a>
</li>
</ul>
</li>
<li class="nav-item">
<a href="#" class="nav-link" onclick="locateUser()">
<i class="nav-icon fas fa-location-arrow" style="color: #dc3545;"></i>
<p>Cari Lokasi Saya</p>
</a>
</li>
<li class="nav-header">EXPORT DATA</li>
<li class="nav-item">
<a href="#" class="nav-link" onclick="exportToKMZ()">
<i class="nav-icon fas fa-download" style="color: #28a745;"></i>
<p>Export ke KMZ</p>
</a>
</li>
<li class="nav-header">ITEM FTTH</li>
<li class="nav-item">
<a href="#" class="nav-link" onclick="addNewItem('OLT')">
<i class="nav-icon fas fa-cloud" style="color: #FF6B6B;"></i>
<p>Tambah OLT</p>
</a>
</li>
<li class="nav-item">
<a href="#" class="nav-link" onclick="addNewItem('Tiang Tumpu')">
<i class="nav-icon fas fa-tower-broadcast" style="color: #4ECDC4;"></i>
<p>Tambah Tiang Tumpu</p>
</a>
</li>
<li class="nav-item">
<a href="#" class="nav-link" onclick="addNewItem('ODP')">
<i class="nav-icon fas fa-project-diagram" style="color: #45B7D1;"></i>
<p>Tambah ODP</p>
</a>
</li>
<li class="nav-item">
<a href="#" class="nav-link" onclick="addNewItem('ODC')">
<i class="nav-icon fas fa-network-wired" style="color: #96CEB4;"></i>
<p>Tambah ODC</p>
</a>
</li>
<li class="nav-item">
<a href="#" class="nav-link" onclick="addNewItem('Pelanggan')">
<i class="nav-icon fas fa-wifi" style="color: #FFA500;"></i>
<p>Tambah Pelanggan</p>
</a>
</li>
</ul>
</nav>
</div>
</aside>
<!-- Content Wrapper -->
<div class="content-wrapper">
<!-- Content Header -->
<div class="content-header">
<div class="container-fluid">
<div class="row mb-2">
<div class="col-sm-6">
<h1 class="m-0">Dashboard FTTH Megadata</h1>
</div>
<div class="col-sm-6">
<ol class="breadcrumb float-sm-right">
<li class="breadcrumb-item"><a href="#">Home</a></li>
<li class="breadcrumb-item active">Dashboard</li>
</ol>
</div>
</div>
</div>
</div>
<!-- Main content -->
<section class="content">
<div class="container-fluid">
<!-- Map Container -->
<div class="row">
<div class="col-12">
<div class="card">
<div class="card-header">
<h3 class="card-title">
<i class="fas fa-map mr-1"></i>
Peta Infrastruktur FTTH
</h3>
<div class="card-tools d-flex justify-content-between">
<!-- Kanan: tombol utama/Revisi Tombol -->
<div class="btn-group">
<button type="button" class="btn btn-primary btn-sm" onclick="showAddItemModal()">
<i class="fas fa-plus"></i> Tambah Item
</button>
<button type="button" class="btn btn-info btn-sm" onclick="showRoutingMode()">
<i class="fas fa-route"></i> Route Otomatis
</button>
<button type="button" class="btn btn-info btn-sm" onclick="showRoutingMode()">
<i class="fas fa-map"></i> Route manual
</button>
<button type="button" class="btn btn-success btn-sm" onclick="exportToKMZ()">
<i class="fas fa-download"></i> Export KMZ
</button>
</div>
<!-- Tombol GenieACS & Observium di kanan -->
<div class="btn-group ml-2">
<button type="button" class="btn btn-warning btn-sm" onclick="window.open('auto_login_genieacs.php', '_blank')">
<i class="fas fa-cloud"></i> ACS
</button>
<button type="button" class="btn btn-secondary btn-sm" onclick="window.open('auto_login_observium.php', '_blank')">
<i class="fas fa-chart-line"></i> Observium
</button>
</div>
</div>
<!-- Map Zoom Controls -->
<div class="map-zoom-controls">
<button type="button" class="btn btn-outline-secondary btn-sm" onclick="zoomToItems()" title="Zoom ke Semua Item">
<i class="fas fa-expand-arrows-alt"></i>
</button>
<button type="button" class="btn btn-outline-secondary btn-sm" onclick="locateUser()" title="Cari Lokasi Saya">
<i class="fas fa-location-arrow"></i>
</button>
<button type="button" class="btn btn-outline-secondary btn-sm" onclick="map.setView([-2.5, 118], 5)" title="Zoom ke Indonesia">
<i class="fas fa-wifi"></i>
</button>
<button type="button" class="btn btn-outline-secondary btn-sm" onclick="map.zoomIn()" title="Zoom In (+)">
<i class="fas fa-plus"></i>
</button>
<button type="button" class="btn btn-outline-secondary btn-sm" onclick="map.zoomOut()" title="Zoom Out (-)">
<i class="fas fa-minus"></i>
</button>
</div>
</div>
</div>
<div class="card-body p-0">
<div id="map" style="height: 600px;"></div>
</div>
</div>
</div>
</div>
<!-- Statistics Cards -->
<div class="row">
<div class="col-lg-2 col-6">
<div class="small-box bg-info">
<div class="inner">
<h3><?= $olt ?></h3>
<p>OLT</p>
</div>
<div class="icon">
<i class="fas fa-cloud"></i>
</div>
</div>
</div>
<div class="col-lg-2 col-6">
<div class="small-box bg-success">
<div class="inner">
<h3><?= $tiang ?></h3>
<p>Tiang</p>
</div>
<div class="icon">
<i class="fas fa-tower-broadcast"></i>
</div>
</div>
</div>
<div class="col-lg-2 col-6">
<div class="small-box bg-warning">
<div class="inner">
<h3><?= $odp ?></h3>
<p>ODP</p>
</div>
<div class="icon">
<i class="fas fa-project-diagram"></i>
</div>
</div>
</div>
<div class="col-lg-2 col-6">
<div class="small-box bg-danger">
<div class="inner">
<h3><?= $odc ?></h3>
<p>ODC</p>
</div>
<div class="icon">
<i class="fas fa-network-wired"></i>
</div>
</div>
</div>
<div class="col-lg-2 col-6">
<div class="small-box bg-primary">
<div class="inner">
<h3><?= $pelanggan ?></h3>
<p>Pelanggan</p>
</div>
<div class="icon">
<i class="fas fa-wifi"></i>
</div>
</div>
</div>
<div class="col-lg-2 col-6">
<div class="small-box bg-secondary">
<div class="inner">
<h3><?= $routes ?></h3>
<p>Routes</p>
</div>
<div class="icon">
<i class="fas fa-route"></i>
</div>
</div>
</div>
</div>
</div>
</section>
</div>
<!-- Footer -->
<footer class="main-footer">
<strong>Copyright &copy; 2025 <a href="#">FTTH Megadata</a> by Kuli Jaringan.</strong>
Semua hak dilindungi undang-undang.
<div class="float-right d-none d-sm-inline-block">
<b>Versi</b> 1.0.0
</div>
</footer>
</div>
<!-- Add/Edit Item Modal -->
<div class="modal fade" id="itemModal" tabindex="-1" role="dialog">
<div class="modal-dialog modal-lg" role="document">
<div class="modal-content">
<div class="modal-header">
<h4 class="modal-title" id="itemModalTitle">Tambah Item FTTH</h4>
<button type="button" class="close" data-dismiss="modal">
<span>&times;</span>
</button>
</div>
<form id="itemForm">
<div class="modal-body">
<input type="hidden" id="itemId" name="id">
<!-- Pilihan Jenis Item -->
<div class="row">
<div class="col-md-6">
<div class="form-group">
<label for="itemType">Jenis Item</label>
<select class="form-control" id="itemType" name="item_type" required>
<option value="">Pilih Jenis Item</option>
<option value="1">OLT</option>
<option value="2">Tiang Tumpu</option>
<option value="3">ODP</option>
<option value="4">ODC</option>
<option value="5">Pelanggan</option>
</select>
</div>
</div>
<div class="col-md-6">
<div class="form-group">
<label for="itemName">Nama Item</label>
<input type="text" class="form-control" id="itemName" name="name" required>
</div>
</div>
</div>
<!-- Form Umum -->
<div id="generalFields">
<div class="form-group">
<label for="itemDescription">Deskripsi</label>
<textarea class="form-control" id="itemDescription" name="description" rows="3"></textarea>
</div>
<div class="form-group">
<label for="itemAddress">Alamat</label>
<textarea class="form-control" id="itemAddress" name="address" rows="2"></textarea>
</div>
<div class="row">
<div class="col-md-6">
<div class="form-group">
<label for="itemLat">Latitude <span class="text-danger">*</span></label>
<input type="number" step="any" class="form-control" id="itemLat" name="latitude" placeholder="Contoh: -6.2088">
</div>
</div>
<div class="col-md-6">
<div class="form-group">
<label for="itemLng">Longitude <span class="text-danger">*</span></label>
<input type="number" step="any" class="form-control" id="itemLng" name="longitude" placeholder="Contoh: 106.8456">
</div>
</div>
</div>
</div>
<!-- Form Warna Tube & Core -->
<div id="colorFields">
<div class="row">
<div class="col-md-6">
<div class="form-group">
<label for="tubeColor">Warna Tube</label>
<select class="form-control" id="tubeColor" name="tube_color_id">
<option value="">Pilih Warna Tube</option>
</select>
</div>
</div>
<div class="col-md-6">
<div class="form-group">
<label for="coreColor">Warna Core</label>
<select class="form-control" id="coreColor" name="core_color_id">
<option value="">Pilih Warna Core</option>
</select>
</div>
</div>
</div>
</div>
<!-- Form Kabel -->
<div id="cableFields">
<div class="row">
<div class="col-md-6">
<div class="form-group">
<label for="cableType">Jenis Kabel</label>
<select class="form-control" id="cableType" name="item_cable_type">
<option value="">Pilih Jenis Kabel</option>
<option value="backbone">Backbone</option>
<option value="distribution">Distribution</option>
<option value="drop_core">Drop Core</option>
<option value="feeder">Feeder</option>
<option value="branch">Branch</option>
</select>
</div>
</div>
<div class="col-md-6">
<div class="form-group">
<label for="totalCoreCapacity">Kapasitas Core Total</label>
<select class="form-control" id="totalCoreCapacity" name="total_core_capacity">
<option value="1">1 Core</option>
<option value="2">2 Core</option>
<option value="4">4 Core</option>
<option value="6">6 Core</option>
<option value="8">8 Core</option>
<option value="12">12 Core</option>
<option value="24" selected>24 Core</option>
<option value="48">48 Core</option>
<option value="72">72 Core</option>
<option value="96">96 Core</option>
<option value="144">144 Core</option>
<option value="216">216 Core</option>
<option value="288">288 Core</option>
</select>
</div>
</div>
</div>
</div>
<!-- Form Core -->
<div id="coreFields">
<div class="row">
<div class="col-md-6">
<div class="form-group">
<label for="coreUsed">Core yang Digunakan</label>
<input type="number" class="form-control" id="coreUsed" name="core_used" min="0" max="288">
</div>
</div>
<div class="col-md-6">
<div class="form-group">
<label>Core Tersedia</label>
<input type="text" class="form-control" id="coreAvailable" readonly>
</div>
</div>
</div>
</div>
<!-- Form Splitter -->
<div id="splitterFields">
<div class="row">
<div class="col-md-6">
<div class="form-group">
<label for="splitterMain">Splitter Jaringan Utama</label>
<select class="form-control" id="splitterMain" name="splitter_main_id">
<option value="">Pilih Splitter Utama</option>
</select>
</div>
</div>
<div class="col-md-6">
<div class="form-group">
<label for="splitterOdp">Splitter ODP</label>
<select class="form-control" id="splitterOdp" name="splitter_odp_id">
<option value="">Pilih Splitter ODP</option>
</select>
</div>
</div>
</div>
</div>
<!-- Form Serial Number & RX Power -->
<div id="ontFields">
<div class="form-group">
<label for="serial_number">ID GenieACS TR069</label>
<input type="text" class="form-control" id="serial_number" name="serial_number" placeholder="Contoh: 00259E-HG8546M-485754431A8E6A9C">
</div>
<div class="form-group">
<label for="rx_power">RX Power (dBm)</label>
<input type="number" step="0.01" class="form-control" id="rx_power" name="rx_power" placeholder="Otomatis dari GenieACS" readonly>
</div>
</div>
<!-- Form Status -->
<div id="statusFields">
<div class="form-group">
<label for="itemStatus">Status</label>
<select class="form-control" id="itemStatus" name="status">
<option value="active">Aktif</option>
<option value="inactive">Tidak Aktif</option>
<option value="maintenance">Maintenance</option>
</select>
</div>
</div>
<!-- Form isi data ONU -->
<div class="form-group">
<label for="sn_onu">SN ONU (OLT)</label>
<input type="text" class="form-control" id="sn_onu" name="sn_onu" placeholder="Contoh: HWTC6B926A9C">
</div>
<!-- Form isi nomor telfon -->
<div class="form-group">
<label>Nomor HP Pelanggan</label>
<input type="text" name="customer_phone" class="form-control" placeholder="628xxxxxxxxxx">
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-dismiss="modal">Batal</button>
<button type="submit" class="btn btn-primary">Simpan</button>
</div>
</form>
</div>
</div>
</div>
<script>
document.addEventListener('DOMContentLoaded', function() {
const itemType = document.getElementById('itemType');
const ontFields = document.getElementById('ontFields');
const generalFields = document.getElementById('generalFields');
const colorFields = document.getElementById('colorFields');
const cableFields = document.getElementById('cableFields');
const coreFields = document.getElementById('coreFields');
const splitterFields = document.getElementById('splitterFields');
const statusFields = document.getElementById('statusFields');
const addressField = document.getElementById('itemAddress');
const addressWrapper = addressField.closest('.form-group');
const latField = document.getElementById('itemLat').closest('.col-md-6');
const lngField = document.getElementById('itemLng').closest('.col-md-6');
const descriptionField = document.getElementById('description')?.closest('.form-group'); // Deskripsi
//editan saya
window.toggleFields = function () {
const value = itemType.value;
// Semua field default tampil
generalFields.style.display = '';
colorFields.style.display = '';
cableFields.style.display = '';
coreFields.style.display = '';
splitterFields.style.display = '';
statusFields.style.display = '';
addressWrapper.style.display = '';
if (descriptionField) descriptionField.style.display = '';
latField.style.display = '';
lngField.style.display = '';
ontFields.style.display = 'none';
if (value === '5') {
// === Pelanggan ===
// Semua field tampil termasuk ONT & RX Power
ontFields.style.display = '';
}
else if (value === '2') {
// === Tiang Tumpu ===
// Hanya tampilkan Nama Item, Jenis Item, Latitude & Longitude
generalFields.style.display = ''; // Nama & Jenis item tetap tampil
latField.style.display = '';
lngField.style.display = '';
// Sembunyikan deskripsi, alamat, dan field lainnya
if (descriptionField) descriptionField.style.display = 'none';
addressWrapper.style.display = 'none';
colorFields.style.display = 'none';
cableFields.style.display = 'none';
coreFields.style.display = 'none';
splitterFields.style.display = 'none';
statusFields.style.display = 'none';
ontFields.style.display = 'none';
}
else if (value === '3' || value === '4') {
// === ODP & ODC ===
addressWrapper.style.display = 'none';
ontFields.style.display = 'none';
}
else {
// === OLT ===
// Semua field tampil kecuali ONT
ontFields.style.display = 'none';
}
}
// Jalankan toggle pertama kali
itemType.addEventListener('change', toggleFields);
toggleFields();
// === Script Otomatis Mengambil RX Power ===
const snInput = document.getElementById('serial_number');
const rxPowerInput = document.getElementById('rx_power');
snInput.addEventListener('change', function() {
const sn = this.value.trim();
if (!sn) return;
fetch('get_rx_power.php?sn=' + encodeURIComponent(sn))
.then(response => {
if (!response.ok) throw new Error('Network response was not OK');
return response.json();
})
.then(data => {
if (data.rx_power !== null) {
rxPowerInput.value = data.rx_power;
} else {
alert('⚠ ' + (data.error || 'RX Power tidak ditemukan'));
rxPowerInput.value = '';
}
})
.catch(error => {
alert('❌ Gagal mengambil RX Power dari GenieACS');
rxPowerInput.value = '';
});
});
});
</script>
<!-- jQuery -->
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
<!-- Bootstrap 4 -->
<script src="https://cdn.jsdelivr.net/npm/bootstrap@4.6.2/dist/js/bootstrap.bundle.min.js"></script>
<!-- AdminLTE App -->
<script src="https://cdn.jsdelivr.net/npm/admin-lte@3.2/dist/js/adminlte.min.js"></script>
<!-- Leaflet JS (Latest Version) -->
<script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"></script>
<!-- Leaflet Fullscreen Plugin -->
<script src="https://unpkg.com/leaflet-fullscreen@1.0.1/dist/leaflet.fullscreen.js"></script>
<!-- Leaflet Routing Machine -->
<script src="https://unpkg.com/leaflet-routing-machine@3.2.12/dist/leaflet-routing-machine.js"></script>
<!-- JSZip for KMZ compression -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/jszip/3.10.1/jszip.min.js"></script>
<!-- FileSaver for download -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/FileSaver.js/2.0.5/FileSaver.min.js"></script>
<!-- Custom JS -->
<!--
<script>
$.document().ready(function() {
alert('dasd')
})
</script>
-->
<script src="assets/js/map.js"></script>
<script src="assets/js/app.js"></script>
<script src="assets/js/kmz-export.js"></script>
<link rel="stylesheet" href="https://unpkg.com/leaflet-control-geocoder/dist/Control.Geocoder.css" />
<script src="https://unpkg.com/leaflet-control-geocoder/dist/Control.Geocoder.js"></script>
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
</body>
</html>

132
login.php Normal file
View File

@ -0,0 +1,132 @@
<?php
session_start();
require 'config/database.php'; // memanggil class Database
// Buat koneksi PDO
$db = new Database();
$conn = $db->getConnection();
if ($_SERVER['REQUEST_METHOD'] == 'POST') {
$username = trim($_POST['username']);
$password = trim($_POST['password']);
try {
$stmt = $conn->prepare("SELECT * FROM users WHERE username = ?");
$stmt->execute([$username]);
$user = $stmt->fetch(PDO::FETCH_ASSOC);
if ($user && password_verify($password, $user['password'])) {
// Simpan data penting ke session
$_SESSION['user_id'] = $user['id']; // <-- ditambahkan
$_SESSION['user'] = $user['username'];
$_SESSION['role'] = $user['role'];
header("Location: index.php");
exit;
} else {
$error = "Username atau password salah!";
}
} catch (PDOException $e) {
$error = "Terjadi kesalahan: " . $e->getMessage();
}
}
?>
<!DOCTYPE html>
<html lang="id">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Login - FTTH Megadata</title>
<style>
body {
margin: 0;
font-family: Arial, sans-serif;
height: 100vh;
background: url('bg.jpg') no-repeat center center fixed;
background-size: cover;
display: flex;
align-items: center;
justify-content: center;
padding: 15px;
}
.login-container {
background: rgba(255, 255, 255, 0.9);
padding: 40px;
border-radius: 15px;
box-shadow: 0 0 15px rgba(0,0,0,0.2);
text-align: center;
width: 320px;
max-width: 100%;
}
.login-container img {
max-width: 150px;
height: auto;
border-radius: 0;
object-fit: contain;
margin-bottom: 15px;
}
.login-container h2 {
margin-bottom: 20px;
color: #333;
}
.login-container input {
width: 100%;
padding: 12px;
margin: 8px 0;
border: none;
border-radius: 30px;
background: #f1f1f1;
outline: none;
font-size: 14px;
}
.login-container button {
width: 100%;
padding: 12px;
border: none;
border-radius: 30px;
background: #333;
color: #fff;
font-size: 14px;
cursor: pointer;
}
.login-container button:hover {
background: #555;
}
.login-container p {
font-size: 12px;
margin-top: 15px;
}
.login-container a {
color: #0066cc;
text-decoration: none;
}
.error {
color: red;
margin-bottom: 10px;
}
/* Responsif untuk layar kecil */
@media (max-width: 480px) {
.login-container {
padding: 25px;
width: 100%;
}
.login-container img {
max-width: 100px;
}
}
</style>
</head>
<body>
<div class="login-container">
<img src="megadata.jpeg" alt="Logo">
<h2>Login</h2>
<?php if (!empty($error)) echo "<p class='error'>$error</p>"; ?>
<form method="POST">
<input type="text" name="username" placeholder="Username" required>
<input type="password" name="password" placeholder="Password" required>
<button type="submit">Login</button>
</form>
</div>
</body>
</html>

5
logout.php Normal file
View File

@ -0,0 +1,5 @@
<?php
session_start();
session_destroy();
header("Location: login.php");
exit;

203
manajemen_users.php Normal file
View File

@ -0,0 +1,203 @@
<?php
session_start();
require 'config/database.php'; // gunakan class Database
// Buat koneksi PDO
$db = new Database();
$conn = $db->getConnection();
// CREATE
if (isset($_POST['create'])) {
$username = trim($_POST['username']);
$password = password_hash(trim($_POST['password']), PASSWORD_BCRYPT);
$role = $_POST['role'];
$stmt = $conn->prepare("INSERT INTO users (username, password, role) VALUES (?, ?, ?)");
$stmt->execute([$username, $password, $role]);
header("Location: manajemen_users.php");
exit;
}
// UPDATE
if (isset($_POST['update'])) {
$id = $_POST['id'];
$username = trim($_POST['username']);
$role = $_POST['role'];
if (!empty($_POST['password'])) {
$password = password_hash(trim($_POST['password']), PASSWORD_BCRYPT);
$stmt = $conn->prepare("UPDATE users SET username=?, password=?, role=? WHERE id=?");
$stmt->execute([$username, $password, $role, $id]);
} else {
$stmt = $conn->prepare("UPDATE users SET username=?, role=? WHERE id=?");
$stmt->execute([$username, $role, $id]);
}
header("Location: manajemen_users.php");
exit;
}
// DELETE
if (isset($_GET['delete'])) {
$id = $_GET['delete'];
$stmt = $conn->prepare("DELETE FROM users WHERE id=?");
$stmt->execute([$id]);
header("Location: manajemen_users.php");
exit;
}
// READ
$stmt = $conn->query("SELECT * FROM users");
$users = $stmt->fetchAll(PDO::FETCH_ASSOC);
?>
<!DOCTYPE html>
<html lang="id">
<head>
<meta charset="UTF-8">
<title>Manajemen Users - FTTH Megadata</title>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css">
<style>
body {
margin: 0;
font-family: Arial, sans-serif;
height: 100vh;
background: url('bg.jpg') no-repeat center center fixed;
background-size: cover;
display: flex;
align-items: center;
justify-content: center;
padding: 15px;
}
.users-container {
background: rgba(255, 255, 255, 0.92);
padding: 30px;
border-radius: 15px;
box-shadow: 0 0 15px rgba(0,0,0,0.2);
width: 90%;
max-width: 900px;
}
h2 {
text-align: center;
margin-bottom: 20px;
color: #333;
}
.table thead {
background: #333;
color: #fff;
}
.btn-custom {
border-radius: 30px;
padding: 8px 16px;
}
</style>
</head>
<body>
<div class="users-container">
<h2>👥 Manajemen Users</h2>
<!-- Link ke Register -->
<div class="mb-3 text-end">
<a href="register.php" class="btn btn-success btn-custom">+ Daftar User Baru</a>
</div>
<!--
Form Tambah User
<div class="card mb-4">
<div class="card-header">Tambah User</div>
<div class="card-body">
<form method="POST">
<div class="row">
<div class="col-md-4 mb-2">
<input type="text" name="username" class="form-control" placeholder="Username" required>
</div>
<div class="col-md-4 mb-2">
<input type="password" name="password" class="form-control" placeholder="Password" required>
</div>
<div class="col-md-3 mb-2">
<select name="role" class="form-control" required>
<option value="admin">Admin</option>
<option value="user">User</option>
</select>
</div>
<div class="col-md-1 mb-2">
<button type="submit" name="create" class="btn btn-primary w-100">Tambah</button>
</div>
</div>
</form>
</div>
</div>
-->
<!-- Tabel Users -->
<div class="table-responsive">
<table class="table table-bordered align-middle text-center">
<thead>
<tr>
<th>ID</th>
<th>Username</th>
<th>Role</th>
<th style="width: 180px;">Aksi</th>
</tr>
</thead>
<tbody>
<?php foreach ($users as $row): ?>
<tr>
<td><?= $row['id'] ?></td>
<td><?= $row['username'] ?></td>
<td><?= $row['role'] ?></td>
<td>
<!-- Tombol Edit -->
<button class="btn btn-primary btn-sm" data-bs-toggle="modal" data-bs-target="#edit<?= $row['id'] ?>">Edit</button>
<!-- Tombol Hapus -->
<a href="?delete=<?= $row['id'] ?>" class="btn btn-danger btn-sm" onclick="return confirm('Yakin hapus user ini?')">Hapus</a>
</td>
</tr>
<!-- Modal Edit -->
<div class="modal fade" id="edit<?= $row['id'] ?>" tabindex="-1">
<div class="modal-dialog">
<div class="modal-content">
<form method="POST">
<div class="modal-header">
<h5 class="modal-title">Edit User</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<input type="hidden" name="id" value="<?= $row['id'] ?>">
<div class="mb-2">
<input type="text" name="username" class="form-control" value="<?= $row['username'] ?>" required>
</div>
<div class="mb-2">
<input type="password" name="password" class="form-control" placeholder="Password baru (opsional)">
</div>
<div class="mb-2">
<select name="role" class="form-control">
<option value="admin" <?= $row['role']=="admin"?"selected":"" ?>>Admin</option>
<option value="user" <?= $row['role']=="user"?"selected":"" ?>>User</option>
</select>
</div>
</div>
<div class="modal-footer">
<button type="submit" name="update" class="btn btn-primary">Simpan</button>
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Batal</button>
</div>
</form>
</div>
</div>
</div>
<?php endforeach; ?>
</tbody>
</table>
</div>
<!-- Tombol kembali ke dashboard -->
<div class="text-center mt-3">
<a href="index.php" class="btn btn-dark btn-custom"> Kembali ke Dashboard</a>
</div>
</div>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/js/bootstrap.bundle.min.js"></script>
</body>
</html>

BIN
megadata.jpeg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

153
register.php Normal file
View File

@ -0,0 +1,153 @@
<?php
session_start();
require 'config/database.php';
// Buat koneksi database
$database = new Database();
$conn = $database->getConnection();
$success = "";
$error = "";
if ($_SERVER['REQUEST_METHOD'] == 'POST') {
$username = trim($_POST['username']);
$password_plain = trim($_POST['password']);
$role = $_POST['role'];
// Validasi sederhana
if (empty($username) || empty($password_plain) || empty($role)) {
$error = "Semua field wajib diisi.";
} else {
$password = password_hash($password_plain, PASSWORD_DEFAULT);
try {
$stmt = $conn->prepare("INSERT INTO users (username, password, role) VALUES (?, ?, ?)");
$stmt->execute([$username, $password, $role]);
$success = "Pendaftaran berhasil! Silakan login.";
} catch (PDOException $e) {
if ($e->getCode() == 23000) { // duplicate entry
$error = "Username sudah digunakan.";
} else {
$error = "Terjadi kesalahan: " . $e->getMessage();
}
}
}
}
?>
<!DOCTYPE html>
<html lang="id">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Register - FTTH Megadata</title>
<style>
body {
margin: 0;
font-family: Arial, sans-serif;
height: 100vh;
background: url('bg.jpg') no-repeat center center fixed;
background-size: cover;
display: flex;
align-items: center;
justify-content: center;
padding: 15px;
}
.register-container {
background: rgba(255, 255, 255, 0.9);
padding: 40px;
border-radius: 15px;
box-shadow: 0 0 15px rgba(0,0,0,0.2);
text-align: center;
width: 320px;
max-width: 100%;
}
.register-container img {
max-width: 150px;
height: auto;
object-fit: contain;
margin-bottom: 15px;
}
.register-container h2 {
margin-bottom: 20px;
color: #333;
}
.register-container input,
.register-container select {
width: 100%;
padding: 12px;
margin: 8px 0;
border: none;
border-radius: 30px;
background: #f1f1f1;
outline: none;
font-size: 14px;
}
.register-container button {
width: 100%;
padding: 12px;
border: none;
border-radius: 30px;
background: #333;
color: #fff;
font-size: 14px;
cursor: pointer;
}
.register-container button:hover {
background: #555;
}
.register-container p {
font-size: 12px;
margin-top: 15px;
}
.register-container a {
color: #0066cc;
text-decoration: none;
}
.success {
color: green;
margin-bottom: 10px;
}
.error {
color: red;
margin-bottom: 10px;
}
@media (max-width: 480px) {
.register-container {
padding: 25px;
}
.register-container img {
max-width: 100px;
}
}
</style>
</head>
<body>
<div class="register-container">
<img src="megadata.jpeg" alt="Logo">
<h2>Daftar Akun</h2>
<?php if (!empty($success)) echo "<p class='success'>$success</p>"; ?>
<?php if (!empty($error)) echo "<p class='error'>$error</p>"; ?>
<form method="POST">
<input type="text" name="username" placeholder="Username" required>
<input type="password" name="password" placeholder="Password" required>
<select name="role" required>
<option value="">Pilih Role</option>
<option value="engineer">Superadmin</option>
<option value="teknisi1">Site Bungatan</option>
<option value="teknisi2">Site Besuki</option>
<option value="teknisi3">Site jatisari</option>
<option value="teknisi4">Site Kendit</option>
<option value="teknisi5">Site Gending</option>
<option value="teknisi6">Site Bangkalan</option>
<option value="teknisi7">Site Bondowoso</option>
<option value="teknisi8">Site Arjasa</option>
<option value="teknisi9">Site Lamongan</option>
<option value="teknisi10">Site Asembagus</option>
</select>
<button type="submit">Daftar</button>
</form>
<p>Sudah punya akun? <a href="login.php">Login di sini</a></p>
</div>
</body>
</html>

100
setup_instructions.md Normal file
View File

@ -0,0 +1,100 @@
# Instruksi Setup FTTH Planner
## Langkah 1: Persiapan XAMPP
1. **Download XAMPP**: Unduh dari https://www.apachefriends.org/
2. **Install XAMPP**: Install di `C:\xampp\`
3. **Start Services**:
- Buka XAMPP Control Panel
- Start **Apache** dan **MySQL**
## Langkah 2: Setup Database
1. **Buka phpMyAdmin**:
- Browser → `http://localhost/phpmyadmin`
- Login tanpa password (default)
2. **Buat Database**:
- Klik "New" di sidebar kiri
- Nama database: `ftth_planner`
- Collation: `utf8_general_ci`
- Klik "Create"
3. **Import Database**:
- Pilih database `ftth_planner`
- Tab "Import"
- Choose file: pilih `database.sql`
- Klik "Go"
## Langkah 3: Copy File Aplikasi
1. **Copy folder**: Copy semua file ke `C:\xampp\htdocs\ftthplanner\`
2. **Struktur folder harus**:
```
C:\xampp\htdocs\ftthplanner\
├── index.php
├── config/database.php
├── api/ (semua file API)
├── assets/ (CSS & JS)
├── database.sql
└── README.md
```
## Langkah 4: Test Aplikasi
1. **Buka Browser**: `http://localhost/ftthplanner`
2. **Cek Map**: Pastikan peta OpenStreetMaps muncul
3. **Test Add Item**: Klik di peta → isi form → simpan
4. **Test Drag**: Drag marker yang sudah dibuat
5. **Test Route**: Klik "Mode Routing" → pilih 2 item
## Troubleshooting Umum
### Error: "Connection failed"
- Pastikan MySQL service berjalan di XAMPP
- Cek username/password di `config/database.php`
### Error: "Table doesn't exist"
- Import ulang `database.sql`
- Pastikan database name = `ftth_planner`
### Map tidak muncul
- Cek koneksi internet
- Allow JavaScript di browser
- Cek console browser (F12) untuk error
### Item tidak bisa disimpan
- Cek permission folder (777 di Linux/Mac)
- Pastikan semua field required diisi
- Lihat Network tab di browser untuk error API
## Konfigurasi Lanjutan
### Ganti Default Location Map
Edit di `assets/js/map.js` baris 11:
```javascript
map = L.map('map').setView([-6.2088, 106.8456], 11);
```
Ganti koordinat dengan lokasi yang diinginkan.
### Tambah Warna Tube
Insert ke database:
```sql
INSERT INTO tube_colors (color_name, hex_code) VALUES ('Nama Warna', '#HEX_CODE');
```
### Tambah Jenis Splitter
Insert ke database:
```sql
INSERT INTO splitter_types (type, ratio) VALUES ('main', '1:6');
```
## Kontak Support
Jika mengalami masalah, dokumentasikan:
1. Versi Windows
2. Versi XAMPP
3. Error message lengkap
4. Screenshot jika perlu
Aplikasi sudah siap digunakan! 🎉

144
testing_guide.md Normal file
View File

@ -0,0 +1,144 @@
# Panduan Testing FTTH Planner
## ✅ Masalah yang Telah Diperbaiki
### 1. **Item Pelanggan/End User**
- ✅ Tambah item type "Pelanggan" dengan icon rumah (🏠)
- ✅ Warna orange (#FFA500) untuk pembeda
- ✅ Menu sidebar "Tambah Pelanggan"
- ✅ Form dapat memilih "Pelanggan"
- ✅ Statistik menampilkan jumlah pelanggan
### 2. **Routing Cable (From-To)**
- ✅ Perbaiki bug routing yang tidak berfungsi
- ✅ Tambah error handling dan logging
- ✅ Fallback ke garis lurus jika routing gagal
- ✅ Console log untuk debugging
## 🔧 Cara Setup & Update
### Jika Database Sudah Ada
Jalankan file `update_database.sql` di phpMyAdmin untuk menambah item type Pelanggan:
```sql
INSERT IGNORE INTO item_types (id, name, icon, color) VALUES (5, 'Pelanggan', 'fas fa-home', '#FFA500');
```
### Jika Database Baru
Import file `database.sql` yang sudah diupdate dengan item Pelanggan.
## 🧪 Testing Checklist
### Test 1: Item Pelanggan
1. **Buka aplikasi**: `http://localhost/ftthplanner`
2. **Cek sidebar**: Menu "Tambah Pelanggan" ada dengan icon 🏠
3. **Klik "Tambah Pelanggan"**:
- Form modal terbuka
- Item Type otomatis terisi "Pelanggan"
- Isi nama: "Rumah Pak Budi"
- Klik di peta untuk set lokasi
- Klik "Simpan"
4. **Verifikasi**:
- Marker orange dengan icon rumah muncul di peta
- Statistik "Pelanggan" bertambah
- Popup info benar saat diklik marker
### Test 2: Routing Cable
1. **Buat minimal 2 item** (misal: 1 OLT + 1 Pelanggan)
2. **Klik tombol "Mode Routing"** di header card
3. **Klik marker pertama** (misal OLT)
4. **Klik marker kedua** (misal Pelanggan)
5. **Verifikasi**:
- Garis route muncul (kuning putus-putus)
- Notifikasi "Route berhasil dibuat"
- Statistik "Routes" bertambah
### Test 3: Routing Alternative
Jika routing normal gagal, akan otomatis fallback ke garis lurus:
1. **Buka Console Browser** (F12)
2. **Coba buat route**
3. **Cek console log**:
- "Creating route from [lat,lng] to [lat,lng]"
- Jika ada error: "Leaflet Routing Machine not available"
- "Route sederhana berhasil dibuat"
### Test 4: Drag & Drop
1. **Drag marker** yang sudah dibuat ke lokasi baru
2. **Verifikasi**: Notifikasi "Posisi item berhasil dipindahkan"
### Test 5: Edit/Delete
1. **Klik marker → Edit**: Form terisi data lama
2. **Update** dan simpan: Data terupdate
3. **Klik marker → Hapus**: Item terhapus dari peta
## 🐛 Troubleshooting
### Routing Tidak Berfungsi
**Gejala**: Tidak ada garis route muncul setelah klik 2 marker
**Solusi**:
1. **Cek Console** (F12):
- Error loading Leaflet Routing Machine?
- Network error ke OSRM service?
2. **Cek Internet**: Routing butuh koneksi untuk akses map service
3. **Fallback**: Sistem otomatis buat garis lurus jika routing gagal
**Test Manual**:
```javascript
// Di console browser
console.log(typeof L.Routing); // Should show 'object'
console.log(markers); // Should show object with marker IDs
```
### Item Pelanggan Tidak Muncul
**Solusi**:
1. **Cek Database**: Pastikan item_types id=5 ada
2. **Hard Refresh**: Ctrl+F5 untuk reload cache
3. **Cek Console**: Error JavaScript?
### Statistik Tidak Update
**Gejala**: Angka statistik tidak berubah setelah tambah item
**Solusi**:
1. **Refresh halaman**: F5
2. **Cek API**: `http://localhost/ftthplanner/api/statistics.php`
3. **Cek Database**: Data tersimpan di tabel ftth_items?
## 📊 Expected Results
### Statistik Dashboard
- **OLT**: Jumlah OLT yang dibuat
- **Tiang**: Jumlah Tiang Tumpu
- **ODP**: Jumlah ODP
- **ODC**: Jumlah ODC
- **Pelanggan**: Jumlah Pelanggan ⭐ (BARU)
- **Routes**: Jumlah route cable ⭐ (BARU)
### Legend Map
Harus menampilkan 5 item:
- 🔴 OLT
- 🔵 Tiang Tumpu
- 🔵 ODP
- 🟢 ODC
- 🟠 Pelanggan ⭐ (BARU)
### Routing Visual
- **Garis hijau solid**: Route terpasang
- **Garis kuning putus**: Route perencanaan ⭐
- **Garis merah putus**: Route maintenance
## ✅ Success Criteria
**✅ BERHASIL** jika:
1. Item Pelanggan bisa dibuat dan muncul di peta
2. Routing berfungsi (garis muncul antara 2 marker)
3. Statistik update otomatis
4. Drag & drop masih berfungsi
5. Edit/delete masih berfungsi
**❌ GAGAL** jika:
1. Error JavaScript di console
2. Marker tidak muncul setelah simpan
3. Routing sama sekali tidak ada response
4. Database error di API calls
Silakan test sesuai checklist di atas dan laporkan hasil atau error yang ditemukan! 🚀

8
update_database.sql Normal file
View File

@ -0,0 +1,8 @@
-- Script untuk menambahkan item type Pelanggan ke database yang sudah ada
-- Jalankan script ini jika database sudah dibuat sebelumnya
-- Tambah item type Pelanggan jika belum ada
INSERT IGNORE INTO item_types (id, name, icon, color) VALUES (5, 'Pelanggan', 'fas fa-home', '#FFA500');
-- Cek hasil
SELECT * FROM item_types;

BIN
wifi.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 44 KiB