commit 371ca3292820ecb928462bf524847d39767e1948 Author: karuniaDwi Date: Sat Aug 16 16:47:24 2025 +0700 Fix: initial commit diff --git a/.htaccess b/.htaccess new file mode 100644 index 0000000..252494d --- /dev/null +++ b/.htaccess @@ -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 + + Require all denied + + +# Security - Hide config files + + Require all denied + + +# Security - Hide database files + + Require all denied + + +# MIME Types for better performance + + AddType application/javascript .js + AddType text/css .css + + +# Enable compression + + 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 + + +# Cache control for better performance + + 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" + \ No newline at end of file diff --git a/ITEM_SUMMARY.md b/ITEM_SUMMARY.md new file mode 100644 index 0000000..08980f4 --- /dev/null +++ b/ITEM_SUMMARY.md @@ -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 \ No newline at end of file diff --git a/KMZ_Export_Fix_Guide.md b/KMZ_Export_Fix_Guide.md new file mode 100644 index 0000000..c56609c --- /dev/null +++ b/KMZ_Export_Fix_Guide.md @@ -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. \ No newline at end of file diff --git a/KMZ_Export_Guide.md b/KMZ_Export_Guide.md new file mode 100644 index 0000000..6c9194b --- /dev/null +++ b/KMZ_Export_Guide.md @@ -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 + + + FTTH Planner Export + + + + + + + OLT Jakarta Selatan + Detail lengkap item... + + 106.8456,-6.2088,0 + + + + + + Route: OLT → ODP + + 106.8456,-6.2088,0 106.8500,-6.2100,0 + + + + + +``` + +## 🎓 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. 🌍 \ No newline at end of file diff --git a/KMZ_Export_Summary.md b/KMZ_Export_Summary.md new file mode 100644 index 0000000..44f41ca --- /dev/null +++ b/KMZ_Export_Summary.md @@ -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 + + + FTTH Planner Export + + + Item Name + Detailed info + ... + + + Route + ... + + + +``` + +### 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. \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..24ddf3e --- /dev/null +++ b/README.md @@ -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. \ No newline at end of file diff --git a/api/detail_redaman.php b/api/detail_redaman.php new file mode 100644 index 0000000..f59b3de --- /dev/null +++ b/api/detail_redaman.php @@ -0,0 +1,66 @@ +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' + ] +]); diff --git a/api/items.php b/api/items.php new file mode 100644 index 0000000..37053ad --- /dev/null +++ b/api/items.php @@ -0,0 +1,358 @@ +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); +?> \ No newline at end of file diff --git a/api/routes.php b/api/routes.php new file mode 100644 index 0000000..d34c045 --- /dev/null +++ b/api/routes.php @@ -0,0 +1,278 @@ +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); +?> \ No newline at end of file diff --git a/api/splitters.php b/api/splitters.php new file mode 100644 index 0000000..e1158b6 --- /dev/null +++ b/api/splitters.php @@ -0,0 +1,26 @@ +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); +?> \ No newline at end of file diff --git a/api/statistics.php b/api/statistics.php new file mode 100644 index 0000000..3353a19 --- /dev/null +++ b/api/statistics.php @@ -0,0 +1,59 @@ +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); +?> \ No newline at end of file diff --git a/api/tube_colors.php b/api/tube_colors.php new file mode 100644 index 0000000..5c247e8 --- /dev/null +++ b/api/tube_colors.php @@ -0,0 +1,26 @@ +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); +?> \ No newline at end of file diff --git a/assets/css/custom.css b/assets/css/custom.css new file mode 100644 index 0000000..ffcd95c --- /dev/null +++ b/assets/css/custom.css @@ -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; +} \ No newline at end of file diff --git a/assets/img/favicon.ico b/assets/img/favicon.ico new file mode 100644 index 0000000..e736631 Binary files /dev/null and b/assets/img/favicon.ico differ diff --git a/assets/js/app.js b/assets/js/app.js new file mode 100644 index 0000000..14ffeec --- /dev/null +++ b/assets/js/app.js @@ -0,0 +1,1248 @@ +// App.js - FTTH Planner Application Logic + +// Global variables +let editingItemId = null; +let tempClickLatLng = null; + +// Initialize application +$(document).ready(function() { + loadFormData(); + initializeEventListeners(); + + // Add core capacity change listener + $(document).on('change input', '#totalCoreCapacity, #coreUsed', calculateCoreAvailable); +}); + +// Load form data (tube colors, splitters) +function loadFormData() { + // Load tube colors + $.ajax({ + url: 'api/tube_colors.php', + method: 'GET', + success: function(response) { + if (response.success) { + // Populate tube color dropdown + let tubeColorSelect = $('#tubeColor'); + tubeColorSelect.empty().append(''); + + // Populate core color dropdown + let coreColorSelect = $('#coreColor'); + coreColorSelect.empty().append(''); + + response.data.forEach(function(color) { + let option = ``; + tubeColorSelect.append(option); + coreColorSelect.append(option); + }); + } + } + }); + + // Load splitter types + $.ajax({ + url: 'api/splitters.php', + method: 'GET', + success: function(response) { + if (response.success) { + let mainSplitterSelect = $('#splitterMain'); + let odpSplitterSelect = $('#splitterOdp'); + + mainSplitterSelect.empty().append(''); + odpSplitterSelect.empty().append(''); + + response.data.forEach(function(splitter) { + let option = ``; + + if (splitter.type === 'main') { + mainSplitterSelect.append(option); + } else { + odpSplitterSelect.append(option); + } + }); + } + } + }); +} + +// Initialize event listeners +function initializeEventListeners() { + // Item form submission + $('#itemForm').on('submit', function(e) { + e.preventDefault(); + saveItem(); + }); + + // Modal events + $('#itemModal').on('hidden.bs.modal', function() { + resetForm(); + }); + + // Tube color change event to show color preview + $('#tubeColor').on('change', function() { + updateColorPreview(); + }); +} + +// Show add item modal +function showAddItemModal(lat = null, lng = null) { + tempClickLatLng = lat && lng ? {lat: lat, lng: lng} : null; + + $('#itemModalTitle').text('Tambah Item FTTH'); + $('#itemId').val(''); + editingItemId = null; + + if (tempClickLatLng) { + $('#itemLat').val(tempClickLatLng.lat); + $('#itemLng').val(tempClickLatLng.lng); + } + + $('#itemModal').modal('show'); +} + +// Add new item (from sidebar) +function addNewItem(itemType) { + showAddItemModal(); + + // Set item type based on parameter + let itemTypeId = getItemTypeId(itemType); + if (itemTypeId) { + $('#itemType').val(itemTypeId); + } +} + +// Get item type ID from name +function getItemTypeId(typeName) { + switch(typeName) { + case 'OLT': return '1'; + case 'Tiang Tumpu': return '2'; + case 'ODP': return '3'; + case 'ODC': return '4'; + case 'Pelanggan': return '5'; + default: return ''; + } +} + +// Edit existing item +function editItem(itemId) { + editingItemId = itemId; + + $.ajax({ + url: 'api/items.php', + method: 'GET', + data: { id: itemId }, + success: function(response) { + if (response.success && response.data) { + let item = response.data; + + $('#itemModalTitle').text('Edit Item FTTH'); + $('#itemId').val(item.id); + $('#itemType').val(item.item_type_id); + $('#itemName').val(item.name); + $('#itemDescription').val(item.description); + $('#itemAddress').val(item.address); + $('#itemLat').val(item.latitude); + $('#itemLng').val(item.longitude); + $('#tubeColor').val(item.tube_color_id); + $('#coreColor').val(item.core_color_id); + $('#cableType').val(item.item_cable_type || 'distribution'); + $('#totalCoreCapacity').val(item.total_core_capacity || 24); + $('#coreUsed').val(item.core_used); + $('#splitterMain').val(item.splitter_main_id); + $('#splitterOdp').val(item.splitter_odp_id); + $('#itemStatus').val(item.status); + + // Calculate and display core available + setTimeout(() => calculateCoreAvailable(), 100); + + updateColorPreview(); + $('#itemModal').modal('show'); + } + }, + error: function() { + showNotification('Error loading item data', 'error'); + } + }); +} + +// Save item (create or update) +function saveItem() { + let method = editingItemId ? 'PUT' : 'POST'; + + // Validate required fields + if (!$('#itemType').val() || !$('#itemName').val()) { + showNotification('Harap isi semua field yang wajib', 'warning'); + return; + } + + // If no coordinates provided and not editing, get from temp click + if (!$('#itemLat').val() && !$('#itemLng').val() && tempClickLatLng) { + $('#itemLat').val(tempClickLatLng.lat); + $('#itemLng').val(tempClickLatLng.lng); + } + + // Always use POST with FormData for compatibility + let formData = new FormData($('#itemForm')[0]); + + // Log original method and current state + console.log('🔧 SAVEITEM DEBUG:'); + console.log('Original method:', method); + console.log('editingItemId:', editingItemId); + console.log('Item ID field value:', $('#itemId').val()); + + // For PUT requests, add _method parameter + if (method === 'PUT') { + formData.append('_method', 'PUT'); + + // Ensure ID is included for PUT request + if (editingItemId && !formData.get('id')) { + formData.set('id', editingItemId); + } + + // Also ensure we have the ID from the hidden field + if ($('#itemId').val() && !formData.get('id')) { + formData.set('id', $('#itemId').val()); + } + + // Log all data being sent + console.log('🚀 PUT Data being sent (all fields):'); + for (let pair of formData.entries()) { + console.log(' ' + pair[0] + ': ' + pair[1]); + } + } else { + console.log('🚀 POST Data being sent (new item)'); + } + + // Force POST method with explicit type declaration + let requestConfig = { + url: 'api/items.php', + type: 'POST', // Use 'type' instead of 'method' for better compatibility + method: 'POST', // Also set method for newer jQuery versions + data: formData, + processData: false, + contentType: false, + dataType: 'json', + cache: false, // Disable caching + success: function(response) { + if (response && response.success) { + $('#itemModal').modal('hide'); + + if (editingItemId) { + // Update existing marker + updateMarker(editingItemId, response.data); + showNotification('Item berhasil diupdate', 'success'); + } else { + // Add new marker + addMarkerToMap(response.data); + showNotification('Item berhasil ditambahkan', 'success'); + } + + updateStatistics(); + } else { + showNotification(response?.message || 'Error saving item', 'error'); + } + }, + error: function(xhr, status, error) { + console.error('AJAX Error:', error, xhr.responseText); + console.error('Response Text:', xhr.responseText); + showNotification('Error saving item: ' + error, 'error'); + } + }; + + console.log('🚀 Final request config:', { + url: requestConfig.url, + type: requestConfig.type, + method: requestConfig.method, + dataType: requestConfig.dataType + }); + + $.ajax(requestConfig); +} + +// Update marker on map +function updateMarker(itemId, itemData) { + if (markers[itemId]) { + // Remove old marker + map.removeLayer(markers[itemId]); + delete markers[itemId]; + } + + // Add updated marker + addMarkerToMap(itemData); +} + +// Delete item +function deleteItem(itemId) { + if (confirm('Apakah Anda yakin ingin menghapus item ini?')) { + $.ajax({ + url: 'api/items.php', + method: 'DELETE', + data: { id: itemId }, + success: function(response) { + if (response.success) { + // Remove marker from map + if (markers[itemId]) { + map.removeLayer(markers[itemId]); + delete markers[itemId]; + } + + // Remove any routes connected to this item + removeRoutesForItem(itemId); + + showNotification('Item berhasil dihapus', 'success'); + updateStatistics(); + } else { + showNotification(response.message || 'Error deleting item', 'error'); + } + }, + error: function() { + showNotification('Error deleting item', 'error'); + } + }); + } +} + +// Remove routes connected to item +function removeRoutesForItem(itemId) { + $.ajax({ + url: 'api/routes.php', + method: 'DELETE', + data: { item_id: itemId }, + success: function(response) { + if (response.success && response.deleted_routes) { + response.deleted_routes.forEach(function(routeId) { + if (routes[routeId]) { + map.removeLayer(routes[routeId]); + delete routes[routeId]; + } + }); + } + } + }); +} + +// Reset form +function resetForm() { + $('#itemForm')[0].reset(); + $('#itemId').val(''); + editingItemId = null; + tempClickLatLng = null; + updateColorPreview(); +} + +// Update color preview +function updateColorPreview() { + let selectedColor = $('#tubeColor option:selected').data('color'); + if (selectedColor) { + $('#tubeColor').css('border-left', `5px solid ${selectedColor}`); + } else { + $('#tubeColor').css('border-left', 'none'); + } +} + +// Show item list +function showItemList() { + $.ajax({ + url: 'api/items.php', + method: 'GET', + success: function(response) { + if (response.success) { + let itemListHtml = generateItemListHtml(response.data); + showModal('Daftar Item FTTH', itemListHtml, 'modal-xl'); + } + }, + error: function() { + showNotification('Error loading item list', 'error'); + } + }); +} + +// Generate item list HTML +function generateItemListHtml(items) { + let html = ` +
+ + + + + + + + + + + + + `; + + items.forEach(function(item) { + html += ` + + + + + + + + + `; + }); + + html += ` + +
JenisNamaAlamatKoordinatStatusAksi
+ + ${item.item_type_name} + ${item.name}${item.address || '-'}${(isNaN(parseFloat(item.latitude)) || isNaN(parseFloat(item.longitude))) ? 'Koordinat tidak valid' : `${parseFloat(item.latitude).toFixed(6)}, ${parseFloat(item.longitude).toFixed(6)}`} + + ${getStatusText(item.status)} + + + + + +
+
+ `; + + return html; +} + +// Get item color +function getItemColor(typeName) { + switch(typeName) { + case 'OLT': return '#FF6B6B'; + case 'Tiang Tumpu': return '#4ECDC4'; + case 'ODP': return '#45B7D1'; + case 'ODC': return '#96CEB4'; + case 'Pelanggan': return '#FFA500'; + default: return '#999'; + } +} + +// Focus on item in map +function focusOnItem(itemId) { + if (markers[itemId]) { + let marker = markers[itemId]; + map.setView(marker.getLatLng(), 16); + marker.openPopup(); + } +} + +// Show route list +function showRouteList() { + $.ajax({ + url: 'api/routes.php', + method: 'GET', + success: function(response) { + if (response.success) { + let routeListHtml = generateRouteListHtml(response.data); + showModal('Daftar Routing Kabel', routeListHtml, 'modal-xl'); + } + }, + error: function() { + showNotification('Error loading route list', 'error'); + } + }); +} + +// Generate route list HTML +function generateRouteListHtml(routes) { + let html = ` +
+ + + + + + + + + + + + + + `; + + routes.forEach(function(route) { + let distance = route.distance ? (route.distance / 1000).toFixed(2) + ' km' : '-'; + + html += ` + + + + + + + + + + `; + }); + + html += ` + +
DariKeJarakTipe KabelCoreStatusAksi
${route.from_item_name || 'Unknown'}${route.to_item_name || 'Unknown'}${distance}${route.cable_type || '-'}${route.core_count || '-'} + + ${getStatusText(route.status)} + + + + + +
+
+ `; + + return html; +} + +// Focus on route in map +function focusOnRoute(routeId) { + if (routes[routeId]) { + let route = routes[routeId]; + map.fitBounds(route.getBounds()); + route.openPopup(); + } +} + +// Delete route +function deleteRoute(routeId) { + if (confirm('Apakah Anda yakin ingin menghapus route ini?')) { + $.ajax({ + url: 'api/routes.php', + method: 'DELETE', + data: { id: routeId }, + success: function(response) { + if (response.success) { + if (routes[routeId]) { + map.removeLayer(routes[routeId]); + delete routes[routeId]; + } + showNotification('Route berhasil dihapus', 'success'); + } else { + showNotification(response.message || 'Error deleting route', 'error'); + } + }, + error: function() { + showNotification('Error deleting route', 'error'); + } + }); + } +} + +// Generic modal function +function showModal(title, content, size = 'modal-lg') { + if (!$('#genericModal').length) { + $('body').append(` + + `); + } + + $('#genericModalTitle').text(title); + $('#genericModalBody').html(content); + $('#genericModal').modal('show'); +} + +// Edit route function +function editRoute(routeId) { + // Get route data first + $.ajax({ + url: 'api/routes.php', + method: 'GET', + data: { id: routeId }, + success: function(response) { + if (response.success && response.data) { + let route = response.data; + showEditRouteModal(route); + } else { + showNotification('Error loading route data', 'error'); + } + }, + error: function() { + showNotification('Error loading route data', 'error'); + } + }); +} + +//terpasang kabel rx power +// Contoh marker pelanggan +// Event saat dropdown status diubah +// Fungsi untuk memuat RX Power di tengah kabel + + +// Show edit route modal +function showEditRouteModal(route) { + let modalHtml = ` +
+ + +
+
+
+ + +
+
+
+
+ + +
+
+
+ +
+
+
+ + +
+
+
+
+ + +
+
+
+
+ + +
+
+
+
+ + + Jarak dihitung otomatis berdasarkan routing +
+ +
+ + +
+
+ `; + + // Create modal if doesn't exist + if (!$('#routeEditModal').length) { + $('body').append(` + + `); + } + + $('#routeEditModalBody').html(modalHtml); + $('#routeEditModal').modal('show'); + + // Handle form submission + $('#editRouteForm').on('submit', function(e) { + e.preventDefault(); + saveRouteEdit(); + }); +} + +// Save route edit +function saveRouteEdit() { + let formData = new FormData(); + formData.append('_method', 'PUT'); + formData.append('id', $('#editRouteId').val()); + formData.append('cable_type', $('#editCableType').val()); + formData.append('core_count', $('#editCoreCount').val()); + formData.append('status', $('#editRouteStatus').val()); + + console.log('🚀 Route Edit Data being sent:'); + for (let pair of formData.entries()) { + console.log(' ' + pair[0] + ': ' + pair[1]); + } + + $.ajax({ + url: 'api/routes.php', + type: 'POST', + method: 'POST', + data: formData, + processData: false, + contentType: false, + dataType: 'json', + cache: false, + success: function(response) { + console.log('✅ Route update response:', response); + if (response.success) { + $('#routeEditModal').modal('hide'); + showNotification('Route berhasil diupdate', 'success'); + + // Refresh route list if open + if ($('#genericModal').hasClass('show')) { + showRouteList(); + } + + // Update route on map + loadRoutes(); + } else { + console.error('❌ Route update failed:', response.message); + showNotification(response.message || 'Error updating route', 'error'); + } + }, + error: function(xhr, status, error) { + console.error('❌ AJAX Error:', error, xhr.responseText); + console.error('Response Text:', xhr.responseText); + try { + let errorResponse = JSON.parse(xhr.responseText); + showNotification(errorResponse.message || 'Error updating route', 'error'); + } catch(e) { + showNotification('Error updating route: ' + error, 'error'); + } + } + }); +} + +// Calculate core available +function calculateCoreAvailable() { + let totalCapacity = parseInt($('#totalCoreCapacity').val()) || 0; + let coreUsed = parseInt($('#coreUsed').val()) || 0; + let coreAvailable = totalCapacity - coreUsed; + + $('#coreAvailable').val(coreAvailable + ' / ' + totalCapacity + ' Core'); + + // Set color based on availability + if (coreAvailable <= 0) { + $('#coreAvailable').removeClass('text-success text-warning').addClass('text-danger'); + } else if (coreAvailable <= totalCapacity * 0.2) { + $('#coreAvailable').removeClass('text-success text-danger').addClass('text-warning'); + } else { + $('#coreAvailable').removeClass('text-danger text-warning').addClass('text-success'); + } +} + +// Sync core usage from routes +function syncCoreUsageFromRoutes(itemId) { + if (!itemId) return; + + $.ajax({ + url: 'api/routes.php', + method: 'GET', + success: function(response) { + if (response.success) { + let totalCoreUsed = 0; + + response.data.forEach(function(route) { + if (route.from_item_id == itemId || route.to_item_id == itemId) { + totalCoreUsed += parseInt(route.core_count) || 0; + } + }); + + // Update core used in form + $('#coreUsed').val(totalCoreUsed); + calculateCoreAvailable(); + + console.log(`📊 Core usage synced for item ${itemId}: ${totalCoreUsed} cores used`); + } + }, + error: function() { + console.error('Failed to sync core usage from routes'); + } + }); +} + +// Enhanced edit item to include core sync +function editItemEnhanced(itemId) { + editItem(itemId); + // Sync core usage after loading item data + setTimeout(() => syncCoreUsageFromRoutes(itemId), 500); +} + +// Show item detail +function showItemDetail(itemId) { + $.ajax({ + url: 'api/items.php', + method: 'GET', + data: { id: itemId }, + success: function(response) { + if (response.success && response.data) { + let item = response.data; + showItemDetailModal(item); + } else { + showNotification('Error loading item data', 'error'); + } + }, + error: function() { + showNotification('Error loading item data', 'error'); + } + }); +} + +// Show item detail modal //ini detail jika mau edit +function showItemDetailModal(item) { + let modalHtml = ` +
+
+
+
+
+ + ${item.name} +
+
+
+
+ +
+
+ Informasi Dasar +
+ + + + + + + + + + + + + + + + + + + + + + + + + +
ID:${item.id}
Jenis Item: + + + ${item.item_type_name} + +
Nama:${item.name}
Deskripsi:${item.description || '-'}
Alamat:${item.address || '-'}
Status: + + ${getStatusText(item.status)} + +
+
+ + +
+
+ Informasi Lokasi +
+ + + + + + + + + + + + + + + + + +
Latitude:${item.latitude}
Longitude:${item.longitude}
Koordinat: + ${item.latitude}, ${item.longitude} + +
Google Maps: + + Buka di Maps + +
+
+
+ +
+ +
+ +
+
+ Informasi Core & Kabel +
+ + + + + + + + + + + + + + + + + + + + + + + + + +
Warna Tube: + ${item.tube_color_name ? ` + + ${item.tube_color_name} + ` : '-'} +
Warna Core: + ${item.core_color_name ? ` + + ${item.core_color_name} + ` : '-'} +
Jenis Kabel: + ${item.item_cable_type ? ` + + ${getCableTypeText(item.item_cable_type)} + + ` : '-'} +
Kapasitas Core: + ${item.total_core_capacity || 24} Core +
Core Digunakan: + + ${item.core_used || 0} Core + +
Core Tersedia: + + ${(item.total_core_capacity || 24) - (item.core_used || 0)} Core + +
+
+ + +
+
+ Informasi Splitter +
+ + + + + + + + + +
Splitter Utama: + ${item.splitter_main_ratio ? ` + ${item.splitter_main_ratio} + ` : '-'} +
Splitter ODP: + ${item.splitter_odp_ratio ? ` + ${item.splitter_odp_ratio} + ` : '-'} +
+ +
+ Timestamp +
+ + + + + + + + + +
Dibuat:${formatDate(item.created_at)}
Diupdate:${formatDate(item.updated_at)}
+
+
+
+ +
+
+
+ `; + + // Create modal if doesn't exist + if (!$('#itemDetailModal').length) { + $('body').append(` + + `); + } + + $('#itemDetailModalBody').html(modalHtml); + $('#itemDetailModal').modal('show'); +} + +//detailredaman +// Fungsi untuk menampilkan detail redaman +function showRedamanDetail(item) { + let modalHtml = ` +
+
+
+
+
+ Detail Redaman +
+
+
+ + + + + + + + + + + + + + +
Serial Number (SN):${item.serial_number}
Rx Power:${item.rx_power}
PPPoE IP: + ${item.pppoe_ip && item.pppoe_ip !== 'Tidak tersedia' + ? `${item.pppoe_ip}` + : item.pppoe_ip} +
+
+ +
+
+
+ `; + + // Buat modal jika belum ada + if (!$('#redamanDetailModal').length) { + $('body').append(` + + `); + } + + $('#redamanDetailModalBody').html(modalHtml); + $('#redamanDetailModal').modal('show'); +} +// Contoh memanggil data dari API redaman +function loadRedamanDetail(id) { + if (!id) { + console.warn('ID tidak valid:', id); + alert('ID item tidak valid.'); + return; + } +//redaman url bisa + $.ajax({ + url: 'https://172.16.96.10/ftthplanner/api/detail_redaman.php', // path sesuai lokasi HTML/JS + method: 'GET', + data: { id: id }, // kirim 'id' sesuai PHP + dataType: 'json', + success: function(response) { + if (response && response.success && response.data) { + showRedamanDetail(response.data); + } else { + console.warn('Response tidak sesuai harapan:', response); + alert('Gagal memuat data redaman'); + } + }, + error: function(xhr, status, error) { + console.error('AJAX Error:', status, error, 'Response:', xhr.responseText); + alert('Terjadi kesalahan saat memuat data redaman'); + } + }); +} + + + + +// Helper function to copy text to clipboard +function copyToClipboard(text) { + navigator.clipboard.writeText(text).then(function() { + showNotification('Koordinat disalin ke clipboard', 'success'); + }).catch(function() { + showNotification('Gagal menyalin koordinat', 'error'); + }); +} + +// Format date helper +function formatDate(dateString) { + if (!dateString) return '-'; + const date = new Date(dateString); + return date.toLocaleString('id-ID', { + year: 'numeric', + month: 'long', + day: 'numeric', + hour: '2-digit', + minute: '2-digit' + }); +} + +// Export functions to global scope +window.showAddItemModal = showAddItemModal; +window.addNewItem = addNewItem; +window.editItem = editItem; +window.editItemEnhanced = editItemEnhanced; +window.deleteItem = deleteItem; +window.showItemDetail = showItemDetail; +window.showItemList = showItemList; +window.showRouteList = showRouteList; +window.focusOnItem = focusOnItem; +window.focusOnRoute = focusOnRoute; +window.deleteRoute = deleteRoute; +window.editRoute = editRoute; +window.calculateCoreAvailable = calculateCoreAvailable; +window.syncCoreUsageFromRoutes = syncCoreUsageFromRoutes; +window.copyToClipboard = copyToClipboard; + +// Helper functions for display +function getCableTypeBadge(cableType) { + switch(cableType) { + case 'backbone': return 'danger'; + case 'distribution': return 'primary'; + case 'drop_core': return 'success'; + case 'feeder': return 'info'; + case 'branch': return 'warning'; + default: return 'secondary'; + } +} + +function getCableTypeText(cableType) { + switch(cableType) { + case 'backbone': return 'Backbone'; + case 'distribution': return 'Distribution'; + case 'drop_core': return 'Drop Core'; + case 'feeder': return 'Feeder'; + case 'branch': return 'Branch'; + default: return '-'; + } +} + +function getCoreUsageBadge(used, total) { + if (!used || !total) return 'secondary'; + + let percentage = (used / total) * 100; + if (percentage >= 90) return 'danger'; + if (percentage >= 70) return 'warning'; + if (percentage >= 50) return 'info'; + return 'success'; +} + +// Export helper functions +window.getCableTypeBadge = getCableTypeBadge; +window.getCableTypeText = getCableTypeText; +window.getCoreUsageBadge = getCoreUsageBadge; \ No newline at end of file diff --git a/assets/js/kmz-export.js b/assets/js/kmz-export.js new file mode 100644 index 0000000..7295add --- /dev/null +++ b/assets/js/kmz-export.js @@ -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 = ` + + + FTTH Planner Export + Export data infrastruktur FTTH dari FTTH Planner + + ${generateStyles()} + ${generateItemPlacemarks(items)} + ${generateRoutePlacemarks(routes)} + + +`; + + return kml; +} + +// Generate KML styles for different item types +function generateStyles() { + return ` + + + + + + + + + + + + + + + + + + + + + + `; +} + +// 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 += ` + + ${escapeXML(item.name)} + + #${styleId} + + ${lng},${lat},0 + + `; + }); + + 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 += ` + + Route: ${escapeXML(route.from_item_name)} → ${escapeXML(route.to_item_name)} + + #${styleId} + + 1 + ${coordinates} + + `; + } + }); + + 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 = ` + + + `; + + if (item.description) { + description += ``; + } + + if (item.address) { + description += ``; + } + + // 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 += ``; + + if (item.tube_color_name) { + description += ``; + } + + if (item.core_used) { + description += ``; + } + + if (item.splitter_main_ratio) { + description += ``; + } + + if (item.splitter_odp_ratio) { + description += ``; + } + + description += ``; + description += `
Jenis:${item.item_type_name}
Nama:${escapeXML(item.name)}
Deskripsi:${escapeXML(item.description)}
Alamat:${escapeXML(item.address)}
Koordinat:${coordText}
Warna Tube:${item.tube_color_name}
Core Digunakan:${item.core_used}
Splitter Utama:${item.splitter_main_ratio}
Splitter ODP:${item.splitter_odp_ratio}
Status:${getStatusText(item.status)}
`; + + return description; +} + +// Generate route description HTML +function generateRouteDescription(route) { + let distance = route.distance ? (route.distance / 1000).toFixed(2) + ' km' : 'Unknown'; + + return ` + + + + + + + +
Dari:${escapeXML(route.from_item_name || 'Unknown')}
Ke:${escapeXML(route.to_item_name || 'Unknown')}
Jarak:${distance}
Tipe Kabel:${escapeXML(route.cable_type || 'Fiber Optic')}
Jumlah Core:${route.core_count || 24}
Status:${getStatusText(route.status)}
+ `; +} + +// Escape XML special characters +function escapeXML(text) { + if (!text) return ''; + return text.toString() + .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; \ No newline at end of file diff --git a/assets/js/map.js b/assets/js/map.js new file mode 100644 index 0000000..f5f7b7a --- /dev/null +++ b/assets/js/map.js @@ -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: '© OpenStreetMap contributors', + maxZoom: 19 + }), + + "CartoDB Positron": L.tileLayer('https://{s}.basemaps.cartocdn.com/light_all/{z}/{x}/{y}{r}.png', { + attribution: '© OpenStreetMap contributors © CARTO', + maxZoom: 20 + }), + + "CartoDB Dark": L.tileLayer('https://{s}.basemaps.cartocdn.com/dark_all/{z}/{x}/{y}{r}.png', { + attribution: '© OpenStreetMap contributors © CARTO', + 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: © OpenStreetMap contributors, SRTM | Map style: © OpenTopoMap (CC-BY-SA)', + 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 = ''; + 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 = ' 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: ` +
+ +
+ ${rxPower} +
+ + +
+ `, + 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 = ` +
+
${item.name}
+ + `; + + // 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(` +
+
Route Kabel
+

