Compare commits
3 Commits
c8a62f9b88
...
0281397c6d
| Author | SHA1 | Date |
|---|---|---|
|
|
0281397c6d | |
|
|
f43f0f5afe | |
|
|
371ca32928 |
|
|
@ -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>
|
||||
|
|
@ -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
|
||||
|
|
@ -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.
|
||||
|
|
@ -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. 🌍
|
||||
|
|
@ -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.
|
||||
|
|
@ -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.
|
||||
|
|
@ -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 ✅";
|
||||
?>
|
||||
|
|
@ -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>";
|
||||
}
|
||||
|
|
@ -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]);
|
||||
}
|
||||
|
|
@ -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>";
|
||||
}
|
||||
}
|
||||
|
|
@ -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>";
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
@ -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 ✅";
|
||||
?>
|
||||
|
|
@ -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>";
|
||||
}
|
||||
|
|
@ -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.
|
||||
|
||||
|
|
@ -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>";
|
||||
|
|
@ -0,0 +1 @@
|
|||
Not found
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
|
|
@ -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'
|
||||
]
|
||||
]);
|
||||
|
|
@ -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,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";
|
||||
|
|
@ -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'
|
||||
]);
|
||||
}
|
||||
|
|
@ -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
|
||||
]);
|
||||
|
|
@ -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
|
||||
]);
|
||||
|
|
@ -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
|
||||
]);
|
||||
|
|
@ -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);
|
||||
?>
|
||||
|
|
@ -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
|
||||
]);
|
||||
|
|
@ -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
|
|
@ -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);
|
||||
|
|
@ -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);
|
||||
?>
|
||||
|
|
@ -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);
|
||||
?>
|
||||
|
|
@ -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);
|
||||
?>
|
||||
|
|
@ -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; }
|
||||
}
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 15 KiB |
File diff suppressed because it is too large
Load Diff
|
|
@ -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, '&')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
.replace(/"/g, '"')
|
||||
.replace(/'/g, ''');
|
||||
}
|
||||
|
||||
// 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;
|
||||
File diff suppressed because it is too large
Load Diff
|
|
@ -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();
|
||||
?>
|
||||
|
|
@ -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();
|
||||
?>
|
||||
|
|
@ -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)
|
||||
|
|
@ -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)
|
||||
|
|
@ -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>
|
||||
`
|
||||
: ''}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
?>
|
||||
|
|
@ -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');
|
||||
File diff suppressed because it is too large
Load Diff
|
|
@ -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()
|
||||
]);
|
||||
}
|
||||
|
|
@ -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);
|
||||
|
|
@ -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;
|
||||
}
|
||||
?>
|
||||
|
|
@ -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 © 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>×</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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
<?php
|
||||
session_start();
|
||||
session_destroy();
|
||||
header("Location: login.php");
|
||||
exit;
|
||||
|
|
@ -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>
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 20 KiB |
|
|
@ -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>
|
||||
|
|
@ -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! 🎉
|
||||
|
|
@ -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! 🚀
|
||||
|
|
@ -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;
|
||||
Loading…
Reference in New Issue