- 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
353 lines
15 KiB
PHP
353 lines
15 KiB
PHP
<div class="card card-primary">
|
|
<div class="card-header">
|
|
<h3 class="card-title">
|
|
<i class="fa fa-edit"></i> Tambah Data</h3>
|
|
</div>
|
|
<form action="" method="post" enctype="multipart/form-data">
|
|
<div class="card-body">
|
|
|
|
<div class="form-group row">
|
|
<label class="col-sm-2 col-form-label">Foto KK</label>
|
|
<div class="col-sm-6">
|
|
<img id="preview_kk" src="#" alt="Preview Foto" class="img-fluid mb-2" style="display:none; max-height: 300px; border: 1px solid #ddd; border-radius: 5px;">
|
|
<input type="file" class="form-control mb-2" id="foto_kk" name="foto_kk" accept=".jpg, .jpeg, .png">
|
|
<button type="button" class="btn btn-success btn-block" id="btnScanKK">
|
|
<i class="fas fa-magic"></i> Scan KK dengan AI
|
|
</button>
|
|
<small class="text-muted">Pilih foto, crop, lalu scan.</small>
|
|
<input type="hidden" id="foto_cropped" name="foto_cropped">
|
|
</div>
|
|
</div>
|
|
|
|
<?php include 'admin/scanner_modal.php'; ?>
|
|
|
|
<div class="form-group row">
|
|
<label class="col-sm-2 col-form-label">No KK</label>
|
|
<div class="col-sm-6">
|
|
<input type="text" class="form-control" id="no_kk" name="no_kk" placeholder="No KK" required>
|
|
<input type="hidden" id="anggota_json" name="anggota_json">
|
|
</div>
|
|
</div>
|
|
|
|
<div class="form-group row">
|
|
<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="Kepala Keluarga" required>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="form-group row">
|
|
<label class="col-sm-2 col-form-label">Desa</label>
|
|
<div class="col-sm-6">
|
|
<input type="text" class="form-control" id="desa" name="desa" placeholder="Desa" required>
|
|
</div>
|
|
</div>
|
|
|
|
<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" value="000" required>
|
|
</div>
|
|
<div class="col-sm-3">
|
|
<input type="text" class="form-control" id="rw" name="rw" placeholder="RW" value="000" required>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="form-group row">
|
|
<label class="col-sm-2 col-form-label">Kecamatan</label>
|
|
<div class="col-sm-6">
|
|
<input type="text" class="form-control" id="kec" name="kec" placeholder="Kecamatan" required>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="form-group row">
|
|
<label class="col-sm-2 col-form-label">Kabupaten</label>
|
|
<div class="col-sm-6">
|
|
<input type="text" class="form-control" id="kab" name="kab" placeholder="Kabupaten" required>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="form-group row">
|
|
<label class="col-sm-2 col-form-label">Provinsi</label>
|
|
<div class="col-sm-6">
|
|
<input type="text" class="form-control" id="prov" name="prov" placeholder="Provinsi" required>
|
|
</div>
|
|
</div>
|
|
|
|
|
|
|
|
</div>
|
|
<div class="card-footer">
|
|
<input type="submit" name="Simpan" value="Simpan" class="btn btn-info">
|
|
<a href="?page=data-kartu" title="Kembali" class="btn btn-secondary">Batal</a>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
|
|
<script>
|
|
window.addEventListener('load', function() {
|
|
// Scanner Logic
|
|
var inputImage = document.getElementById('foto_kk');
|
|
var preview = document.getElementById('preview_kk');
|
|
var hiddenInput = document.getElementById('foto_cropped');
|
|
|
|
window.handleScannerResult = function(base64) {
|
|
preview.src = base64;
|
|
preview.style.display = 'block'; // Make sure it's visible
|
|
hiddenInput.value = base64;
|
|
};
|
|
|
|
inputImage.addEventListener('change', function(e) {
|
|
var files = e.target.files;
|
|
if (files && files.length > 0) {
|
|
if (window.openScanner) {
|
|
window.openScanner(files[0]);
|
|
}
|
|
inputImage.value = '';
|
|
}
|
|
});
|
|
|
|
// Scan
|
|
document.getElementById('btnScanKK').addEventListener('click', function() {
|
|
var fileInput = document.getElementById('foto_kk');
|
|
var croppedVal = document.getElementById('foto_cropped').value;
|
|
|
|
if(fileInput.files.length === 0 && !croppedVal) {
|
|
Swal.fire('Info', 'Silakan pilih foto KK terlebih dahulu.', 'warning');
|
|
return;
|
|
}
|
|
|
|
var btn = this;
|
|
var originalText = btn.innerHTML;
|
|
btn.innerHTML = '<i class="fas fa-spinner fa-spin"></i> Menganalisa...';
|
|
btn.disabled = true;
|
|
|
|
var formData = new FormData();
|
|
var croppedData = document.getElementById('foto_cropped').value;
|
|
|
|
if (croppedData) {
|
|
var byteString = atob(croppedData.split(',')[1]);
|
|
var ab = new ArrayBuffer(byteString.length);
|
|
var ia = new Uint8Array(ab);
|
|
for (var i = 0; i < byteString.length; i++) {
|
|
ia[i] = byteString.charCodeAt(i);
|
|
}
|
|
var blob = new Blob([ab], { type: 'image/jpeg' });
|
|
formData.append('image', blob, 'cropped_kk.jpg');
|
|
} else if (fileInput.files.length > 0) {
|
|
formData.append('image', fileInput.files[0]);
|
|
} else {
|
|
Swal.fire('Info', 'Belum ada foto yang dipilih / dicrop.', 'warning');
|
|
btn.disabled = false;
|
|
btn.innerHTML = originalText;
|
|
return;
|
|
}
|
|
|
|
formData.append('type', 'kk');
|
|
fetch('admin/api/ocr_helper.php', {
|
|
method: 'POST',
|
|
body: formData
|
|
})
|
|
.then(response => response.json())
|
|
.then(data => {
|
|
if(data.success) {
|
|
var d = data.data;
|
|
// Show Confirmation
|
|
Swal.fire({
|
|
title: 'Hasil Scan KK',
|
|
html: `
|
|
<div style="text-align: left; font-size: 0.9rem;">
|
|
<table class="table table-bordered table-sm">
|
|
<tr><td width="30%">No KK</td><td><b>${d.no_kk || '-'}</b></td></tr>
|
|
<tr><td>Kepala Kel</td><td><b>${d.kepala_keluarga || '-'}</b></td></tr>
|
|
<tr><td>Alamat</td><td>${d.desa || '-'}</td></tr>
|
|
<tr><td>RT/RW</td><td>${d.rt || '-'}/${d.rw || '-'}</td></tr>
|
|
<tr><td>Kecamatan</td><td>${d.kecamatan || '-'}</td></tr>
|
|
<tr><td>Kabupaten</td><td>${d.kabupaten || '-'}</td></tr>
|
|
<tr><td>Provinsi</td><td>${d.provinsi || '-'}</td></tr>
|
|
</table>
|
|
|
|
<hr>
|
|
<strong>Ditemukan ${d.anggota ? d.anggota.length : 0} Anggota:</strong>
|
|
<div style="max-height: 150px; overflow-y: auto; background: #f8f9fa; border: 1px solid #ddd; padding: 5px;">
|
|
${d.anggota && d.anggota.length > 0 ?
|
|
'<ul style="padding-left: 20px; margin-bottom: 0;">' +
|
|
d.anggota.map(m => `<li><b>${m.nama}</b><br><small>${m.nik} (${m.hubungan})</small></li>`).join('') +
|
|
'</ul>'
|
|
: '<i class="text-muted">Tidak ada anggota terdeteksi</i>'}
|
|
</div>
|
|
|
|
<p class="mb-0 text-muted mt-2">Gunakan data ini?</p>
|
|
</div>
|
|
`,
|
|
icon: 'question',
|
|
showCancelButton: true,
|
|
confirmButtonText: 'Ya, Gunakan',
|
|
cancelButtonText: 'Batal'
|
|
}).then((result) => {
|
|
if (result.value) {
|
|
if(d.no_kk) document.getElementsByName('no_kk')[0].value = d.no_kk;
|
|
if(d.anggota) document.getElementById('anggota_json').value = JSON.stringify(d.anggota);
|
|
if(d.kepala_keluarga) document.getElementsByName('kepala')[0].value = d.kepala_keluarga;
|
|
if(d.desa) document.getElementsByName('desa')[0].value = d.desa;
|
|
if(d.rt) document.getElementsByName('rt')[0].value = d.rt;
|
|
if(d.rw) document.getElementsByName('rw')[0].value = d.rw;
|
|
if(d.kecamatan) document.getElementsByName('kec')[0].value = d.kecamatan;
|
|
if(d.kabupaten) document.getElementsByName('kab')[0].value = d.kabupaten;
|
|
if(d.provinsi) document.getElementsByName('prov')[0].value = d.provinsi;
|
|
|
|
Swal.fire('Berhasil!', 'Data KK telah masuk ke form.', 'success');
|
|
}
|
|
});
|
|
} else {
|
|
Swal.fire('Gagal', data.message, 'error');
|
|
}
|
|
})
|
|
.catch(err => {
|
|
console.error(err);
|
|
Swal.fire('Error', 'Terjadi kesalahan: ' + err.message, 'error');
|
|
})
|
|
.finally(() => {
|
|
btn.innerHTML = originalText;
|
|
btn.disabled = false;
|
|
});
|
|
});
|
|
});
|
|
</script>
|
|
|
|
<?php
|
|
|
|
if (isset ($_POST['Simpan'])){
|
|
|
|
$target = 'foto/kk/';
|
|
$nama_file = @$_FILES['foto_kk']['name'];
|
|
|
|
// Cek Crop
|
|
if (!empty($_POST['foto_cropped'])) {
|
|
$data = $_POST['foto_cropped'];
|
|
$parts = explode(',', $data);
|
|
$data = $parts[1];
|
|
$data = base64_decode($data);
|
|
$nama_file = "KK-" . time() . ".jpg";
|
|
file_put_contents($target . $nama_file, $data);
|
|
} else {
|
|
$sumber = @$_FILES['foto_kk']['tmp_name'];
|
|
if(!empty($sumber)) {
|
|
move_uploaded_file($sumber, $target.$nama_file);
|
|
}
|
|
}
|
|
|
|
//mulai proses simpan data
|
|
$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'
|
|
}).then((result) => {if (result.value){
|
|
window.location = 'index.php?page=add-kartu';
|
|
}
|
|
})</script>";
|
|
return;
|
|
}
|
|
|
|
// Sanitize Inputs
|
|
$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',
|
|
'$kepala',
|
|
'$desa',
|
|
'$rt',
|
|
'$rw',
|
|
'$kec',
|
|
'$kab',
|
|
'$prov',
|
|
'$nama_file')";
|
|
$query_simpan = mysqli_query($koneksi, $sql_simpan);
|
|
|
|
// 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);
|
|
$anggota_list = json_decode($_POST['anggota_json'], true);
|
|
|
|
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
|
|
$sql_cek_pend = "SELECT id_pend FROM tb_pdd WHERE nik='$nik_mem'";
|
|
$q_cek_pend = mysqli_query($koneksi, $sql_cek_pend);
|
|
if ($row_pend = mysqli_fetch_assoc($q_cek_pend)) {
|
|
$id_pend_found = $row_pend['id_pend'];
|
|
|
|
// Insert into tb_anggota
|
|
$sql_add_ang = "INSERT INTO tb_anggota (id_kk, id_pend, hubungan) VALUES ('$id_kk_baru', '$id_pend_found', '$hub_mem')";
|
|
mysqli_query($koneksi, $sql_add_ang);
|
|
$linked_count++;
|
|
} else {
|
|
$failed_count++;
|
|
$failed_members[] = ['nik' => $nik_mem, 'nama' => $nama_mem, 'hubungan' => $hub_mem];
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
mysqli_close($koneksi);
|
|
|
|
if ($query_simpan) {
|
|
$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>
|
|
Swal.fire({title: 'Tambah Data Berhasil',html: 'Data KK disimpan.$msg_add',icon: 'success',confirmButtonText: 'OK'
|
|
}).then((result) => {if (result.value){
|
|
window.location = 'index.php?page=data-kartu';
|
|
}
|
|
})</script>";
|
|
}else{
|
|
echo "<script>
|
|
Swal.fire({title: 'Tambah Data Gagal',text: '',icon: 'error',confirmButtonText: 'OK'
|
|
}).then((result) => {if (result.value){
|
|
window.location = 'index.php?page=add-kartu';
|
|
}
|
|
})</script>";
|
|
}}
|
|
//selesai proses simpan data
|