Autosave: 20260215-213226

This commit is contained in:
Flatlogic Bot 2026-02-15 21:32:26 +00:00
parent b710d6acf5
commit 516273c8b4
6 changed files with 506 additions and 75 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 83 KiB

View File

@ -30,121 +30,273 @@ if ($check->fetchColumn() > 0) {
$positions = $pdo->prepare("SELECT * FROM positions WHERE election_id = ? ORDER BY sort_order ASC");
$positions->execute([$id]);
$positions = $positions->fetchAll();
$endTime = strtotime($election['end_date_and_time']) * 1000;
?>
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<title>Vote: <?= htmlspecialchars($election['title']) ?></title>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">
<link rel="stylesheet" href="assets/css/dashboard.css?v=<?= time() ?>">
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Ballot: <?= htmlspecialchars($election['title']) ?></title>
<link href="https://fonts.googleapis.com/css2?family=Plus+Jakarta+Sans:wght@400;500;600;700;800&display=swap" rel="stylesheet">
<script src="https://unpkg.com/lucide@latest"></script>
<style>
body { background: #f8fafc; color: #1e293b; font-family: 'Inter', sans-serif; }
.ballot-container { max-width: 800px; margin: 40px auto; padding: 0 20px; }
.ballot-header { text-align: center; margin-bottom: 48px; }
.ballot-header h1 { font-size: 2.5rem; font-weight: 800; color: #1e293b; margin-bottom: 12px; letter-spacing: -0.025em; }
.ballot-header p { color: #64748b; font-size: 1.125rem; }
.position-group { margin-bottom: 40px; background: white; border-radius: 24px; border: 1px solid #e2e8f0; padding: 32px; box-shadow: 0 1px 3px 0 rgb(0 0 0 / 0.1); }
.position-title { font-size: 1.25rem; font-weight: 700; color: #1e293b; margin-bottom: 24px; display: flex; align-items: center; gap: 12px; }
.position-title i { color: #4f46e5; }
.candidates-grid { display: grid; grid-template-columns: 1fr; gap: 12px; }
.candidate-label { cursor: pointer; display: block; }
:root {
--primary: #6366f1;
--primary-hover: #4f46e5;
--bg: #f8fafc;
--text: #0f172a;
--text-muted: #64748b;
--glass: rgba(255, 255, 255, 0.7);
--glass-border: rgba(255, 255, 255, 0.5);
}
body {
background: linear-gradient(135deg, #f8fafc 0%, #e2e8f0 100%);
color: var(--text);
font-family: 'Plus Jakarta Sans', sans-serif;
margin: 0;
min-height: 100vh;
}
.ballot-container {
max-width: 800px;
margin: 0 auto;
padding: 40px 20px 100px;
}
.timer-banner {
background: var(--text);
color: white;
padding: 12px 24px;
border-radius: 100px;
display: inline-flex;
align-items: center;
gap: 12px;
font-weight: 700;
font-size: 0.875rem;
margin-bottom: 32px;
box-shadow: 0 10px 25px -5px rgba(15, 23, 42, 0.2);
}
#countdown {
color: #fbbf24;
font-variant-numeric: tabular-nums;
}
.ballot-header {
margin-bottom: 64px;
}
.ballot-header h1 {
font-size: 3rem;
font-weight: 800;
color: var(--text);
margin: 0 0 16px 0;
letter-spacing: -0.05em;
line-height: 1;
}
.ballot-header p {
color: var(--text-muted);
font-size: 1.25rem;
max-width: 500px;
margin: 0;
line-height: 1.6;
}
.position-group {
margin-bottom: 48px;
background: var(--glass);
backdrop-filter: blur(12px);
-webkit-backdrop-filter: blur(12px);
border-radius: 32px;
border: 1px solid var(--glass-border);
padding: 40px;
box-shadow: 0 20px 40px -15px rgba(0,0,0,0.05);
}
.position-title {
font-size: 1.5rem;
font-weight: 800;
color: var(--text);
margin-bottom: 32px;
display: flex;
align-items: center;
gap: 16px;
}
.position-title i {
padding: 10px;
background: white;
border-radius: 14px;
color: var(--primary);
box-shadow: 0 4px 6px -1px rgba(0,0,0,0.05);
}
.candidates-grid {
display: grid;
grid-template-columns: 1fr;
gap: 16px;
}
.candidate-label {
cursor: pointer;
display: block;
}
.candidate-card {
border: 2px solid #e2e8f0;
border-radius: 16px;
padding: 20px;
transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
border: 2px solid transparent;
border-radius: 24px;
padding: 24px;
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
display: flex;
align-items: center;
gap: 16px;
gap: 20px;
background: white;
position: relative;
}
.candidate-card:hover { border-color: #cbd5e1; background: #f8fafc; }
.candidate-card:hover {
transform: translateY(-2px);
box-shadow: 0 12px 20px -8px rgba(0,0,0,0.1);
}
input[type="radio"]:checked + .candidate-card,
input[type="checkbox"]:checked + .candidate-card {
border-color: #4f46e5;
border-color: var(--primary);
background: #f5f3ff;
box-shadow: 0 0 0 1px #4f46e5;
}
input[type="radio"]:checked + .candidate-card .check-icon,
input[type="checkbox"]:checked + .candidate-card .check-icon {
background: #4f46e5;
background: var(--primary);
color: white;
border-color: var(--primary);
}
input[type="radio"]:checked + .candidate-card .avatar-placeholder {
background: var(--primary);
color: white;
border-color: #4f46e5;
}
input[type="radio"], input[type="checkbox"] { display: none; }
.avatar-placeholder {
width: 56px;
height: 56px;
width: 64px;
height: 64px;
background: #f1f5f9;
border-radius: 14px;
border-radius: 20px;
display: flex;
align-items: center;
justify-content: center;
font-weight: 700;
color: #4f46e5;
font-size: 1.25rem;
border: 1px solid #e2e8f0;
font-weight: 800;
color: var(--primary);
font-size: 1.5rem;
transition: all 0.3s;
}
.candidate-info h3 { margin: 0; font-size: 1.125rem; font-weight: 700; color: #1e293b; }
.candidate-info p { margin: 4px 0 0 0; font-size: 0.875rem; color: #64748b; font-weight: 500; }
.candidate-info h3 {
margin: 0;
font-size: 1.25rem;
font-weight: 800;
color: var(--text);
}
.candidate-info p {
margin: 4px 0 0 0;
font-size: 1rem;
color: var(--text-muted);
font-weight: 600;
}
.check-icon {
margin-left: auto;
width: 24px;
height: 24px;
width: 28px;
height: 28px;
border: 2px solid #e2e8f0;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
color: transparent;
transition: all 0.3s;
}
.submit-bar {
position: fixed;
bottom: 32px;
left: 50%;
transform: translateX(-50%);
width: calc(100% - 40px);
max-width: 800px;
background: var(--text);
padding: 20px 40px;
border-radius: 24px;
display: flex;
align-items: center;
justify-content: space-between;
color: white;
box-shadow: 0 25px 50px -12px rgba(0,0,0,0.5);
z-index: 100;
backdrop-filter: blur(12px);
}
.submit-bar p { margin: 0; font-size: 0.875rem; color: #94a3b8; }
.submit-bar h4 { margin: 4px 0 0 0; font-size: 1.125rem; font-weight: 700; }
.btn-submit {
background: var(--primary);
color: white;
border: none;
padding: 14px 32px;
border-radius: 16px;
font-size: 1rem;
font-weight: 800;
cursor: pointer;
transition: all 0.2s;
}
.submit-section { margin-top: 64px; text-align: center; padding: 48px; background: #1e293b; border-radius: 24px; color: white; }
.btn-submit {
background: #4f46e5;
color: white;
border: none;
padding: 16px 48px;
border-radius: 12px;
font-size: 1.125rem;
font-weight: 700;
cursor: pointer;
transition: all 0.2s;
box-shadow: 0 10px 15px -3px rgba(79, 70, 229, 0.4);
.btn-submit:hover {
background: var(--primary-hover);
transform: scale(1.05);
}
@media (max-width: 640px) {
.submit-bar {
flex-direction: column;
gap: 16px;
text-align: center;
padding: 24px;
}
.btn-submit { width: 100%; }
.ballot-header h1 { font-size: 2.25rem; }
}
.btn-submit:hover { transform: translateY(-2px); background: #4338ca; }
.btn-submit:active { transform: translateY(0); }
</style>
</head>
<body>
<div class="ballot-container">
<div class="ballot-header animate-fade-in">
<div style="display: inline-flex; align-items: center; gap: 8px; background: #e0e7ff; color: #4338ca; padding: 6px 16px; border-radius: 100px; font-size: 0.875rem; font-weight: 700; margin-bottom: 16px;">
<i data-lucide="vote" style="width: 16px;"></i> OFFICIAL BALLOT
<div style="text-align: center;">
<div class="timer-banner">
<i data-lucide="clock" style="width: 18px;"></i>
<span>TIME REMAINING:</span>
<span id="countdown">00:00:00</span>
</div>
<h1><?= htmlspecialchars($election['title']) ?></h1>
<p>Your vote is secure and anonymous. Choose your representatives below.</p>
</div>
<form action="api/submit_vote.php" method="POST" onsubmit="return confirm('Are you sure you want to cast your vote? This action cannot be undone.')">
<div class="ballot-header">
<h1><?= htmlspecialchars($election['title']) ?></h1>
<p>Your choice matters. Review the candidates carefully and cast your secure vote below.</p>
</div>
<form id="ballotForm" action="api/submit_vote.php" method="POST">
<input type="hidden" name="election_id" value="<?= $id ?>">
<?php foreach ($positions as $index => $pos): ?>
<div class="position-group animate-stagger" style="--order: <?= $index ?>">
<div class="position-group">
<div class="position-title">
<i data-lucide="award"></i>
<i data-lucide="shield-check"></i>
<?= htmlspecialchars($pos['name']) ?>
</div>
@ -163,8 +315,8 @@ $positions = $positions->fetchAll();
?>
<?php if (empty($candidates)): ?>
<div style="padding: 24px; background: #f8fafc; border-radius: 12px; text-align: center; border: 1px dashed #cbd5e1;">
<p style="margin: 0; color: #64748b; font-size: 0.875rem;">No candidates for this position.</p>
<div style="padding: 32px; background: white; border-radius: 20px; text-align: center; border: 2px dashed #e2e8f0;">
<p style="margin: 0; color: var(--text-muted); font-weight: 600;">No candidates available for your track.</p>
</div>
<?php else: ?>
<div class="candidates-grid">
@ -180,7 +332,7 @@ $positions = $positions->fetchAll();
<p><?= htmlspecialchars($cand['party_name'] ?: 'Independent') ?></p>
</div>
<div class="check-icon">
<i data-lucide="check" style="width: 14px;"></i>
<i data-lucide="check" style="width: 16px;"></i>
</div>
</div>
</label>
@ -190,21 +342,51 @@ $positions = $positions->fetchAll();
</div>
<?php endforeach; ?>
<div class="submit-section animate-fade-in">
<h2 style="margin: 0 0 12px 0; font-size: 1.5rem;">Ready to submit?</h2>
<p style="margin: 0 0 32px 0; color: #94a3b8; font-size: 1rem;">Please review your selections before casting your vote.</p>
<div class="submit-bar">
<div>
<p>READY TO SUBMIT?</p>
<h4>Review your selections</h4>
</div>
<button type="submit" class="btn-submit">
Cast My Vote
</button>
<div style="margin-top: 24px; display: flex; align-items: center; justify-content: center; gap: 8px; color: #64748b; font-size: 0.875rem;">
<i data-lucide="shield-check" style="width: 16px; color: #10b981;"></i> Verified Secure Election
</div>
</div>
</form>
</div>
<script>
lucide.createIcons();
// Countdown Timer
const endTime = <?= $endTime ?>;
function updateCountdown() {
const now = new Date().getTime();
const distance = endTime - now;
if (distance < 0) {
document.getElementById("countdown").innerHTML = "EXPIRED";
document.getElementById("ballotForm").style.opacity = "0.5";
document.getElementById("ballotForm").style.pointerEvents = "none";
return;
}
const hours = Math.floor((distance % (1000 * 60 * 60 * 24)) / (1000 * 60 * 60));
const minutes = Math.floor((distance % (1000 * 60 * 60)) / (1000 * 60));
const seconds = Math.floor((distance % (1000 * 60)) / 1000);
document.getElementById("countdown").innerHTML =
(hours < 10 ? "0" : "") + hours + ":" +
(minutes < 10 ? "0" : "") + minutes + ":" +
(seconds < 10 ? "0" : "") + seconds;
}
setInterval(updateCountdown, 1000);
updateCountdown();
document.getElementById('ballotForm').onsubmit = function() {
return confirm('Are you sure you want to cast your vote? This action is permanent.');
};
</script>
</body>
</html>

View File

@ -6,16 +6,39 @@ define('DB_USER', 'app_38458');
define('DB_PASS', 'c217529c-a428-4a97-8f31-773c420377a7');
// Supabase Configuration - Provide your project URL and Service Role Key
define('SUPABASE_URL', getenv('SUPABASE_URL') ?: 'https://your-project.supabase.co');
define('SUPABASE_SERVICE_ROLE_KEY', getenv('SUPABASE_SERVICE_ROLE_KEY') ?: 'your-service-role-key');
define('SUPABASE_URL', getenv('SUPABASE_URL') ?: 'https://siqeqnizegizxemrfgkf.supabase.co');
define('SUPABASE_SERVICE_ROLE_KEY', getenv('SUPABASE_SERVICE_ROLE_KEY') ?: 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6InNpcWVxbml6ZWdpenhlbXJmZ2tmIiwicm9sZSI6InNlcnZpY2Vfcm9sZSIsImlhdCI6MTc3MDEyMjgzMywiZXhwIjoyMDg1Njk4ODMzfQ.8K66Y3hSfSq5mRDeU8YT6pH9tqPVngxA9dEwCgQUCl0');
define('SUPABASE_DB_PASS', getenv('SUPABASE_DB_PASS') ?: 'gA82h8K80T5QUAwi'); // Set your DB password here for PostgreSQL migration
function db() {
static $pdo;
if (!$pdo) {
$pdo = new PDO('mysql:host='.DB_HOST.';dbname='.DB_NAME.';charset=utf8mb4', DB_USER, DB_PASS, [
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
]);
if (defined('SUPABASE_DB_PASS') && !empty(SUPABASE_DB_PASS)) {
// Use Supabase PostgreSQL
$host = 'aws-1-ap-southeast-1.pooler.supabase.com';
$port = '6543';
$dbname = 'postgres';
$user = 'postgres.siqeqnizegizxemrfgkf';
$pass = SUPABASE_DB_PASS;
try {
$pdo = new PDO("pgsql:host=$host;port=$port;dbname=$dbname", $user, $pass, [
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
]);
} catch (PDOException $e) {
// Fallback to local MariaDB if PostgreSQL fails
$pdo = new PDO('mysql:host='.DB_HOST.';dbname='.DB_NAME.';charset=utf8mb4', DB_USER, DB_PASS, [
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
]);
}
} else {
// Use local MariaDB
$pdo = new PDO('mysql:host='.DB_HOST.';dbname='.DB_NAME.';charset=utf8mb4', DB_USER, DB_PASS, [
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
]);
}
}
return $pdo;
}

29
find_region.php Normal file
View File

@ -0,0 +1,29 @@
<?php
require_once __DIR__ . '/db/config.php';
$regions = [
'us-east-1', 'us-east-2', 'us-west-1', 'us-west-2',
'ap-southeast-1', 'ap-southeast-2', 'ap-northeast-1', 'ap-northeast-2',
'eu-central-1', 'eu-west-1', 'eu-west-2', 'eu-west-3',
'sa-east-1', 'ca-central-1', 'me-south-1', 'af-south-1'
];
$pass = 'gA82h8K80T5QUAwi';
$projectRef = 'siqeqnizegizxemrfgkf';
foreach ($regions as $region) {
$host = "aws-0-$region.pooler.supabase.com";
$user = "postgres.$projectRef";
echo "Testing $region ($host)... ";
try {
$dsn = "pgsql:host=$host;port=6543;dbname=postgres;connect_timeout=5";
$pdo = new PDO($dsn, $user, $pass);
echo "SUCCESS!\n";
exit(0);
} catch (Exception $e) {
if (strpos($e->getMessage(), 'Tenant or user not found') !== false) {
echo "Not here.\n";
} else {
echo "Error: " . $e->getMessage() . "\n";
}
}
}

View File

@ -13,7 +13,41 @@ if (in_array($user['role'], ['Admin', 'Adviser', 'Officer'])) {
}
$pdo = db();
$elections = $pdo->query("SELECT * FROM elections WHERE archived = FALSE ORDER BY created_at DESC")->fetchAll();
// Voter redirection logic
if ($user['role'] === 'Voter') {
// Find ongoing elections that this voter is assigned to
$stmt = $pdo->prepare("
SELECT e.* FROM elections e
JOIN election_assignments ea ON e.id = ea.election_id
WHERE ea.user_id = ?
AND e.status = 'Ongoing'
AND e.archived = FALSE
AND e.end_date_and_time > CURRENT_TIMESTAMP
");
$stmt->execute([$user['id']]);
$activeElections = $stmt->fetchAll();
// Filter out elections where the user has already voted
$votedElectionsStmt = $pdo->prepare("SELECT election_id FROM votes WHERE voter_id = ?");
$votedElectionsStmt->execute([$user['id']]);
$votedIds = $votedElectionsStmt->fetchAll(PDO::FETCH_COLUMN);
$eligibleElections = array_filter($activeElections, function($e) use ($votedIds) {
return !in_array($e['id'], $votedIds);
});
if (count($eligibleElections) === 1) {
$singleElection = reset($eligibleElections);
header("Location: ballot.php?id=" . $singleElection['id']);
exit;
}
// For voters, only show their assigned elections in the list
$elections = $activeElections;
} else {
$elections = $pdo->query("SELECT * FROM elections WHERE archived = FALSE ORDER BY created_at DESC")->fetchAll();
}
$projectDescription = $_SERVER['PROJECT_DESCRIPTION'] ?? 'Online Election System for Senior High School';
?>

163
supabase_migration.php Normal file
View File

@ -0,0 +1,163 @@
<?php
require_once __DIR__ . '/db/config.php';
// This script migrates the local MariaDB database to Supabase PostgreSQL.
// It requires the Supabase Database Password.
if (php_sapi_name() !== 'cli') {
die("This script must be run from the command line.");
}
$dbPassword = $argv[1] ?? '';
if (!$dbPassword) {
echo "Usage: php supabase_migration.php [SUPABASE_DB_PASSWORD]\n";
exit(1);
}
$supabaseHost = "aws-1-ap-southeast-1.pooler.supabase.com";
$supabaseUser = "postgres.siqeqnizegizxemrfgkf";
$supabaseDb = "postgres";
$supabasePort = "6543";
try {
echo "Connecting to local MariaDB...\n";
$localPdo = new PDO('mysql:host='.DB_HOST.';dbname='.DB_NAME.';charset=utf8mb4', DB_USER, DB_PASS, [
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
]);
echo "Connecting to Supabase PostgreSQL...\n";
$dsn = "pgsql:host=$supabaseHost;port=$supabasePort;dbname=$supabaseDb";
$supabasePdo = new PDO($dsn, $supabaseUser, $dbPassword, [
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION
]);
echo "Converting and creating tables in Supabase...\n";
// Define tables and their PostgreSQL schemas
$schemas = [
"users" => "CREATE TABLE IF NOT EXISTS users (
id VARCHAR(255) PRIMARY KEY,
supabase_uid VARCHAR(255),
student_id VARCHAR(10) UNIQUE NOT NULL,
name VARCHAR(255) NOT NULL,
email VARCHAR(255) UNIQUE NOT NULL,
password_hash VARCHAR(255),
grade_level INTEGER,
track VARCHAR(100),
section VARCHAR(100),
role VARCHAR(50) DEFAULT 'Voter',
access_level INTEGER DEFAULT 0,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
deleted_at TIMESTAMP
)",
"elections" => "CREATE TABLE IF NOT EXISTS elections (
id VARCHAR(255) PRIMARY KEY,
title VARCHAR(255) NOT NULL,
description TEXT,
status VARCHAR(50) DEFAULT 'Preparing',
start_date_and_time TIMESTAMP NOT NULL,
end_date_and_time TIMESTAMP NOT NULL,
created_by VARCHAR(255) REFERENCES users(id),
archived BOOLEAN DEFAULT FALSE,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
)",
"positions" => "CREATE TABLE IF NOT EXISTS positions (
id VARCHAR(255) PRIMARY KEY,
election_id VARCHAR(255) REFERENCES elections(id) ON DELETE CASCADE,
name VARCHAR(255) NOT NULL,
type VARCHAR(50) DEFAULT 'Uniform',
max_votes INTEGER DEFAULT 1,
sort_order INTEGER DEFAULT 0
)",
"candidates" => "CREATE TABLE IF NOT EXISTS candidates (
id VARCHAR(255) PRIMARY KEY,
election_id VARCHAR(255) REFERENCES elections(id) ON DELETE CASCADE,
position_id VARCHAR(255) REFERENCES positions(id) ON DELETE CASCADE,
user_id VARCHAR(255) REFERENCES users(id) ON DELETE CASCADE,
party_name VARCHAR(255),
manifesto TEXT,
approved BOOLEAN DEFAULT FALSE,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
)",
"votes" => "CREATE TABLE IF NOT EXISTS votes (
id VARCHAR(255) PRIMARY KEY,
election_id VARCHAR(255) REFERENCES elections(id),
position_id VARCHAR(255) REFERENCES positions(id),
candidate_id VARCHAR(255) REFERENCES candidates(id),
voter_id VARCHAR(255) REFERENCES users(id),
casted_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
ip_address VARCHAR(45),
user_agent TEXT,
UNIQUE (election_id, position_id, voter_id)
)",
"election_assignments" => "CREATE TABLE IF NOT EXISTS election_assignments (
id VARCHAR(255) PRIMARY KEY,
election_id VARCHAR(255) REFERENCES elections(id) ON DELETE CASCADE,
user_id VARCHAR(255) REFERENCES users(id) ON DELETE CASCADE,
role_in_election VARCHAR(50) DEFAULT 'Voter',
assigned_by VARCHAR(255) REFERENCES users(id),
assigned_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
)",
"parties" => "CREATE TABLE IF NOT EXISTS parties (
id VARCHAR(255) PRIMARY KEY,
election_id VARCHAR(255) REFERENCES elections(id) ON DELETE CASCADE,
name VARCHAR(255) NOT NULL,
description TEXT,
logo_url TEXT,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
)",
"audit_logs" => "CREATE TABLE IF NOT EXISTS audit_logs (
id VARCHAR(255) PRIMARY KEY,
user_id VARCHAR(255) REFERENCES users(id),
action VARCHAR(255) NOT NULL,
details TEXT,
table_name VARCHAR(100),
record_id VARCHAR(255),
old_values TEXT,
new_values TEXT,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
election_id VARCHAR(255) REFERENCES elections(id) ON DELETE CASCADE
)"
];
foreach ($schemas as $tableName => $sql) {
echo "Creating table: $tableName...\n";
$supabasePdo->exec($sql);
}
echo "Migrating data...\n";
$tables = array_keys($schemas);
// Order matters for foreign keys: users, elections, positions, candidates, assignments, votes, audit_logs
$orderedTables = ["users", "elections", "positions", "election_assignments", "parties", "candidates", "votes", "audit_logs"];
foreach ($orderedTables as $table) {
echo "Migrating data for $table...\n";
$stmt = $localPdo->query("SELECT * FROM $table");
$rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
if (empty($rows)) {
echo "No data for $table.\n";
continue;
}
$columns = array_keys($rows[0]);
$placeholders = implode(',', array_fill(0, count($columns), '?'));
$insertSql = "INSERT INTO $table (" . implode(',', $columns) . ") VALUES ($placeholders) ON CONFLICT (id) DO NOTHING";
$insertStmt = $supabasePdo->prepare($insertSql);
$count = 0;
foreach ($rows as $row) {
$insertStmt->execute(array_values($row));
$count++;
}
echo "Migrated $count rows for $table.\n";
}
echo "Migration completed successfully!\n";
} catch (Exception $e) {
echo "Error: " . $e->getMessage() . "\n";
exit(1);
}