Revert to version 577ca93

This commit is contained in:
Flatlogic Bot 2025-11-04 16:30:37 +00:00
parent 5d284ffea8
commit de5eb6b5d1
12 changed files with 141 additions and 888 deletions

View File

View File

View File

@ -1,311 +0,0 @@
<?php
// LocalAIApi — proxy client for the Responses API.
// Usage:
// require_once __DIR__ . '/ai/LocalAIApi.php';
// $response = LocalAIApi::createResponse([
// 'input' => [
// ['role' => 'system', 'content' => 'You are a helpful assistant.'],
// ['role' => 'user', 'content' => 'Tell me a bedtime story.'],
// ],
// ]);
// if (!empty($response['success'])) {
// $decoded = LocalAIApi::decodeJsonFromResponse($response);
// }
class LocalAIApi
{
/** @var array<string,mixed>|null */
private static ?array $configCache = null;
/**
* Signature compatible with the OpenAI Responses API.
*
* @param array<string,mixed> $params Request body (model, input, text, reasoning, metadata, etc.).
* @param array<string,mixed> $options Extra options (timeout, verify_tls, headers, path, project_uuid).
* @return array{
* success:bool,
* status?:int,
* data?:mixed,
* error?:string,
* response?:mixed,
* message?:string
* }
*/
public static function createResponse(array $params, array $options = []): array
{
$cfg = self::config();
$payload = $params;
if (empty($payload['input']) || !is_array($payload['input'])) {
return [
'success' => false,
'error' => 'input_missing',
'message' => 'Parameter "input" is required and must be an array.',
];
}
if (!isset($payload['model']) || $payload['model'] === '') {
$payload['model'] = $cfg['default_model'];
}
return self::request($options['path'] ?? null, $payload, $options);
}
/**
* Snake_case alias for createResponse (matches the provided example).
*
* @param array<string,mixed> $params
* @param array<string,mixed> $options
* @return array<string,mixed>
*/
public static function create_response(array $params, array $options = []): array
{
return self::createResponse($params, $options);
}
/**
* Perform a raw request to the AI proxy.
*
* @param string $path Endpoint (may be an absolute URL).
* @param array<string,mixed> $payload JSON payload.
* @param array<string,mixed> $options Additional request options.
* @return array<string,mixed>
*/
public static function request(?string $path = null, array $payload = [], array $options = []): array
{
if (!function_exists('curl_init')) {
return [
'success' => false,
'error' => 'curl_missing',
'message' => 'PHP cURL extension is missing. Install or enable it on the VM.',
];
}
$cfg = self::config();
$projectUuid = $cfg['project_uuid'];
if (empty($projectUuid)) {
return [
'success' => false,
'error' => 'project_uuid_missing',
'message' => 'PROJECT_UUID is not defined; aborting AI request.',
];
}
$defaultPath = $cfg['responses_path'] ?? null;
$resolvedPath = $path ?? ($options['path'] ?? $defaultPath);
if (empty($resolvedPath)) {
return [
'success' => false,
'error' => 'project_id_missing',
'message' => 'PROJECT_ID is not defined; cannot resolve AI proxy endpoint.',
];
}
$url = self::buildUrl($resolvedPath, $cfg['base_url']);
$baseTimeout = isset($cfg['timeout']) ? (int) $cfg['timeout'] : 30;
$timeout = isset($options['timeout']) ? (int) $options['timeout'] : $baseTimeout;
if ($timeout <= 0) {
$timeout = 30;
}
$baseVerifyTls = array_key_exists('verify_tls', $cfg) ? (bool) $cfg['verify_tls'] : true;
$verifyTls = array_key_exists('verify_tls', $options)
? (bool) $options['verify_tls']
: $baseVerifyTls;
$projectHeader = $cfg['project_header'];
$headers = [
'Content-Type: application/json',
'Accept: application/json',
];
$headers[] = $projectHeader . ': ' . $projectUuid;
if (!empty($options['headers']) && is_array($options['headers'])) {
foreach ($options['headers'] as $header) {
if (is_string($header) && $header !== '') {
$headers[] = $header;
}
}
}
if (!empty($projectUuid) && !array_key_exists('project_uuid', $payload)) {
$payload['project_uuid'] = $projectUuid;
}
$body = json_encode($payload, JSON_UNESCAPED_UNICODE);
if ($body === false) {
return [
'success' => false,
'error' => 'json_encode_failed',
'message' => 'Failed to encode request body to JSON.',
];
}
$ch = curl_init($url);
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, $body);
curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_TIMEOUT, $timeout);
curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 5);
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, $verifyTls);
curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, $verifyTls ? 2 : 0);
curl_setopt($ch, CURLOPT_FAILONERROR, false);
$responseBody = curl_exec($ch);
if ($responseBody === false) {
$error = curl_error($ch) ?: 'Unknown cURL error';
curl_close($ch);
return [
'success' => false,
'error' => 'curl_error',
'message' => $error,
];
}
$status = (int) curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
$decoded = null;
if ($responseBody !== '' && $responseBody !== null) {
$decoded = json_decode($responseBody, true);
if (json_last_error() !== JSON_ERROR_NONE) {
$decoded = null;
}
}
if ($status >= 200 && $status < 300) {
return [
'success' => true,
'status' => $status,
'data' => $decoded ?? $responseBody,
];
}
$errorMessage = 'AI proxy request failed';
if (is_array($decoded)) {
$errorMessage = $decoded['error'] ?? $decoded['message'] ?? $errorMessage;
} elseif (is_string($responseBody) && $responseBody !== '') {
$errorMessage = $responseBody;
}
return [
'success' => false,
'status' => $status,
'error' => $errorMessage,
'response' => $decoded ?? $responseBody,
];
}
/**
* Extract plain text from a Responses API payload.
*
* @param array<string,mixed> $response Result of LocalAIApi::createResponse|request.
* @return string
*/
public static function extractText(array $response): string
{
$payload = $response['data'] ?? $response;
if (!is_array($payload)) {
return '';
}
if (!empty($payload['output']) && is_array($payload['output'])) {
$combined = '';
foreach ($payload['output'] as $item) {
if (!isset($item['content']) || !is_array($item['content'])) {
continue;
}
foreach ($item['content'] as $block) {
if (is_array($block) && ($block['type'] ?? '') === 'output_text' && !empty($block['text'])) {
$combined .= $block['text'];
}
}
}
if ($combined !== '') {
return $combined;
}
}
if (!empty($payload['choices'][0]['message']['content'])) {
return (string) $payload['choices'][0]['message']['content'];
}
return '';
}
/**
* Attempt to decode JSON emitted by the model (handles markdown fences).
*
* @param array<string,mixed> $response
* @return array<string,mixed>|null
*/
public static function decodeJsonFromResponse(array $response): ?array
{
$text = self::extractText($response);
if ($text === '') {
return null;
}
$decoded = json_decode($text, true);
if (is_array($decoded)) {
return $decoded;
}
$stripped = preg_replace('/^```json|```$/m', '', trim($text));
if ($stripped !== null && $stripped !== $text) {
$decoded = json_decode($stripped, true);
if (is_array($decoded)) {
return $decoded;
}
}
return null;
}
/**
* Load configuration from ai/config.php.
*
* @return array<string,mixed>
*/
private static function config(): array
{
if (self::$configCache === null) {
$configPath = __DIR__ . '/config.php';
if (!file_exists($configPath)) {
throw new RuntimeException('AI config file not found: ai/config.php');
}
$cfg = require $configPath;
if (!is_array($cfg)) {
throw new RuntimeException('Invalid AI config format: expected array');
}
self::$configCache = $cfg;
}
return self::$configCache;
}
/**
* Build an absolute URL from base_url and a path.
*/
private static function buildUrl(string $path, string $baseUrl): string
{
$trimmed = trim($path);
if ($trimmed === '') {
return $baseUrl;
}
if (str_starts_with($trimmed, 'http://') || str_starts_with($trimmed, 'https://')) {
return $trimmed;
}
if ($trimmed[0] === '/') {
return $baseUrl . $trimmed;
}
return $baseUrl . '/' . $trimmed;
}
}
// Legacy alias for backward compatibility with the previous class name.
if (!class_exists('OpenAIService')) {
class_alias(LocalAIApi::class, 'OpenAIService');
}

