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