Sindbad~EG File Manager
<?php
require_once '../../config/config.php';
checkLogin();
checkAccess('assembly');
$db = Database::getInstance()->getConnection();
$pageTitle = 'Bulk Excel Edit - ' . APP_NAME;
// AJAX: Fetch members data
if (isset($_GET['action']) && $_GET['action'] === 'fetch_members' && isset($_GET['ids'])) {
header('Content-Type: application/json');
$memberIds = explode(',', $_GET['ids']);
$memberIds = array_filter($memberIds, 'is_numeric');
if (empty($memberIds)) {
echo json_encode(['success' => false, 'message' => 'No valid member IDs']);
exit;
}
$placeholders = str_repeat('?,', count($memberIds) - 1) . '?';
$query = "SELECT m.*, a.area_name, d.district_name, asm.assembly_name
FROM members m
LEFT JOIN areas a ON m.area_id = a.id
LEFT JOIN districts d ON m.district_id = d.id
LEFT JOIN assemblies asm ON m.assembly_id = asm.id
WHERE m.id IN ($placeholders)
ORDER BY m.id";
$stmt = $db->prepare($query);
$stmt->execute($memberIds);
$members = $stmt->fetchAll(PDO::FETCH_ASSOC);
// Prepare data for spreadsheet
$data = [];
foreach ($members as $member) {
$data[] = [
$member['id'],
$member['title'] ?? '',
$member['first_name'] ?? '',
$member['middle_name'] ?? '',
$member['last_name'] ?? '',
$member['gender'] ?? '',
$member['date_of_birth'] ?? '',
$member['place_of_birth'] ?? '',
$member['marital_status'] ?? '',
$member['member_type'] ?? '',
$member['phone'] ?? '',
$member['email'] ?? '',
$member['address_line'] ?? '',
$member['gps_address'] ?? '',
$member['street'] ?? '',
$member['city'] ?? '',
$member['hometown'] ?? '',
$member['area_name'] ?? '',
$member['district_name'] ?? '',
$member['assembly_name'] ?? '',
$member['family_id'] ?? '',
$member['water_baptism'] ? 'Yes' : 'No',
$member['water_baptism_date'] ?? '',
$member['water_baptism_minister'] ?? '',
$member['holyghost_baptism'] ? 'Yes' : 'No',
$member['holyghost_baptism_date'] ?? '',
$member['holyghost_baptism_minister'] ?? '',
$member['communicant'] ? 'Yes' : 'No',
$member['communicant_date'] ?? '',
$member['communicant_minister'] ?? '',
$member['dedicated'] ? 'Yes' : 'No',
$member['dedicated_date'] ?? '',
$member['dedicated_minister'] ?? '',
$member['occupation'] ?? '',
$member['education_level'] ?? '',
$member['parent_name'] ?? '',
$member['parent_relationship'] ?? ''
];
}
echo json_encode(['success' => true, 'data' => $data]);
exit;
}
// AJAX: Save changes
if (isset($_POST['action']) && $_POST['action'] === 'save_changes') {
header('Content-Type: application/json');
$data = json_decode(file_get_contents('php://input'), true);
if (!isset($data['changes']) || empty($data['changes'])) {
echo json_encode(['success' => false, 'message' => 'No changes to save']);
exit;
}
try {
$db->beginTransaction();
$updatedCount = 0;
$errors = [];
foreach ($data['changes'] as $row) {
try {
$memberId = $row[0]; // ID is always first column
if (empty($memberId)) {
continue;
}
// Prepare update
$sql = "UPDATE members SET
title = ?,
first_name = ?,
middle_name = ?,
last_name = ?,
gender = ?,
date_of_birth = ?,
place_of_birth = ?,
marital_status = ?,
member_type = ?,
phone = ?,
email = ?,
address_line = ?,
gps_address = ?,
street = ?,
city = ?,
hometown = ?,
family_id = ?,
water_baptism = ?,
water_baptism_date = ?,
water_baptism_minister = ?,
holyghost_baptism = ?,
holyghost_baptism_date = ?,
holyghost_baptism_minister = ?,
communicant = ?,
communicant_date = ?,
communicant_minister = ?,
dedicated = ?,
dedicated_date = ?,
dedicated_minister = ?,
occupation = ?,
education_level = ?,
parent_name = ?,
parent_relationship = ?,
updated_at = NOW()
WHERE id = ?";
$updateStmt = $db->prepare($sql);
$params = [
$row[1], // title
$row[2], // first_name
$row[3], // middle_name
$row[4], // last_name
$row[5], // gender
$row[6] ?: null, // date_of_birth
$row[7], // place_of_birth
$row[8], // marital_status
$row[9], // member_type
$row[10], // phone
$row[11], // email
$row[12], // address_line
$row[13], // gps_address
$row[14], // street
$row[15], // city
$row[16], // hometown
$row[20], // family_id
in_array(strtolower($row[21]), ['yes', '1']) ? 1 : 0, // water_baptism
$row[22] ?: null, // water_baptism_date
$row[23], // water_baptism_minister
in_array(strtolower($row[24]), ['yes', '1']) ? 1 : 0, // holyghost_baptism
$row[25] ?: null, // holyghost_baptism_date
$row[26], // holyghost_baptism_minister
in_array(strtolower($row[27]), ['yes', '1']) ? 1 : 0, // communicant
$row[28] ?: null, // communicant_date
$row[29], // communicant_minister
in_array(strtolower($row[30]), ['yes', '1']) ? 1 : 0, // dedicated
$row[31] ?: null, // dedicated_date
$row[32], // dedicated_minister
$row[33], // occupation
$row[34], // education_level
$row[35], // parent_name
$row[36], // parent_relationship
$memberId
];
if ($updateStmt->execute($params)) {
$updatedCount++;
} else {
$errors[] = "Failed to update member ID $memberId";
}
} catch (Exception $e) {
$errors[] = "Member ID " . ($row[0] ?? 'unknown') . ": " . $e->getMessage();
}
}
$db->commit();
echo json_encode([
'success' => true,
'message' => "Successfully updated $updatedCount member(s)",
'updated' => $updatedCount,
'errors' => $errors
]);
} catch (Exception $e) {
$db->rollBack();
echo json_encode(['success' => false, 'message' => 'Error: ' . $e->getMessage()]);
}
exit;
}
// If no POST data, redirect back
if (!isset($_POST['member_ids']) || empty($_POST['member_ids'])) {
$_SESSION['error'] = 'No members selected.';
header('Location: members_edit.php');
exit;
}
$memberIds = array_filter($_POST['member_ids'], 'is_numeric');
$memberCount = count($memberIds);
$memberIdsString = implode(',', $memberIds);
include '../../includes/header.php';
include '../../includes/sidebar.php';
?>
<!-- jSpreadsheet CSS -->
<link rel="stylesheet" href="https://bossanova.uk/jspreadsheet/v4/jexcel.css" type="text/css" />
<link rel="stylesheet" href="https://jsuites.net/v4/jsuites.css" type="text/css" />
<style>
#spreadsheet {
border: 2px solid #e5e7eb;
border-radius: 0.5rem;
overflow: hidden;
}
.jexcel_content {
max-height: calc(100vh - 400px);
overflow: auto;
}
.jexcel thead td {
background: linear-gradient(135deg, #1E40AF 0%, #3B82F6 100%) !important;
color: white !important;
font-weight: 600 !important;
text-align: center !important;
padding: 12px 8px !important;
font-size: 0.875rem !important;
}
.jexcel tbody tr:nth-child(even) {
background-color: #f9fafb;
}
.jexcel tbody tr:hover {
background-color: #eff6ff;
}
.jexcel tbody td {
padding: 8px !important;
border: 1px solid #e5e7eb !important;
}
.jexcel tbody td:first-child {
background-color: #f3f4f6 !important;
font-weight: 600;
color: #1f2937;
}
.readonly-column {
background-color: #fef3c7 !important;
cursor: not-allowed;
}
</style>
<main class="flex-1 md:ml-64 mt-16">
<div class="container mx-auto px-4 py-8">
<!-- Page Header -->
<div class="mb-8">
<h1 class="text-3xl font-bold text-gray-800">
<i class="fas fa-table text-blue-500 mr-3"></i>Bulk Excel Edit
</h1>
<p class="text-gray-600 mt-2">Edit <?php echo $memberCount; ?> member(s) in real-time using Excel-like interface</p>
</div>
<!-- Instructions Card -->
<div class="bg-blue-50 border-l-4 border-blue-500 p-6 mb-8 rounded-lg">
<h3 class="text-lg font-semibold text-blue-800 mb-3">
<i class="fas fa-info-circle mr-2"></i>How to Use
</h3>
<ul class="list-disc list-inside space-y-2 text-blue-900">
<li><strong>Click any cell</strong> to start editing (except ID, Area, District, Assembly columns)</li>
<li><strong>Use keyboard shortcuts:</strong> Tab to move right, Enter to move down, Arrow keys to navigate</li>
<li><strong>Copy/Paste</strong> works just like Excel - select cells and use Ctrl+C / Ctrl+V</li>
<li><strong>Click "Save All Changes"</strong> button when done to update the database</li>
</ul>
<div class="mt-4 p-3 bg-yellow-50 border-l-4 border-yellow-400 rounded">
<p class="text-yellow-800 text-sm">
<i class="fas fa-exclamation-triangle mr-2"></i>
<strong>Read-Only Columns:</strong> ID, Area, District, and Assembly columns cannot be edited (shown in yellow).
</p>
</div>
</div>
<!-- Action Buttons -->
<div class="flex justify-between items-center mb-6">
<div class="flex gap-3">
<button onclick="saveChanges()" class="bg-green-600 text-white px-6 py-3 rounded-lg font-semibold hover:bg-green-700 transition shadow-lg hover:shadow-xl">
<i class="fas fa-save mr-2"></i>Save All Changes
</button>
<button onclick="resetData()" class="bg-yellow-600 text-white px-6 py-3 rounded-lg font-semibold hover:bg-yellow-700 transition shadow-lg">
<i class="fas fa-undo mr-2"></i>Reset to Original
</button>
</div>
<a href="members_edit.php" class="bg-gray-500 text-white px-6 py-3 rounded-lg font-semibold hover:bg-gray-600 transition shadow-lg">
<i class="fas fa-times mr-2"></i>Cancel
</a>
</div>
<!-- Loading Indicator -->
<div id="loading" class="text-center py-12">
<i class="fas fa-spinner fa-spin text-4xl text-blue-500 mb-4"></i>
<p class="text-gray-600">Loading member data...</p>
</div>
<!-- Spreadsheet Container -->
<div id="spreadsheet-container" style="display:none;" class="bg-white rounded-lg shadow-lg p-6">
<div id="spreadsheet"></div>
<div class="mt-6 flex justify-between items-center">
<div class="text-sm text-gray-600">
<i class="fas fa-lightbulb text-yellow-500 mr-2"></i>
<strong>Tip:</strong> You can select multiple cells and drag to fill, just like Excel!
</div>
<div class="text-sm text-gray-600">
Editing <strong><?php echo $memberCount; ?></strong> member(s)
</div>
</div>
</div>
</div>
</main>
<!-- jSpreadsheet JS -->
<script src="https://bossanova.uk/jspreadsheet/v4/jexcel.js"></script>
<script src="https://jsuites.net/v4/jsuites.js"></script>
<script>
let spreadsheet = null;
let originalData = [];
// Column headers
const columns = [
{ title: 'ID', width: 80, type: 'text', readOnly: true },
{ title: 'Title', width: 100, type: 'dropdown', source: ['Mr.', 'Mrs.', 'Miss', 'Ms.', 'Dr.', 'Rev.', 'Prof.', 'Pastor', 'Elder', 'Deacon', 'Deaconess', 'Evangelist'] },
{ title: 'First Name', width: 150, type: 'text' },
{ title: 'Middle Name', width: 150, type: 'text' },
{ title: 'Last Name', width: 150, type: 'text' },
{ title: 'Gender', width: 100, type: 'dropdown', source: ['Male', 'Female'] },
{ title: 'Date of Birth', width: 120, type: 'calendar', options: { format: 'YYYY-MM-DD' } },
{ title: 'Place of Birth', width: 150, type: 'text' },
{ title: 'Marital Status', width: 120, type: 'dropdown', source: ['Single', 'Married', 'Divorced', 'Widowed'] },
{ title: 'Member Type', width: 120, type: 'dropdown', source: ['Regular', 'Youth', 'Children', 'Senior', 'Missionary'] },
{ title: 'Phone', width: 130, type: 'text' },
{ title: 'Email', width: 200, type: 'text' },
{ title: 'Address Line', width: 200, type: 'text' },
{ title: 'GPS Address', width: 150, type: 'text' },
{ title: 'Street', width: 150, type: 'text' },
{ title: 'City', width: 120, type: 'text' },
{ title: 'Hometown', width: 120, type: 'text' },
{ title: 'Area', width: 120, type: 'text', readOnly: true },
{ title: 'District', width: 120, type: 'text', readOnly: true },
{ title: 'Assembly', width: 150, type: 'text', readOnly: true },
{ title: 'Family ID', width: 100, type: 'text' },
{ title: 'Water Baptism', width: 120, type: 'dropdown', source: ['Yes', 'No'] },
{ title: 'Water Baptism Date', width: 140, type: 'calendar', options: { format: 'YYYY-MM-DD' } },
{ title: 'Water Baptism Minister', width: 180, type: 'text' },
{ title: 'Holy Ghost Baptism', width: 140, type: 'dropdown', source: ['Yes', 'No'] },
{ title: 'Holy Ghost Date', width: 140, type: 'calendar', options: { format: 'YYYY-MM-DD' } },
{ title: 'Holy Ghost Minister', width: 180, type: 'text' },
{ title: 'Communicant', width: 120, type: 'dropdown', source: ['Yes', 'No'] },
{ title: 'Communicant Date', width: 140, type: 'calendar', options: { format: 'YYYY-MM-DD' } },
{ title: 'Communicant Minister', width: 180, type: 'text' },
{ title: 'Dedicated', width: 120, type: 'dropdown', source: ['Yes', 'No'] },
{ title: 'Dedicated Date', width: 140, type: 'calendar', options: { format: 'YYYY-MM-DD' } },
{ title: 'Dedicated Minister', width: 180, type: 'text' },
{ title: 'Occupation', width: 150, type: 'text' },
{ title: 'Education Level', width: 130, type: 'dropdown', source: ['None', 'Primary', 'JHS', 'SHS', 'Tertiary', 'Postgraduate'] },
{ title: 'Parent Name', width: 150, type: 'text' },
{ title: 'Parent Relationship', width: 140, type: 'dropdown', source: ['Father', 'Mother', 'Guardian', 'Other'] }
];
// Load member data
async function loadMembers() {
try {
const response = await fetch('bulk_csv_edit.php?action=fetch_members&ids=<?php echo $memberIdsString; ?>');
const result = await response.json();
if (result.success) {
originalData = JSON.parse(JSON.stringify(result.data)); // Deep copy
initializeSpreadsheet(result.data);
document.getElementById('loading').style.display = 'none';
document.getElementById('spreadsheet-container').style.display = 'block';
} else {
alert('Error loading members: ' + result.message);
}
} catch (error) {
alert('Error: ' + error.message);
}
}
// Initialize spreadsheet
function initializeSpreadsheet(data) {
spreadsheet = jspreadsheet(document.getElementById('spreadsheet'), {
data: data,
columns: columns,
minDimensions: [37, data.length],
tableOverflow: true,
tableWidth: '100%',
freezeColumns: 1,
contextMenu: function() {
return false; // Disable context menu
},
updateTable: function(instance, cell, col, row, val, label, cellName) {
// Highlight readonly columns
if (col === 0 || col === 17 || col === 18 || col === 19) {
cell.classList.add('readonly-column');
}
}
});
}
// Save changes
async function saveChanges() {
if (!spreadsheet) {
alert('No data to save');
return;
}
if (!confirm('Are you sure you want to save all changes? This will update <?php echo $memberCount; ?> member(s) in the database.')) {
return;
}
const currentData = spreadsheet.getData();
// Show loading
const saveBtn = event.target;
const originalText = saveBtn.innerHTML;
saveBtn.innerHTML = '<i class="fas fa-spinner fa-spin mr-2"></i>Saving...';
saveBtn.disabled = true;
try {
const response = await fetch('bulk_csv_edit.php?action=save_changes', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
action: 'save_changes',
changes: currentData
})
});
const result = await response.json();
if (result.success) {
alert('✓ ' + result.message + (result.errors.length > 0 ? '\n\nWarnings:\n' + result.errors.join('\n') : ''));
// Update original data
originalData = JSON.parse(JSON.stringify(currentData));
// Option to return
if (confirm('Changes saved successfully! Do you want to return to the members list?')) {
window.location.href = 'members_edit.php';
}
} else {
alert('Error: ' + result.message);
}
} catch (error) {
alert('Error saving changes: ' + error.message);
} finally {
saveBtn.innerHTML = originalText;
saveBtn.disabled = false;
}
}
// Reset to original data
function resetData() {
if (!confirm('Are you sure you want to reset all changes? This will discard all edits.')) {
return;
}
spreadsheet.setData(JSON.parse(JSON.stringify(originalData)));
alert('Data reset to original values');
}
// Load on page ready
document.addEventListener('DOMContentLoaded', function() {
loadMembers();
});
</script>
<?php include '../../includes/footer.php'; ?>
Sindbad File Manager Version 1.0, Coded By Sindbad EG ~ The Terrorists