View File

@ -1,52 +0,0 @@
<?php
// OpenAI proxy configuration (workspace scope).
// Reads values from environment variables or executor/.env.
$projectUuid = getenv('PROJECT_UUID');
$projectId = getenv('PROJECT_ID');
if (
($projectUuid === false || $projectUuid === null || $projectUuid === '') ||
($projectId === false || $projectId === null || $projectId === '')
) {
$envPath = realpath(__DIR__ . '/../../.env'); // executor/.env
if ($envPath && is_readable($envPath)) {
$lines = @file($envPath, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES) ?: [];
foreach ($lines as $line) {
$line = trim($line);
if ($line === '' || $line[0] === '#') {
continue;
}
if (!str_contains($line, '=')) {
continue;
}
[$key, $value] = array_map('trim', explode('=', $line, 2));
if ($key === '') {
continue;
}
$value = trim($value, "\"' ");
if (getenv($key) === false || getenv($key) === '') {
putenv("{$key}={$value}");
}
}
$projectUuid = getenv('PROJECT_UUID');
$projectId = getenv('PROJECT_ID');
}
}
$projectUuid = ($projectUuid === false) ? null : $projectUuid;
$projectId = ($projectId === false) ? null : $projectId;
$baseUrl = 'https://flatlogic.com';
$responsesPath = $projectId ? "/projects/{$projectId}/ai-request" : null;
return [
'base_url' => $baseUrl,
'responses_path' => $responsesPath,
'project_id' => $projectId,
'project_uuid' => $projectUuid,
'project_header' => 'project-uuid',
'default_model' => 'gpt-5',
'timeout' => 30,
'verify_tls' => true,
];

