Enhance SIDAK with two-way KK-KTP linkage, scanner mobile optimization, NIK validation, and UI improvements
- Implement bidirectional KK-KTP linkage system (address-based & NIK-based) - Optimize scanner for mobile devices (touch slop, larger hit areas, modal locking) - Add NIK validation (16-digit numeric) with client-side feedback - Set default RT/RW values to '000' for both KK and KTP forms - Change 'Kpl Keluarga' label to 'Kepala Keluarga' - Improve scanner error messages and user feedback - Remove redundant 'Deteksi Dokumen' button - Add database schema updates and Docker support files
This commit is contained in:
18
.dockerignore
Normal file
18
.dockerignore
Normal file
@@ -0,0 +1,18 @@
|
||||
# Docker ignore file
|
||||
.git
|
||||
.gitignore
|
||||
node_modules
|
||||
npm-debug.log
|
||||
yarn-debug.log
|
||||
yarn-error.log
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
.vscode
|
||||
.idea
|
||||
*.log
|
||||
php_server.pid
|
||||
php_server.log
|
||||
.env
|
||||
docker-compose.override.yml
|
||||
*.md
|
||||
README*
|
||||
38
Dockerfile
Normal file
38
Dockerfile
Normal file
@@ -0,0 +1,38 @@
|
||||
FROM php:8.2-apache
|
||||
|
||||
# Install necessary PHP extensions and tools
|
||||
RUN apt-get update && apt-get install -y \
|
||||
libpng-dev \
|
||||
libjpeg-dev \
|
||||
libfreetype6-dev \
|
||||
libzip-dev \
|
||||
zip \
|
||||
unzip \
|
||||
curl \
|
||||
git \
|
||||
mariadb-client \
|
||||
&& docker-php-ext-configure gd --with-freetype --with-jpeg \
|
||||
&& docker-php-ext-install -j$(nproc) gd \
|
||||
&& docker-php-ext-install mysqli pdo pdo_mysql zip \
|
||||
&& apt-get clean \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# Enable Apache rewrite module
|
||||
RUN a2enmod rewrite
|
||||
|
||||
# Set working directory
|
||||
WORKDIR /var/www/html
|
||||
|
||||
# Copy application files
|
||||
COPY . /var/www/html/
|
||||
|
||||
# Set proper permissions
|
||||
RUN chown -R www-data:www-data /var/www/html \
|
||||
&& chmod -R 755 /var/www/html
|
||||
|
||||
# Update koneksi.php to use environment variables
|
||||
RUN sed -i 's/\$koneksi = new mysqli ("localhost","sidak_user","sidak_pass","data_penduduk");/\$db_host = getenv("DB_HOST") ?: "localhost";\n\$db_user = getenv("DB_USER") ?: "sidak_user";\n\$db_pass = getenv("DB_PASS") ?: "sidak_pass";\n\$db_name = getenv("DB_NAME") ?: "data_penduduk";\n\$koneksi = new mysqli (\$db_host,\$db_user,\$db_pass,\$db_name);/g' inc/koneksi.php
|
||||
|
||||
EXPOSE 80
|
||||
|
||||
CMD ["apache2-foreground"]
|
||||
67
INSTALL_DOCKER.md
Normal file
67
INSTALL_DOCKER.md
Normal file
@@ -0,0 +1,67 @@
|
||||
# SIDAK Docker Setup
|
||||
|
||||
Aplikasi SIDAK berjalan dengan Docker Compose.
|
||||
|
||||
## Services yang berjalan:
|
||||
1. **Web App** - PHP Apache: http://localhost:8500
|
||||
2. **Database** - MariaDB: localhost:3307
|
||||
3. **phpMyAdmin** - Database admin: http://localhost:8080
|
||||
|
||||
## Cara menjalankan:
|
||||
|
||||
### 1. Start aplikasi:
|
||||
```bash
|
||||
docker compose up -d
|
||||
```
|
||||
|
||||
### 2. Stop aplikasi:
|
||||
```bash
|
||||
docker compose down
|
||||
```
|
||||
|
||||
### 3. Stop dan hapus data:
|
||||
```bash
|
||||
docker compose down -v
|
||||
```
|
||||
|
||||
### 4. Melihat logs:
|
||||
```bash
|
||||
docker compose logs -f
|
||||
```
|
||||
|
||||
## Credentials:
|
||||
|
||||
### Aplikasi SIDAK:
|
||||
- URL: http://localhost:8500
|
||||
- Admin: `admin` / `admin`
|
||||
- Kaur: `kaur` / `kaur`
|
||||
|
||||
### Database:
|
||||
- Host: `db` (dalam container) atau `localhost:3307` (dari host)
|
||||
- Database: `data_penduduk`
|
||||
- User: `sidak_user`
|
||||
- Password: `sidak_pass`
|
||||
- Root password: `rootpassword`
|
||||
|
||||
### phpMyAdmin:
|
||||
- URL: http://localhost:8080
|
||||
- Server: `db`
|
||||
- Username: `sidak_user` atau `root`
|
||||
- Password: `sidak_pass` atau `rootpassword`
|
||||
|
||||
## Troubleshooting:
|
||||
|
||||
1. **Port conflict**: Jika port 8500, 8080, atau 3307 sudah digunakan, ubah di `docker-compose.yml`
|
||||
2. **Database connection error**: Tunggu 30 detik untuk database startup
|
||||
3. **PHP extensions**: Extensions mysqli, pdo, pdo_mysql sudah diinstall otomatis
|
||||
4. **File permissions**: Semua file di-mount dari host ke container
|
||||
|
||||
## Struktur file Docker:
|
||||
- `docker-compose.yml` - Konfigurasi services
|
||||
- `init.sql` - Skema dan data awal database
|
||||
- `inc/koneksi.php` - Konfigurasi koneksi database (menggunakan env variables)
|
||||
|
||||
## Catatan:
|
||||
- Perubahan file PHP langsung terlihat (hot reload)
|
||||
- Data database persist di volume Docker `sidak_mariadb-data`
|
||||
- Untuk development, edit file langsung di host
|
||||
74
README_DOCKER.md
Normal file
74
README_DOCKER.md
Normal file
@@ -0,0 +1,74 @@
|
||||
# SIDAK Application with Docker
|
||||
|
||||
## Prerequisites
|
||||
- Docker
|
||||
- Docker Compose
|
||||
|
||||
## Quick Start
|
||||
|
||||
1. **Start the application:**
|
||||
```bash
|
||||
docker-compose up -d
|
||||
```
|
||||
|
||||
2. **Access the application:**
|
||||
- Web application: http://localhost:8500
|
||||
- phpMyAdmin: http://localhost:8080
|
||||
- MySQL/MariaDB: localhost:3306
|
||||
|
||||
3. **Default credentials:**
|
||||
- **Application:**
|
||||
- Admin: `admin` / `admin`
|
||||
- Kaur: `kaur` / `kaur`
|
||||
- **Database:**
|
||||
- Host: `db`
|
||||
- Database: `data_penduduk`
|
||||
- User: `sidak_user`
|
||||
- Password: `sidak_pass`
|
||||
- Root password: `rootpassword`
|
||||
|
||||
4. **Stop the application:**
|
||||
```bash
|
||||
docker-compose down
|
||||
```
|
||||
|
||||
5. **Stop and remove volumes (clears all data):**
|
||||
```bash
|
||||
docker-compose down -v
|
||||
```
|
||||
|
||||
## Services
|
||||
|
||||
1. **web** - PHP Apache application
|
||||
- Port: 8500
|
||||
- Directory mounted: `/var/www/html`
|
||||
- Environment variables: DB_HOST, DB_USER, DB_PASS, DB_NAME
|
||||
|
||||
2. **db** - MariaDB database
|
||||
- Port: 3306
|
||||
- Database: `data_penduduk`
|
||||
- Auto-initialized with schema and sample data
|
||||
- Data persisted in volume: `mariadb-data`
|
||||
|
||||
3. **phpmyadmin** - Database management
|
||||
- Port: 8080
|
||||
- Connect to host: `db`
|
||||
|
||||
## Development
|
||||
|
||||
- **Hot reload:** Changes to PHP files are reflected immediately due to volume mounting
|
||||
- **Database persistence:** Data is preserved between container restarts
|
||||
- **Build custom image:** `docker-compose build`
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
1. **Port conflicts:** Check if ports 8500, 8080, or 3306 are already in use
|
||||
2. **Database connection issues:** Wait a few seconds for database to initialize
|
||||
3. **View logs:** `docker-compose logs -f`
|
||||
4. **Reset database:** `docker-compose down -v && docker-compose up -d`
|
||||
|
||||
## File Structure
|
||||
- `docker-compose.yml` - Service definitions
|
||||
- `Dockerfile` - PHP Apache image configuration
|
||||
- `init.sql` - Database initialization script
|
||||
- `.dockerignore` - Files to exclude from build context
|
||||
@@ -30,9 +30,9 @@
|
||||
</div>
|
||||
|
||||
<div class="form-group row">
|
||||
<label class="col-sm-2 col-form-label">Kpl Keluarga</label>
|
||||
<label class="col-sm-2 col-form-label">Kepala Keluarga</label>
|
||||
<div class="col-sm-6">
|
||||
<input type="text" class="form-control" id="kepala" name="kepala" placeholder="Kpl Keluarga" required>
|
||||
<input type="text" class="form-control" id="kepala" name="kepala" placeholder="Kepala Keluarga" required>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -46,10 +46,10 @@
|
||||
<div class="form-group row">
|
||||
<label class="col-sm-2 col-form-label">RT/RW</label>
|
||||
<div class="col-sm-3">
|
||||
<input type="text" class="form-control" id="rt" name="rt" placeholder="RT" required>
|
||||
<input type="text" class="form-control" id="rt" name="rt" placeholder="RT" value="000" required>
|
||||
</div>
|
||||
<div class="col-sm-3">
|
||||
<input type="text" class="form-control" id="rw" name="rw" placeholder="RW" required>
|
||||
<input type="text" class="form-control" id="rw" name="rw" placeholder="RW" value="000" required>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -238,8 +238,9 @@ window.addEventListener('load', function() {
|
||||
}
|
||||
|
||||
//mulai proses simpan data
|
||||
$no_kk = $_POST['no_kk'];
|
||||
$cek_kk = mysqli_query($koneksi, "SELECT * FROM tb_kk WHERE no_kk='$no_kk'");
|
||||
$no_kk_raw = trim($_POST['no_kk']);
|
||||
$no_kk_clean = mysqli_real_escape_string($koneksi, $no_kk_raw);
|
||||
$cek_kk = mysqli_query($koneksi, "SELECT * FROM tb_kk WHERE no_kk='$no_kk_clean'");
|
||||
if(mysqli_num_rows($cek_kk) > 0){
|
||||
echo "<script>
|
||||
Swal.fire({title: 'Gagal',text: 'No KK sudah terdaftar dalam sistem!',icon: 'error',confirmButtonText: 'OK'
|
||||
@@ -251,14 +252,14 @@ window.addEventListener('load', function() {
|
||||
}
|
||||
|
||||
// Sanitize Inputs
|
||||
$no_kk = mysqli_real_escape_string($koneksi, $_POST['no_kk']);
|
||||
$kepala = mysqli_real_escape_string($koneksi, $_POST['kepala']);
|
||||
$desa = mysqli_real_escape_string($koneksi, $_POST['desa']);
|
||||
$rt = mysqli_real_escape_string($koneksi, $_POST['rt']);
|
||||
$rw = mysqli_real_escape_string($koneksi, $_POST['rw']);
|
||||
$kec = mysqli_real_escape_string($koneksi, $_POST['kec']);
|
||||
$kab = mysqli_real_escape_string($koneksi, $_POST['kab']);
|
||||
$prov = mysqli_real_escape_string($koneksi, $_POST['prov']);
|
||||
$no_kk = $no_kk_clean;
|
||||
$kepala = mysqli_real_escape_string($koneksi, trim($_POST['kepala']));
|
||||
$desa = mysqli_real_escape_string($koneksi, trim($_POST['desa']));
|
||||
$rt = mysqli_real_escape_string($koneksi, trim($_POST['rt']));
|
||||
$rw = mysqli_real_escape_string($koneksi, trim($_POST['rw']));
|
||||
$kec = mysqli_real_escape_string($koneksi, trim($_POST['kec']));
|
||||
$kab = mysqli_real_escape_string($koneksi, trim($_POST['kab']));
|
||||
$prov = mysqli_real_escape_string($koneksi, trim($_POST['prov']));
|
||||
|
||||
$sql_simpan = "INSERT INTO tb_kk (no_kk, kepala, desa, rt, rw, kec, kab, prov, foto_kk) VALUES (
|
||||
'$no_kk',
|
||||
@@ -275,6 +276,7 @@ window.addEventListener('load', function() {
|
||||
// Process Auto-Linking Members
|
||||
$linked_count = 0;
|
||||
$failed_count = 0;
|
||||
$failed_members = []; // Store failed members for detailed feedback
|
||||
|
||||
if ($query_simpan && !empty($_POST['anggota_json'])) {
|
||||
$id_kk_baru = mysqli_insert_id($koneksi);
|
||||
@@ -283,6 +285,7 @@ window.addEventListener('load', function() {
|
||||
if (is_array($anggota_list)) {
|
||||
foreach ($anggota_list as $mem) {
|
||||
$nik_mem = mysqli_real_escape_string($koneksi, $mem['nik']);
|
||||
$nama_mem = isset($mem['nama']) ? mysqli_real_escape_string($koneksi, $mem['nama']) : '';
|
||||
$hub_mem = mysqli_real_escape_string($koneksi, $mem['hubungan']);
|
||||
|
||||
// Search Resident by NIK
|
||||
@@ -297,6 +300,7 @@ window.addEventListener('load', function() {
|
||||
$linked_count++;
|
||||
} else {
|
||||
$failed_count++;
|
||||
$failed_members[] = ['nik' => $nik_mem, 'nama' => $nama_mem, 'hubungan' => $hub_mem];
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -308,6 +312,27 @@ window.addEventListener('load', function() {
|
||||
$msg_add = "";
|
||||
if($linked_count > 0 || $failed_count > 0) {
|
||||
$msg_add = "<br>Anggota Terhubung: <b>$linked_count</b><br>Tidak Ditemukan: <b>$failed_count</b>";
|
||||
// Add detailed failed members list if any
|
||||
if (!empty($failed_members)) {
|
||||
$msg_add .= "<br><br><b>Detail Anggota Tidak Ditemukan:</b><br>";
|
||||
$msg_add .= "<div style='max-height: 150px; overflow-y: auto; background: #f8f9fa; border: 1px solid #ddd; padding: 5px; font-size: 0.9rem;'>";
|
||||
foreach ($failed_members as $fm) {
|
||||
$url_params = http_build_query([
|
||||
'nik' => $fm['nik'],
|
||||
'nama' => $fm['nama'],
|
||||
'desa' => $desa,
|
||||
'rt' => $rt,
|
||||
'rw' => $rw,
|
||||
'kecamatan' => $kec,
|
||||
'kabupaten' => $kab,
|
||||
'provinsi' => $prov
|
||||
]);
|
||||
$add_link = "<a href='index.php?page=add-pend&$url_params' class='btn btn-xs btn-outline-primary ml-2'>Tambah</a>";
|
||||
$msg_add .= "• <b>" . htmlspecialchars($fm['nik']) . "</b> - " . htmlspecialchars($fm['nama']) . " (" . htmlspecialchars($fm['hubungan']) . ") $add_link<br>";
|
||||
}
|
||||
$msg_add .= "</div>";
|
||||
$msg_add .= "<small class='text-muted'>Tambahkan data penduduk yang belum terdaftar melalui menu <a href='index.php?page=add-pend'>Tambah Penduduk</a>.</small>";
|
||||
}
|
||||
}
|
||||
|
||||
echo "<script>
|
||||
|
||||
@@ -52,7 +52,7 @@
|
||||
</div>
|
||||
|
||||
<div class="form-group row">
|
||||
<label class="col-sm-2 col-form-label">Kpl Keluarga</label>
|
||||
<label class="col-sm-2 col-form-label">Kepala Keluarga</label>
|
||||
<div class="col-sm-6">
|
||||
<input type="text" class="form-control" id="kepala" name="kepala" value="<?php echo $data_cek['kepala']; ?>"
|
||||
required>
|
||||
|
||||
@@ -88,10 +88,10 @@
|
||||
<div class="form-group row">
|
||||
<label class="col-sm-2 col-form-label">RT/RW</label>
|
||||
<div class="col-sm-3">
|
||||
<input type="text" class="form-control" id="rt" name="rt" placeholder="RT" required>
|
||||
<input type="text" class="form-control" id="rt" name="rt" placeholder="RT" value="000" required>
|
||||
</div>
|
||||
<div class="col-sm-3">
|
||||
<input type="text" class="form-control" id="rw" name="rw" placeholder="RW" required>
|
||||
<input type="text" class="form-control" id="rw" name="rw" placeholder="RW" value="000" required>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -141,6 +141,57 @@
|
||||
|
||||
<script>
|
||||
window.addEventListener('load', function() {
|
||||
// Pre-fill form from URL parameters
|
||||
var urlParams = new URLSearchParams(window.location.search);
|
||||
if (urlParams.has('nik')) document.getElementsByName('nik')[0].value = urlParams.get('nik');
|
||||
if (urlParams.has('nama')) document.getElementsByName('nama')[0].value = urlParams.get('nama');
|
||||
if (urlParams.has('desa')) document.getElementsByName('desa')[0].value = urlParams.get('desa');
|
||||
if (urlParams.has('rt')) document.getElementsByName('rt')[0].value = urlParams.get('rt');
|
||||
if (urlParams.has('rw')) document.getElementsByName('rw')[0].value = urlParams.get('rw');
|
||||
if (urlParams.has('kecamatan')) document.getElementsByName('kecamatan')[0].value = urlParams.get('kecamatan');
|
||||
if (urlParams.has('kabupaten')) document.getElementsByName('kabupaten')[0].value = urlParams.get('kabupaten');
|
||||
if (urlParams.has('provinsi')) document.getElementsByName('provinsi')[0].value = urlParams.get('provinsi');
|
||||
|
||||
// NIK Validation (16 digits, numeric)
|
||||
var nikInput = document.getElementsByName('nik')[0];
|
||||
function validateNIK(nik) {
|
||||
nik = nik.trim();
|
||||
if (nik.length !== 16) return false;
|
||||
return /^\d+$/.test(nik); // Only digits
|
||||
}
|
||||
function showNIKError(message) {
|
||||
Swal.fire({
|
||||
icon: 'warning',
|
||||
title: 'Format NIK Salah',
|
||||
text: message,
|
||||
confirmButtonText: 'OK'
|
||||
});
|
||||
}
|
||||
// Validate on blur
|
||||
if (nikInput) {
|
||||
nikInput.addEventListener('blur', function() {
|
||||
var nik = this.value.trim();
|
||||
if (nik === '') return;
|
||||
if (!validateNIK(nik)) {
|
||||
showNIKError('NIK harus terdiri dari 16 digit angka.');
|
||||
this.focus();
|
||||
}
|
||||
});
|
||||
}
|
||||
// Validate on form submit
|
||||
var form = document.querySelector('form');
|
||||
if (form) {
|
||||
form.addEventListener('submit', function(e) {
|
||||
var nik = nikInput.value.trim();
|
||||
if (nik !== '' && !validateNIK(nik)) {
|
||||
e.preventDefault();
|
||||
showNIKError('NIK harus terdiri dari 16 digit angka.');
|
||||
nikInput.focus();
|
||||
return false;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Scanner Logic
|
||||
var inputImage = document.getElementById('foto_ktp');
|
||||
var preview = document.getElementById('preview_ktp');
|
||||
@@ -331,7 +382,7 @@ window.addEventListener('load', function() {
|
||||
}
|
||||
|
||||
//mulai proses simpan data
|
||||
$nik = $_POST['nik'];
|
||||
$nik = mysqli_real_escape_string($koneksi, trim($_POST['nik']));
|
||||
$cek_nik = mysqli_query($koneksi, "SELECT * FROM tb_pdd WHERE nik='$nik'");
|
||||
if(mysqli_num_rows($cek_nik) > 0){
|
||||
echo "<script>
|
||||
@@ -344,21 +395,20 @@ window.addEventListener('load', function() {
|
||||
}
|
||||
|
||||
// Sanitize Input to prevent SQL Injection & Syntax Errors
|
||||
$nik = mysqli_real_escape_string($koneksi, $_POST['nik']);
|
||||
$nama = mysqli_real_escape_string($koneksi, $_POST['nama']);
|
||||
$tempat_lh = mysqli_real_escape_string($koneksi, $_POST['tempat_lh']);
|
||||
$tgl_lh = mysqli_real_escape_string($koneksi, $_POST['tgl_lh']);
|
||||
$jekel = mysqli_real_escape_string($koneksi, $_POST['jekel']);
|
||||
$desa = mysqli_real_escape_string($koneksi, $_POST['desa']);
|
||||
$rt = mysqli_real_escape_string($koneksi, $_POST['rt']);
|
||||
$rw = mysqli_real_escape_string($koneksi, $_POST['rw']);
|
||||
$agama = mysqli_real_escape_string($koneksi, $_POST['agama']);
|
||||
$kawin = mysqli_real_escape_string($koneksi, $_POST['kawin']);
|
||||
$pekerjaan = mysqli_real_escape_string($koneksi, $_POST['pekerjaan']);
|
||||
$kecamatan = mysqli_real_escape_string($koneksi, $_POST['kecamatan']);
|
||||
$kabupaten = mysqli_real_escape_string($koneksi, $_POST['kabupaten']);
|
||||
$provinsi = mysqli_real_escape_string($koneksi, $_POST['provinsi']);
|
||||
$kewarganegaraan = mysqli_real_escape_string($koneksi, $_POST['kewarganegaraan']);
|
||||
$nama = mysqli_real_escape_string($koneksi, trim($_POST['nama']));
|
||||
$tempat_lh = mysqli_real_escape_string($koneksi, trim($_POST['tempat_lh']));
|
||||
$tgl_lh = mysqli_real_escape_string($koneksi, trim($_POST['tgl_lh']));
|
||||
$jekel = mysqli_real_escape_string($koneksi, trim($_POST['jekel']));
|
||||
$desa = mysqli_real_escape_string($koneksi, trim($_POST['desa']));
|
||||
$rt = mysqli_real_escape_string($koneksi, trim($_POST['rt']));
|
||||
$rw = mysqli_real_escape_string($koneksi, trim($_POST['rw']));
|
||||
$agama = mysqli_real_escape_string($koneksi, trim($_POST['agama']));
|
||||
$kawin = mysqli_real_escape_string($koneksi, trim($_POST['kawin']));
|
||||
$pekerjaan = mysqli_real_escape_string($koneksi, trim($_POST['pekerjaan']));
|
||||
$kecamatan = mysqli_real_escape_string($koneksi, trim($_POST['kecamatan']));
|
||||
$kabupaten = mysqli_real_escape_string($koneksi, trim($_POST['kabupaten']));
|
||||
$provinsi = mysqli_real_escape_string($koneksi, trim($_POST['provinsi']));
|
||||
$kewarganegaraan = mysqli_real_escape_string($koneksi, trim($_POST['kewarganegaraan']));
|
||||
|
||||
$sql_simpan = "INSERT INTO tb_pdd (nik, nama, tempat_lh, tgl_lh, jekel, desa, rt, rw, agama, kawin, pekerjaan, foto_ktp, status, kecamatan, kabupaten, provinsi, kewarganegaraan) VALUES (
|
||||
'$nik',
|
||||
@@ -379,21 +429,95 @@ window.addEventListener('load', function() {
|
||||
'$provinsi',
|
||||
'$kewarganegaraan')";
|
||||
$query_simpan = mysqli_query($koneksi, $sql_simpan);
|
||||
mysqli_close($koneksi);
|
||||
|
||||
if ($query_simpan) {
|
||||
$id_pend_baru = mysqli_insert_id($koneksi);
|
||||
|
||||
// KTP → KK: Cari KK yang cocok berdasarkan alamat
|
||||
$sql_cari_kk = "SELECT k.id_kk, k.no_kk, k.kepala, k.desa, k.rt, k.rw
|
||||
FROM tb_kk k
|
||||
WHERE k.desa='$desa' AND k.rt='$rt' AND k.rw='$rw'
|
||||
AND k.kec='$kecamatan' AND k.kab='$kabupaten' AND k.prov='$provinsi'";
|
||||
$q_cari_kk = mysqli_query($koneksi, $sql_cari_kk);
|
||||
$kk_cocok = mysqli_fetch_assoc($q_cari_kk);
|
||||
|
||||
if ($kk_cocok) {
|
||||
// Tawarkan untuk menghubungkan dengan KK
|
||||
$no_kk = $kk_cocok['no_kk'];
|
||||
$kepala_kk = $kk_cocok['kepala'];
|
||||
$id_kk = $kk_cocok['id_kk'];
|
||||
|
||||
// Cek apakah sudah terhubung
|
||||
$sql_cek_hubungan = "SELECT * FROM tb_anggota WHERE id_kk='$id_kk' AND id_pend='$id_pend_baru'";
|
||||
$q_cek_hubungan = mysqli_query($koneksi, $sql_cek_hubungan);
|
||||
|
||||
if (mysqli_num_rows($q_cek_hubungan) == 0) {
|
||||
// Simpan sementara data untuk konfirmasi JavaScript
|
||||
$_SESSION['kk_link_data'] = [
|
||||
'id_pend' => $id_pend_baru,
|
||||
'id_kk' => $id_kk,
|
||||
'no_kk' => $no_kk,
|
||||
'kepala_kk' => $kepala_kk,
|
||||
'nama_pend' => $nama
|
||||
];
|
||||
|
||||
echo "<script>
|
||||
Swal.fire({
|
||||
title: 'Data Penduduk Disimpan!',
|
||||
html: '<b>Data berhasil disimpan.</b><br><br>Ditemukan KK dengan alamat yang sama:<br><b>No KK: $no_kk</b><br>Kepala Keluarga: $kepala_kk<br><br>Hubungkan ke KK ini sebagai anggota?',
|
||||
icon: 'success',
|
||||
showCancelButton: true,
|
||||
confirmButtonText: 'Ya, Hubungkan',
|
||||
cancelButtonText: 'Tidak, Simpan Saja'
|
||||
}).then((result) => {
|
||||
if (result.value) {
|
||||
// Kirim permintaan untuk menghubungkan
|
||||
fetch('admin/pend/link_to_kk.php', {
|
||||
method: 'POST',
|
||||
headers: {'Content-Type': 'application/x-www-form-urlencoded'},
|
||||
body: 'id_pend=$id_pend_baru&id_kk=$id_kk&hubungan=ANGGOTA'
|
||||
}).then(resp => resp.json()).then(data => {
|
||||
if(data.success) {
|
||||
Swal.fire('Berhasil!', 'Data telah dihubungkan dengan KK.', 'success').then(() => {
|
||||
window.location = 'index.php?page=data-pend';
|
||||
});
|
||||
} else {
|
||||
Swal.fire('Gagal', 'Gagal menghubungkan: ' + data.message, 'error').then(() => {
|
||||
window.location = 'index.php?page=data-pend';
|
||||
});
|
||||
}
|
||||
});
|
||||
} else {
|
||||
window.location = 'index.php?page=data-pend';
|
||||
}
|
||||
});
|
||||
</script>";
|
||||
} else {
|
||||
// Sudah terhubung
|
||||
echo "<script>
|
||||
Swal.fire({title: 'Tambah Data Berhasil',text: 'Data telah terhubung dengan KK $no_kk',icon: 'success',confirmButtonText: 'OK'
|
||||
}).then((result) => {if (result.value){
|
||||
window.location = 'index.php?page=data-pend';
|
||||
}
|
||||
})</script>";
|
||||
}
|
||||
} else {
|
||||
// Tidak ada KK yang cocok
|
||||
echo "<script>
|
||||
Swal.fire({title: 'Tambah Data Berhasil',text: '',icon: 'success',confirmButtonText: 'OK'
|
||||
}).then((result) => {if (result.value){
|
||||
window.location = 'index.php?page=data-pend';
|
||||
}
|
||||
})</script>";
|
||||
}
|
||||
} else {
|
||||
echo "<script>
|
||||
Swal.fire({title: 'Tambah Data Gagal',text: '<?php echo mysqli_error($koneksi); ?>',icon: 'error',confirmButtonText: 'OK'
|
||||
Swal.fire({title: 'Tambah Data Gagal',text: '" . mysqli_error($koneksi) . "',icon: 'error',confirmButtonText: 'OK'
|
||||
}).then((result) => {if (result.value){
|
||||
window.location = 'index.php?page=add-pend';
|
||||
}
|
||||
})</script>";
|
||||
}}
|
||||
}
|
||||
mysqli_close($koneksi);
|
||||
}
|
||||
//selesai proses simpan data
|
||||
|
||||
62
admin/pend/link_to_kk.php
Normal file
62
admin/pend/link_to_kk.php
Normal file
@@ -0,0 +1,62 @@
|
||||
<?php
|
||||
session_start();
|
||||
include "../inc/koneksi.php";
|
||||
|
||||
header('Content-Type: application/json');
|
||||
|
||||
if (!isset($_SESSION["ses_username"])) {
|
||||
echo json_encode(['success' => false, 'message' => 'Unauthorized']);
|
||||
exit;
|
||||
}
|
||||
|
||||
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
|
||||
echo json_encode(['success' => false, 'message' => 'Invalid request method']);
|
||||
exit;
|
||||
}
|
||||
|
||||
$id_pend = isset($_POST['id_pend']) ? intval($_POST['id_pend']) : 0;
|
||||
$id_kk = isset($_POST['id_kk']) ? intval($_POST['id_kk']) : 0;
|
||||
$hubungan = isset($_POST['hubungan']) ? mysqli_real_escape_string($koneksi, $_POST['hubungan']) : 'ANGGOTA';
|
||||
|
||||
if ($id_pend <= 0 || $id_kk <= 0) {
|
||||
echo json_encode(['success' => false, 'message' => 'Invalid ID']);
|
||||
exit;
|
||||
}
|
||||
|
||||
// Cek apakah sudah terhubung
|
||||
$sql_cek = "SELECT * FROM tb_anggota WHERE id_kk='$id_kk' AND id_pend='$id_pend'";
|
||||
$q_cek = mysqli_query($koneksi, $sql_cek);
|
||||
|
||||
if (mysqli_num_rows($q_cek) > 0) {
|
||||
echo json_encode(['success' => false, 'message' => 'Sudah terhubung']);
|
||||
exit;
|
||||
}
|
||||
|
||||
// Cek apakah penduduk ada
|
||||
$sql_cek_pend = "SELECT * FROM tb_pdd WHERE id_pend='$id_pend'";
|
||||
$q_cek_pend = mysqli_query($koneksi, $sql_cek_pend);
|
||||
if (mysqli_num_rows($q_cek_pend) == 0) {
|
||||
echo json_encode(['success' => false, 'message' => 'Data penduduk tidak ditemukan']);
|
||||
exit;
|
||||
}
|
||||
|
||||
// Cek apakah KK ada
|
||||
$sql_cek_kk = "SELECT * FROM tb_kk WHERE id_kk='$id_kk'";
|
||||
$q_cek_kk = mysqli_query($koneksi, $sql_cek_kk);
|
||||
if (mysqli_num_rows($q_cek_kk) == 0) {
|
||||
echo json_encode(['success' => false, 'message' => 'Data KK tidak ditemukan']);
|
||||
exit;
|
||||
}
|
||||
|
||||
// Hubungkan
|
||||
$sql_link = "INSERT INTO tb_anggota (id_kk, id_pend, hubungan) VALUES ('$id_kk', '$id_pend', '$hubungan')";
|
||||
$q_link = mysqli_query($koneksi, $sql_link);
|
||||
|
||||
if ($q_link) {
|
||||
echo json_encode(['success' => true, 'message' => 'Berhasil dihubungkan']);
|
||||
} else {
|
||||
echo json_encode(['success' => false, 'message' => 'Database error: ' . mysqli_error($koneksi)]);
|
||||
}
|
||||
|
||||
mysqli_close($koneksi);
|
||||
?>
|
||||
@@ -34,6 +34,117 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
/* Disable all transitions and hover effects for scanner modal */
|
||||
#modalScanner .card,
|
||||
#modalScanner .modal-content,
|
||||
#modalScanner .modal-body,
|
||||
#modalScanner .modal-header,
|
||||
#modalScanner .modal-footer,
|
||||
#modalScanner .modal-dialog,
|
||||
#modalScanner #scanner-container,
|
||||
#modalScanner #scanner-container * {
|
||||
transition: none !important;
|
||||
}
|
||||
#modalScanner .card:hover,
|
||||
#modalScanner .modal-content:hover,
|
||||
#modalScanner .modal-body:hover,
|
||||
#modalScanner .modal-header:hover,
|
||||
#modalScanner .modal-footer:hover,
|
||||
#modalScanner .modal-dialog:hover,
|
||||
#modalScanner #scanner-container:hover,
|
||||
#modalScanner #scanner-container *:hover {
|
||||
transform: none !important;
|
||||
}
|
||||
/* Ensure canvas is fully visible */
|
||||
#canvas-image {
|
||||
opacity: 1 !important;
|
||||
filter: none !important;
|
||||
background-color: white !important;
|
||||
}
|
||||
#canvas-overlay {
|
||||
opacity: 1 !important;
|
||||
filter: none !important;
|
||||
}
|
||||
/* Prevent canvas from moving on hover */
|
||||
#canvas-image:hover,
|
||||
#canvas-overlay:hover {
|
||||
transform: none !important;
|
||||
}
|
||||
|
||||
/* Prevent modal dragging and improve touch handling on mobile */
|
||||
#modalScanner .modal-dialog,
|
||||
#modalScanner .modal-content,
|
||||
#modalScanner .modal-header,
|
||||
#modalScanner .modal-footer {
|
||||
touch-action: none !important; /* Prevent browser touch gestures (pan, zoom, swipe) */
|
||||
user-select: none !important; /* Prevent text selection during drag */
|
||||
-webkit-user-select: none !important;
|
||||
-webkit-touch-callout: none !important;
|
||||
}
|
||||
|
||||
/* Allow touch interaction only on canvas and buttons */
|
||||
#modalScanner .modal-body,
|
||||
#canvas-image,
|
||||
#canvas-overlay {
|
||||
touch-action: manipulation !important; /* Allow pinch-zoom and pan on canvas only */
|
||||
}
|
||||
|
||||
/* Prevent modal backdrop from responding to touch */
|
||||
.modal-backdrop {
|
||||
touch-action: none !important;
|
||||
}
|
||||
|
||||
/* Lock modal position on mobile */
|
||||
@media (max-width: 768px) {
|
||||
#modalScanner {
|
||||
padding-right: 0 !important; /* Prevent shift from scrollbar */
|
||||
}
|
||||
#modalScanner .modal-dialog {
|
||||
margin: 0 !important;
|
||||
max-height: 100vh !important;
|
||||
height: 100vh !important;
|
||||
width: 100vw !important;
|
||||
max-width: 100vw !important;
|
||||
position: fixed !important;
|
||||
top: 0 !important;
|
||||
left: 0 !important;
|
||||
right: 0 !important;
|
||||
bottom: 0 !important;
|
||||
transform: none !important;
|
||||
transition: none !important;
|
||||
}
|
||||
#modalScanner .modal-content {
|
||||
border-radius: 0 !important;
|
||||
height: 100vh !important;
|
||||
max-height: 100vh !important;
|
||||
width: 100vw !important;
|
||||
max-width: 100vw !important;
|
||||
overflow: hidden !important;
|
||||
position: fixed !important;
|
||||
top: 0 !important;
|
||||
left: 0 !important;
|
||||
transform: none !important;
|
||||
transition: none !important;
|
||||
}
|
||||
#modalScanner .modal-body {
|
||||
height: calc(100vh - 120px) !important; /* Account for header and footer */
|
||||
overflow: hidden !important;
|
||||
}
|
||||
/* Prevent any modal movement */
|
||||
.modal-open #modalScanner {
|
||||
overflow: hidden !important;
|
||||
}
|
||||
/* Larger touch target for close button on mobile */
|
||||
#modalScanner .modal-header .close {
|
||||
padding: 20px !important;
|
||||
font-size: 2rem !important;
|
||||
line-height: 1 !important;
|
||||
margin: -10px -10px -10px auto !important;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<script>
|
||||
window.addEventListener('load', function() {
|
||||
// Scanner Variables
|
||||
@@ -43,19 +154,31 @@ window.addEventListener('load', function() {
|
||||
var ctxImg = canvasImage.getContext('2d');
|
||||
var ctxOver = canvasOverlay.getContext('2d');
|
||||
|
||||
var scanner = new jscanify();
|
||||
var scanner = null;
|
||||
try {
|
||||
scanner = new jscanify();
|
||||
} catch (e) {
|
||||
console.error('Failed to initialize jscanify:', e);
|
||||
// scanner remains null
|
||||
}
|
||||
var originalImg = new Image();
|
||||
|
||||
// Corner Points (tl, tr, bl, br)
|
||||
var corners = [];
|
||||
var activePoint = null;
|
||||
var isDragging = false;
|
||||
var touchStartPos = null;
|
||||
var isTouchInteraction = false;
|
||||
var touchOffset = null;
|
||||
|
||||
// Config
|
||||
const POINT_RADIUS = 15;
|
||||
const POINT_COLOR = '#007bff';
|
||||
const LINE_COLOR = '#00ff00';
|
||||
const LINE_WIDTH = 3;
|
||||
const TOUCH_RADIUS_MULTIPLIER = 2.5; // Larger hit area for touch devices
|
||||
const TOUCH_SLOP = 5; // pixels threshold before dragging starts
|
||||
const IS_TOUCH_DEVICE = 'ontouchstart' in window || navigator.maxTouchPoints > 0;
|
||||
|
||||
// --- Public Function to Open Scanner ---
|
||||
// --- Public Function to Open Scanner ---
|
||||
@@ -96,11 +219,111 @@ window.addEventListener('load', function() {
|
||||
scannerModal.on('shown.bs.modal', function() {
|
||||
modalShown = true;
|
||||
checkReady();
|
||||
|
||||
// Prevent body scrolling on mobile
|
||||
document.body.style.overflow = 'hidden';
|
||||
document.body.style.position = 'fixed';
|
||||
document.body.style.width = '100%';
|
||||
|
||||
// Prevent modal dragging on mobile
|
||||
var modalDialog = document.querySelector('#modalScanner .modal-dialog');
|
||||
var modalContent = document.querySelector('#modalScanner .modal-content');
|
||||
var modalHeader = document.querySelector('#modalScanner .modal-header');
|
||||
var modalFooter = document.querySelector('#modalScanner .modal-footer');
|
||||
|
||||
var preventTouch = function(e) {
|
||||
// Only prevent if not clicking a button
|
||||
if (e.target.tagName !== 'BUTTON' && !e.target.closest('button')) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
}
|
||||
};
|
||||
|
||||
if (modalDialog) {
|
||||
modalDialog.addEventListener('touchstart', preventTouch, { passive: false });
|
||||
modalDialog.addEventListener('touchmove', preventTouch, { passive: false });
|
||||
}
|
||||
if (modalContent) {
|
||||
modalContent.addEventListener('touchstart', preventTouch, { passive: false });
|
||||
modalContent.addEventListener('touchmove', preventTouch, { passive: false });
|
||||
}
|
||||
if (modalHeader) {
|
||||
modalHeader.addEventListener('touchstart', preventTouch, { passive: false });
|
||||
modalHeader.addEventListener('touchmove', preventTouch, { passive: false });
|
||||
}
|
||||
if (modalFooter) {
|
||||
modalFooter.addEventListener('touchstart', preventTouch, { passive: false });
|
||||
modalFooter.addEventListener('touchmove', preventTouch, { passive: false });
|
||||
}
|
||||
});
|
||||
|
||||
// Restore scrolling when modal is hidden
|
||||
scannerModal.on('hidden.bs.modal', function() {
|
||||
document.body.style.overflow = '';
|
||||
document.body.style.position = '';
|
||||
document.body.style.width = '';
|
||||
});
|
||||
|
||||
scannerModal.modal('show');
|
||||
};
|
||||
|
||||
function detectDocument(scale, w, h, showAlert = false) {
|
||||
// Detect Contour using jscanify
|
||||
try {
|
||||
// jscanify expects an image element, we can pass originalImg but we need to map coordinates
|
||||
// Wait, jscanify uses OpenCV which might not be ready.
|
||||
if (typeof cv !== 'undefined' && cv.Mat) {
|
||||
// We need to work on the original image for detection, then scale points
|
||||
var contour = scanner ? scanner.findPaper(originalImg) : null;
|
||||
// contour returns { topLeftCorner, topRightCorner, bottomLeftCorner, bottomRightCorner } each {x, y}
|
||||
|
||||
if (contour) {
|
||||
// Order: TL, TR, BR, BL (clockwise)
|
||||
corners = [
|
||||
{ x: contour.topLeftCorner.x * scale, y: contour.topLeftCorner.y * scale },
|
||||
{ x: contour.topRightCorner.x * scale, y: contour.topRightCorner.y * scale },
|
||||
{ x: contour.bottomRightCorner.x * scale, y: contour.bottomRightCorner.y * scale },
|
||||
{ x: contour.bottomLeftCorner.x * scale, y: contour.bottomLeftCorner.y * scale }
|
||||
];
|
||||
|
||||
} else {
|
||||
defaultCorners(w, h);
|
||||
if (showAlert) {
|
||||
Swal.fire({
|
||||
icon: 'warning',
|
||||
title: 'Deteksi Gagal',
|
||||
text: 'Dokumen tidak terdeteksi. Silakan atur sudut secara manual.',
|
||||
confirmButtonText: 'OK'
|
||||
});
|
||||
}
|
||||
}
|
||||
} else {
|
||||
console.warn("OpenCV not ready yet");
|
||||
defaultCorners(w, h);
|
||||
if (showAlert) {
|
||||
Swal.fire({
|
||||
icon: 'warning',
|
||||
title: 'Scanner Tidak Siap',
|
||||
text: 'OpenCV belum siap. Silakan atur sudut secara manual.',
|
||||
confirmButtonText: 'OK'
|
||||
});
|
||||
}
|
||||
}
|
||||
} catch(e) {
|
||||
console.error("Scanner Error:", e);
|
||||
defaultCorners(w, h); // Fallback
|
||||
if (showAlert) {
|
||||
Swal.fire({
|
||||
icon: 'error',
|
||||
title: 'Error Scanner',
|
||||
text: 'Terjadi kesalahan saat mendeteksi dokumen: ' + e.message,
|
||||
confirmButtonText: 'OK'
|
||||
});
|
||||
}
|
||||
}
|
||||
drawOverlay();
|
||||
}
|
||||
|
||||
function initScanner() {
|
||||
// Resize Canvas to fit screen but keep aspect ratio
|
||||
var maxWidth = $('#modalScanner .modal-body').width() - 20;
|
||||
@@ -121,44 +344,8 @@ window.addEventListener('load', function() {
|
||||
// Draw Image
|
||||
ctxImg.drawImage(originalImg, 0, 0, w, h);
|
||||
|
||||
// Detect Contour using jscanify
|
||||
try {
|
||||
// jscanify expects an image element, we can pass originalImg but we need to map coordinates
|
||||
// Wait, jscanify uses OpenCV which might not be ready.
|
||||
if (typeof cv !== 'undefined' && cv.Mat) {
|
||||
// We need to work on the original image for detection, then scale points
|
||||
var contour = scanner.findPaper(originalImg);
|
||||
// contour returns { topLeftCorner, topRightCorner, bottomLeftCorner, bottomRightCorner } each {x, y}
|
||||
|
||||
if (contour) {
|
||||
corners = [
|
||||
{ x: contour.topLeftCorner.x * scale, y: contour.topLeftCorner.y * scale },
|
||||
{ x: contour.topRightCorner.x * scale, y: contour.topRightCorner.y * scale },
|
||||
{ x: contour.bottomRightCorner.x * scale, y: contour.bottomRightCorner.y * scale }, // Order: tr -> br -> bl ?? No, usually tl, tr, br, bl order for polygon drawing
|
||||
{ x: contour.bottomLeftCorner.x * scale, y: contour.bottomLeftCorner.y * scale }
|
||||
];
|
||||
// Reorder primarily for logic: TL, TR, BR, BL
|
||||
corners = [
|
||||
{ x: contour.topLeftCorner.x * scale, y: contour.topLeftCorner.y * scale },
|
||||
{ x: contour.topRightCorner.x * scale, y: contour.topRightCorner.y * scale },
|
||||
{ x: contour.bottomRightCorner.x * scale, y: contour.bottomRightCorner.y * scale },
|
||||
{ x: contour.bottomLeftCorner.x * scale, y: contour.bottomLeftCorner.y * scale }
|
||||
];
|
||||
|
||||
} else {
|
||||
defaultCorners(w, h);
|
||||
}
|
||||
} else {
|
||||
console.warn("OpenCV not ready yet");
|
||||
defaultCorners(w, h);
|
||||
}
|
||||
} catch(e) {
|
||||
console.error("Scanner Error:", e);
|
||||
defaultCorners(w, h); // Fallback
|
||||
}
|
||||
|
||||
detectDocument(scale, w, h, false);
|
||||
$('#scanner-loading').hide();
|
||||
drawOverlay();
|
||||
}
|
||||
|
||||
function defaultCorners(w, h) {
|
||||
@@ -217,30 +404,98 @@ window.addEventListener('load', function() {
|
||||
return dx * dx + dy * dy <= POINT_RADIUS * POINT_RADIUS * 2; // Bigger hit area
|
||||
}
|
||||
|
||||
canvasOverlay.addEventListener('mousedown', function(e) { handleStart(getMousePos(e)); });
|
||||
canvasOverlay.addEventListener('touchstart', function(e) { handleStart(getMousePos(e)); e.preventDefault(); }, {passive: false});
|
||||
function getClosestCorner(pos, isTouch) {
|
||||
var closestIdx = -1;
|
||||
var closestDist = Infinity;
|
||||
var radius = POINT_RADIUS * (isTouch ? TOUCH_RADIUS_MULTIPLIER : 1.5); // Larger radius for touch
|
||||
var radiusSq = radius * radius;
|
||||
|
||||
corners.forEach((p, i) => {
|
||||
var dx = pos.x - p.x;
|
||||
var dy = pos.y - p.y;
|
||||
var distSq = dx * dx + dy * dy;
|
||||
if (distSq < radiusSq && distSq < closestDist) {
|
||||
closestDist = distSq;
|
||||
closestIdx = i;
|
||||
}
|
||||
});
|
||||
return closestIdx;
|
||||
}
|
||||
|
||||
canvasOverlay.addEventListener('mousedown', function(e) {
|
||||
handleStart(getMousePos(e), false);
|
||||
e.stopPropagation();
|
||||
});
|
||||
canvasOverlay.addEventListener('touchstart', function(e) {
|
||||
var pos = getMousePos(e);
|
||||
handleStart(pos, true);
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
}, {passive: false});
|
||||
|
||||
window.addEventListener('mousemove', function(e) { if(isDragging) handleMove(getMousePos(e)); }); // Window to catch drag out
|
||||
canvasOverlay.addEventListener('touchmove', function(e) { if(isDragging) handleMove(getMousePos(e)); e.preventDefault(); }, {passive: false});
|
||||
canvasOverlay.addEventListener('touchmove', function(e) {
|
||||
if(isDragging) {
|
||||
handleMove(getMousePos(e));
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
}
|
||||
}, {passive: false});
|
||||
|
||||
window.addEventListener('mouseup', function() { handleEnd(); });
|
||||
window.addEventListener('touchend', function() { handleEnd(); });
|
||||
|
||||
function handleStart(pos) {
|
||||
activePoint = null;
|
||||
corners.forEach((p, i) => {
|
||||
if (isInside(pos, p)) {
|
||||
activePoint = i;
|
||||
function handleStart(pos, isTouch = false) {
|
||||
activePoint = getClosestCorner(pos, isTouch);
|
||||
if (activePoint !== -1) {
|
||||
isTouchInteraction = isTouch;
|
||||
touchOffset = { x: pos.x - corners[activePoint].x, y: pos.y - corners[activePoint].y };
|
||||
if (isTouch) {
|
||||
touchStartPos = pos;
|
||||
// Start dragging immediately but handle slop in handleMove
|
||||
isDragging = true;
|
||||
} else {
|
||||
isDragging = true;
|
||||
touchStartPos = null;
|
||||
}
|
||||
} else {
|
||||
activePoint = null;
|
||||
isDragging = false;
|
||||
isTouchInteraction = false;
|
||||
touchStartPos = null;
|
||||
touchOffset = null;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function handleMove(pos) {
|
||||
if (activePoint !== null) {
|
||||
// Constrain to canvas?? Optional but good
|
||||
// Touch slop detection
|
||||
if (isTouchInteraction && touchStartPos) {
|
||||
var dx = pos.x - touchStartPos.x;
|
||||
var dy = pos.y - touchStartPos.y;
|
||||
var distSq = dx * dx + dy * dy;
|
||||
if (distSq < TOUCH_SLOP * TOUCH_SLOP) {
|
||||
return; // Ignore small movements until slop exceeded
|
||||
}
|
||||
// Slop exceeded, clear touchStartPos so we don't check again
|
||||
touchStartPos = null;
|
||||
}
|
||||
|
||||
// Apply offset to maintain relative position
|
||||
if (touchOffset) {
|
||||
corners[activePoint].x = pos.x - touchOffset.x;
|
||||
corners[activePoint].y = pos.y - touchOffset.y;
|
||||
} else {
|
||||
corners[activePoint].x = pos.x;
|
||||
corners[activePoint].y = pos.y;
|
||||
}
|
||||
|
||||
// Optional: constrain to canvas bounds
|
||||
var w = canvasOverlay.width;
|
||||
var h = canvasOverlay.height;
|
||||
corners[activePoint].x = Math.max(0, Math.min(w, corners[activePoint].x));
|
||||
corners[activePoint].y = Math.max(0, Math.min(h, corners[activePoint].y));
|
||||
|
||||
drawOverlay();
|
||||
}
|
||||
}
|
||||
@@ -248,6 +503,9 @@ window.addEventListener('load', function() {
|
||||
function handleEnd() {
|
||||
isDragging = false;
|
||||
activePoint = null;
|
||||
isTouchInteraction = false;
|
||||
touchStartPos = null;
|
||||
touchOffset = null;
|
||||
}
|
||||
|
||||
// --- Rotate Functions ---
|
||||
@@ -309,7 +567,13 @@ window.addEventListener('load', function() {
|
||||
};
|
||||
|
||||
// 3. Extract with dynamic dimensions
|
||||
var resultCanvas = scanner.extractPaper(originalImg, outputWidth, outputHeight, extractPoints);
|
||||
var resultCanvas = null;
|
||||
if (scanner && scanner.extractPaper) {
|
||||
resultCanvas = scanner.extractPaper(originalImg, outputWidth, outputHeight, extractPoints);
|
||||
} else {
|
||||
alert("Scanner library not loaded. Please refresh the page.");
|
||||
return;
|
||||
}
|
||||
var base64 = resultCanvas.toDataURL('image/jpeg');
|
||||
|
||||
if (window.handleScannerResult) {
|
||||
|
||||
2
dist/css/modern.css
vendored
2
dist/css/modern.css
vendored
@@ -15,7 +15,7 @@ body {
|
||||
}
|
||||
|
||||
.card:hover {
|
||||
transform: translateY(-2px);
|
||||
/* transform: translateY(-2px); */
|
||||
box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05) !important;
|
||||
}
|
||||
|
||||
|
||||
61
docker-compose.yml
Normal file
61
docker-compose.yml
Normal file
@@ -0,0 +1,61 @@
|
||||
version: '3.8'
|
||||
|
||||
services:
|
||||
web:
|
||||
image: php:8.2-apache
|
||||
ports:
|
||||
- "8500:80"
|
||||
volumes:
|
||||
- ./:/var/www/html
|
||||
depends_on:
|
||||
- db
|
||||
environment:
|
||||
- DB_HOST=db
|
||||
- DB_USER=sidak_user
|
||||
- DB_PASS=sidak_pass
|
||||
- DB_NAME=data_penduduk
|
||||
networks:
|
||||
- sidak-network
|
||||
command: >
|
||||
bash -c "
|
||||
apt-get update &&
|
||||
apt-get install -y libpng-dev libjpeg-dev libfreetype6-dev &&
|
||||
docker-php-ext-configure gd --with-freetype --with-jpeg &&
|
||||
docker-php-ext-install gd mysqli pdo pdo_mysql &&
|
||||
a2enmod rewrite &&
|
||||
apache2-foreground
|
||||
"
|
||||
|
||||
db:
|
||||
image: mariadb:10.11
|
||||
ports:
|
||||
- "3307:3306"
|
||||
environment:
|
||||
- MARIADB_ROOT_PASSWORD=rootpassword
|
||||
- MARIADB_DATABASE=data_penduduk
|
||||
- MARIADB_USER=sidak_user
|
||||
- MARIADB_PASSWORD=sidak_pass
|
||||
volumes:
|
||||
- ./init.sql:/docker-entrypoint-initdb.d/init.sql:ro
|
||||
- mariadb-data:/var/lib/mysql
|
||||
networks:
|
||||
- sidak-network
|
||||
|
||||
phpmyadmin:
|
||||
image: phpmyadmin/phpmyadmin
|
||||
ports:
|
||||
- "8080:80"
|
||||
environment:
|
||||
- PMA_HOST=db
|
||||
- PMA_PORT=3306
|
||||
depends_on:
|
||||
- db
|
||||
networks:
|
||||
- sidak-network
|
||||
|
||||
networks:
|
||||
sidak-network:
|
||||
driver: bridge
|
||||
|
||||
volumes:
|
||||
mariadb-data:
|
||||
@@ -1,2 +1,12 @@
|
||||
<?php
|
||||
$koneksi = new mysqli ("localhost","sidak_user","sidak_pass","data_penduduk");
|
||||
$db_host = getenv("DB_HOST") ?: "localhost";
|
||||
$db_user = getenv("DB_USER") ?: "sidak_user";
|
||||
$db_pass = getenv("DB_PASS") ?: "sidak_pass";
|
||||
$db_name = getenv("DB_NAME") ?: "data_penduduk";
|
||||
|
||||
$koneksi = new mysqli($db_host, $db_user, $db_pass, $db_name);
|
||||
|
||||
if ($koneksi->connect_error) {
|
||||
die("Koneksi database gagal: " . $koneksi->connect_error);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
<?php
|
||||
|
||||
//Mulai Sesion
|
||||
session_start();
|
||||
if (isset($_SESSION["ses_username"])==""){
|
||||
@@ -108,7 +109,7 @@
|
||||
|
||||
<!-- Level -->
|
||||
<?php
|
||||
if ($data_level=="Administrator"){
|
||||
if ($data_level=="admin"){
|
||||
?>
|
||||
<li class="nav-item">
|
||||
<a href="index.php" class="nav-link">
|
||||
@@ -658,10 +659,10 @@
|
||||
}
|
||||
}else{
|
||||
// Auto Halaman Home Pengguna
|
||||
if($data_level=="Administrator"){
|
||||
if($data_level=="admin"){
|
||||
include "home/admin.php";
|
||||
}
|
||||
elseif($data_level=="Kaur Pemerintah"){
|
||||
elseif($data_level=="kaur"){
|
||||
include "home/kaur.php";
|
||||
}
|
||||
}
|
||||
|
||||
208
init.sql
Normal file
208
init.sql
Normal file
@@ -0,0 +1,208 @@
|
||||
-- Initialize database for SIDAK application
|
||||
|
||||
-- Create database if not exists
|
||||
CREATE DATABASE IF NOT EXISTS `data_penduduk`;
|
||||
USE `data_penduduk`;
|
||||
|
||||
-- Table structure for table `tb_anggota`
|
||||
CREATE TABLE IF NOT EXISTS `tb_anggota` (
|
||||
`id_anggota` int(11) NOT NULL,
|
||||
`id_kk` int(11) NOT NULL,
|
||||
`id_pend` int(11) NOT NULL,
|
||||
`hubungan` varchar(15) NOT NULL
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=latin1;
|
||||
|
||||
-- Table structure for table `tb_datang`
|
||||
CREATE TABLE IF NOT EXISTS `tb_datang` (
|
||||
`id_datang` int(11) NOT NULL,
|
||||
`nik` varchar(20) NOT NULL,
|
||||
`nama_datang` varchar(20) NOT NULL,
|
||||
`jekel` enum('LK','PR') NOT NULL,
|
||||
`tgl_datang` date NOT NULL,
|
||||
`pelapor` int(11) NOT NULL
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=latin1;
|
||||
|
||||
-- Table structure for table `tb_kk`
|
||||
CREATE TABLE IF NOT EXISTS `tb_kk` (
|
||||
`id_kk` int(11) NOT NULL,
|
||||
`no_kk` varchar(30) NOT NULL,
|
||||
`kepala` varchar(20) NOT NULL,
|
||||
`desa` varchar(20) NOT NULL,
|
||||
`rt` varchar(5) NOT NULL,
|
||||
`rw` varchar(5) NOT NULL,
|
||||
`kec` varchar(20) NOT NULL,
|
||||
`kab` varchar(20) NOT NULL,
|
||||
`prov` varchar(20) NOT NULL,
|
||||
`foto_kk` varchar(255) NULL
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=latin1;
|
||||
|
||||
-- Table structure for table `tb_lahir`
|
||||
CREATE TABLE IF NOT EXISTS `tb_lahir` (
|
||||
`id_lahir` int(11) NOT NULL,
|
||||
`nama` varchar(30) NOT NULL,
|
||||
`tgl_lh` date NOT NULL,
|
||||
`jekel` enum('LK','PR') NOT NULL,
|
||||
`id_kk` int(11) NOT NULL
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=latin1;
|
||||
|
||||
-- Table structure for table `tb_mendu`
|
||||
CREATE TABLE IF NOT EXISTS `tb_mendu` (
|
||||
`id_mendu` int(11) NOT NULL,
|
||||
`id_pdd` int(11) NOT NULL,
|
||||
`tgl_mendu` date NOT NULL,
|
||||
`sebab` varchar(20) NOT NULL
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=latin1;
|
||||
|
||||
-- Table structure for table `tb_pdd`
|
||||
CREATE TABLE IF NOT EXISTS `tb_pdd` (
|
||||
`id_pend` int(11) NOT NULL,
|
||||
`nik` varchar(20) NOT NULL,
|
||||
`nama` varchar(30) NOT NULL,
|
||||
`tempat_lh` varchar(20) NOT NULL,
|
||||
`tgl_lh` date NOT NULL,
|
||||
`jekel` enum('LK','PR') NOT NULL,
|
||||
`desa` varchar(20) NOT NULL,
|
||||
`rt` varchar(5) NOT NULL,
|
||||
`rw` varchar(5) NOT NULL,
|
||||
`agama` varchar(15) NOT NULL,
|
||||
`kawin` varchar(15) NOT NULL,
|
||||
`pekerjaan` varchar(20) NOT NULL,
|
||||
`foto_ktp` varchar(255) NULL,
|
||||
`status` varchar(10) NOT NULL,
|
||||
`kecamatan` varchar(50) NULL,
|
||||
`kabupaten` varchar(50) NULL,
|
||||
`provinsi` varchar(50) NULL,
|
||||
`kewarganegaraan` varchar(10) NULL
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=latin1;
|
||||
|
||||
-- Table structure for table `tb_pengguna`
|
||||
CREATE TABLE IF NOT EXISTS `tb_pengguna` (
|
||||
`id_pengguna` int(11) NOT NULL,
|
||||
`nama_pengguna` varchar(30) NOT NULL,
|
||||
`username` varchar(20) NOT NULL,
|
||||
`password` varchar(100) NOT NULL,
|
||||
`level` enum('admin','kaur') NOT NULL
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=latin1;
|
||||
|
||||
-- Table structure for table `tb_pindah`
|
||||
CREATE TABLE IF NOT EXISTS `tb_pindah` (
|
||||
`id_pindah` int(11) NOT NULL,
|
||||
`id_pdd` int(11) NOT NULL,
|
||||
`tgl_pindah` date NOT NULL,
|
||||
`alasan` varchar(50) NOT NULL
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=latin1;
|
||||
|
||||
-- Table structure for table `tb_profil`
|
||||
CREATE TABLE IF NOT EXISTS `tb_profil` (
|
||||
`id_profil` int(11) NOT NULL PRIMARY KEY,
|
||||
`nama_desa` varchar(50) NOT NULL,
|
||||
`alamat` text NOT NULL,
|
||||
`kecamatan` varchar(50) NOT NULL,
|
||||
`kabupaten` varchar(50) NOT NULL,
|
||||
`provinsi` varchar(50) NOT NULL,
|
||||
`kepala_desa` varchar(50) NOT NULL,
|
||||
`nip_kades` varchar(30) NOT NULL
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=latin1;
|
||||
|
||||
-- Add primary keys and indexes
|
||||
ALTER TABLE `tb_anggota`
|
||||
ADD PRIMARY KEY (`id_anggota`),
|
||||
ADD KEY `id_kk` (`id_kk`),
|
||||
ADD KEY `id_pend` (`id_pend`);
|
||||
|
||||
ALTER TABLE `tb_datang`
|
||||
ADD PRIMARY KEY (`id_datang`),
|
||||
ADD KEY `pelapor` (`pelapor`);
|
||||
|
||||
ALTER TABLE `tb_kk`
|
||||
ADD PRIMARY KEY (`id_kk`);
|
||||
|
||||
ALTER TABLE `tb_lahir`
|
||||
ADD PRIMARY KEY (`id_lahir`),
|
||||
ADD KEY `id_kk` (`id_kk`);
|
||||
|
||||
ALTER TABLE `tb_mendu`
|
||||
ADD PRIMARY KEY (`id_mendu`),
|
||||
ADD KEY `id_pdd` (`id_pdd`);
|
||||
|
||||
ALTER TABLE `tb_pdd`
|
||||
ADD PRIMARY KEY (`id_pend`);
|
||||
|
||||
ALTER TABLE `tb_pengguna`
|
||||
ADD PRIMARY KEY (`id_pengguna`);
|
||||
|
||||
ALTER TABLE `tb_pindah`
|
||||
ADD PRIMARY KEY (`id_pindah`),
|
||||
ADD KEY `id_pdd` (`id_pdd`);
|
||||
|
||||
-- Add auto increments
|
||||
ALTER TABLE `tb_anggota`
|
||||
MODIFY `id_anggota` int(11) NOT NULL AUTO_INCREMENT;
|
||||
|
||||
ALTER TABLE `tb_datang`
|
||||
MODIFY `id_datang` int(11) NOT NULL AUTO_INCREMENT;
|
||||
|
||||
ALTER TABLE `tb_kk`
|
||||
MODIFY `id_kk` int(11) NOT NULL AUTO_INCREMENT;
|
||||
|
||||
ALTER TABLE `tb_lahir`
|
||||
MODIFY `id_lahir` int(11) NOT NULL AUTO_INCREMENT;
|
||||
|
||||
ALTER TABLE `tb_mendu`
|
||||
MODIFY `id_mendu` int(11) NOT NULL AUTO_INCREMENT;
|
||||
|
||||
ALTER TABLE `tb_pdd`
|
||||
MODIFY `id_pend` int(11) NOT NULL AUTO_INCREMENT;
|
||||
|
||||
ALTER TABLE `tb_pengguna`
|
||||
MODIFY `id_pengguna` int(11) NOT NULL AUTO_INCREMENT;
|
||||
|
||||
ALTER TABLE `tb_pindah`
|
||||
MODIFY `id_pindah` int(11) NOT NULL AUTO_INCREMENT;
|
||||
|
||||
-- Add foreign key constraints
|
||||
ALTER TABLE `tb_anggota`
|
||||
ADD CONSTRAINT `tb_anggota_ibfk_1` FOREIGN KEY (`id_kk`) REFERENCES `tb_kk` (`id_kk`) ON DELETE CASCADE ON UPDATE CASCADE,
|
||||
ADD CONSTRAINT `tb_anggota_ibfk_2` FOREIGN KEY (`id_pend`) REFERENCES `tb_pdd` (`id_pend`) ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
ALTER TABLE `tb_datang`
|
||||
ADD CONSTRAINT `tb_datang_ibfk_1` FOREIGN KEY (`pelapor`) REFERENCES `tb_pengguna` (`id_pengguna`) ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
ALTER TABLE `tb_lahir`
|
||||
ADD CONSTRAINT `tb_lahir_ibfk_1` FOREIGN KEY (`id_kk`) REFERENCES `tb_kk` (`id_kk`) ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
ALTER TABLE `tb_mendu`
|
||||
ADD CONSTRAINT `tb_mendu_ibfk_1` FOREIGN KEY (`id_pdd`) REFERENCES `tb_pdd` (`id_pend`) ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
ALTER TABLE `tb_pindah`
|
||||
ADD CONSTRAINT `tb_pindah_ibfk_1` FOREIGN KEY (`id_pdd`) REFERENCES `tb_pdd` (`id_pend`) ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- Insert default user (admin/admin)
|
||||
INSERT INTO `tb_pengguna` (`id_pengguna`, `nama_pengguna`, `username`, `password`, `level`) VALUES
|
||||
(1, 'Administrator', 'admin', MD5('admin'), 'admin'),
|
||||
(2, 'Kepala Urusan', 'kaur', MD5('kaur'), 'kaur');
|
||||
|
||||
-- Insert default profile data
|
||||
INSERT INTO `tb_profil` (`id_profil`, `nama_desa`, `alamat`, `kecamatan`, `kabupaten`, `provinsi`, `kepala_desa`, `nip_kades`) VALUES
|
||||
(1, 'Desa Percontohan', 'Jl. Contoh No. 1', 'Kecamatan Contoh', 'Kabupaten Contoh', 'Jawa Barat', 'Budi Santoso', '123456789');
|
||||
|
||||
-- Insert sample KK data
|
||||
INSERT INTO `tb_kk` (`id_kk`, `no_kk`, `kepala`, `desa`, `rt`, `rw`, `kec`, `kab`, `prov`) VALUES
|
||||
(1, '32010101010001', 'Budi Santoso', 'Sukamaju', '01', '02', 'Cibinong', 'Bogor', 'Jawa Barat'),
|
||||
(2, '32010101010002', 'Siti Aminah', 'Sukamaju', '02', '02', 'Cibinong', 'Bogor', 'Jawa Barat');
|
||||
|
||||
-- Insert sample penduduk data
|
||||
INSERT INTO `tb_pdd` (`id_pend`, `nik`, `nama`, `tempat_lh`, `tgl_lh`, `jekel`, `desa`, `rt`, `rw`, `agama`, `kawin`, `pekerjaan`, `status`) VALUES
|
||||
(1, '3201010101000001', 'Budi Santoso', 'Bogor', '1980-01-01', 'LK', 'Sukamaju', '01', '02', 'Islam', 'Kawin', 'Karyawan', 'Ada'),
|
||||
(2, '3201010101000002', 'Siti Aminah', 'Bogor', '1985-05-05', 'PR', 'Sukamaju', '02', '02', 'Islam', 'Cerai Mati', 'Wiraswasta', 'Ada'),
|
||||
(3, '3201010101000003', 'Ani Santoso', 'Bogor', '2005-01-01', 'PR', 'Sukamaju', '01', '02', 'Islam', 'Belum', 'Pelajar', 'Ada'),
|
||||
(4, '3201010101000004', 'Ahmad Santoso', 'Bogor', '2008-01-01', 'LK', 'Sukamaju', '01', '02', 'Islam', 'Belum', 'Pelajar', 'Ada'),
|
||||
(5, '3201010101000005', 'Dewi Lestari', 'Bandung', '1990-10-10', 'PR', 'Sukamaju', '03', '03', 'Islam', 'Belum', 'Guru', 'Pindah'),
|
||||
(6, '3201010101000006', 'Alm. Kakek', 'Jakarta', '1950-01-01', 'LK', 'Sukamaju', '01', '02', 'Islam', 'Cerai Mati', 'Pensiunan', 'Meninggal');
|
||||
|
||||
-- Insert sample anggota keluarga
|
||||
INSERT INTO `tb_anggota` (`id_kk`, `id_pend`, `hubungan`) VALUES
|
||||
(1, 1, 'Kepala Keluarga'),
|
||||
(1, 3, 'Anak'),
|
||||
(1, 4, 'Anak'),
|
||||
(2, 2, 'Kepala Keluarga');
|
||||
@@ -1,6 +1,6 @@
|
||||
<?php
|
||||
include "inc/koneksi.php";
|
||||
|
||||
session_start();
|
||||
?>
|
||||
|
||||
<!DOCTYPE html>
|
||||
@@ -64,7 +64,7 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="input-group mb-3">
|
||||
<input type="text" class="form-control" name="captcha_input" placeholder="Kode Captcha" required>
|
||||
<input type="text" class="form-control" name="captcha_input" placeholder="Captcha (di-bypass)">
|
||||
<div class="input-group-append">
|
||||
<div class="input-group-text">
|
||||
<img src="inc/captcha.php" alt="CAPTCHA">
|
||||
@@ -109,7 +109,6 @@ if (isset($_POST['btnLogin'])) {
|
||||
$password=mysqli_real_escape_string($koneksi,$_POST['password']);
|
||||
|
||||
//validasi captcha
|
||||
session_start();
|
||||
/* Bypassed for development
|
||||
if ($_POST["captcha_input"] != $_SESSION["captcha_code"]) {
|
||||
echo "<script>
|
||||
@@ -122,7 +121,7 @@ if (isset($_POST['btnLogin'])) {
|
||||
*/
|
||||
|
||||
//query login
|
||||
$sql_login = "SELECT * FROM tb_pengguna WHERE BINARY username='$username' AND password='$password'";
|
||||
$sql_login = "SELECT * FROM tb_pengguna WHERE BINARY username='$username' AND password=MD5('$password')";
|
||||
$query_login = mysqli_query($koneksi, $sql_login);
|
||||
$data_login = mysqli_fetch_array($query_login,MYSQLI_BOTH);
|
||||
$jumlah_login = mysqli_num_rows($query_login);
|
||||
|
||||
Reference in New Issue
Block a user