Jarak: ${(route.summary.totalDistance / 1000).toFixed(2)} km

+

Tipe Kabel: Fiber Optic

+

Jumlah Core: 24

+

Status: Perencanaan

+
+ `); + + 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(` +
+
Route Kabel (Direct)
+

Jarak: ${(distance / 1000).toFixed(2)} km

+

Tipe Kabel: Fiber Optic

+

Jumlah Core: 24

+

Status: Perencanaan

+
+ `); + + 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(` +
+
Route Kabel
+

Jarak: ${(route.distance / 1000).toFixed(2)} km

+

Tipe Kabel: ${route.cable_type}

+

Jumlah Core: ${route.core_count}

+

Status: ${getStatusText(route.status)}

+
+ `); + + 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 = ` +
Legend
+
+
+ OLT +
+
+
+ Tiang Tumpu +
+
+
+ ODP +
+
+
+ ODC +
+
+
+ Pelanggan +
+
+
+
━━━ Terpasang
+
┅┅┅ Perencanaan
+
┅┅┅ Maintenance
+
+ `; + 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 = ` + + `; + + $('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 = ''; + 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 +}); diff --git a/auto_login_genieacs.php b/auto_login_genieacs.php new file mode 100644 index 0000000..197fd7e --- /dev/null +++ b/auto_login_genieacs.php @@ -0,0 +1,39 @@ + $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(); +?> diff --git a/auto_login_observium.php b/auto_login_observium.php new file mode 100644 index 0000000..a541c08 --- /dev/null +++ b/auto_login_observium.php @@ -0,0 +1,36 @@ + $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(); +?> diff --git a/bg.jpg b/bg.jpg new file mode 100644 index 0000000..f4bfe51 Binary files /dev/null and b/bg.jpg differ diff --git a/config/database.php b/config/database.php new file mode 100644 index 0000000..2d7d074 --- /dev/null +++ b/config/database.php @@ -0,0 +1,22 @@ +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; + } +} +?> \ No newline at end of file diff --git a/database.sql b/database.sql new file mode 100644 index 0000000..9a6df76 --- /dev/null +++ b/database.sql @@ -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'); diff --git a/get_clients.php b/get_clients.php new file mode 100644 index 0000000..b5479c3 --- /dev/null +++ b/get_clients.php @@ -0,0 +1,37 @@ +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() + ]); +} diff --git a/get_rx_power.php b/get_rx_power.php new file mode 100644 index 0000000..6c595ce --- /dev/null +++ b/get_rx_power.php @@ -0,0 +1,43 @@ + 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); diff --git a/hapus.php b/hapus.php new file mode 100644 index 0000000..6f9c076 --- /dev/null +++ b/hapus.php @@ -0,0 +1,15 @@ + 0) { + $stmt = $pdo->prepare("DELETE FROM items WHERE id = :id"); + $stmt->execute(['id' => $id]); + } + + header("Location: index.php"); + exit; +} +?> diff --git a/index.php b/index.php new file mode 100644 index 0000000..ffe2044 --- /dev/null +++ b/index.php @@ -0,0 +1,651 @@ + + + + + + + FTTH Megadata | Dashboard + + + + + + + + + + + + + + + + + +
+ + +
+ +

FTTH Megadata

+
+ + + + + + + + +
+ +
+
+
+
+

Dashboard FTTH Megadata

+
+
+ +
+
+
+
+ + +
+
+ +
+
+
+
+

+ + Peta Infrastruktur FTTH +

+
+ +
+ + + + + + + +
+ + +
+ + +
+
+ + + + +
+ + + + + +
+
+
+
+
+
+
+
+
+ + +
+
+
+
+

0

+

OLT

+
+
+ +
+
+
+
+
+
+

0

+

Tiang

+
+
+ +
+
+
+
+
+
+

0

+

ODP

+
+
+ +
+
+
+
+
+
+

0

+

ODC

+
+
+ +
+
+
+
+
+
+

0

+

Pelanggan

+
+
+ +
+
+
+
+
+
+

0

+

Routes

+
+
+ +
+
+
+
+
+ +
+ + +
+ Copyright © 2025 FTTH Megadata by Kuli Jaringan. + Semua hak dilindungi undang-undang. +
+ Versi 1.0.0 +
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/login.php b/login.php new file mode 100644 index 0000000..3868bcd --- /dev/null +++ b/login.php @@ -0,0 +1,129 @@ +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(); + } +} +?> + + + + + + Login - FTTH Megadata + + + +
+ Logo +

Login

+ $error

"; ?> +
+ + + +
+
+ + diff --git a/logout.php b/logout.php new file mode 100644 index 0000000..7649023 --- /dev/null +++ b/logout.php @@ -0,0 +1,5 @@ +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); +?> + + + + + Manajemen Users - FTTH Megadata + + + + +
+

👥 Manajemen Users

+ + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + +
IDUsernameRoleAksi
+ + + + Hapus +
+
+ + + +
+ + + + diff --git a/megadata.jpeg b/megadata.jpeg new file mode 100644 index 0000000..fe7c0c0 Binary files /dev/null and b/megadata.jpeg differ diff --git a/register.php b/register.php new file mode 100644 index 0000000..e65d9ad --- /dev/null +++ b/register.php @@ -0,0 +1,133 @@ +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."; + } +} +?> + + + + + + Register - FTTH Megadata + + + +
+ Logo +

Daftar Akun

+ $success

"; + if (!empty($error)) echo "

$error

"; + ?> +
+ + + + +
+

Sudah punya akun? Login di sini

+
+ + diff --git a/setup_instructions.md b/setup_instructions.md new file mode 100644 index 0000000..8edb6dd --- /dev/null +++ b/setup_instructions.md @@ -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! 🎉 \ No newline at end of file diff --git a/testing_guide.md b/testing_guide.md new file mode 100644 index 0000000..40cd42c --- /dev/null +++ b/testing_guide.md @@ -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! 🚀 \ No newline at end of file diff --git a/update_database.sql b/update_database.sql new file mode 100644 index 0000000..8f5f6d8 --- /dev/null +++ b/update_database.sql @@ -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; \ No newline at end of file diff --git a/wifi.png b/wifi.png new file mode 100644 index 0000000..1926972 Binary files /dev/null and b/wifi.png differ