558 lines
22 KiB
PHP
558 lines
22 KiB
PHP
<?php
|
|
// ============================================================
|
|
// api/houses/index.php — Households CRUD (Adapted for new schema)
|
|
// ============================================================
|
|
declare(strict_types=1);
|
|
require_once __DIR__ . '/../../config/bootstrap.php';
|
|
|
|
$method = $_SERVER['REQUEST_METHOD'];
|
|
$action = $_GET['action'] ?? 'list';
|
|
$id = isset($_GET['id']) ? (int)$_GET['id'] : null;
|
|
|
|
switch ("$method:$action") {
|
|
|
|
// ================================================================
|
|
// LIST — untuk map markers
|
|
// ================================================================
|
|
case 'GET:list':
|
|
case 'GET:': {
|
|
requireAuth();
|
|
$pdo = Database::get();
|
|
$where = ['h.is_active = 1'];
|
|
$params = [];
|
|
|
|
if (!empty($_GET['poverty_status'])) {
|
|
$allowed = ['terdata','rentan_miskin','miskin','sangat_miskin'];
|
|
if (in_array($_GET['poverty_status'], $allowed, true)) {
|
|
$where[] = 'h.poverty_status = ?';
|
|
$params[] = $_GET['poverty_status'];
|
|
}
|
|
}
|
|
if (!empty($_GET['aid_status'])) {
|
|
if (in_array($_GET['aid_status'], ['not_yet','received'], true)) {
|
|
$where[] = 'h.aid_status = ?';
|
|
$params[] = $_GET['aid_status'];
|
|
}
|
|
}
|
|
if (!empty($_GET['house_condition'])) {
|
|
if (in_array($_GET['house_condition'], ['layak','tidak_layak'], true)) {
|
|
$where[] = 'h.house_condition = ?';
|
|
$params[] = $_GET['house_condition'];
|
|
}
|
|
}
|
|
if (!empty($_GET['center_id'])) {
|
|
$where[] = 'h.managing_center_id = ?';
|
|
$params[] = (int)$_GET['center_id'];
|
|
}
|
|
if (!empty($_GET['q'])) {
|
|
$where[] = '(h.head_name LIKE ? OR h.full_address LIKE ? OR h.head_nik LIKE ?)';
|
|
$q = '%' . $_GET['q'] . '%';
|
|
$params[] = $q; $params[] = $q; $params[] = $q;
|
|
}
|
|
|
|
$whereSQL = implode(' AND ', $where);
|
|
$limit = min((int)($_GET['limit'] ?? PAGE_SIZE), PAGE_SIZE);
|
|
$offset = max(0, (int)($_GET['offset'] ?? 0));
|
|
|
|
$stmt = $pdo->prepare("
|
|
SELECT
|
|
h.*,
|
|
rc.name AS center_name,
|
|
(SELECT COUNT(*) FROM household_members hm WHERE hm.household_id = h.id) AS member_count,
|
|
(SELECT COUNT(*) FROM aid_history ah WHERE ah.household_id = h.id) AS aid_count
|
|
FROM households h
|
|
LEFT JOIN religious_centers rc ON rc.id = h.managing_center_id
|
|
WHERE $whereSQL
|
|
ORDER BY h.created_at DESC
|
|
LIMIT $limit OFFSET $offset
|
|
");
|
|
$stmt->execute($params);
|
|
$rows = $stmt->fetchAll();
|
|
|
|
$cntStmt = $pdo->prepare("SELECT COUNT(*) FROM households h WHERE $whereSQL");
|
|
$cntStmt->execute($params);
|
|
$total = (int)$cntStmt->fetchColumn();
|
|
|
|
foreach ($rows as &$r) {
|
|
castHousehold($r);
|
|
}
|
|
unset($r);
|
|
|
|
Response::success(['households' => $rows, 'total' => $total]);
|
|
break;
|
|
}
|
|
|
|
// ================================================================
|
|
// SHOW — detail with aid history and family members
|
|
// ================================================================
|
|
case 'GET:show': {
|
|
requireAuth();
|
|
if (!$id) Response::error('ID is required.', 400);
|
|
|
|
$pdo = Database::get();
|
|
$stmt = $pdo->prepare("
|
|
SELECT
|
|
h.*,
|
|
TIMESTAMPDIFF(YEAR, h.head_date_of_birth, CURDATE()) AS age,
|
|
rc.name AS center_name,
|
|
rc.worship_type AS center_type,
|
|
rc.latitude AS center_lat,
|
|
rc.longitude AS center_lng,
|
|
rc.radius AS center_radius
|
|
FROM households h
|
|
LEFT JOIN religious_centers rc ON rc.id = h.managing_center_id
|
|
WHERE h.id = ? AND h.is_active = 1
|
|
");
|
|
$stmt->execute([$id]);
|
|
$row = $stmt->fetch();
|
|
if (!$row) Response::notFound('Household not found.');
|
|
|
|
castHousehold($row);
|
|
|
|
// ⭐ FIX: Pastikan aid_history selalu diambil dengan lengkap
|
|
$aidStmt = $pdo->prepare("
|
|
SELECT
|
|
ah.*,
|
|
rc.name AS center_name,
|
|
DATE_FORMAT(ah.aid_date, '%Y-%m-%d') AS aid_date_formatted
|
|
FROM aid_history ah
|
|
LEFT JOIN religious_centers rc ON rc.id = ah.center_id
|
|
WHERE ah.household_id = ?
|
|
ORDER BY ah.aid_date DESC
|
|
LIMIT 20
|
|
");
|
|
$aidStmt->execute([$id]);
|
|
$aidHistory = $aidStmt->fetchAll();
|
|
|
|
// ⭐ Pastikan format data bantuan konsisten
|
|
foreach ($aidHistory as &$aid) {
|
|
$aid['aid_type_label'] = match($aid['aid_type'] ?? '') {
|
|
'sembako' => 'Sembako',
|
|
'pendanaan' => 'Pendanaan',
|
|
'pelatihan' => 'Pelatihan',
|
|
'sembako_pendanaan' => 'Sembako + Pendanaan',
|
|
'sembako_pelatihan' => 'Sembako + Pelatihan',
|
|
'pendanaan_pelatihan' => 'Pendanaan + Pelatihan',
|
|
'lengkap' => 'Lengkap (Semua)',
|
|
default => $aid['aid_type'] ?? 'Bantuan'
|
|
};
|
|
}
|
|
unset($aid);
|
|
|
|
$row['aid_history'] = $aidHistory;
|
|
|
|
// Family members
|
|
$depStmt = $pdo->prepare("
|
|
SELECT * FROM household_members
|
|
WHERE household_id = ?
|
|
ORDER BY id
|
|
");
|
|
$depStmt->execute([$id]);
|
|
$row['household_members'] = $depStmt->fetchAll();
|
|
|
|
Response::success($row);
|
|
break;
|
|
}
|
|
|
|
// ================================================================
|
|
// CREATE
|
|
// ================================================================
|
|
case 'POST:create': {
|
|
requireAuth();
|
|
$data = Validator::json();
|
|
|
|
$v = Validator::make($data, [
|
|
'head_name' => 'required|string|maxlen:150',
|
|
'head_nik' => 'required|string|maxlen:16',
|
|
'full_address' => 'required|string',
|
|
'latitude' => 'required|latitude',
|
|
'longitude' => 'required|longitude',
|
|
'house_condition' => 'in:layak,tidak_layak',
|
|
'head_education' => 'in:tidak_sekolah,sd,smp,sma,diploma,sarjana,pascasarjana',
|
|
'aid_status' => 'in:not_yet,received',
|
|
'head_gender' => 'in:male,female',
|
|
'head_date_of_birth' => 'date',
|
|
'land_ownership' => 'in:milik,sewa,numpang,lainnya',
|
|
]);
|
|
$v->validate_or_fail();
|
|
|
|
$pdo = Database::get();
|
|
|
|
$calc = PovertyCalculator::calculate(
|
|
(int)($data['head_monthly_income'] ?? $data['income'] ?? 0),
|
|
(int)($data['dependents'] ?? 1),
|
|
$data['house_condition'] ?? 'layak',
|
|
$data['head_education'] ?? 'sd',
|
|
$data['land_ownership'] ?? 'milik'
|
|
);
|
|
|
|
$managingId = resolveManagingCenter($pdo, (float)$data['latitude'], (float)$data['longitude']);
|
|
|
|
$stmt = $pdo->prepare("
|
|
INSERT INTO households (
|
|
rt, rw, kelurahan, kecamatan, full_address,
|
|
latitude, longitude, house_condition, managing_center_id,
|
|
head_name, head_nik, head_gender, head_date_of_birth, head_education,
|
|
head_employment_status, head_job_name, head_institution_name, head_monthly_income,
|
|
poverty_score, poverty_status, aid_status, notes
|
|
) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)
|
|
");
|
|
$stmt->execute([
|
|
$data['rt'] ?? null,
|
|
$data['rw'] ?? null,
|
|
$data['kelurahan'] ?? null,
|
|
$data['kecamatan'] ?? null,
|
|
$data['full_address'],
|
|
(float)$data['latitude'],
|
|
(float)$data['longitude'],
|
|
$data['house_condition'] ?? 'layak',
|
|
$managingId,
|
|
$data['head_name'],
|
|
$data['head_nik'],
|
|
$data['head_gender'] ?? 'male',
|
|
$data['head_date_of_birth'],
|
|
$data['head_education'] ?? 'sd',
|
|
$data['head_employment_status'] ?? 'unemployed',
|
|
$data['head_job_name'] ?? null,
|
|
$data['head_institution_name'] ?? null,
|
|
(int)($data['head_monthly_income'] ?? $data['income'] ?? 0),
|
|
$calc['score'] ?? 0,
|
|
$calc['status'],
|
|
$data['aid_status'] ?? 'not_yet',
|
|
$data['notes'] ?? $data['description'] ?? null,
|
|
]);
|
|
|
|
$newId = (int)$pdo->lastInsertId();
|
|
|
|
// Save family members
|
|
if (!empty($data['household_members']) && is_array($data['household_members'])) {
|
|
saveMembers($pdo, $newId, $data['household_members']);
|
|
}
|
|
|
|
AuditLog::record('Tambah Rumah', 'households', $newId, null, $data);
|
|
Response::created([
|
|
'id' => $newId,
|
|
'poverty_status' => $calc['status'],
|
|
'poverty_label' => $calc['label'],
|
|
'marker_color' => PovertyCalculator::markerColor($calc['status']),
|
|
'managing_center_id' => $managingId,
|
|
], 'Rumah berhasil ditambahkan.');
|
|
break;
|
|
}
|
|
|
|
// ================================================================
|
|
// UPDATE
|
|
// ================================================================
|
|
case 'POST:update': {
|
|
requireAuth();
|
|
if (!$id) Response::error('ID is required.', 400);
|
|
|
|
$data = Validator::json();
|
|
$v = Validator::make($data, [
|
|
'head_name' => 'required|string|maxlen:150',
|
|
'head_nik' => 'required|string|maxlen:16',
|
|
'full_address' => 'required|string',
|
|
'latitude' => 'required|latitude',
|
|
'longitude' => 'required|longitude',
|
|
'house_condition' => 'in:layak,tidak_layak',
|
|
'head_education' => 'in:tidak_sekolah,sd,smp,sma,diploma,sarjana,pascasarjana',
|
|
'aid_status' => 'in:not_yet,received',
|
|
'head_gender' => 'in:male,female',
|
|
'head_date_of_birth' => 'date',
|
|
'land_ownership' => 'in:milik,sewa,numpang,lainnya',
|
|
]);
|
|
$v->validate_or_fail();
|
|
|
|
$pdo = Database::get();
|
|
$old = $pdo->prepare("SELECT * FROM households WHERE id = ? AND is_active = 1");
|
|
$old->execute([$id]);
|
|
$oldRow = $old->fetch();
|
|
if (!$oldRow) Response::notFound('Household not found.');
|
|
|
|
$calc = PovertyCalculator::calculate(
|
|
(int)($data['head_monthly_income'] ?? $data['income'] ?? 0),
|
|
(int)($data['dependents'] ?? 1),
|
|
$data['house_condition'] ?? 'layak',
|
|
$data['head_education'] ?? 'sd',
|
|
$data['land_ownership'] ?? 'milik'
|
|
);
|
|
|
|
$managingId = resolveManagingCenter($pdo, (float)$data['latitude'], (float)$data['longitude']);
|
|
|
|
$pdo->prepare("
|
|
UPDATE households SET
|
|
rt = ?, rw = ?, kelurahan = ?, kecamatan = ?, full_address = ?,
|
|
latitude = ?, longitude = ?, house_condition = ?, managing_center_id = ?,
|
|
head_name = ?, head_nik = ?, head_gender = ?, head_date_of_birth = ?, head_education = ?,
|
|
head_employment_status = ?, head_job_name = ?, head_institution_name = ?, head_monthly_income = ?,
|
|
poverty_score = ?, poverty_status = ?, aid_status = ?, notes = ?
|
|
WHERE id = ? AND is_active = 1
|
|
")->execute([
|
|
$data['rt'] ?? $oldRow['rt'],
|
|
$data['rw'] ?? $oldRow['rw'],
|
|
$data['kelurahan'] ?? $oldRow['kelurahan'],
|
|
$data['kecamatan'] ?? $oldRow['kecamatan'],
|
|
$data['full_address'],
|
|
(float)$data['latitude'],
|
|
(float)$data['longitude'],
|
|
$data['house_condition'] ?? 'layak',
|
|
$managingId,
|
|
$data['head_name'],
|
|
$data['head_nik'],
|
|
$data['head_gender'] ?? 'male',
|
|
$data['head_date_of_birth'],
|
|
$data['head_education'] ?? 'sd',
|
|
$data['head_employment_status'] ?? 'unemployed',
|
|
$data['head_job_name'] ?? null,
|
|
$data['head_institution_name'] ?? null,
|
|
(int)($data['head_monthly_income'] ?? $data['income'] ?? 0),
|
|
$calc['score'] ?? 0,
|
|
$calc['status'],
|
|
$data['aid_status'] ?? 'not_yet',
|
|
$data['notes'] ?? $data['description'] ?? null,
|
|
$id,
|
|
]);
|
|
|
|
if (isset($data['household_members']) && is_array($data['household_members'])) {
|
|
saveMembers($pdo, $id, $data['household_members']);
|
|
}
|
|
|
|
AuditLog::record('Update Rumah', 'households', $id, $oldRow, $data);
|
|
Response::success([
|
|
'id' => $id,
|
|
'poverty_status' => $calc['status'],
|
|
'poverty_label' => $calc['label'],
|
|
'marker_color' => PovertyCalculator::markerColor($calc['status']),
|
|
'managing_center_id' => $managingId,
|
|
], 'Data rumah diperbarui.');
|
|
break;
|
|
}
|
|
|
|
// ================================================================
|
|
// PATCH — untuk drag marker
|
|
// ================================================================
|
|
case 'POST:patch': {
|
|
requireAuth();
|
|
if (!$id) Response::error('ID is required.', 400);
|
|
|
|
$data = Validator::json();
|
|
|
|
// Validasi: minimal latitude dan longitude harus ada
|
|
if (!isset($data['latitude']) || !isset($data['longitude'])) {
|
|
Response::error('Latitude dan longitude diperlukan.', 400);
|
|
}
|
|
|
|
Validator::make($data, [
|
|
'latitude' => 'required|latitude',
|
|
'longitude' => 'required|longitude',
|
|
])->validate_or_fail();
|
|
|
|
$pdo = Database::get();
|
|
|
|
// Cek apakah household ada
|
|
$check = $pdo->prepare("SELECT id, managing_center_id FROM households WHERE id = ? AND is_active = 1");
|
|
$check->execute([$id]);
|
|
$existing = $check->fetch();
|
|
if (!$existing) Response::notFound('Household tidak ditemukan.');
|
|
|
|
// Hitung ulang managing_center_id berdasarkan posisi baru
|
|
$managingId = resolveManagingCenter($pdo, (float)$data['latitude'], (float)$data['longitude']);
|
|
|
|
// Update query
|
|
$sql = "UPDATE households SET latitude = ?, longitude = ?, managing_center_id = ?";
|
|
$params = [(float)$data['latitude'], (float)$data['longitude'], $managingId];
|
|
|
|
if (!empty($data['full_address'])) {
|
|
$sql .= ", full_address = ?";
|
|
$params[] = Validator::sanitizeString($data['full_address']);
|
|
}
|
|
|
|
$sql .= " WHERE id = ? AND is_active = 1";
|
|
$params[] = $id;
|
|
|
|
$pdo->prepare($sql)->execute($params);
|
|
|
|
// Ambil data center untuk response
|
|
$centerName = null;
|
|
if ($managingId) {
|
|
$cStmt = $pdo->prepare("SELECT name FROM religious_centers WHERE id = ?");
|
|
$cStmt->execute([$managingId]);
|
|
$center = $cStmt->fetch();
|
|
$centerName = $center ? $center['name'] : null;
|
|
}
|
|
|
|
AuditLog::record('Pindah Posisi Rumah', 'households', $id, null, $data);
|
|
Response::success([
|
|
'managing_center_id' => $managingId,
|
|
'center_name' => $centerName
|
|
], 'Posisi rumah berhasil diperbarui.');
|
|
break;
|
|
}
|
|
|
|
// ================================================================
|
|
// DELETE (soft delete)
|
|
// ================================================================
|
|
case 'POST:delete': {
|
|
requireAdmin();
|
|
if (!$id) Response::error('ID is required.', 400);
|
|
|
|
$pdo = Database::get();
|
|
$old = $pdo->prepare("SELECT * FROM households WHERE id = ? AND is_active = 1");
|
|
$old->execute([$id]);
|
|
$oldRow = $old->fetch();
|
|
if (!$oldRow) Response::notFound('Household not found.');
|
|
|
|
$pdo->prepare("UPDATE households SET is_active = 0 WHERE id = ?")->execute([$id]);
|
|
AuditLog::record('Hapus Rumah', 'households', $id, $oldRow);
|
|
Response::success(null, 'Data rumah dihapus.');
|
|
break;
|
|
}
|
|
|
|
// ================================================================
|
|
// DELETE PHOTO — hapus foto rumah tangga satu per satu
|
|
// ================================================================
|
|
case 'POST:delete_photo': {
|
|
requireAuth();
|
|
if (!$id) Response::error('ID is required.', 400);
|
|
|
|
$data = Validator::json();
|
|
$filename = trim($data['filename'] ?? '');
|
|
|
|
if ($filename === '' || preg_match('/[\/\\\\]/', $filename) ||
|
|
!preg_match('/^[a-zA-Z0-9_\-\.]+$/', $filename)) {
|
|
Response::error('Nama file tidak valid.', 400);
|
|
}
|
|
|
|
$ext = strtolower(pathinfo($filename, PATHINFO_EXTENSION));
|
|
if (!in_array($ext, ['jpg', 'jpeg', 'png'], true)) {
|
|
Response::error('Ekstensi file tidak diizinkan.', 400);
|
|
}
|
|
|
|
$pdo = Database::get();
|
|
|
|
$stmt = $pdo->prepare("SELECT house_photos FROM households WHERE id = ? AND is_active = 1");
|
|
$stmt->execute([$id]);
|
|
$row = $stmt->fetch(PDO::FETCH_ASSOC);
|
|
|
|
if (!$row) {
|
|
Response::notFound('Data rumah tangga tidak ditemukan.');
|
|
}
|
|
|
|
$photos = json_decode($row['house_photos'] ?? '[]', true) ?: [];
|
|
|
|
if (!in_array($filename, $photos, true)) {
|
|
Response::error('File tidak ditemukan dalam data ini.', 404);
|
|
}
|
|
|
|
$photos = array_values(array_filter($photos, fn($p) => $p !== $filename));
|
|
|
|
$upd = $pdo->prepare("UPDATE households SET house_photos = ? WHERE id = ?");
|
|
$upd->execute([json_encode($photos, JSON_UNESCAPED_UNICODE), $id]);
|
|
|
|
$dir = __DIR__ . '/../../uploads/houses/';
|
|
$fullPath = realpath($dir . $filename);
|
|
$realDir = realpath($dir);
|
|
if ($fullPath && $realDir && str_starts_with($fullPath, $realDir)) {
|
|
@unlink($fullPath);
|
|
}
|
|
|
|
AuditLog::record('Hapus Foto Rumah', 'households', $id, null, ['filename' => $filename]);
|
|
Response::success(['remaining' => $photos], 'Foto berhasil dihapus.');
|
|
break;
|
|
}
|
|
|
|
default:
|
|
Response::methodNotAllowed();
|
|
}
|
|
|
|
// ================================================================
|
|
// HELPERS
|
|
// ================================================================
|
|
|
|
function castHousehold(array &$r): void
|
|
{
|
|
$r['latitude'] = (float)$r['latitude'];
|
|
$r['longitude'] = (float)$r['longitude'];
|
|
$r['age'] = isset($r['age']) ? (int)$r['age'] : null;
|
|
$r['is_active'] = (bool)$r['is_active'];
|
|
$r['marker_color'] = PovertyCalculator::markerColor($r['poverty_status'] ?? '');
|
|
$r['poverty_label'] = PovertyCalculator::label($r['poverty_status'] ?? '');
|
|
|
|
// Backward compatibility untuk frontend lama
|
|
$r['address'] = $r['full_address'] ?? '';
|
|
$r['nik'] = $r['head_nik'] ?? '';
|
|
$r['head_name'] = $r['head_name'] ?? '';
|
|
$r['head_nik'] = $r['head_nik'] ?? '';
|
|
$r['head_education'] = $r['head_education'] ?? 'sd';
|
|
$r['head_employment_status'] = $r['head_employment_status'] ?? 'unemployed';
|
|
$r['head_job_name'] = $r['head_job_name'] ?? '';
|
|
$r['head_monthly_income'] = (int)($r['head_monthly_income'] ?? 0);
|
|
$r['house_condition'] = $r['house_condition'] ?? 'layak';
|
|
$r['aid_status'] = $r['aid_status'] ?? 'not_yet';
|
|
$r['notes'] = $r['notes'] ?? '';
|
|
$r['dependents'] = (int)($r['family_members_count'] ?? 0) + 1;
|
|
$r['income'] = $r['head_monthly_income'] ?? 0;
|
|
$r['job'] = $r['head_job_name'] ?? '';
|
|
}
|
|
|
|
function resolveManagingCenter(\PDO $pdo, float $lat, float $lng): ?int
|
|
{
|
|
$stmt = $pdo->prepare("
|
|
SELECT sub.id FROM (
|
|
SELECT
|
|
rc.id,
|
|
rc.radius,
|
|
(6371000 * ACOS(
|
|
COS(RADIANS(:lat1)) * COS(RADIANS(rc.latitude)) *
|
|
COS(RADIANS(rc.longitude) - RADIANS(:lng)) +
|
|
SIN(RADIANS(:lat2)) * SIN(RADIANS(rc.latitude))
|
|
)) AS distance_m,
|
|
(SELECT COUNT(*) FROM households hh
|
|
WHERE hh.managing_center_id = rc.id AND hh.is_active = 1) AS load_count
|
|
FROM religious_centers rc
|
|
WHERE rc.is_active = 1
|
|
) sub
|
|
WHERE sub.distance_m <= sub.radius
|
|
ORDER BY sub.load_count ASC, sub.distance_m ASC
|
|
LIMIT 1
|
|
");
|
|
$stmt->execute([':lat1' => $lat, ':lng' => $lng, ':lat2' => $lat]);
|
|
$row = $stmt->fetch();
|
|
return $row ? (int)$row['id'] : null;
|
|
}
|
|
|
|
function saveMembers(\PDO $pdo, int $householdId, array $data): void
|
|
{
|
|
try {
|
|
// Delete existing members
|
|
$pdo->prepare("DELETE FROM household_members WHERE household_id = ?")->execute([$householdId]);
|
|
|
|
$stmt = $pdo->prepare("
|
|
INSERT INTO household_members
|
|
(household_id, name, nik, gender, date_of_birth, education,
|
|
relationship, employment_status, job_name, institution_name, monthly_income)
|
|
VALUES (?,?,?,?,?,?,?,?,?,?,?)
|
|
");
|
|
|
|
foreach ($data as $member) {
|
|
$name = trim($member['name'] ?? '');
|
|
if (!$name) continue;
|
|
|
|
$stmt->execute([
|
|
$householdId,
|
|
$name,
|
|
$member['nik'] ?? null,
|
|
$member['gender'] ?? 'male',
|
|
$member['date_of_birth'] ?? null,
|
|
$member['education'] ?? 'sd',
|
|
$member['relationship'] ?? 'lainnya',
|
|
$member['employment_status'] ?? 'unemployed',
|
|
$member['job_name'] ?? null,
|
|
$member['institution_name'] ?? null,
|
|
(int)($member['monthly_income'] ?? 0),
|
|
]);
|
|
}
|
|
} catch (\Throwable $e) {
|
|
error_log('Save members error: ' . $e->getMessage());
|
|
}
|
|
} |