feat: Integrate MySQL backend and rebrand to LPD Gerana

- Add Node.js Express backend with REST API
- Create database schema and server.js
- Migrate frontend from IndexedDB to MySQL API
- Add environment configuration (.env)
- Rebrand from Koperasi to LPD Gerana
- Update all documentation and UI text
- Add configurable server port setting

Features:
- POST /api/dokumentasi - Create documentation
- GET /api/dokumentasi - Retrieve all (with search)
- GET /api/dokumentasi/:id - Get single record
- DELETE /api/dokumentasi/:id - Delete record
- Full CRUD operations with MySQL persistence
This commit is contained in:
2026-01-19 13:41:01 +08:00
parent 162f8a38a4
commit b9b255ec79
8 changed files with 2472 additions and 171 deletions

View File

@@ -1,4 +1,4 @@
# 📸 Aplikasi Dokumentasi Nasabah Koperasi
# 📸 Aplikasi Dokumentasi Nasabah LPD Gerana
Aplikasi web untuk mengambil foto dokumentasi nasabah saat penandatanganan surat perjanjian menggunakan webcam USB.
@@ -12,7 +12,7 @@ Aplikasi web untuk mengambil foto dokumentasi nasabah saat penandatanganan surat
### 2. Input Data Nasabah
- ✅ Nama lengkap nasabah
- ✅ Nomor anggota koperasi
- ✅ Nomor anggota LPD Gerana
- ✅ Jenis perjanjian/dokumen
- ✅ Tanggal penandatanganan (auto-fill hari ini)
- ✅ Catatan tambahan (opsional)
@@ -115,11 +115,11 @@ Aplikasi web untuk mengambil foto dokumentasi nasabah saat penandatanganan surat
## 🎨 Kustomisasi
### Mengubah Logo Koperasi
### Mengubah Logo LPD Gerana
Edit file `style.css` dan tambahkan logo di header:
```css
.app-header::before {
content: url('logo-koperasi.png');
content: url('logo-lpd-gerana.png');
}
```
@@ -183,11 +183,11 @@ Edit file `style.css` di bagian `:root`:
## 📄 Lisensi
Aplikasi ini bebas digunakan untuk keperluan internal Koperasi.
Aplikasi ini bebas digunakan untuk keperluan internal LPD Gerana.
## 🤝 Dukungan
Untuk pertanyaan atau masalah, silakan hubungi IT Support Koperasi.
Untuk pertanyaan atau masalah, silakan hubungi IT Support LPD Gerana.
---

165
app.js
View File