View File

@ -1,80 +0,0 @@
<?php
header('Content-Type: application/json');
// --- Authentication ---
$authHeader = $_SERVER['HTTP_AUTHORIZATION'] ?? null;
$expectedToken = 'Bearer aSecretToken123'; // Dummy token
if ($authHeader !== $expectedToken) {
http_response_code(401);
echo json_encode([
'statusCode' => 401,
'statusMessage' => 'Unauthorized'
]);
exit;
}
// --- Request Method & Body ---
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
http_response_code(405);
echo json_encode([
'statusCode' => 405,
'statusMessage' => 'Method Not Allowed'
]);
exit;
}
$json_payload = file_get_contents('php://input');
$data = json_decode($json_payload, true);
// --- Validation ---
$required_fields = ['name', 'accountNumber', 'accountName', 'amount'];
$missing_fields = [];
foreach ($required_fields as $field) {
if (empty($data[$field])) {
$missing_fields[] = $field;
}
}
if (!empty($missing_fields)) {
http_response_code(400);
echo json_encode([
'statusCode' => 400,
'statusMessage' => 'Bad Request',
'error' => 'Missing required fields: ' . implode(', ', $missing_fields)
]);
exit;
}
// --- Log Request (for debugging the dummy API itself) ---
// In a real app, you might not log to a file here, but it's useful for the dummy.
// The main app will log to its own DB.
$log_entry = [
'timestamp' => date('c'),
'request' => $data,
];
// --- Process and Respond ---
// Simulate some random failures for realism
$should_fail = rand(1, 10) > 8; // 20% chance of failure
if ($should_fail) {
http_response_code(500);
$response = [
'statusCode' => 500,
'statusMessage' => 'Internal Server Error',
'error' => 'A simulated random error occurred.'
];
} else {
http_response_code(200);
$response = [
'statusCode' => 200,
'statusMessage' => 'sukses',
'transactionId' => 'txn_' . uniqid()
];
}
$log_entry['response'] = $response;
file_put_contents('dummy_api_log.txt', json_encode($log_entry) . PHP_EOL, FILE_APPEND);
echo json_encode($response);

View File

@ -1,23 +0,0 @@
body {
font-family: 'Inter', sans-serif;
background-color: #F8F9FA;
}
.navbar-brand {
font-size: 1.5rem;
}
.card {
border: none;
border-radius: 0.5rem;
box-shadow: 0 0.5rem 1rem rgba(0, 0, 0, 0.05);
transition: transform 0.2s ease-in-out;
}
.card:hover {
transform: translateY(-5px);
}
.table-hover tbody tr:hover {
background-color: rgba(0, 0, 0, 0.025);
}

View File

