Fix: initial commit
This commit is contained in:
commit
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,66 @@
|
|||
<?php
|
||||
require_once __DIR__ . '/../config/database.php';
|
||||
header('Content-Type: application/json');
|
||||
header('Access-Control-Allow-Origin: *');
|
||||
//header('Content-Type: application/json');
|
||||
|
||||
|
||||
$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'];
|
||||
|
||||
// Ambil RX Power dan PPPoE IP dari API
|
||||
$rxPower = null;
|
||||
$pppoeIP = null;
|
||||
|
||||
if ($serialNumber) {
|
||||
$query = json_encode(["_id" => $serialNumber]);
|
||||
$url = "http://localhost: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);
|
||||
|
||||
// Ambil RX Power
|
||||
if (isset($data[0]['VirtualParameters']['RXPower']['_value'])) {
|
||||
$rxPower = $data[0]['VirtualParameters']['RXPower']['_value'] . " dBm";
|
||||
}
|
||||
|
||||
// Ambil PPPoE IP
|
||||
if (isset($data[0]['VirtualParameters']['pppoeIP']['_value'])) {
|
||||
$pppoeIP = $data[0]['VirtualParameters']['pppoeIP']['_value'];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Output JSON untuk AJAX
|
||||
echo json_encode([
|
||||
'success' => true,
|
||||
'data' => [
|
||||
'serial_number' => $serialNumber,
|
||||
'rx_power' => $rxPower ?? 'Tidak tersedia',
|
||||
'pppoe_ip' => $pppoeIP ?? 'Tidak tersedia'
|
||||
]
|
||||
]);
|
||||
|
|
@ -0,0 +1,358 @@
|
|||
<?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();
|
||||
}
|
||||
|
||||
require_once __DIR__ . '/../config/database.php';
|
||||
|
||||
// Function to manually parse multipart form data
|
||||
function parseMultipartFormData($input, $contentType) {
|
||||
$data = array();
|
||||
|
||||
// Extract boundary from content type
|
||||
if (preg_match('/boundary=(.+)$/', $contentType, $matches)) {
|
||||
$boundary = $matches[1];
|
||||
|
||||
// Split the input by boundary
|
||||
$parts = array_slice(explode('--' . $boundary, $input), 1);
|
||||
|
||||
foreach ($parts as $part) {
|
||||
// Skip empty parts and closing boundary
|
||||
if (trim($part) == '--' || empty(trim($part))) continue;
|
||||
|
||||
// Split headers and body
|
||||
$sections = explode("\r\n\r\n", $part, 2);
|
||||
if (count($sections) != 2) continue;
|
||||
|
||||
$headers = $sections[0];
|
||||
$body = rtrim($sections[1], "\r\n");
|
||||
|
||||
// Extract field name from Content-Disposition header
|
||||
if (preg_match('/name="([^"]*)"/', $headers, $matches)) {
|
||||
$fieldName = $matches[1];
|
||||
$data[$fieldName] = $body;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $data;
|
||||
}
|
||||
|
||||
$database = new Database();
|
||||
$db = $database->getConnection();
|
||||
|
||||
// Handle method override and multipart data parsing
|
||||
$method = $_SERVER['REQUEST_METHOD'];
|
||||
$parsed_data = array();
|
||||
|
||||
// Parse multipart form data manually if needed
|
||||
if (($method === 'PUT' || $method === 'PATCH') && empty($_POST) &&
|
||||
isset($_SERVER['CONTENT_TYPE']) && strpos($_SERVER['CONTENT_TYPE'], 'multipart/form-data') !== false) {
|
||||
|
||||
// Force treat as POST to get parsed form data
|
||||
$raw_input = file_get_contents('php://input');
|
||||
$parsed_data = parseMultipartFormData($raw_input, $_SERVER['CONTENT_TYPE']);
|
||||
|
||||
// If we found form data, treat this as a method override
|
||||
if (!empty($parsed_data)) {
|
||||
if (isset($parsed_data['_method'])) {
|
||||
$method = strtoupper($parsed_data['_method']);
|
||||
unset($parsed_data['_method']);
|
||||
}
|
||||
// Populate $_POST with parsed data for compatibility
|
||||
$_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) - now works with manually parsed data too
|
||||
if (isset($_POST['_method'])) {
|
||||
$method = strtoupper($_POST['_method']);
|
||||
unset($_POST['_method']); // Clean it up
|
||||
}
|
||||
|
||||
$response = array('success' => false, 'message' => '', 'data' => null);
|
||||
|
||||
// Log only important requests for production
|
||||
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'])) {
|
||||
// Get single item
|
||||
$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', $_GET['id']);
|
||||
$stmt->execute();
|
||||
|
||||
$item = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||
if ($item) {
|
||||
$response['success'] = true;
|
||||
$response['data'] = $item;
|
||||
} else {
|
||||
$response['message'] = 'Item not found';
|
||||
}
|
||||
} else {
|
||||
// Get all items
|
||||
$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
|
||||
ORDER BY i.created_at DESC";
|
||||
|
||||
|
||||
$stmt = $db->prepare($query);
|
||||
$stmt->execute();
|
||||
|
||||
$items = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||
$response['success'] = true;
|
||||
$response['data'] = $items;
|
||||
}
|
||||
break;
|
||||
|
||||
case 'POST':
|
||||
// Create new item
|
||||
$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; // <--- Tambahan
|
||||
|
||||
// Handle foreign key fields - convert empty strings to 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 = $_POST['item_cable_type'] ?? 'distribution';
|
||||
$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;
|
||||
}
|
||||
|
||||
$query = "INSERT INTO ftth_items (
|
||||
item_type_id, 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, status
|
||||
) VALUES (
|
||||
:item_type_id, :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, :status
|
||||
)";
|
||||
|
||||
$stmt = $db->prepare($query);
|
||||
$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); // <--- Bind
|
||||
$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);
|
||||
|
||||
|
||||
if ($stmt->execute()) {
|
||||
$item_id = $db->lastInsertId();
|
||||
|
||||
// Get the created item with joins
|
||||
$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':
|
||||
// Update item - now $_POST should be properly populated
|
||||
$put_data = $_POST;
|
||||
|
||||
$id = $put_data['id'] ?? null;
|
||||
|
||||
if (!$id) {
|
||||
$response['message'] = 'ID required for update';
|
||||
break;
|
||||
}
|
||||
|
||||
// Build dynamic update query
|
||||
$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', '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";
|
||||
|
||||
// Handle empty values for foreign key fields - convert to NULL
|
||||
$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";
|
||||
|
||||
$stmt = $db->prepare($query);
|
||||
|
||||
if ($stmt->execute($params)) {
|
||||
// Get updated item
|
||||
$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', $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':
|
||||
// Delete item
|
||||
$input = file_get_contents("php://input");
|
||||
$delete_data = array();
|
||||
|
||||
// Try to parse JSON first, then form data
|
||||
$json_data = json_decode($input, true);
|
||||
if ($json_data) {
|
||||
$delete_data = $json_data;
|
||||
} else {
|
||||
parse_str($input, $delete_data);
|
||||
}
|
||||
|
||||
// Also check for regular POST data (for compatibility)
|
||||
if (empty($delete_data)) {
|
||||
$delete_data = $_POST;
|
||||
}
|
||||
|
||||
$id = $delete_data['id'] ?? null;
|
||||
|
||||
if (!$id) {
|
||||
$response['message'] = 'ID required for deletion';
|
||||
break;
|
||||
}
|
||||
|
||||
// Delete related routes first
|
||||
$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();
|
||||
|
||||
// Delete the item
|
||||
$query = "DELETE FROM ftth_items WHERE id = :id";
|
||||
$stmt = $db->prepare($query);
|
||||
$stmt->bindParam(':id', $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,278 @@
|
|||
<?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();
|
||||
}
|
||||
|
||||
require_once '../config/database.php';
|
||||
|
||||
$database = new Database();
|
||||
$db = $database->getConnection();
|
||||
|
||||
// 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";
|
||||
|
||||
$stmt = $db->prepare($query);
|
||||
$stmt->bindParam(':id', $_GET['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
|
||||
ORDER BY r.created_at DESC";
|
||||
|
||||
$stmt = $db->prepare($query);
|
||||
$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)
|
||||
VALUES (:from_item_id, :to_item_id, :route_coordinates, :distance, :cable_type, :core_count, :status)";
|
||||
|
||||
|
||||
$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);
|
||||
|
||||
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':
|
||||
// Update route - now using $_POST populated by parser
|
||||
$put_data = $_POST;
|
||||
|
||||
// Debug logging
|
||||
error_log('🔧 Route PUT Request - Method: ' . $method);
|
||||
error_log('🔧 POST data: ' . print_r($put_data, true));
|
||||
|
||||
$id = $put_data['id'] ?? null;
|
||||
|
||||
if (!$id) {
|
||||
error_log('❌ Route update failed: No ID provided');
|
||||
$response['message'] = 'ID required for update';
|
||||
break;
|
||||
}
|
||||
|
||||
// Build dynamic update query
|
||||
$update_fields = array();
|
||||
$params = array(':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 = array();
|
||||
|
||||
// Try to parse JSON first, then form data
|
||||
$json_data = json_decode($input, true);
|
||||
if ($json_data) {
|
||||
$delete_data = $json_data;
|
||||
} else {
|
||||
parse_str($input, $delete_data);
|
||||
}
|
||||
|
||||
// Also check for regular POST data (for compatibility)
|
||||
if (empty($delete_data)) {
|
||||
$delete_data = $_POST;
|
||||
}
|
||||
|
||||
if (isset($delete_data['id'])) {
|
||||
// Delete single route
|
||||
$id = $delete_data['id'];
|
||||
|
||||
$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'])) {
|
||||
// Delete all routes connected to an item
|
||||
$item_id = $delete_data['item_id'];
|
||||
|
||||
// Get route IDs first
|
||||
$query = "SELECT id FROM cable_routes WHERE from_item_id = :item_id OR to_item_id = :item_id";
|
||||
$stmt = $db->prepare($query);
|
||||
$stmt->bindParam(':item_id', $item_id);
|
||||
$stmt->execute();
|
||||
$route_ids = $stmt->fetchAll(PDO::FETCH_COLUMN);
|
||||
|
||||
// Delete routes
|
||||
$query = "DELETE FROM cable_routes WHERE from_item_id = :item_id OR to_item_id = :item_id";
|
||||
$stmt = $db->prepare($query);
|
||||
$stmt->bindParam(':item_id', $item_id);
|
||||
|
||||
if ($stmt->execute()) {
|
||||
$response['success'] = true;
|
||||
$response['message'] = 'Routes deleted successfully';
|
||||
$response['deleted_routes'] = $route_ids;
|
||||
} 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,486 @@
|
|||
/* 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;
|
||||
}
|
||||
|
||||
/* 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;
|
||||
}
|
||||
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;
|
||||
|
|
@ -0,0 +1,972 @@
|
|||
// Map.js - FTTH Planner Map Functionality
|
||||
|
||||
let map;
|
||||
let markers = {};
|
||||
let routes = {};
|
||||
let isRoutingMode = false;
|
||||
let routingFromItem = null;
|
||||
let currentRoutes = [];
|
||||
|
||||
// Initialize map
|
||||
function initMap() {
|
||||
// Create map with enhanced options
|
||||
map = L.map('map', {
|
||||
center: [-7.70298100, 114.01477000], // situbondo, Indonesia
|
||||
zoom: 11,
|
||||
minZoom: 5,
|
||||
maxZoom: 20,
|
||||
zoomControl: false, // We'll add custom zoom control
|
||||
fullscreenControl: true,
|
||||
fullscreenControlOptions: {
|
||||
position: 'topleft'
|
||||
}
|
||||
});
|
||||
|
||||
// Buat custom search control
|
||||
// Buat kontrol pencarian//tombol cari lokasi
|
||||
var searchMarker = null; // Simpan marker yang sedang aktif
|
||||
|
||||
var SearchControl = L.Control.extend({
|
||||
onAdd: function(map) {
|
||||
var container = L.DomUtil.create('div', 'leaflet-bar leaflet-control leaflet-control-custom');
|
||||
|
||||
// Styling container lebih kecil
|
||||
container.style.display = 'flex';
|
||||
container.style.alignItems = 'center';
|
||||
container.style.padding = '4px 8px';
|
||||
container.style.gap = '6px';
|
||||
container.style.minWidth = '160px';
|
||||
container.style.width = '28vw';
|
||||
container.style.maxWidth = '280px';
|
||||
container.style.boxSizing = 'border-box';
|
||||
|
||||
// Buat icon pencarian
|
||||
var icon = L.DomUtil.create('span', '', container);
|
||||
icon.innerHTML = '🔍';
|
||||
icon.style.fontSize = '14px';
|
||||
|
||||
// Buat input
|
||||
var input = L.DomUtil.create('input', '', container);
|
||||
input.type = "text";
|
||||
input.placeholder = "Cari lokasi...";
|
||||
input.style.flex = "1";
|
||||
input.style.padding = "4px";
|
||||
input.style.border = "none";
|
||||
input.style.outline = "none";
|
||||
input.style.fontSize = "13px";
|
||||
|
||||
// Tombol X untuk clear
|
||||
var clearBtn = L.DomUtil.create('span', '', container);
|
||||
clearBtn.innerHTML = '✖';
|
||||
clearBtn.style.cursor = 'pointer';
|
||||
clearBtn.style.fontSize = '12px';
|
||||
clearBtn.style.display = 'none';
|
||||
|
||||
clearBtn.addEventListener('click', function() {
|
||||
if (searchMarker) {
|
||||
map.removeLayer(searchMarker);
|
||||
searchMarker = null;
|
||||
}
|
||||
input.value = '';
|
||||
clearBtn.style.display = 'none';
|
||||
});
|
||||
|
||||
// Event Enter untuk pencarian
|
||||
L.DomEvent.addListener(input, 'keydown', function(e) {
|
||||
if (e.key === 'Enter') {
|
||||
fetch(`https://nominatim.openstreetmap.org/search?format=json&q=${encodeURIComponent(input.value)}`)
|
||||
.then(res => res.json())
|
||||
.then(data => {
|
||||
if (data.length > 0) {
|
||||
var lat = parseFloat(data[0].lat);
|
||||
var lon = parseFloat(data[0].lon);
|
||||
map.setView([lat, lon], 15);
|
||||
|
||||
if (searchMarker) {
|
||||
map.removeLayer(searchMarker);
|
||||
}
|
||||
|
||||
searchMarker = L.marker([lat, lon]).addTo(map).bindPopup(input.value).openPopup();
|
||||
clearBtn.style.display = 'inline';
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
L.DomEvent.disableClickPropagation(container);
|
||||
return container;
|
||||
}
|
||||
});
|
||||
|
||||
// Tambahkan ke peta
|
||||
map.addControl(new SearchControl());
|
||||
|
||||
// Pindahkan ke container utama map
|
||||
var searchEl = document.querySelector('.leaflet-control-custom');
|
||||
document.querySelector('.leaflet-container').appendChild(searchEl);
|
||||
|
||||
// Posisi kiri atas dengan jarak dari fullscreen
|
||||
searchEl.style.position = 'absolute';
|
||||
searchEl.style.top = '10px';
|
||||
searchEl.style.left = '50px'; // kasih jarak 50px biar gak nabrak tombol fullscreen
|
||||
searchEl.style.background = 'white';
|
||||
searchEl.style.borderRadius = '4px';
|
||||
searchEl.style.boxShadow = '0 2px 6px rgba(0,0,0,0.3)';
|
||||
searchEl.style.zIndex = 1000;
|
||||
|
||||
|
||||
|
||||
// Define multiple tile layers
|
||||
const tileLayers = {
|
||||
"OpenStreetMap": L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
|
||||
attribution: '© <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors',
|
||||
maxZoom: 19
|
||||
}),
|
||||
|
||||
"CartoDB Positron": L.tileLayer('https://{s}.basemaps.cartocdn.com/light_all/{z}/{x}/{y}{r}.png', {
|
||||
attribution: '© <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors © <a href="https://carto.com/attributions">CARTO</a>',
|
||||
maxZoom: 20
|
||||
}),
|
||||
|
||||
"CartoDB Dark": L.tileLayer('https://{s}.basemaps.cartocdn.com/dark_all/{z}/{x}/{y}{r}.png', {
|
||||
attribution: '© <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors © <a href="https://carto.com/attributions">CARTO</a>',
|
||||
maxZoom: 20
|
||||
}),
|
||||
|
||||
"Satellite": L.tileLayer('https://server.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile/{z}/{y}/{x}', {
|
||||
attribution: 'Tiles © Esri — Source: Esri, i-cubed, USDA, USGS, AEX, GeoEye, Getmapping, Aerogrid, IGN, IGP, UPR-EGP, and the GIS User Community',
|
||||
maxZoom: 20
|
||||
}),
|
||||
|
||||
"Terrain": L.tileLayer('https://{s}.tile.opentopomap.org/{z}/{x}/{y}.png', {
|
||||
attribution: 'Map data: © <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors, <a href="http://viewfinderpanoramas.org">SRTM</a> | Map style: © <a href="https://opentopomap.org">OpenTopoMap</a> (<a href="https://creativecommons.org/licenses/by-sa/3.0/">CC-BY-SA</a>)',
|
||||
maxZoom: 17
|
||||
}),
|
||||
|
||||
"Google Hybrid": L.tileLayer('https://mt1.google.com/vt/lyrs=y&x={x}&y={y}&z={z}', {
|
||||
attribution: '© Google',
|
||||
maxZoom: 20
|
||||
})
|
||||
};
|
||||
|
||||
// Add default layer (OpenStreetMap)
|
||||
tileLayers["OpenStreetMap"].addTo(map);
|
||||
|
||||
// Add layer control
|
||||
L.control.layers(tileLayers, null, {
|
||||
position: 'topright',
|
||||
collapsed: false
|
||||
}).addTo(map);
|
||||
|
||||
// Add custom zoom control with home button
|
||||
const zoomControl = L.control.zoom({
|
||||
position: 'topleft'
|
||||
}).addTo(map);
|
||||
|
||||
// Add home button to zoom control
|
||||
const homeControl = L.Control.extend({
|
||||
options: {
|
||||
position: 'topleft'
|
||||
},
|
||||
onAdd: function(map) {
|
||||
const container = L.DomUtil.create('div', 'leaflet-bar leaflet-control leaflet-control-custom');
|
||||
container.style.backgroundColor = 'white';
|
||||
container.style.backgroundImage = 'none';
|
||||
container.style.width = '26px';
|
||||
container.style.height = '26px';
|
||||
container.style.cursor = 'pointer';
|
||||
container.innerHTML = '<i class="fas fa-home" style="font-size: 14px; line-height: 26px; text-align: center; width: 26px; display: block;"></i>';
|
||||
container.title = 'Zoom to Indonesia';
|
||||
|
||||
container.onclick = function() {
|
||||
map.setView([-2.5, 118], 5); // Indonesia overview
|
||||
};
|
||||
|
||||
return container;
|
||||
}
|
||||
});
|
||||
|
||||
new homeControl().addTo(map);
|
||||
|
||||
// Add scale control
|
||||
L.control.scale({
|
||||
position: 'bottomright',
|
||||
metric: true,
|
||||
imperial: false
|
||||
}).addTo(map);
|
||||
|
||||
// Add coordinates display
|
||||
const coordsControl = L.control({position: 'bottomleft'});
|
||||
coordsControl.onAdd = function(map) {
|
||||
this._div = L.DomUtil.create('div', 'leaflet-control-coords');
|
||||
this._div.style.background = 'rgba(255,255,255,0.8)';
|
||||
this._div.style.padding = '5px';
|
||||
this._div.style.margin = '0';
|
||||
this._div.style.fontSize = '11px';
|
||||
this._div.innerHTML = 'Move mouse over map';
|
||||
return this._div;
|
||||
};
|
||||
coordsControl.update = function(lat, lng) {
|
||||
this._div.innerHTML = `Lat: ${lat.toFixed(6)}, Lng: ${lng.toFixed(6)}`;
|
||||
};
|
||||
coordsControl.addTo(map);
|
||||
|
||||
// Update coordinates on mouse move
|
||||
map.on('mousemove', function(e) {
|
||||
coordsControl.update(e.latlng.lat, e.latlng.lng);
|
||||
});
|
||||
|
||||
// Enhanced zoom behavior
|
||||
map.on('zoomend', function() {
|
||||
const zoom = map.getZoom();
|
||||
if (zoom < 10) {
|
||||
// Hide detailed markers at low zoom
|
||||
Object.values(markers).forEach(marker => {
|
||||
if (marker._icon) {
|
||||
marker._icon.style.opacity = '0.7';
|
||||
}
|
||||
});
|
||||
} else {
|
||||
// Show detailed markers at high zoom
|
||||
Object.values(markers).forEach(marker => {
|
||||
if (marker._icon) {
|
||||
marker._icon.style.opacity = '1';
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Add map click event for adding new items
|
||||
map.on('click', function(e) {
|
||||
if (!isRoutingMode) {
|
||||
showAddItemModal(e.latlng.lat, e.latlng.lng);
|
||||
}
|
||||
});
|
||||
|
||||
// Load existing items
|
||||
loadItems();
|
||||
loadRoutes();
|
||||
|
||||
// Add legend
|
||||
addMapLegend();
|
||||
|
||||
console.log('🗺️ Enhanced map initialized with multiple tile layers and zoom controls');
|
||||
|
||||
// Add loading indicator
|
||||
const mapContainer = document.getElementById('map');
|
||||
const loadingDiv = document.createElement('div');
|
||||
loadingDiv.className = 'map-loading';
|
||||
loadingDiv.innerHTML = '<i class="fas fa-spinner fa-spin"></i> Loading map...';
|
||||
mapContainer.appendChild(loadingDiv);
|
||||
|
||||
// Hide loading indicator after tiles load
|
||||
map.on('tilesloaded', function() {
|
||||
if (loadingDiv.parentNode) {
|
||||
loadingDiv.parentNode.removeChild(loadingDiv);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Create custom marker icon
|
||||
// Objek global untuk menyimpan marker berdasarkan SN
|
||||
// Fungsi buat custom icon
|
||||
function createCustomIcon(itemType, color) {
|
||||
let iconClass = 'fas fa-circle';
|
||||
|
||||
switch(itemType) {
|
||||
case 'OLT':
|
||||
case '1':
|
||||
iconClass = 'fas fa-server';
|
||||
break;
|
||||
case 'Tiang Tumpu':
|
||||
case '2':
|
||||
iconClass = 'fas fa-tower-broadcast';
|
||||
break;
|
||||
case 'ODP':
|
||||
case '3':
|
||||
iconClass = 'fas fa-project-diagram';
|
||||
break;
|
||||
case 'ODC':
|
||||
case '4':
|
||||
iconClass = 'fas fa-network-wired';
|
||||
break;
|
||||
case 'Pelanggan':
|
||||
case '5':
|
||||
iconClass = 'fas fa-home';
|
||||
break;
|
||||
}
|
||||
|
||||
// Default RX Power dulu
|
||||
let rxPower = "Memuat...";
|
||||
|
||||
return L.divIcon({
|
||||
className: 'custom-div-icon',
|
||||
html: `
|
||||
<div class="custom-marker marker-${itemType.toLowerCase().replace(' ', '')}"
|
||||
style="background-color: ${color}; width: 30px; height: 30px;
|
||||
border-radius: 50%; display: flex; flex-direction: column;
|
||||
align-items: center; justify-content: flex-start; border: 3px solid white;
|
||||
box-shadow: 0 2px 5px rgba(255, 255, 255, 0); position: relative;">
|
||||
|
||||
<div class="rx-power-box"
|
||||
style="width: 40px; height: 16px; margin-bottom: 4px;
|
||||
border: 1px solid white; background-color: rgba(0, 0, 0, 1);
|
||||
color: white; font-size: 10px; text-align: center;
|
||||
position: absolute; top: -18px; left: 50%; transform: translateX(-50%);">
|
||||
${rxPower}
|
||||
</div>
|
||||
|
||||
<i class="${iconClass}" style="color: white; font-size: 14px;"></i>
|
||||
</div>
|
||||
`,
|
||||
iconSize: [40, 50],
|
||||
iconAnchor: [20, 40],
|
||||
popupAnchor: [0, -25]
|
||||
});
|
||||
}
|
||||
|
||||
// Fungsi update RX Power untuk marker tertentu
|
||||
|
||||
|
||||
// map.js//modifan saya
|
||||
|
||||
// pastikan variabel sudah benar
|
||||
// update RX Power setelah marker dirender
|
||||
|
||||
|
||||
// Create popup content for item
|
||||
function createPopupContent(item) {
|
||||
let tubeColorName = item.tube_color_name || 'Tidak ada';
|
||||
let splitterMain = item.splitter_main_ratio || 'Tidak ada';
|
||||
let splitterOdp = item.splitter_odp_ratio || 'Tidak ada';
|
||||
let serialNumber = item.serial_number ? item.serial_number.trim() : '';
|
||||
|
||||
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>
|
||||
<div class="popup-actions">
|
||||
<button class="btn btn-info btn-sm" onclick="loadRedamanDetail(${item.id})"> Detail Redaman</button>
|
||||
<button class="btn btn-info btn-sm" onclick="showItemDetail(${item.id})"><i class="fas fa-info-circle"></i> Detail</button>
|
||||
<button class="btn btn-primary btn-sm" onclick="editItem(${item.id})"><i class="fas fa-edit"></i> Edit</button>
|
||||
<button class="btn btn-success btn-sm" onclick="startRouting(${item.id})"><i class="fas fa-route"></i> Route</button>
|
||||
<button class="btn btn-danger btn-sm" onclick="deleteItem(${item.id})"><i class="fas fa-trash"></i> Hapus</button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
// Ambil RX Power setelah popup ada di DOM
|
||||
// Lebih baik menggunakan requestAnimationFrame untuk memastikan DOM sudah diupdate
|
||||
|
||||
return html;
|
||||
}
|
||||
// Get item icon based on type (moved to bottom for global export)
|
||||
|
||||
// Get status badge class
|
||||
function getStatusBadgeClass(status) {
|
||||
switch(status) {
|
||||
case 'active': return 'success';
|
||||
case 'inactive': return 'secondary';
|
||||
case 'maintenance': return 'warning';
|
||||
default: return 'primary';
|
||||
}
|
||||
}
|
||||
|
||||
// Get status text
|
||||
function getStatusText(status) {
|
||||
switch(status) {
|
||||
case 'active': return 'Aktif';
|
||||
case 'inactive': return 'Tidak Aktif';
|
||||
case 'maintenance': return 'Maintenance';
|
||||
default: return status;
|
||||
}
|
||||
}
|
||||
|
||||
// Load items from database
|
||||
function loadItems() {
|
||||
$.ajax({
|
||||
url: 'api/items.php',
|
||||
method: 'GET',
|
||||
success: function(response) {
|
||||
if (response.success) {
|
||||
response.data.forEach(function(item) {
|
||||
addMarkerToMap(item);
|
||||
});
|
||||
updateStatistics();
|
||||
}
|
||||
},
|
||||
error: function() {
|
||||
showNotification('Error loading items', 'error');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Add marker to map
|
||||
function addMarkerToMap(item) {
|
||||
let itemTypeColors = {
|
||||
'OLT': '#FF6B6B',
|
||||
'Tiang Tumpu': '#4ECDC4',
|
||||
'ODP': '#45B7D1',
|
||||
'ODC': '#96CEB4',
|
||||
'Pelanggan': '#FFA500'
|
||||
};
|
||||
|
||||
let color = itemTypeColors[item.item_type_name] || '#999';
|
||||
let icon = createCustomIcon(item.item_type_name, color);
|
||||
|
||||
let marker = L.marker([item.latitude, item.longitude], {
|
||||
icon: icon,
|
||||
draggable: true,
|
||||
itemType: item.item_type_name,
|
||||
itemId: item.id,
|
||||
itemData: item
|
||||
}).addTo(map);
|
||||
|
||||
marker.bindPopup(createPopupContent(item));
|
||||
|
||||
// Add drag event
|
||||
marker.on('dragend', function(e) {
|
||||
let newPos = e.target.getLatLng();
|
||||
updateItemPosition(item.id, newPos.lat, newPos.lng);
|
||||
});
|
||||
|
||||
// Add click event for routing mode
|
||||
marker.on('click', function(e) {
|
||||
if (isRoutingMode) {
|
||||
handleRoutingClick(item);
|
||||
e.originalEvent.stopPropagation();
|
||||
}
|
||||
});
|
||||
|
||||
markers[item.id] = marker;
|
||||
}
|
||||
|
||||
// Update item position
|
||||
function updateItemPosition(itemId, lat, lng) {
|
||||
// Use FormData with method override for consistency
|
||||
let formData = new FormData();
|
||||
formData.append('_method', 'PUT');
|
||||
formData.append('id', itemId);
|
||||
formData.append('latitude', lat);
|
||||
formData.append('longitude', lng);
|
||||
|
||||
$.ajax({
|
||||
url: 'api/items.php',
|
||||
method: 'POST',
|
||||
data: formData,
|
||||
processData: false,
|
||||
contentType: false,
|
||||
dataType: 'json',
|
||||
success: function(response) {
|
||||
if (response && response.success) {
|
||||
showNotification('Posisi item berhasil dipindahkan', 'success');
|
||||
} else {
|
||||
showNotification(response?.message || 'Error updating position', 'error');
|
||||
}
|
||||
},
|
||||
error: function(xhr, status, error) {
|
||||
console.error('Position update error:', error, xhr.responseText);
|
||||
showNotification('Error updating position: ' + error, 'error');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Start routing mode
|
||||
function startRouting(itemId) {
|
||||
isRoutingMode = true;
|
||||
routingFromItem = itemId;
|
||||
map.getContainer().style.cursor = 'crosshair';
|
||||
showNotification('Pilih item tujuan untuk membuat route', 'info');
|
||||
}
|
||||
|
||||
// Handle routing click
|
||||
function handleRoutingClick(toItem) {
|
||||
if (routingFromItem && routingFromItem !== toItem.id) {
|
||||
createRoute(routingFromItem, toItem.id);
|
||||
exitRoutingMode();
|
||||
}
|
||||
}
|
||||
|
||||
// Exit routing mode
|
||||
function exitRoutingMode() {
|
||||
isRoutingMode = false;
|
||||
routingFromItem = null;
|
||||
map.getContainer().style.cursor = '';
|
||||
}
|
||||
|
||||
// Create route between two items
|
||||
function createRoute(fromItemId, toItemId) {
|
||||
let fromMarker = markers[fromItemId];
|
||||
let toMarker = markers[toItemId];
|
||||
|
||||
if (!fromMarker || !toMarker) {
|
||||
showNotification('Marker tidak ditemukan', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
let fromPos = fromMarker.getLatLng();
|
||||
let toPos = toMarker.getLatLng();
|
||||
|
||||
console.log('Creating route from', fromPos, 'to', toPos);
|
||||
|
||||
// Check if Leaflet Routing Machine is available
|
||||
if (typeof L.Routing === 'undefined') {
|
||||
console.log('Leaflet Routing Machine not available, creating simple line');
|
||||
// Create simple straight line if routing machine not available
|
||||
createSimpleRoute(fromItemId, toItemId, fromPos, toPos);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// Use routing machine to create route following roads
|
||||
let routing = L.Routing.control({
|
||||
waypoints: [fromPos, toPos],
|
||||
routeWhileDragging: false,
|
||||
show: false,
|
||||
createMarker: function() { return null; }, // Don't create default markers
|
||||
addWaypoints: false,
|
||||
draggableWaypoints: false,
|
||||
fitSelectedRoutes: false
|
||||
});
|
||||
|
||||
routing.on('routesfound', function(e) {
|
||||
console.log('Route found:', e.routes[0]);
|
||||
let route = e.routes[0];
|
||||
let coordinates = route.coordinates;
|
||||
|
||||
// Save route to database
|
||||
$.ajax({
|
||||
url: 'api/routes.php',
|
||||
method: 'POST',
|
||||
data: {
|
||||
from_item_id: fromItemId,
|
||||
to_item_id: toItemId,
|
||||
route_coordinates: JSON.stringify(coordinates),
|
||||
distance: route.summary.totalDistance,
|
||||
cable_type: 'Fiber Optic',
|
||||
core_count: 24,
|
||||
status: 'planned'
|
||||
},
|
||||
success: function(response) {
|
||||
console.log('Route save response:', response);
|
||||
if (response.success) {
|
||||
// Add route line to map
|
||||
let routeLine = L.polyline(coordinates, {
|
||||
color: '#ffc107',
|
||||
weight: 4,
|
||||
opacity: 0.8,
|
||||
dashArray: '10, 5'
|
||||
}).addTo(map);
|
||||
|
||||
// Add popup to route
|
||||
routeLine.bindPopup(`
|
||||
<div>
|
||||
<h6>Route Kabel</h6>
|
||||
<p><strong>Jarak:</strong> ${(route.summary.totalDistance / 1000).toFixed(2)} km</p>
|
||||
<p><strong>Tipe Kabel:</strong> Fiber Optic</p>
|
||||
<p><strong>Jumlah Core:</strong> 24</p>
|
||||
<p><strong>Status:</strong> Perencanaan</p>
|
||||
</div>
|
||||
`);
|
||||
|
||||
routes[response.route_id] = routeLine;
|
||||
showNotification('Route berhasil dibuat', 'success');
|
||||
|
||||
// Remove routing control
|
||||
map.removeControl(routing);
|
||||
} else {
|
||||
showNotification(response.message || 'Gagal menyimpan route', 'error');
|
||||
}
|
||||
},
|
||||
error: function(xhr, status, error) {
|
||||
console.error('Error saving route:', error);
|
||||
showNotification('Error menyimpan route: ' + error, 'error');
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
routing.on('routingerror', function(e) {
|
||||
console.error('Routing error:', e.error);
|
||||
showNotification('Error routing: ' + e.error.message, 'error');
|
||||
// Fallback to simple line
|
||||
createSimpleRoute(fromItemId, toItemId, fromPos, toPos);
|
||||
});
|
||||
|
||||
routing.addTo(map);
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error creating route:', error);
|
||||
showNotification('Error creating route, using simple line', 'warning');
|
||||
createSimpleRoute(fromItemId, toItemId, fromPos, toPos);
|
||||
}
|
||||
}
|
||||
|
||||
// Create simple straight line route (fallback)
|
||||
function createSimpleRoute(fromItemId, toItemId, fromPos, toPos) {
|
||||
let coordinates = [[fromPos.lat, fromPos.lng], [toPos.lat, toPos.lng]];
|
||||
let distance = fromPos.distanceTo(toPos);
|
||||
|
||||
$.ajax({
|
||||
url: 'api/routes.php',
|
||||
method: 'POST',
|
||||
data: {
|
||||
from_item_id: fromItemId,
|
||||
to_item_id: toItemId,
|
||||
route_coordinates: JSON.stringify(coordinates),
|
||||
distance: distance,
|
||||
cable_type: 'Fiber Optic',
|
||||
core_count: 24,
|
||||
status: 'planned'
|
||||
},
|
||||
success: function(response) {
|
||||
if (response.success) {
|
||||
// Add route line to map
|
||||
let routeLine = L.polyline(coordinates, {
|
||||
color: '#ffc107',
|
||||
weight: 4,
|
||||
opacity: 0.8,
|
||||
dashArray: '10, 5'
|
||||
}).addTo(map);
|
||||
|
||||
routeLine.bindPopup(`
|
||||
<div>
|
||||
<h6>Route Kabel (Direct)</h6>
|
||||
<p><strong>Jarak:</strong> ${(distance / 1000).toFixed(2)} km</p>
|
||||
<p><strong>Tipe Kabel:</strong> Fiber Optic</p>
|
||||
<p><strong>Jumlah Core:</strong> 24</p>
|
||||
<p><strong>Status:</strong> Perencanaan</p>
|
||||
</div>
|
||||
`);
|
||||
|
||||
routes[response.route_id] = routeLine;
|
||||
showNotification('Route sederhana berhasil dibuat', 'success');
|
||||
}
|
||||
},
|
||||
error: function() {
|
||||
showNotification('Error menyimpan route', 'error');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Load routes from database
|
||||
function loadRoutes() {
|
||||
$.ajax({
|
||||
url: 'api/routes.php',
|
||||
method: 'GET',
|
||||
success: function(response) {
|
||||
if (response.success) {
|
||||
response.data.forEach(function(route) {
|
||||
if (route.route_coordinates) {
|
||||
let coordinates = JSON.parse(route.route_coordinates);
|
||||
let color = getRouteColor(route.status);
|
||||
let dashArray = route.status === 'installed' ? null : '10, 5';
|
||||
|
||||
let routeLine = L.polyline(coordinates, {
|
||||
color: color,
|
||||
weight: 4,
|
||||
opacity: 0.8,
|
||||
dashArray: dashArray
|
||||
}).addTo(map);
|
||||
|
||||
routeLine.bindPopup(`
|
||||
<div>
|
||||
<h6>Route Kabel</h6>
|
||||
<p><strong>Jarak:</strong> ${(route.distance / 1000).toFixed(2)} km</p>
|
||||
<p><strong>Tipe Kabel:</strong> ${route.cable_type}</p>
|
||||
<p><strong>Jumlah Core:</strong> ${route.core_count}</p>
|
||||
<p><strong>Status:</strong> ${getStatusText(route.status)}</p>
|
||||
</div>
|
||||
`);
|
||||
|
||||
routes[route.id] = routeLine;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Get route color based on status
|
||||
function getRouteColor(status) {
|
||||
switch(status) {
|
||||
case 'installed': return '#28a745';
|
||||
case 'planned': return '#ffc107';
|
||||
case 'maintenance': return '#dc3545';
|
||||
default: return '#6c757d';
|
||||
}
|
||||
}
|
||||
|
||||
// Add map legend
|
||||
function addMapLegend() {
|
||||
let legend = L.control({position: 'bottomleft'});
|
||||
|
||||
legend.onAdd = function(map) {
|
||||
let div = L.DomUtil.create('div', 'map-legend');
|
||||
div.innerHTML = `
|
||||
<h6 style="margin-bottom: 10px; font-weight: bold;">Legend</h6>
|
||||
<div class="legend-item">
|
||||
<div class="legend-icon" style="background-color: #FF6B6B;"><i class="fas fa-server" style="color: white; font-size: 10px;"></i></div>
|
||||
<span>OLT</span>
|
||||
</div>
|
||||
<div class="legend-item">
|
||||
<div class="legend-icon" style="background-color: #4ECDC4;"><i class="fas fa-tower-broadcast" style="color: white; font-size: 10px;"></i></div>
|
||||
<span>Tiang Tumpu</span>
|
||||
</div>
|
||||
<div class="legend-item">
|
||||
<div class="legend-icon" style="background-color: #45B7D1;"><i class="fas fa-project-diagram" style="color: white; font-size: 10px;"></i></div>
|
||||
<span>ODP</span>
|
||||
</div>
|
||||
<div class="legend-item">
|
||||
<div class="legend-icon" style="background-color: #96CEB4;"><i class="fas fa-network-wired" style="color: white; font-size: 10px;"></i></div>
|
||||
<span>ODC</span>
|
||||
</div>
|
||||
<div class="legend-item">
|
||||
<div class="legend-icon" style="background-color: #FFA500;"><i class="fas fa-home" style="color: white; font-size: 10px;"></i></div>
|
||||
<span>Pelanggan</span>
|
||||
</div>
|
||||
<hr style="margin: 10px 0;">
|
||||
<div style="font-size: 12px;">
|
||||
<div><span style="border-bottom: 3px solid #28a745; padding-bottom: 1px;">━━━</span> Terpasang</div>
|
||||
<div><span style="border-bottom: 3px dashed #ffc107; padding-bottom: 1px;">┅┅┅</span> Perencanaan</div>
|
||||
<div><span style="border-bottom: 3px dashed #dc3545; padding-bottom: 1px;">┅┅┅</span> Maintenance</div>
|
||||
</div>
|
||||
`;
|
||||
return div;
|
||||
};
|
||||
|
||||
legend.addTo(map);
|
||||
}
|
||||
|
||||
//buatan saya
|
||||
|
||||
|
||||
// Update statistics
|
||||
function updateStatistics() {
|
||||
$.ajax({
|
||||
url: 'api/statistics.php',
|
||||
method: 'GET',
|
||||
success: function(response) {
|
||||
if (response.success) {
|
||||
$('#stat-olt').text(response.data.olt || 0);
|
||||
$('#stat-tiang').text(response.data.tiang_tumpu || 0);
|
||||
$('#stat-odp').text(response.data.odp || 0);
|
||||
$('#stat-odc').text(response.data.odc || 0);
|
||||
$('#stat-pelanggan').text(response.data.pelanggan || 0);
|
||||
$('#stat-routes').text(response.data.total_routes || 0);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Show notification
|
||||
function showNotification(message, type) {
|
||||
let alertClass = 'alert-info';
|
||||
switch(type) {
|
||||
case 'success': alertClass = 'alert-success'; break;
|
||||
case 'error': alertClass = 'alert-danger'; break;
|
||||
case 'warning': alertClass = 'alert-warning'; break;
|
||||
}
|
||||
|
||||
let notification = `
|
||||
<div class="alert ${alertClass} alert-dismissible fade show" role="alert" style="position: fixed; top: 20px; right: 20px; z-index: 9999; min-width: 300px;">
|
||||
${message}
|
||||
<button type="button" class="close" data-dismiss="alert">
|
||||
<span>×</span>
|
||||
</button>
|
||||
</div>
|
||||
`;
|
||||
|
||||
$('body').append(notification);
|
||||
|
||||
setTimeout(function() {
|
||||
$('.alert').fadeOut();
|
||||
}, 5000);
|
||||
}
|
||||
|
||||
// Show routing mode
|
||||
function showRoutingMode() {
|
||||
if (isRoutingMode) {
|
||||
exitRoutingMode();
|
||||
showNotification('Mode routing dinonaktifkan', 'info');
|
||||
} else {
|
||||
isRoutingMode = true;
|
||||
map.getContainer().style.cursor = 'crosshair';
|
||||
showNotification('Mode routing aktif. Klik dua item untuk membuat route.', 'info');
|
||||
}
|
||||
}
|
||||
|
||||
// Zoom to specific bounds
|
||||
function zoomToItems() {
|
||||
if (Object.keys(markers).length > 0) {
|
||||
const group = new L.featureGroup(Object.values(markers));
|
||||
map.fitBounds(group.getBounds().pad(0.1));
|
||||
} else {
|
||||
showNotification('Tidak ada item untuk di-zoom', 'warning');
|
||||
}
|
||||
}
|
||||
|
||||
// Zoom to specific item type
|
||||
function zoomToItemType(itemType) {
|
||||
const filteredMarkers = Object.values(markers).filter(marker => {
|
||||
return marker.options && marker.options.itemType === itemType;
|
||||
});
|
||||
|
||||
if (filteredMarkers.length > 0) {
|
||||
const group = new L.featureGroup(filteredMarkers);
|
||||
map.fitBounds(group.getBounds().pad(0.1));
|
||||
|
||||
// Highlight markers of this type temporarily
|
||||
filteredMarkers.forEach(marker => {
|
||||
if (marker._icon) {
|
||||
marker._icon.style.transform += ' scale(1.3)';
|
||||
marker._icon.style.zIndex = '1000';
|
||||
setTimeout(() => {
|
||||
marker._icon.style.transform = marker._icon.style.transform.replace(' scale(1.3)', '');
|
||||
marker._icon.style.zIndex = '';
|
||||
}, 2000);
|
||||
}
|
||||
});
|
||||
|
||||
showNotification(`Menampilkan ${filteredMarkers.length} ${itemType}`, 'success');
|
||||
} else {
|
||||
showNotification(`Tidak ada ${itemType} ditemukan`, 'info');
|
||||
}
|
||||
}
|
||||
|
||||
// Enhanced locate user function
|
||||
function locateUser() {
|
||||
if (navigator.geolocation) {
|
||||
map.locate({
|
||||
setView: true,
|
||||
maxZoom: 16,
|
||||
enableHighAccuracy: true,
|
||||
timeout: 10000
|
||||
});
|
||||
|
||||
map.on('locationfound', function(e) {
|
||||
L.circle(e.latlng, e.accuracy).addTo(map)
|
||||
.bindPopup('Anda berada di sekitar area ini').openPopup();
|
||||
showNotification('Lokasi berhasil ditemukan', 'success');
|
||||
});
|
||||
|
||||
map.on('locationerror', function(e) {
|
||||
showNotification('Gagal menemukan lokasi: ' + e.message, 'error');
|
||||
});
|
||||
} else {
|
||||
showNotification('Geolocation tidak didukung browser ini', 'error');
|
||||
}
|
||||
}
|
||||
|
||||
// Add keyboard shortcuts for zoom
|
||||
function addKeyboardShortcuts() {
|
||||
document.addEventListener('keydown', function(e) {
|
||||
if (e.target.tagName.toLowerCase() === 'input' || e.target.tagName.toLowerCase() === 'textarea') {
|
||||
return; // Don't interfere with form inputs
|
||||
}
|
||||
|
||||
switch(e.key) {
|
||||
case '+':
|
||||
case '=':
|
||||
map.zoomIn();
|
||||
break;
|
||||
case '-':
|
||||
map.zoomOut();
|
||||
break;
|
||||
case 'h':
|
||||
case 'H':
|
||||
map.setView([-2.5, 118], 5); // Home to Indonesia
|
||||
break;
|
||||
case 'f':
|
||||
case 'F':
|
||||
if (map.isFullscreen && map.isFullscreen()) {
|
||||
map.toggleFullscreen();
|
||||
} else if (map.toggleFullscreen) {
|
||||
map.toggleFullscreen();
|
||||
}
|
||||
break;
|
||||
case 'l':
|
||||
case 'L':
|
||||
locateUser();
|
||||
break;
|
||||
case 'a':
|
||||
case 'A':
|
||||
zoomToItems();
|
||||
break;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Enhanced map ready function
|
||||
function onMapReady() {
|
||||
addKeyboardShortcuts();
|
||||
|
||||
// Add help tooltip
|
||||
const helpControl = L.control({position: 'bottomright'});
|
||||
helpControl.onAdd = function(map) {
|
||||
const div = L.DomUtil.create('div', 'leaflet-control-help');
|
||||
div.innerHTML = '<i class="fas fa-question-circle" title="Shortcuts: +/- zoom, H home, F fullscreen, L locate, A zoom to all"></i>';
|
||||
div.style.background = 'rgba(255,255,255,0.8)';
|
||||
div.style.padding = '5px';
|
||||
div.style.borderRadius = '3px';
|
||||
div.style.cursor = 'help';
|
||||
return div;
|
||||
};
|
||||
helpControl.addTo(map);
|
||||
|
||||
console.log('🎮 Map keyboard shortcuts enabled: +/- zoom, H home, F fullscreen, L locate, A zoom to all');
|
||||
}
|
||||
|
||||
// Helper functions needed by detail modal
|
||||
function getItemIcon(typeName) {
|
||||
switch(typeName) {
|
||||
case 'OLT': return 'fas fa-server';
|
||||
case 'Tiang Tumpu': return 'fas fa-tower-broadcast';
|
||||
case 'ODP': return 'fas fa-project-diagram';
|
||||
case 'ODC': return 'fas fa-network-wired';
|
||||
case 'Pelanggan': return 'fas fa-home';
|
||||
default: return 'fas fa-circle';
|
||||
}
|
||||
}
|
||||
|
||||
function getStatusBadgeClass(status) {
|
||||
switch(status) {
|
||||
case 'active': return 'success';
|
||||
case 'inactive': return 'secondary';
|
||||
case 'maintenance': return 'warning';
|
||||
default: return 'secondary';
|
||||
}
|
||||
}
|
||||
|
||||
function getStatusText(status) {
|
||||
switch(status) {
|
||||
case 'active': return 'Aktif';
|
||||
case 'inactive': return 'Tidak Aktif';
|
||||
case 'maintenance': return 'Maintenance';
|
||||
default: return status || 'Unknown';
|
||||
}
|
||||
}
|
||||
|
||||
// Export functions to global scope for button access
|
||||
window.zoomToItems = zoomToItems;
|
||||
window.zoomToItemType = zoomToItemType;
|
||||
window.locateUser = locateUser;
|
||||
window.loadRoutes = loadRoutes;
|
||||
window.getItemIcon = getItemIcon;
|
||||
window.getStatusBadgeClass = getStatusBadgeClass;
|
||||
window.getStatusText = getStatusText;
|
||||
|
||||
// Initialize map when document is ready
|
||||
$(document).ready(function() {
|
||||
initMap();
|
||||
setTimeout(onMapReady, 1000); // Wait for map to fully initialize
|
||||
});
|
||||
|
|
@ -0,0 +1,39 @@
|
|||
<?php
|
||||
// auto_login_genieacs.php
|
||||
|
||||
// Konfigurasi login GenieACS
|
||||
$genieacs_host = 'http://172.16.96.10: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://172.16.96.6'; // 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,22 @@
|
|||
<?php
|
||||
// Konfigurasi Database untuk FTTH Planner
|
||||
class Database {
|
||||
private $host = "localhost";
|
||||
private $db_name = "ftth_planner";
|
||||
private $username = "root";
|
||||
private $password = "";
|
||||
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');
|
||||
|
|
@ -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://localhost: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,651 @@
|
|||
<?php
|
||||
session_start();
|
||||
if (!isset($_SESSION['user'])) {
|
||||
header("Location: login.php");
|
||||
exit;
|
||||
}
|
||||
|
||||
|
||||
// Akses teknisi tidak diarahkan ke map.php, tetap di dashboard
|
||||
?>
|
||||
<!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-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-server 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-server" 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-home" 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 -->
|
||||
<div class="btn-group">
|
||||
<?php if ($_SESSION["role"] === "engineer") : ?>
|
||||
<button type="button" class="btn btn-primary btn-sm" onclick="showAddItemModal()">
|
||||
<?php else: ?>
|
||||
<button type="button" class="btn btn-primary btn-sm" disabled style="opacity:0.5;cursor:not-allowed;">
|
||||
<?php endif; ?>
|
||||
<i class="fas fa-plus"></i> Tambah Item
|
||||
</button>
|
||||
|
||||
<?php if ($_SESSION["role"] === "engineer") : ?>
|
||||
<button type="button" class="btn btn-info btn-sm" onclick="showRoutingMode()">
|
||||
<?php else: ?>
|
||||
<button type="button" class="btn btn-info btn-sm" disabled style="opacity:0.5;cursor:not-allowed;">
|
||||
<?php endif; ?>
|
||||
<i class="fas fa-route"></i> Mode Routing
|
||||
</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-server"></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-home"></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 id="stat-olt">0</h3>
|
||||
<p>OLT</p>
|
||||
</div>
|
||||
<div class="icon">
|
||||
<i class="fas fa-server"></i>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-lg-2 col-6">
|
||||
<div class="small-box bg-success">
|
||||
<div class="inner">
|
||||
<h3 id="stat-tiang">0</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 id="stat-odp">0</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 id="stat-odc">0</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 id="stat-pelanggan">0</h3>
|
||||
<p>Pelanggan</p>
|
||||
</div>
|
||||
<div class="icon">
|
||||
<i class="fas fa-home"></i>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-lg-2 col-6">
|
||||
<div class="small-box bg-secondary">
|
||||
<div class="inner">
|
||||
<h3 id="stat-routes">0</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">
|
||||
|
||||
<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>
|
||||
|
||||
<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">
|
||||
<small class="form-text text-muted">Koordinat Lintang (Latitude)</small>
|
||||
</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">
|
||||
<small class="form-text text-muted">Koordinat Bujur (Longitude)</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="alert alert-info" role="alert">
|
||||
<i class="fas fa-info-circle"></i>
|
||||
<strong>Cara menentukan lokasi:</strong>
|
||||
<br>• Klik tombol "Pilih di Peta" di bawah ini
|
||||
<br>• Atau tutup dialog ini dan klik langsung di peta, lalu pilih "Tambah Item"
|
||||
</div>
|
||||
|
||||
<div class="text-center mb-3">
|
||||
<button type="button" class="btn btn-outline-primary" onclick="selectLocationOnMap()">
|
||||
<i class="fas fa-map-marker-alt"></i> Pilih Lokasi di Peta
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<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 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="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 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">
|
||||
<small class="form-text text-muted">Otomatis terisi dari perhitungan routing</small>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<div class="form-group">
|
||||
<label>Core Tersedia</label>
|
||||
<input type="text" class="form-control" id="coreAvailable" readonly>
|
||||
<small class="form-text text-muted">Kapasitas Total - Core Digunakan</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<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 class="form-group">
|
||||
<label for="serial_number">Serial Number ONT</label>
|
||||
<input type="text" class="form-control" id="serial_number" name="serial_number" placeholder="Contoh: ZTEGC1234567" required>
|
||||
</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>
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
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 => {
|
||||
// console.log(data);
|
||||
if (data.rx_power !== null) {
|
||||
rxPowerInput.value = data.rx_power;
|
||||
} else {
|
||||
alert('⚠ ' + (data.error || 'RX Power tidak ditemukan'));
|
||||
rxPowerInput.value = '';
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
// console.error('Fetch error:', error);
|
||||
alert('❌ Gagal mengambil RX Power dari GenieACS');
|
||||
rxPowerInput.value = '';
|
||||
});
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
|
||||
|
||||
<script>
|
||||
|
||||
</script>
|
||||
|
||||
|
||||
|
||||
<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>
|
||||
<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>
|
||||
|
||||
<!-- 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 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>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
|
|
@ -0,0 +1,129 @@
|
|||
<?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'])) {
|
||||
$_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,133 @@
|
|||
<?php
|
||||
session_start();
|
||||
require 'config/database.php';
|
||||
|
||||
// Buat koneksi database
|
||||
$database = new Database();
|
||||
$conn = $database->getConnection();
|
||||
|
||||
if ($_SERVER['REQUEST_METHOD'] == 'POST') {
|
||||
$username = trim($_POST['username']);
|
||||
$password = password_hash(trim($_POST['password']), PASSWORD_DEFAULT);
|
||||
$role = $_POST['role'];
|
||||
|
||||
try {
|
||||
$stmt = $conn->prepare("INSERT INTO users (username, password, role) VALUES (?, ?, ?)");
|
||||
$stmt->execute([$username, $password, $role]);
|
||||
$success = "Pendaftaran berhasil! Silakan login.";
|
||||
} catch (PDOException $e) {
|
||||
$error = "Username sudah digunakan atau terjadi kesalahan.";
|
||||
}
|
||||
}
|
||||
?>
|
||||
<!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;
|
||||
}
|
||||
|
||||
/* Responsif untuk layar kecil */
|
||||
@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>";
|
||||
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">Administrator</option>
|
||||
<option value="teknisi">Teknisi</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