@@ -1,5 +1,5 @@
// ============================================
// APLIKASI DOKUMENTASI NASABAH KOPERASI
// APLIKASI DOKUMENTASI NASABAH LPD GERANA
// ============================================
// Global Variables
@@ -50,114 +50,101 @@ const elements = {
};
// ============================================
// INDEXEDDB SETUP
// API CONFIGURATION
// ============================================
function initDatabase() {
return new Promise((resolve, reject) => {
const request = indexedDB.open('KoperasiDB', 1);
const API_BASE_URL = 'http://localhost:3000/api';
request.onerror = () => {
showToast('Gagal membuka database', 'error');
reject(request.error);
};
request.onsuccess = () => {
db = request.result;
console.log('Database berhasil dibuka');
resolve(db);
};
request.onupgradeneeded = (event) => {
db = event.target.result;
if (!db.objectStoreNames.contains('dokumentasi')) {
const objectStore = db.createObjectStore('dokumentasi', {
keyPath: 'id',
autoIncrement: true
});
// Create indexes for searching
objectStore.createIndex('nama', 'nama', { unique: false });
objectStore.createIndex('noAnggota', 'noAnggota', { unique: false });
objectStore.createIndex('tanggal', 'tanggal', { unique: false });
objectStore.createIndex('timestamp', 'timestamp', { unique: false });
console.log('Object store "dokumentasi" berhasil dibuat');
async function checkApiConnection() {
try {
const response = await fetch(`${API_BASE_URL}/health`);
if (!response.ok) {
throw new Error('API not responding');
}
console.log('✅ API connection successful');
return true;
} catch (error) {
console.error('❌ API connection failed:', error);
showToast('⚠️ Tidak dapat terhubung ke server. Pastikan server berjalan.', 'error');
return false;
}
};
});
}
// ============================================
// DATABASE OPERATIONS
// ============================================
function saveToDatabase(data) {
return new Promise((resolve, reject) => {
const transaction = db.transaction(['dokumentasi'], 'readwrite');
const objectStore = transaction.objectStore('dokumentasi');
const request = objectStore.add(data);
request.onsuccess = () => {
console.log('Data berhasil disimpan dengan ID:', request.result);
resolve(request.result);
};
request.onerror = () => {
console.error('Gagal menyimpan data:', request.error);
reject(request.error);
};
async function saveToDatabase(data) {
try {
const response = await fetch(`${API_BASE_URL}/dokumentasi`, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(data)
});
if (!response.ok) {
const error = await response.json();
throw new Error(error.error || 'Failed to save');
}
const result = await response.json();
console.log('Data berhasil disimpan dengan ID:', result.id);
return result.id;
} catch (error) {
console.error('Error saving to database:', error);
throw error;
}
}
function getAllData() {
return new Promise((resolve, reject) => {
const transaction = db.transaction(['dokumentasi'], 'readonly');
const objectStore = transaction.objectStore('dokumentasi');
const request = objectStore.getAll();
async function getAllData() {
try {
const response = await fetch(`${API_BASE_URL}/dokumentasi`);
request.onsuccess = () => {
resolve(request.result);
};
if (!response.ok) {
throw new Error('Failed to fetch data');
}
request.onerror = () => {
reject(request.error);
};
});
const result = await response.json();
return result.data || [];
} catch (error) {
console.error('Error fetching data:', error);
throw error;
}
}
function getDataById(id) {
return new Promise((resolve, reject) => {
const transaction = db.transaction(['dokumentasi'], 'readonly');
const objectStore = transaction.objectStore('dokumentasi');
const request = objectStore.get(id);
async function getDataById(id) {
try {
const response = await fetch(`${API_BASE_URL}/dokumentasi/${id}`);
request.onsuccess = () => {
resolve(request.result);
};
if (!response.ok) {
throw new Error('Data not found');
}
request.onerror = () => {
reject(request.error);
};
});
const result = await response.json();
return result.data;
} catch (error) {
console.error('Error fetching data by ID:', error);
throw error;
}
}
function deleteData(id) {
return new Promise((resolve, reject) => {
const transaction = db.transaction(['dokumentasi'], 'readwrite');
const objectStore = transaction.objectStore('dokumentasi');
const request = objectStore.delete(id);
request.onsuccess = () => {
resolve();
};
request.onerror = () => {
reject(request.error);
};
async function deleteData(id) {
try {
const response = await fetch(`${API_BASE_URL}/dokumentasi/${id}`, {
method: 'DELETE'
});
if (!response.ok) {
throw new Error('Failed to delete');
}
return true;
} catch (error) {
console.error('Error deleting data:', error);
throw error;
}
}
// ============================================
@@ -618,8 +605,8 @@ function setupEventListeners() {
async function init() {
try {
// Initialize database
await initDatabase();
// Check API connection
const isConnected = await checkApiConnection();
// Set default date
setDefaultDate();

630
app.js.backup Normal file
View File

@@ -0,0 +1,630 @@
// ============================================
// APLIKASI DOKUMENTASI NASABAH KOPERASI
// ============================================
// Global Variables
let stream = null;
let capturedImage = null;
let db = null;
let currentDetailId = null;
// DOM Elements
const elements = {
// Webcam
webcam: document.getElementById('webcam'),
canvas: document.getElementById('canvas'),
preview: document.getElementById('preview'),
previewImage: document.getElementById('previewImage'),
// Buttons
startBtn: document.getElementById('startBtn'),
captureBtn: document.getElementById('captureBtn'),
retakeBtn: document.getElementById('retakeBtn'),
saveBtn: document.getElementById('saveBtn'),
printBtn: document.getElementById('printBtn'),
deleteBtn: document.getElementById('deleteBtn'),
exportBtn: document.getElementById('exportBtn'),
// Form
form: document.getElementById('nasabahForm'),
nama: document.getElementById('nama'),
noAnggota: document.getElementById('noAnggota'),
jenisPerjanjian: document.getElementById('jenisPerjanjian'),
tanggal: document.getElementById('tanggal'),
catatan: document.getElementById('catatan'),
// Gallery
gallery: document.getElementById('gallery'),
emptyState: document.getElementById('emptyState'),
searchInput: document.getElementById('searchInput'),
galleryStats: document.getElementById('galleryStats'),
// Modal
modal: document.getElementById('detailModal'),
modalBody: document.getElementById('modalBody'),
closeModal: document.querySelector('.close-modal'),
// Status
webcamStatus: document.getElementById('webcamStatus'),
toast: document.getElementById('toast')
};
// ============================================
// API CONFIGURATION
// ============================================
const API_BASE_URL = 'http://localhost:3000/api';
async function checkApiConnection() {
try {
const response = await fetch(`${API_BASE_URL}/health`);
if (!response.ok) {
throw new Error('API not responding');
}
console.log('✅ API connection successful');
return true;
} catch (error) {
console.error('❌ API connection failed:', error);
showToast('⚠️ Tidak dapat terhubung ke server. Pastikan server berjalan.', 'error');
return false;
}
}
// ============================================
// DATABASE OPERATIONS
// ============================================
function saveToDatabase(data) {
return new Promise((resolve, reject) => {
const transaction = db.transaction(['dokumentasi'], 'readwrite');
const objectStore = transaction.objectStore('dokumentasi');
const request = objectStore.add(data);
request.onsuccess = () => {
console.log('Data berhasil disimpan dengan ID:', request.result);
resolve(request.result);
};
request.onerror = () => {
console.error('Gagal menyimpan data:', request.error);
reject(request.error);
};
});
}
function getAllData() {
return new Promise((resolve, reject) => {
const transaction = db.transaction(['dokumentasi'], 'readonly');
const objectStore = transaction.objectStore('dokumentasi');
const request = objectStore.getAll();
request.onsuccess = () => {
resolve(request.result);
};
request.onerror = () => {
reject(request.error);
};
});
}
function getDataById(id) {
return new Promise((resolve, reject) => {
const transaction = db.transaction(['dokumentasi'], 'readonly');
const objectStore = transaction.objectStore('dokumentasi');
const request = objectStore.get(id);
request.onsuccess = () => {
resolve(request.result);
};
request.onerror = () => {
reject(request.error);
};
});
}
function deleteData(id) {
return new Promise((resolve, reject) => {
const transaction = db.transaction(['dokumentasi'], 'readwrite');
const objectStore = transaction.objectStore('dokumentasi');
const request = objectStore.delete(id);
request.onsuccess = () => {
resolve();
};
request.onerror = () => {
reject(request.error);
};
});
}
// ============================================
// WEBCAM FUNCTIONS
// ============================================
async function startWebcam() {
try {
showStatus('Meminta akses webcam...', 'info');
stream = await navigator.mediaDevices.getUserMedia({
video: {
width: { ideal: 1280 },
height: { ideal: 720 },
facingMode: 'user'
},
audio: false
});
elements.webcam.srcObject = stream;
elements.startBtn.style.display = 'none';
elements.captureBtn.style.display = 'inline-flex';
showStatus('Webcam aktif. Posisikan nasabah dan klik "Ambil Foto"', 'success');
showToast('✅ Webcam berhasil diaktifkan', 'success');
} catch (error) {
console.error('Error accessing webcam:', error);
showStatus('Gagal mengakses webcam. Pastikan webcam terhubung dan izin diberikan.', 'error');
showToast('❌ Gagal mengakses webcam', 'error');
}
}
function capturePhoto() {
const canvas = elements.canvas;
const context = canvas.getContext('2d');
// Set canvas size to match video
canvas.width = elements.webcam.videoWidth;
canvas.height = elements.webcam.videoHeight;
// Draw video frame to canvas
context.drawImage(elements.webcam, 0, 0, canvas.width, canvas.height);
// Get image data
capturedImage = canvas.toDataURL('image/jpeg', 0.9);
// Show preview
elements.previewImage.src = capturedImage;
elements.webcam.style.display = 'none';
elements.preview.style.display = 'block';
// Update buttons
elements.captureBtn.style.display = 'none';
elements.retakeBtn.style.display = 'inline-flex';
elements.saveBtn.style.display = 'inline-flex';
showStatus('Foto berhasil diambil. Periksa hasilnya dan klik "Simpan" jika sudah sesuai.', 'success');
showToast('📸 Foto berhasil diambil', 'success');
}
function retakePhoto() {
capturedImage = null;
elements.webcam.style.display = 'block';
elements.preview.style.display = 'none';
elements.retakeBtn.style.display = 'none';
elements.saveBtn.style.display = 'none';
elements.captureBtn.style.display = 'inline-flex';
showStatus('Webcam aktif. Ambil foto ulang.', 'info');
}
function stopWebcam() {
if (stream) {
stream.getTracks().forEach(track => track.stop());
stream = null;
}
elements.webcam.srcObject = null;
elements.webcam.style.display = 'block';
elements.preview.style.display = 'none';
elements.startBtn.style.display = 'inline-flex';
elements.captureBtn.style.display = 'none';
elements.retakeBtn.style.display = 'none';
elements.saveBtn.style.display = 'none';
capturedImage = null;
}
// ============================================
// FORM FUNCTIONS
// ============================================
async function saveDocumentation() {
// Validate form
if (!elements.form.checkValidity()) {
elements.form.reportValidity();
showToast('⚠️ Lengkapi semua data yang wajib diisi', 'error');
return;
}
// Validate photo
if (!capturedImage) {
showToast('⚠️ Ambil foto terlebih dahulu', 'error');
return;
}
// Prepare data
const data = {
nama: elements.nama.value.trim(),
noAnggota: elements.noAnggota.value.trim(),
jenisPerjanjian: elements.jenisPerjanjian.value,
tanggal: elements.tanggal.value,
catatan: elements.catatan.value.trim(),
foto: capturedImage,
timestamp: new Date().toISOString()
};
try {
await saveToDatabase(data);
showToast('✅ Dokumentasi berhasil disimpan', 'success');
// Reset form and webcam
elements.form.reset();
setDefaultDate();
stopWebcam();
showStatus('', '');
// Reload gallery
await loadGallery();
} catch (error) {
console.error('Error saving documentation:', error);
showToast('❌ Gagal menyimpan dokumentasi', 'error');
}
}
function setDefaultDate() {
const today = new Date().toISOString().split('T')[0];
elements.tanggal.value = today;
}
// ============================================
// GALLERY FUNCTIONS
// ============================================
async function loadGallery(searchTerm = '') {
try {
const allData = await getAllData();
// Filter by search term
let filteredData = allData;
if (searchTerm) {
const term = searchTerm.toLowerCase();
filteredData = allData.filter(item =>
item.nama.toLowerCase().includes(term) ||
item.noAnggota.toLowerCase().includes(term)
);
}
// Sort by timestamp (newest first)
filteredData.sort((a, b) => new Date(b.timestamp) - new Date(a.timestamp));
// Update stats
updateGalleryStats(filteredData.length, allData.length);
// Show empty state if no data
if (filteredData.length === 0) {
elements.gallery.innerHTML = '';
elements.emptyState.style.display = 'block';
return;
}
elements.emptyState.style.display = 'none';
// Render gallery items
elements.gallery.innerHTML = filteredData.map(item => `
<div class="gallery-item" data-id="${item.id}" onclick="showDetail(${item.id})">
<img src="${item.foto}" alt="${item.nama}" class="gallery-item-image">
<div class="gallery-item-info">
<h3>${escapeHtml(item.nama)}</h3>
<p><strong>No. Anggota:</strong> ${escapeHtml(item.noAnggota)}</p>
<p><strong>Perjanjian:</strong> ${escapeHtml(item.jenisPerjanjian)}</p>
<p class="gallery-item-date">📅 ${formatDate(item.tanggal)} • 🕐 ${formatTime(item.timestamp)}</p>
</div>
</div>
`).join('');
} catch (error) {
console.error('Error loading gallery:', error);
showToast('❌ Gagal memuat galeri', 'error');
}
}
function updateGalleryStats(shown, total) {
if (total === 0) {
elements.galleryStats.innerHTML = '';
return;
}
let statsText = `📊 Total: ${total} dokumentasi`;
if (shown < total) {
statsText += ` • Ditampilkan: ${shown}`;
}
elements.galleryStats.innerHTML = statsText;
}
// ============================================
// DETAIL & PRINT FUNCTIONS
// ============================================
async function showDetail(id) {
try {
const data = await getDataById(id);
if (!data) {
showToast('❌ Data tidak ditemukan', 'error');
return;
}
currentDetailId = id;
elements.modalBody.innerHTML = `
<div class="detail-container">
<div>
<img src="${data.foto}" alt="${escapeHtml(data.nama)}" class="detail-image">
</div>
<div class="detail-info">
<h2>📋 Detail Dokumentasi</h2>
<div class="detail-row">
<div class="detail-label">Nama Lengkap</div>
<div class="detail-value">${escapeHtml(data.nama)}</div>
</div>
<div class="detail-row">
<div class="detail-label">Nomor Anggota</div>
<div class="detail-value">${escapeHtml(data.noAnggota)}</div>
</div>
<div class="detail-row">
<div class="detail-label">Jenis Perjanjian</div>
<div class="detail-value">${escapeHtml(data.jenisPerjanjian)}</div>
</div>
<div class="detail-row">
<div class="detail-label">Tanggal Penandatanganan</div>
<div class="detail-value">${formatDate(data.tanggal)}</div>
</div>
${data.catatan ? `
<div class="detail-row">
<div class="detail-label">Catatan</div>
<div class="detail-value">${escapeHtml(data.catatan)}</div>
</div>
` : ''}
<div class="detail-row">
<div class="detail-label">Waktu Pengambilan Foto</div>
<div class="detail-value">${formatDateTime(data.timestamp)}</div>
</div>
</div>
</div>
`;
elements.modal.classList.add('show');
} catch (error) {
console.error('Error showing detail:', error);
showToast('❌ Gagal memuat detail', 'error');
}
}
function closeModal() {
elements.modal.classList.remove('show');
currentDetailId = null;
}
function printDetail() {
window.print();
}
async function deleteDetail() {
if (!currentDetailId) return;
if (!confirm('Apakah Anda yakin ingin menghapus dokumentasi ini?')) {
return;
}
try {
await deleteData(currentDetailId);
showToast('✅ Dokumentasi berhasil dihapus', 'success');
closeModal();
await loadGallery();
} catch (error) {
console.error('Error deleting data:', error);
showToast('❌ Gagal menghapus dokumentasi', 'error');
}
}
// ============================================
// EXPORT FUNCTION
// ============================================
async function exportData() {
try {
const allData = await getAllData();
if (allData.length === 0) {
showToast('⚠️ Tidak ada data untuk diekspor', 'error');
return;
}
// Create CSV data (without photos for file size)
const csvData = convertToCSV(allData);
// Download CSV
const blob = new Blob([csvData], { type: 'text/csv;charset=utf-8;' });
const link = document.createElement('a');
const url = URL.createObjectURL(blob);
link.setAttribute('href', url);
link.setAttribute('download', `dokumentasi_nasabah_${new Date().toISOString().split('T')[0]}.csv`);
link.style.visibility = 'hidden';
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
showToast('✅ Data berhasil diekspor', 'success');
} catch (error) {
console.error('Error exporting data:', error);
showToast('❌ Gagal mengekspor data', 'error');
}
}
function convertToCSV(data) {
const headers = ['ID', 'Nama', 'No. Anggota', 'Jenis Perjanjian', 'Tanggal', 'Catatan', 'Waktu Input'];
const rows = data.map(item => [
item.id,
item.nama,
item.noAnggota,
item.jenisPerjanjian,
formatDate(item.tanggal),
item.catatan || '',
formatDateTime(item.timestamp)
]);
const csvContent = [
headers.join(','),
...rows.map(row => row.map(cell => `"${cell}"`).join(','))
].join('\n');
return csvContent;
}
// ============================================
// UTILITY FUNCTIONS
// ============================================
function showStatus(message, type) {
if (!message) {
elements.webcamStatus.innerHTML = '';
elements.webcamStatus.className = 'status-message';
return;
}
elements.webcamStatus.innerHTML = message;
elements.webcamStatus.className = `status-message ${type}`;
}
function showToast(message, type = 'success') {
elements.toast.textContent = message;
elements.toast.className = `toast ${type} show`;
setTimeout(() => {
elements.toast.classList.remove('show');
}, 3000);
}
function escapeHtml(text) {
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}
function formatDate(dateString) {
const options = { year: 'numeric', month: 'long', day: 'numeric' };
return new Date(dateString).toLocaleDateString('id-ID', options);
}
function formatTime(isoString) {
return new Date(isoString).toLocaleTimeString('id-ID', {
hour: '2-digit',
minute: '2-digit'
});
}
function formatDateTime(isoString) {
const date = new Date(isoString);
return `${date.toLocaleDateString('id-ID', {
year: 'numeric',
month: 'long',
day: 'numeric'
})} pukul ${date.toLocaleTimeString('id-ID', {
hour: '2-digit',
minute: '2-digit'
})}`;
}
// ============================================
// EVENT LISTENERS
// ============================================
function setupEventListeners() {
// Webcam controls
elements.startBtn.addEventListener('click', startWebcam);
elements.captureBtn.addEventListener('click', capturePhoto);
elements.retakeBtn.addEventListener('click', retakePhoto);
elements.saveBtn.addEventListener('click', saveDocumentation);
// Modal
elements.closeModal.addEventListener('click', closeModal);
elements.modal.addEventListener('click', (e) => {
if (e.target === elements.modal) {
closeModal();
}
});
elements.printBtn.addEventListener('click', printDetail);
elements.deleteBtn.addEventListener('click', deleteDetail);
// Search
elements.searchInput.addEventListener('input', (e) => {
loadGallery(e.target.value);
});
// Export
elements.exportBtn.addEventListener('click', exportData);
// Keyboard shortcuts
document.addEventListener('keydown', (e) => {
if (e.key === 'Escape') {
closeModal();
}
});
}
// ============================================
// INITIALIZATION
// ============================================
async function init() {
try {
// Initialize database
await initDatabase();
// Set default date
setDefaultDate();
// Setup event listeners
setupEventListeners();
// Load gallery
await loadGallery();
console.log('✅ Aplikasi berhasil diinisialisasi');
} catch (error) {
console.error('Error initializing app:', error);
showToast('❌ Gagal menginisialisasi aplikasi', 'error');
}
}
// Start the app when DOM is ready
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', init);
} else {
init();
}
// Make showDetail function globally accessible
window.showDetail = showDetail;

34
database.sql Normal file
View File

@@ -0,0 +1,34 @@
-- Database Schema untuk Aplikasi Dokumentasi Nasabah LPD Gerana
-- Buat database jika belum ada
CREATE DATABASE IF NOT EXISTS dokumentasi CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
USE dokumentasi;
-- Tabel dokumentasi
CREATE TABLE IF NOT EXISTS dokumentasi (
id INT AUTO_INCREMENT PRIMARY KEY,
nama VARCHAR(255) NOT NULL,
no_anggota VARCHAR(100) NOT NULL,
jenis_perjanjian VARCHAR(255) NOT NULL,
tanggal DATE NOT NULL,
catatan TEXT,
foto LONGTEXT NOT NULL COMMENT 'Base64 encoded image',
timestamp TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
INDEX idx_nama (nama),
INDEX idx_no_anggota (no_anggota),
INDEX idx_tanggal (tanggal),
INDEX idx_timestamp (timestamp)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
-- Tabel untuk tracking aktivitas (optional)
CREATE TABLE IF NOT EXISTS activity_log (
id INT AUTO_INCREMENT PRIMARY KEY,
action VARCHAR(50) NOT NULL,
dokumentasi_id INT,
details TEXT,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
INDEX idx_dokumentasi_id (dokumentasi_id),
INDEX idx_created_at (created_at),
FOREIGN KEY (dokumentasi_id) REFERENCES dokumentasi(id) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;

View File

@@ -3,7 +3,7 @@
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Dokumentasi Nasabah Koperasi</title>
<title>Dokumentasi Nasabah LPD Gerana</title>
<link rel="stylesheet" href="style.css">
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
@@ -14,7 +14,7 @@
<!-- Header -->
<header class="app-header">
<div class="header-content">
<h1>📸 Dokumentasi Nasabah Koperasi</h1>
<h1>📸 Dokumentasi Nasabah LPD Gerana</h1>
<p>Sistem Pengambilan Foto Penandatanganan Perjanjian</p>
</div>
</header>

1357
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

28
package.json Normal file
View File

@@ -0,0 +1,28 @@
{
"name": "dokumentasi-nasabah-lpd-gerana",
"version": "1.0.0",
"description": "Aplikasi dokumentasi nasabah saat penandatanganan perjanjian LPD Gerana",
"main": "server.js",
"scripts": {
"start": "node server.js",
"dev": "nodemon server.js",
"setup-db": "mysql -u doc -p'doc2026!' < database.sql"
},
"keywords": [
"lpd-gerana",
"dokumentasi",
"webcam",
"mysql"
],
"author": "",
"license": "ISC",
"dependencies": {
"express": "^4.18.2",
"mysql2": "^3.6.5",
"dotenv": "^16.3.1",
"cors": "^2.8.5"
},
"devDependencies": {
"nodemon": "^3.0.2"
}
}

265
server.js Normal file
View File

@@ -0,0 +1,265 @@
require('dotenv').config();
const express = require('express');
const mysql = require('mysql2/promise');
const cors = require('cors');
const app = express();
const PORT = process.env.SERVER_PORT || 3000;
// Middleware
app.use(cors());
app.use(express.json({ limit: '50mb' })); // Increased limit for base64 images
app.use(express.urlencoded({ extended: true, limit: '50mb' }));
app.use(express.static('.')); // Serve static files from current directory
// Database connection pool
const pool = mysql.createPool({
host: process.env.DB_HOST,
port: process.env.DB_PORT,
user: process.env.DB_USERNAME,
password: process.env.DB_PASSWORD,
database: process.env.DB_DATABASE,
waitForConnections: true,
connectionLimit: 10,
queueLimit: 0
});
// Test database connection
async function testConnection() {
try {
const connection = await pool.getConnection();
console.log('✅ Database connected successfully');
connection.release();
} catch (error) {
console.error('❌ Database connection failed:', error.message);
process.exit(1);
}
}
// ============================================
// API ENDPOINTS
// ============================================
// Health check
app.get('/api/health', (req, res) => {
res.json({ status: 'OK', message: 'Server is running' });
});
// Create new documentation
app.post('/api/dokumentasi', async (req, res) => {
try {
const { nama, noAnggota, jenisPerjanjian, tanggal, catatan, foto } = req.body;
// Validation
if (!nama || !noAnggota || !jenisPerjanjian || !tanggal || !foto) {
return res.status(400).json({
error: 'Missing required fields',
required: ['nama', 'noAnggota', 'jenisPerjanjian', 'tanggal', 'foto']
});
}
const [result] = await pool.execute(
`INSERT INTO dokumentasi (nama, no_anggota, jenis_perjanjian, tanggal, catatan, foto)
VALUES (?, ?, ?, ?, ?, ?)`,
[nama, noAnggota, jenisPerjanjian, tanggal, catatan || null, foto]
);
// Log activity
await pool.execute(
`INSERT INTO activity_log (action, dokumentasi_id, details) VALUES (?, ?, ?)`,
['CREATE', result.insertId, `Created documentation for ${nama}`]
);
res.status(201).json({
success: true,
message: 'Dokumentasi berhasil disimpan',
id: result.insertId
});
} catch (error) {
console.error('Error creating documentation:', error);
res.status(500).json({
error: 'Failed to create documentation',
details: error.message
});
}
});
// Get all documentation (with optional search)
app.get('/api/dokumentasi', async (req, res) => {
try {
const { search } = req.query;
let query = 'SELECT id, nama, no_anggota as noAnggota, jenis_perjanjian as jenisPerjanjian, tanggal, catatan, foto, timestamp FROM dokumentasi';
let params = [];
if (search) {
query += ' WHERE nama LIKE ? OR no_anggota LIKE ?';
params = [`%${search}%`, `%${search}%`];
}
query += ' ORDER BY timestamp DESC';
const [rows] = await pool.execute(query, params);
res.json({
success: true,
count: rows.length,
data: rows
});
} catch (error) {
console.error('Error fetching documentation:', error);
res.status(500).json({
error: 'Failed to fetch documentation',
details: error.message
});
}
});
// Get single documentation by ID
app.get('/api/dokumentasi/:id', async (req, res) => {
try {
const { id } = req.params;
const [rows] = await pool.execute(
`SELECT id, nama, no_anggota as noAnggota, jenis_perjanjian as jenisPerjanjian,
tanggal, catatan, foto, timestamp
FROM dokumentasi WHERE id = ?`,
[id]
);
if (rows.length === 0) {
return res.status(404).json({
error: 'Documentation not found'
});
}
res.json({
success: true,
data: rows[0]
});
} catch (error) {
console.error('Error fetching documentation:', error);
res.status(500).json({
error: 'Failed to fetch documentation',
details: error.message
});
}
});
// Delete documentation
app.delete('/api/dokumentasi/:id', async (req, res) => {
try {
const { id } = req.params;
// Check if exists
const [existing] = await pool.execute(
'SELECT nama FROM dokumentasi WHERE id = ?',
[id]
);
if (existing.length === 0) {
return res.status(404).json({
error: 'Documentation not found'
});
}
// Delete (activity_log will cascade delete automatically)
await pool.execute('DELETE FROM dokumentasi WHERE id = ?', [id]);
res.json({
success: true,
message: 'Dokumentasi berhasil dihapus'
});
} catch (error) {
console.error('Error deleting documentation:', error);
res.status(500).json({
error: 'Failed to delete documentation',
details: error.message
});
}
});
// Get statistics
app.get('/api/stats', async (req, res) => {
try {
const [countResult] = await pool.execute(
'SELECT COUNT(*) as total FROM dokumentasi'
);
const [recentResult] = await pool.execute(
'SELECT COUNT(*) as recent FROM dokumentasi WHERE DATE(timestamp) = CURDATE()'
);
res.json({
success: true,
data: {
total: countResult[0].total,
today: recentResult[0].recent
}
});
} catch (error) {
console.error('Error fetching stats:', error);
res.status(500).json({
error: 'Failed to fetch statistics',
details: error.message
});
}
});
// ============================================
// ERROR HANDLING
// ============================================
// 404 handler
app.use((req, res) => {
res.status(404).json({ error: 'Endpoint not found' });
});
// Global error handler
app.use((err, req, res, next) => {
console.error('Unhandled error:', err);
res.status(500).json({
error: 'Internal server error',
details: err.message
});
});
// ============================================
// START SERVER
// ============================================
async function startServer() {
try {
await testConnection();
app.listen(PORT, () => {
console.log(`\n🚀 Server running on http://localhost:${PORT}`);
console.log(`📊 API endpoints:`);
console.log(` GET /api/health`);
console.log(` POST /api/dokumentasi`);
console.log(` GET /api/dokumentasi`);
console.log(` GET /api/dokumentasi/:id`);
console.log(` DELETE /api/dokumentasi/:id`);
console.log(` GET /api/stats`);
console.log(`\n✨ Application ready at http://localhost:${PORT}/index.html\n`);
});
} catch (error) {
console.error('Failed to start server:', error);
process.exit(1);
}
}
startServer();
// Graceful shutdown
process.on('SIGTERM', async () => {
console.log('SIGTERM signal received: closing HTTP server');
await pool.end();
process.exit(0);
});
process.on('SIGINT', async () => {
console.log('\nSIGINT signal received: closing HTTP server');
await pool.end();
process.exit(0);
});