@ -1,41 +0,0 @@
$(document).ready(function() {
const successToastEl = document.getElementById('successToast');
const successToast = new bootstrap.Toast(successToastEl);
const previewModalEl = document.getElementById('previewModal');
const previewModal = new bootstrap.Modal(previewModalEl);
$('#uploadForm').on('submit', function(event) {
event.preventDefault();
const form = this;
const submitButton = $(form).find('button[type="submit"]');
const spinner = submitButton.find('.spinner-border');
// Basic validation
if (form.checkValidity() === false) {
event.stopPropagation();
$(form).addClass('was-validated');
return;
}
// Show loading state
submitButton.attr('disabled', true);
spinner.removeClass('d-none');
// Simulate async background process
setTimeout(function() {
// Show success toast
successToast.show();
// Show preview modal
previewModal.show();
// Reset form and button
form.reset();
$(form).removeClass('was-validated');
submitButton.attr('disabled', false);
spinner.addClass('d-none');
}, 1000); // Simulate a 1-second delay for effect
});
});

View File

@ -1,101 +0,0 @@
<?php
// Simple, idempotent script to create necessary tables.
// This should be run once manually or via a simple admin panel.
require_once __DIR__ . '/config.php';
try {
// Connect without DB to create it if it doesn't exist
$pdo_admin = new PDO('mysql:host='.DB_HOST.';charset=utf8mb4', DB_USER, DB_PASS, [
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
]);
$pdo_admin->exec("CREATE DATABASE IF NOT EXISTS `".DB_NAME."`");
// Now connect to the app DB
$pdo = db();
$pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
$statements = [
// Users table for login
"CREATE TABLE IF NOT EXISTS users (
id INT AUTO_INCREMENT PRIMARY KEY,
username VARCHAR(50) NOT NULL UNIQUE,
password VARCHAR(255) NOT NULL,
role ENUM('maker', 'approver') NOT NULL,
createdAt TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);",
// Payroll batches table
"CREATE TABLE IF NOT EXISTS payrolls (
id INT AUTO_INCREMENT PRIMARY KEY,
name VARCHAR(255) NOT NULL,
fileName VARCHAR(255) NOT NULL,
status ENUM('pending', 'approved', 'processing', 'delivered', 'failed') NOT NULL DEFAULT 'pending',
createdAt TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
createdBy INT,
approvedAt TIMESTAMP NULL,
approvedBy INT,
FOREIGN KEY (createdBy) REFERENCES users(id),
FOREIGN KEY (approvedBy) REFERENCES users(id)
);",
// Payroll details (individual records from CSV)
"CREATE TABLE IF NOT EXISTS payroll_details (
id INT AUTO_INCREMENT PRIMARY KEY,
payroll_id INT NOT NULL,
name VARCHAR(255) NOT NULL,
accountNumber VARCHAR(50) NOT NULL,
accountName VARCHAR(255) NOT NULL,
amount DECIMAL(15, 2) NOT NULL,
status ENUM('pending', 'processing', 'delivered', 'failed') NOT NULL DEFAULT 'pending',
createdAt TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
createdBy INT,
approvedAt TIMESTAMP NULL,
approvedBy INT,
api_response TEXT,
FOREIGN KEY (payroll_id) REFERENCES payrolls(id) ON DELETE CASCADE,
FOREIGN KEY (createdBy) REFERENCES users(id),
FOREIGN KEY (approvedBy) REFERENCES users(id)
);",
// API logs
"CREATE TABLE IF NOT EXISTS api_logs (
id INT AUTO_INCREMENT PRIMARY KEY,
payroll_detail_id INT,
request_payload TEXT,
response_payload TEXT,
status_code INT,
createdAt TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (payroll_detail_id) REFERENCES payroll_details(id) ON DELETE SET NULL
);"
];
foreach ($statements as $statement) {
$pdo->exec($statement);
}
echo "Database and tables created or already exist successfully." . PHP_EOL;
// --- User Seeding ---
// To ensure a clean state, we'll remove existing dummy users and recreate them.
// This makes the script safe to re-run.
$pdo->exec("DELETE FROM users WHERE username IN ('maker', 'approver')");
// Use prepared statements for security and reliability
$stmt = $pdo->prepare("INSERT INTO users (username, password, role) VALUES (:username, :password, :role)");
$users_to_seed = [
['username' => 'maker', 'password' => password_hash('password123', PASSWORD_DEFAULT), 'role' => 'maker'],
['username' => 'approver', 'password' => password_hash('password123', PASSWORD_DEFAULT), 'role' => 'approver']
];
foreach ($users_to_seed as $user) {
$stmt->execute($user);
}
echo "Dummy users 'maker' and 'approver' created/reset successfully with password 'password123'." . PHP_EOL;
} catch (PDOException $e) {
die("Database setup failed: " . $e->getMessage());
}

300
index.php
View File

@ -1,170 +1,150 @@
<?php
session_start();
declare(strict_types=1);
@ini_set('display_errors', '1');
@error_reporting(E_ALL);
@date_default_timezone_set('UTC');
// If user is not logged in, redirect to login.php
if (!isset($_SESSION['user_id'])) {
header('Location: login.php');
exit;
}
$phpVersion = PHP_VERSION;
$now = date('Y-m-d H:i:s');
?>
<!DOCTYPE html>
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Payroll</title>
<meta name="description" content="Built with Flatlogic Generator">
<meta name="keywords" content="payroll management, php payroll, csv upload, employee payment, payroll approval, background processing, api integration, flatlogic generator">
<meta property="og:title" content="Payroll">
<meta property="og:description" content="Built with Flatlogic Generator">
<meta property="og:image" content="">
<meta name="twitter:card" content="summary_large_image">
<meta name="twitter:image" content="">
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css" rel="stylesheet">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.min.css">
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">
<link rel="stylesheet" href="assets/css/custom.css?v=<?php echo time(); ?>">
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>New Style</title>
<?php
// Read project preview data from environment
$projectDescription = $_SERVER['PROJECT_DESCRIPTION'] ?? '';
$projectImageUrl = $_SERVER['PROJECT_IMAGE_URL'] ?? '';
?>
<?php if ($projectDescription): ?>
<!-- Meta description -->
<meta name="description" content='<?= htmlspecialchars($projectDescription) ?>' />
<!-- Open Graph meta tags -->
<meta property="og:description" content="<?= htmlspecialchars($projectDescription) ?>" />
<!-- Twitter meta tags -->
<meta property="twitter:description" content="<?= htmlspecialchars($projectDescription) ?>" />
<?php endif; ?>
<?php if ($projectImageUrl): ?>
<!-- Open Graph image -->
<meta property="og:image" content="<?= htmlspecialchars($projectImageUrl) ?>" />
<!-- Twitter image -->
<meta property="twitter:image" content="<?= htmlspecialchars($projectImageUrl) ?>" />
<?php endif; ?>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;700&display=swap" rel="stylesheet">
<style>
:root {
--bg-color-start: #6a11cb;
--bg-color-end: #2575fc;
--text-color: #ffffff;
--card-bg-color: rgba(255, 255, 255, 0.01);
--card-border-color: rgba(255, 255, 255, 0.1);
}
body {
margin: 0;
font-family: 'Inter', sans-serif;
background: linear-gradient(45deg, var(--bg-color-start), var(--bg-color-end));
color: var(--text-color);
display: flex;
justify-content: center;
align-items: center;
min-height: 100vh;
text-align: center;
overflow: hidden;
position: relative;
}
body::before {
content: '';
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-image: url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" width="100" height="100" viewBox="0 0 100 100"><path d="M-10 10L110 10M10 -10L10 110" stroke-width="1" stroke="rgba(255,255,255,0.05)"/></svg>');
animation: bg-pan 20s linear infinite;
z-index: -1;
}
@keyframes bg-pan {
0% { background-position: 0% 0%; }
100% { background-position: 100% 100%; }
}
main {
padding: 2rem;
}
.card {
background: var(--card-bg-color);
border: 1px solid var(--card-border-color);
border-radius: 16px;
padding: 2rem;
backdrop-filter: blur(20px);
-webkit-backdrop-filter: blur(20px);
box-shadow: 0 8px 32px 0 rgba(0, 0, 0, 0.1);
}
.loader {
margin: 1.25rem auto 1.25rem;
width: 48px;
height: 48px;
border: 3px solid rgba(255, 255, 255, 0.25);
border-top-color: #fff;
border-radius: 50%;
animation: spin 1s linear infinite;
}
@keyframes spin {
from { transform: rotate(0deg); }
to { transform: rotate(360deg); }
}
.hint {
opacity: 0.9;
}
.sr-only {
position: absolute;
width: 1px; height: 1px;
padding: 0; margin: -1px;
overflow: hidden;
clip: rect(0, 0, 0, 0);
white-space: nowrap; border: 0;
}
h1 {
font-size: 3rem;
font-weight: 700;
margin: 0 0 1rem;
letter-spacing: -1px;
}
p {
margin: 0.5rem 0;
font-size: 1.1rem;
}
code {
background: rgba(0,0,0,0.2);
padding: 2px 6px;
border-radius: 4px;
font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
}
footer {
position: absolute;
bottom: 1rem;
font-size: 0.8rem;
opacity: 0.7;
}
</style>
</head>
<body>
<nav class="navbar navbar-expand-lg navbar-light bg-white shadow-sm">
<div class="container-fluid">
<a class="navbar-brand fw-bold text-primary" href="#">PayrollApp</a>
<div class="d-flex align-items-center">
<span class="navbar-text me-3">
Welcome, <?php echo htmlspecialchars($_SESSION['username']); ?>! (Role: <?php echo htmlspecialchars($_SESSION['role']); ?>)
</span>
<a href="logout.php" class="btn btn-outline-secondary">Logout</a>
</div>
</div>
</nav>
<main class="container mt-4">
<div class="d-flex justify-content-between align-items-center mb-4">
<h1 class="h3">Payroll Management</h1>
</div>
<div class="row">
<div class="col-lg-4 mb-4">
<div class="card h-100">
<div class="card-body">
<h5 class="card-title"><i class="bi bi-upload me-2"></i>Upload Payroll</h5>
<p class="card-text text-muted">Upload a CSV file to start a new payroll batch.</p>
<form id="uploadForm" enctype="multipart/form-data">
<div class="mb-3">
<label for="payrollName" class="form-label">Payroll Name</label>
<input type="text" class="form-control" id="payrollName" name="name" required>
</div>
<div class="mb-3">
<label for="csvFile" class="form-label">CSV File</label>
<input class="form-control" type="file" id="csvFile" name="file" accept=".csv" required>
</div>
<button type="submit" class="btn btn-primary w-100">
<span class="spinner-border spinner-border-sm d-none" role="status" aria-hidden="true"></span>
Upload & Preview
</button>
</form>
</div>
</div>
</div>
<div class="col-lg-8 mb-4">
<div class="card h-100">
<div class="card-body">
<h5 class="card-title"><i class="bi bi-list-task me-2"></i>Payroll Batches</h5>
<div class="table-responsive">
<table class="table table-hover">
<thead class="table-light">
<tr>
<th>ID</th>
<th>Name</th>
<th>File Name</th>
<th>Created At</th>
<th>Status</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
<tr>
<td colspan="6" class="text-center text-muted pt-4 pb-4">No payroll batches found.</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
</div>
</div>
</main>
<!-- Preview Modal -->
<div class="modal fade" id="previewModal" tabindex="-1" aria-labelledby="previewModalLabel" aria-hidden="true">
<div class="modal-dialog modal-xl modal-dialog-centered modal-dialog-scrollable">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="previewModalLabel">CSV Preview</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<p>This is a preview of the data from the uploaded file. The data is being processed in the background.</p>
<div class="table-responsive">
<table class="table table-striped">
<thead>
<tr>
<th>name</th>
<th>accountNumber</th>
<th>accountName</th>
<th>amount</th>
</tr>
</thead>
<tbody>
<tr>
<td>John Doe</td>
<td>1234567890</td>
<td>John's Savings</td>
<td>5000.00</td>
</tr>
<tr>
<td>Jane Smith</td>
<td>0987654321</td>
<td>Jane's Checking</td>
<td>7500.50</td>
</tr>
<tr>
<td>Peter Jones</td>
<td>1122334455</td>
<td>Peter's Account</td>
<td>3200.75</td>
</tr>
</tbody>
</table>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button>
</div>
</div>
</div>
<main>
<div class="card">
<h1>Analyzing your requirements and generating your website…</h1>
<div class="loader" role="status" aria-live="polite" aria-label="Applying initial changes">
<span class="sr-only">Loading…</span>
</div>
<p class="hint"><?= ($_SERVER['HTTP_HOST'] ?? '') === 'appwizzy.com' ? 'AppWizzy' : 'Flatlogic' ?> AI is collecting your requirements and applying the first changes.</p>
<p class="hint">This page will update automatically as the plan is implemented.</p>
<p>Runtime: PHP <code><?= htmlspecialchars($phpVersion) ?></code> — UTC <code><?= htmlspecialchars($now) ?></code></p>
</div>
<!-- Toast Container -->
<div class="toast-container position-fixed bottom-0 end-0 p-3">
<div id="successToast" class="toast" role="alert" aria-live="assertive" aria-atomic="true">
<div class="toast-header text-white bg-success">
<i class="bi bi-check-circle-fill me-2"></i>
<strong class="me-auto">Success</strong>
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="toast" aria-label="Close"></button>
</div>
<div class="toast-body">
File upload started successfully! The data is being processed in the background.
</div>
</div>
</div>
<script src="https://code.jquery.com/jquery-3.7.1.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/js/bootstrap.bundle.min.js"></script>
<script src="assets/js/main.js?v=<?php echo time(); ?>"></script>
</main>
<footer>
Page updated: <?= htmlspecialchars($now) ?> (UTC)
</footer>
</body>
</html>

View File

@ -1,97 +0,0 @@
<?php
session_start();
// If user is already logged in, redirect to index.php
if (isset($_SESSION['user_id'])) {
header('Location: index.php');
exit;
}
require_once 'db/config.php';
$error_message = '';
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$username = $_POST['username'] ?? '';
$password = $_POST['password'] ?? '';
if (empty($username) || empty($password)) {
$error_message = 'Please enter both username and password.';
} else {
try {
$pdo = db();
$stmt = $pdo->prepare('SELECT * FROM users WHERE username = :username');
$stmt->execute(['username' => $username]);
$user = $stmt->fetch();
if ($user && password_verify($password, $user['password'])) {
$_SESSION['user_id'] = $user['id'];
$_SESSION['username'] = $user['username'];
$_SESSION['role'] = $user['role'];
header('Location: index.php');
exit;
} else {
$error_message = 'Invalid username or password.';
}
} catch (PDOException $e) {
$error_message = 'Database error: ' . $e->getMessage();
}
}
}
?>
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Login - Payroll App</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css" rel="stylesheet">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.min.css">
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">
<link rel="stylesheet" href="assets/css/custom.css">
<style>
body {
background-color: #f8f9fa;
font-family: 'Inter', sans-serif;
}
.login-container {
max-width: 400px;
margin-top: 100px;
}
</style>
</head>
<body>
<div class="container login-container">
<div class="card shadow-sm">
<div class="card-body p-5">
<h3 class="card-title text-center mb-4">Payroll App Login</h3>
<?php if ($error_message): ?>
<div class="alert alert-danger"><?php echo htmlspecialchars($error_message); ?></div>
<?php endif; ?>
<form method="POST" action="login.php">
<div class="mb-3">
<label for="username" class="form-label">Username</label>
<input type="text" class="form-control" id="username" name="username" required>
</div>
<div class="mb-3">
<label for="password" class="form-label">Password</label>
<input type="password" class="form-control" id="password" name="password" required>
</div>
<div class="d-grid">
<button type="submit" class="btn btn-primary">Login</button>
</div>
</form>
</div>
<div class="card-footer text-center py-3">
<small class="text-muted">
Maker: maker / password123<br>
Approver: approver / password123
</small>
</div>
</div>
</div>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/js/bootstrap.bundle.min.js"></script>
</body>
</html>

View File

@ -1,22 +0,0 @@
<?php
session_start();
// Unset all of the session variables.
$_SESSION = [];
// If it's desired to kill the session, also delete the session cookie.
// Note: This will destroy the session, and not just the session data!
if (ini_get("session.use_cookies")) {
$params = session_get_cookie_params();
setcookie(session_name(), '', time() - 42000,
$params["path"], $params["domain"],
$params["secure"], $params["httponly"]
);
}
// Finally, destroy the session.
session_destroy();
// Redirect to login page
header('Location: login.php');
exit;