Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9b5a06451f |
71
_export_sales_report.php
Normal file
71
_export_sales_report.php
Normal file
@ -0,0 +1,71 @@
|
||||
<?php
|
||||
require_once 'includes/auth.php';
|
||||
require_login();
|
||||
|
||||
require_once 'db/config.php';
|
||||
|
||||
$pdo = db();
|
||||
|
||||
// Filter logic
|
||||
$start_date = $_GET['start_date'] ?? '';
|
||||
$end_date = $_GET['end_date'] ?? '';
|
||||
$payment_method = $_GET['payment_method'] ?? '';
|
||||
|
||||
$sql = "SELECT s.id, s.sale_date, s.total_amount, s.payment_method, GROUP_CONCAT(CONCAT(p.name, ' (', si.quantity, ' x $', si.price, ')')) AS items
|
||||
FROM sales s
|
||||
JOIN sale_items si ON s.id = si.sale_id
|
||||
JOIN products p ON si.product_id = p.id";
|
||||
|
||||
$conditions = [];
|
||||
$params = [];
|
||||
|
||||
if ($start_date) {
|
||||
$conditions[] = "s.sale_date >= ?";
|
||||
$params[] = $start_date . ' 00:00:00';
|
||||
}
|
||||
if ($end_date) {
|
||||
$conditions[] = "s.sale_date <= ?";
|
||||
$params[] = $end_date . ' 23:59:59';
|
||||
}
|
||||
if ($payment_method) {
|
||||
$conditions[] = "s.payment_method = ?";
|
||||
$params[] = $payment_method;
|
||||
}
|
||||
|
||||
if (count($conditions) > 0) {
|
||||
$sql .= " WHERE " . implode(' AND ', $conditions);
|
||||
}
|
||||
|
||||
$sql .= " GROUP BY s.id ORDER BY s.sale_date DESC";
|
||||
|
||||
$stmt = $pdo->prepare($sql);
|
||||
$stmt->execute($params);
|
||||
$sales = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||
|
||||
// CSV generation
|
||||
$filename = "sales_report_" . date('Y-m-d') . ".csv";
|
||||
|
||||
header('Content-Type: text/csv');
|
||||
header('Content-Disposition: attachment; filename="' . $filename . '"');
|
||||
|
||||
$output = fopen('php://output', 'w');
|
||||
|
||||
// Add BOM to support UTF-8 in Excel
|
||||
fputs($output, "\xEF\xBB\xBF");
|
||||
|
||||
// Header row
|
||||
fputcsv($output, ['Sale ID', 'Date', 'Total Amount', 'Payment Method', 'Items']);
|
||||
|
||||
// Data rows
|
||||
foreach ($sales as $sale) {
|
||||
fputcsv($output, [
|
||||
$sale['id'],
|
||||
$sale['sale_date'],
|
||||
number_format($sale['total_amount'], 2),
|
||||
$sale['payment_method'],
|
||||
$sale['items']
|
||||
]);
|
||||
}
|
||||
|
||||
fclose($output);
|
||||
exit;
|
||||
27
_get_best_selling_products.php
Normal file
27
_get_best_selling_products.php
Normal file
@ -0,0 +1,27 @@
|
||||
<?php
|
||||
require_once __DIR__ . '/db/config.php';
|
||||
|
||||
try {
|
||||
$pdo = db();
|
||||
|
||||
$stmt = $pdo->query("
|
||||
SELECT
|
||||
p.name,
|
||||
SUM(si.quantity) as total_quantity
|
||||
FROM sale_items si
|
||||
JOIN products p ON si.product_id = p.id
|
||||
GROUP BY p.name
|
||||
ORDER BY total_quantity DESC
|
||||
LIMIT 5
|
||||
");
|
||||
|
||||
$best_selling_products = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||
|
||||
header('Content-Type: application/json');
|
||||
echo json_encode($best_selling_products);
|
||||
|
||||
} catch (PDOException $e) {
|
||||
http_response_code(500);
|
||||
echo json_encode(['error' => 'Database error: ' . $e->getMessage()]);
|
||||
}
|
||||
?>
|
||||
22
_get_sale_items.php
Normal file
22
_get_sale_items.php
Normal file
@ -0,0 +1,22 @@
|
||||
<?php
|
||||
header('Content-Type: application/json');
|
||||
require_once 'db/config.php';
|
||||
|
||||
if (!isset($_GET['sale_id'])) {
|
||||
echo json_encode(['error' => 'Sale ID not provided.']);
|
||||
exit;
|
||||
}
|
||||
|
||||
$sale_id = $_GET['sale_id'];
|
||||
$pdo = db();
|
||||
|
||||
$stmt = $pdo->prepare("
|
||||
SELECT si.quantity, si.price, p.name as product_name
|
||||
FROM sale_items si
|
||||
JOIN products p ON si.product_id = p.id
|
||||
WHERE si.sale_id = ?
|
||||
");
|
||||
$stmt->execute([$sale_id]);
|
||||
$items = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||
|
||||
echo json_encode($items);
|
||||
42
_get_sales_data.php
Normal file
42
_get_sales_data.php
Normal file
@ -0,0 +1,42 @@
|
||||
<?php
|
||||
require_once 'db/config.php';
|
||||
|
||||
header('Content-Type: application/json');
|
||||
|
||||
$p_start_date = date('Y-m-d', strtotime('-6 days'));
|
||||
$p_end_date = date('Y-m-d');
|
||||
|
||||
$sql = "SELECT
|
||||
d.sale_date,
|
||||
COALESCE(SUM(s.total_amount), 0) AS daily_total
|
||||
FROM
|
||||
(SELECT CURDATE() - INTERVAL (a.a + (10 * b.a) + (100 * c.a)) DAY AS sale_date
|
||||
FROM (SELECT 0 AS a UNION ALL SELECT 1 UNION ALL SELECT 2 UNION ALL SELECT 3 UNION ALL SELECT 4 UNION ALL SELECT 5 UNION ALL SELECT 6 UNION ALL SELECT 7 UNION ALL SELECT 8 UNION ALL SELECT 9) AS a
|
||||
CROSS JOIN (SELECT 0 AS a UNION ALL SELECT 1 UNION ALL SELECT 2 UNION ALL SELECT 3 UNION ALL SELECT 4 UNION ALL SELECT 5 UNION ALL SELECT 6 UNION ALL SELECT 7 UNION ALL SELECT 8 UNION ALL SELECT 9) AS b
|
||||
CROSS JOIN (SELECT 0 AS a UNION ALL SELECT 1 UNION ALL SELECT 2 UNION ALL SELECT 3 UNION ALL SELECT 4 UNION ALL SELECT 5 UNION ALL SELECT 6 UNION ALL SELECT 7 UNION ALL SELECT 8 UNION ALL SELECT 9) AS c
|
||||
) AS d
|
||||
LEFT JOIN
|
||||
sales s ON d.sale_date = DATE(s.sale_date)
|
||||
WHERE
|
||||
d.sale_date BETWEEN :start_date AND :end_date
|
||||
GROUP BY
|
||||
d.sale_date
|
||||
ORDER BY
|
||||
d.sale_date ASC;";
|
||||
|
||||
$stmt = db()->prepare($sql);
|
||||
$stmt->bindParam(':start_date', $p_start_date, PDO::PARAM_STR);
|
||||
$stmt->bindParam(':end_date', $p_end_date, PDO::PARAM_STR);
|
||||
$stmt->execute();
|
||||
|
||||
$sales_data = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||
|
||||
$labels = [];
|
||||
$data = [];
|
||||
|
||||
foreach ($sales_data as $row) {
|
||||
$labels[] = date('M d', strtotime($row['sale_date']));
|
||||
$data[] = $row['daily_total'];
|
||||
}
|
||||
|
||||
echo json_encode(['labels' => $labels, 'data' => $data]);
|
||||
32
_handle_add_product.php
Normal file
32
_handle_add_product.php
Normal file
@ -0,0 +1,32 @@
|
||||
<?php
|
||||
require_once __DIR__ . '/db/config.php';
|
||||
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
$name = $_POST['name'] ?? '';
|
||||
$sku = $_POST['sku'] ?? null;
|
||||
$category = $_POST['category'] ?? null;
|
||||
$price = $_POST['price'] ?? 0;
|
||||
$stock = $_POST['stock'] ?? 0;
|
||||
|
||||
// Basic validation
|
||||
if (empty($name) || !is_numeric($price) || !is_numeric($stock)) {
|
||||
// Handle error - maybe redirect back with an error message
|
||||
header('Location: product_add.php?error=Invalid+input');
|
||||
exit;
|
||||
}
|
||||
|
||||
try {
|
||||
$stmt = db()->prepare("INSERT INTO products (name, sku, category, price, stock) VALUES (?, ?, ?, ?, ?)");
|
||||
$stmt->execute([$name, $sku, $category, $price, $stock]);
|
||||
|
||||
// Redirect to product list on success
|
||||
header('Location: products.php?success=Product+added');
|
||||
exit;
|
||||
} catch (PDOException $e) {
|
||||
// Handle error, e.g., duplicate SKU
|
||||
// For now, we'll just redirect with a generic error
|
||||
error_log($e->getMessage());
|
||||
header('Location: product_add.php?error=Could+not+add+product');
|
||||
exit;
|
||||
}
|
||||
}
|
||||
67
_handle_checkout.php
Normal file
67
_handle_checkout.php
Normal file
@ -0,0 +1,67 @@
|
||||
<?php
|
||||
require_once __DIR__ . '/db/config.php';
|
||||
|
||||
header('Content-Type: application/json');
|
||||
|
||||
$response = ['success' => false, 'message' => 'An unknown error occurred.'];
|
||||
|
||||
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
|
||||
http_response_code(405);
|
||||
$response['message'] = 'Invalid request method.';
|
||||
echo json_encode($response);
|
||||
exit;
|
||||
}
|
||||
|
||||
$data = json_decode(file_get_contents('php://input'), true);
|
||||
$cart = $data['cart'] ?? [];
|
||||
$paymentMethod = $data['payment_method'] ?? 'Cash';
|
||||
|
||||
if (empty($cart)) {
|
||||
http_response_code(400);
|
||||
$response['message'] = 'Cart is empty.';
|
||||
echo json_encode($response);
|
||||
exit;
|
||||
}
|
||||
|
||||
$pdo = db();
|
||||
try {
|
||||
$pdo->beginTransaction();
|
||||
|
||||
$totalAmount = 0;
|
||||
foreach ($cart as $item) {
|
||||
$totalAmount += $item['price'] * $item['quantity'];
|
||||
}
|
||||
|
||||
$transactionId = 'TXN-' . strtoupper(uniqid());
|
||||
$stmt = $pdo->prepare("INSERT INTO sales (transaction_id, total_amount, payment_method) VALUES (?, ?, ?)");
|
||||
$stmt->execute([$transactionId, $totalAmount, $paymentMethod]);
|
||||
$saleId = $pdo->lastInsertId();
|
||||
|
||||
$itemStmt = $pdo->prepare("INSERT INTO sale_items (sale_id, product_id, quantity, price) VALUES (?, ?, ?, ?)");
|
||||
$stockStmt = $pdo->prepare("UPDATE products SET stock = stock - ? WHERE id = ?");
|
||||
|
||||
foreach ($cart as $item) {
|
||||
$productId = $item['id'];
|
||||
$quantity = $item['quantity'];
|
||||
$price = $item['price'];
|
||||
|
||||
$itemStmt->execute([$saleId, $productId, $quantity, $price]);
|
||||
$stockStmt->execute([$quantity, $productId]);
|
||||
}
|
||||
|
||||
$pdo->commit();
|
||||
|
||||
$response['success'] = true;
|
||||
$response['message'] = 'Checkout successful!';
|
||||
$response['transaction_id'] = $transactionId;
|
||||
http_response_code(200);
|
||||
|
||||
} catch (Exception $e) {
|
||||
if ($pdo->inTransaction()) {
|
||||
$pdo->rollBack();
|
||||
}
|
||||
http_response_code(500);
|
||||
$response['message'] = 'Checkout failed: ' . $e->getMessage();
|
||||
}
|
||||
|
||||
echo json_encode($response);
|
||||
30
_handle_delete_product.php
Normal file
30
_handle_delete_product.php
Normal file
@ -0,0 +1,30 @@
|
||||
<?php
|
||||
require_once __DIR__ . '/db/config.php';
|
||||
|
||||
// Check if ID is provided
|
||||
if (!isset($_GET['id']) || !is_numeric($_GET['id'])) {
|
||||
header('Location: products.php?error=invalid_id');
|
||||
exit;
|
||||
}
|
||||
|
||||
$id = $_GET['id'];
|
||||
|
||||
// Optional: Add a confirmation step here in a real application
|
||||
|
||||
try {
|
||||
$stmt = db()->prepare('DELETE FROM products WHERE id = ?');
|
||||
$stmt->execute([$id]);
|
||||
|
||||
// Check if any row was deleted
|
||||
if ($stmt->rowCount() > 0) {
|
||||
header('Location: products.php?status=deleted');
|
||||
} else {
|
||||
header('Location: products.php?error=not_found');
|
||||
}
|
||||
exit;
|
||||
} catch (PDOException $e) {
|
||||
// Handle DB error
|
||||
// error_log($e->getMessage());
|
||||
header('Location: products.php?error=db');
|
||||
exit;
|
||||
}
|
||||
39
_handle_edit_product.php
Normal file
39
_handle_edit_product.php
Normal file
@ -0,0 +1,39 @@
|
||||
<?php
|
||||
require_once __DIR__ . '/db/config.php';
|
||||
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
$id = $_POST['id'];
|
||||
$name = $_POST['name'];
|
||||
$sku = $_POST['sku'];
|
||||
$category = $_POST['category'];
|
||||
$price = $_POST['price'];
|
||||
$stock = $_POST['stock'];
|
||||
|
||||
// Basic validation
|
||||
if (empty($name) || empty($price) || !is_numeric($price) || !is_numeric($stock)) {
|
||||
// Handle validation error, e.g., redirect back with an error message
|
||||
header('Location: product_edit.php?id=' . $id . '&error=validation');
|
||||
exit;
|
||||
}
|
||||
|
||||
try {
|
||||
$stmt = db()->prepare(
|
||||
'UPDATE products SET name = ?, sku = ?, category = ?, price = ?, stock = ? WHERE id = ?'
|
||||
);
|
||||
$stmt->execute([$name, $sku, $category, $price, $stock, $id]);
|
||||
|
||||
// Redirect to products list on success
|
||||
header('Location: products.php?status=updated');
|
||||
exit;
|
||||
} catch (PDOException $e) {
|
||||
// Handle DB error, e.g., log error and redirect
|
||||
// For development, you might want to see the error
|
||||
// error_log($e->getMessage());
|
||||
header('Location: product_edit.php?id=' . $id . '&error=db');
|
||||
exit;
|
||||
}
|
||||
}
|
||||
|
||||
// Redirect if accessed directly
|
||||
header('Location: products.php');
|
||||
exit;
|
||||
40
_handle_login.php
Normal file
40
_handle_login.php
Normal file
@ -0,0 +1,40 @@
|
||||
<?php
|
||||
session_start();
|
||||
require_once 'db/config.php';
|
||||
|
||||
header('Content-Type: application/json');
|
||||
|
||||
$response = ['success' => false, 'message' => 'Invalid username or password.'];
|
||||
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
$username = $_POST['username'] ?? '';
|
||||
$password = $_POST['password'] ?? '';
|
||||
|
||||
if (empty($username) || empty($password)) {
|
||||
$response['message'] = 'Username and password are required.';
|
||||
echo json_encode($response);
|
||||
exit;
|
||||
}
|
||||
|
||||
try {
|
||||
$pdo = db();
|
||||
|
||||
$stmt = $pdo->prepare("SELECT id, username, password FROM users WHERE username = ?");
|
||||
$stmt->execute([$username]);
|
||||
$user = $stmt->fetch();
|
||||
|
||||
if ($user && password_verify($password, $user['password'])) {
|
||||
$_SESSION['user_id'] = $user['id'];
|
||||
$_SESSION['username'] = $user['username'];
|
||||
$response['success'] = true;
|
||||
$response['message'] = 'Login successful.';
|
||||
} else {
|
||||
$response['message'] = 'Invalid username or password.';
|
||||
}
|
||||
} catch (PDOException $e) {
|
||||
$response['message'] = 'Database error: ' . $e->getMessage();
|
||||
}
|
||||
|
||||
echo json_encode($response);
|
||||
}
|
||||
?>
|
||||
48
_handle_register.php
Normal file
48
_handle_register.php
Normal file
@ -0,0 +1,48 @@
|
||||
<?php
|
||||
require_once 'db/config.php';
|
||||
|
||||
header('Content-Type: application/json');
|
||||
|
||||
$response = ['success' => false, 'message' => 'An error occurred.'];
|
||||
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
$username = $_POST['username'] ?? '';
|
||||
$password = $_POST['password'] ?? '';
|
||||
|
||||
if (empty($username) || empty($password)) {
|
||||
$response['message'] = 'Username and password are required.';
|
||||
echo json_encode($response);
|
||||
exit;
|
||||
}
|
||||
|
||||
try {
|
||||
$pdo = db();
|
||||
|
||||
// Check if username already exists
|
||||
$stmt = $pdo->prepare("SELECT id FROM users WHERE username = ?");
|
||||
$stmt->execute([$username]);
|
||||
if ($stmt->fetch()) {
|
||||
$response['message'] = 'Username already taken.';
|
||||
echo json_encode($response);
|
||||
exit;
|
||||
}
|
||||
|
||||
// Hash the password
|
||||
$password_hash = password_hash($password, PASSWORD_DEFAULT);
|
||||
|
||||
// Insert the new user
|
||||
$stmt = $pdo->prepare("INSERT INTO users (username, password) VALUES (?, ?)");
|
||||
if ($stmt->execute([$username, $password_hash])) {
|
||||
$response['success'] = true;
|
||||
$response['message'] = 'Registration successful. You can now log in.';
|
||||
} else {
|
||||
$response['message'] = 'Failed to register user.';
|
||||
}
|
||||
} catch (PDOException $e) {
|
||||
// In a real application, you would log this error.
|
||||
$response['message'] = 'Database error: ' . $e->getMessage();
|
||||
}
|
||||
|
||||
echo json_encode($response);
|
||||
}
|
||||
?>
|
||||
58
assets/css/custom.css
Normal file
58
assets/css/custom.css
Normal file
@ -0,0 +1,58 @@
|
||||
/*
|
||||
SinarKasihMart Custom Styles
|
||||
Primary: #0d6efd (Blue)
|
||||
Secondary: #198754 (Green)
|
||||
*/
|
||||
|
||||
body {
|
||||
background-color: #f8f9fa;
|
||||
}
|
||||
|
||||
.navbar {
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
.card {
|
||||
border-radius: 0.375rem;
|
||||
box-shadow: 0 0.125rem 0.25rem rgba(0, 0, 0, 0.075);
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
background-color: #0d6efd;
|
||||
border-color: #0d6efd;
|
||||
}
|
||||
|
||||
.btn-success {
|
||||
background-color: #198754;
|
||||
border-color: #198754;
|
||||
}
|
||||
|
||||
.nav-link.active {
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.product-card {
|
||||
transition: transform .2s ease-in-out, box-shadow .2s ease-in-out;
|
||||
}
|
||||
|
||||
.product-card:hover {
|
||||
transform: translateY(-5px);
|
||||
box-shadow: 0 0.5rem 1rem rgba(0, 0, 0, 0.15);
|
||||
}
|
||||
|
||||
#saleItemsModal .modal-body {
|
||||
max-height: 400px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
#salesChart {
|
||||
max-height: 320px;
|
||||
}
|
||||
|
||||
#bestSellingChart {
|
||||
max-height: 320px;
|
||||
}
|
||||
|
||||
.chart-container {
|
||||
height: 400px; /* Adjust as needed */
|
||||
}
|
||||
41
catalog.php
Normal file
41
catalog.php
Normal file
@ -0,0 +1,41 @@
|
||||
<?php
|
||||
require_once __DIR__ . '/includes/auth.php';
|
||||
require_login();
|
||||
|
||||
require_once __DIR__ . '/db/config.php';
|
||||
require_once __DIR__ . '/includes/header.php';
|
||||
|
||||
// Fetch all products
|
||||
$stmt = db()->query('SELECT * FROM products WHERE stock > 0 ORDER BY created_at DESC');
|
||||
$products = $stmt->fetchAll();
|
||||
|
||||
?>
|
||||
|
||||
<div class="container px-4 py-5">
|
||||
<h2 class="pb-2 border-bottom">Product Catalog</h2>
|
||||
|
||||
<div class="row row-cols-1 row-cols-md-2 row-cols-lg-3 g-4 py-5">
|
||||
<?php if (empty($products)): ?>
|
||||
<div class="col">
|
||||
<p class="text-center">No products are currently available.</p>
|
||||
</div>
|
||||
<?php else: ?>
|
||||
<?php foreach ($products as $product): ?>
|
||||
<div class="col">
|
||||
<div class="card h-100 product-card">
|
||||
<div class="card-body">
|
||||
<h5 class="card-title"><?php echo htmlspecialchars($product['name']); ?></h5>
|
||||
<p class="card-text text-muted"><?php echo htmlspecialchars($product['category']); ?></p>
|
||||
<p class="card-text fs-5 fw-bold">Rp <?php echo number_format($product['price'], 2); ?></p>
|
||||
</div>
|
||||
<div class="card-footer bg-transparent border-top-0">
|
||||
<small class="text-muted">SKU: <?php echo htmlspecialchars($product['sku']); ?></small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<?php endforeach; ?>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<?php require_once __DIR__ . '/includes/footer.php'; ?>
|
||||
@ -8,10 +8,42 @@ define('DB_PASS', '6ba26df8-c17d-4aa6-b713-e17374a6fbd9');
|
||||
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,
|
||||
]);
|
||||
try {
|
||||
$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,
|
||||
]);
|
||||
} catch (PDOException $e) {
|
||||
// If database doesn't exist, create it
|
||||
if ($e->getCode() == 1049) {
|
||||
try {
|
||||
$tempPdo = new PDO('mysql:host='.DB_HOST, DB_USER, DB_PASS);
|
||||
$tempPdo->exec('CREATE DATABASE IF NOT EXISTS '.DB_NAME.' CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci');
|
||||
$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,
|
||||
]);
|
||||
} catch (PDOException $e2) {
|
||||
die('DB ERROR: Could not create database. ' . $e2->getMessage());
|
||||
}
|
||||
} else {
|
||||
die('DB ERROR: ' . $e->getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
return $pdo;
|
||||
}
|
||||
|
||||
function run_migrations() {
|
||||
$pdo = db();
|
||||
$migration_files = glob(__DIR__ . '/migrations/*.sql');
|
||||
foreach ($migration_files as $file) {
|
||||
$sql = file_get_contents($file);
|
||||
try {
|
||||
$pdo->exec($sql);
|
||||
} catch (PDOException $e) {
|
||||
// You might want to log this error instead of dying
|
||||
error_log('Migration failed for file ' . basename($file) . ': ' . $e->getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
9
db/migrations/001_create_products_table.sql
Normal file
9
db/migrations/001_create_products_table.sql
Normal file
@ -0,0 +1,9 @@
|
||||
CREATE TABLE IF NOT EXISTS products (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
name VARCHAR(255) NOT NULL,
|
||||
sku VARCHAR(100) UNIQUE,
|
||||
category VARCHAR(100),
|
||||
price DECIMAL(10, 2) NOT NULL,
|
||||
stock INT NOT NULL DEFAULT 0,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
17
db/migrations/002_create_sales_tables.sql
Normal file
17
db/migrations/002_create_sales_tables.sql
Normal file
@ -0,0 +1,17 @@
|
||||
CREATE TABLE IF NOT EXISTS sales (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
transaction_id VARCHAR(255) NOT NULL UNIQUE,
|
||||
total_amount DECIMAL(10, 2) NOT NULL,
|
||||
payment_method VARCHAR(50) NOT NULL,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS sale_items (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
sale_id INT NOT NULL,
|
||||
product_id INT NOT NULL,
|
||||
quantity INT NOT NULL,
|
||||
price DECIMAL(10, 2) NOT NULL,
|
||||
FOREIGN KEY (sale_id) REFERENCES sales(id) ON DELETE CASCADE,
|
||||
FOREIGN KEY (product_id) REFERENCES products(id) ON DELETE SET NULL
|
||||
);
|
||||
8
db/migrations/003_create_users_table.sql
Normal file
8
db/migrations/003_create_users_table.sql
Normal file
@ -0,0 +1,8 @@
|
||||
CREATE TABLE IF NOT EXISTS `users` (
|
||||
`id` int(11) NOT NULL AUTO_INCREMENT,
|
||||
`username` varchar(50) NOT NULL,
|
||||
`password` varchar(255) NOT NULL,
|
||||
`created_at` datetime DEFAULT CURRENT_TIMESTAMP,
|
||||
PRIMARY KEY (`id`),
|
||||
UNIQUE KEY `username` (`username`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=latin1;
|
||||
16
includes/auth.php
Normal file
16
includes/auth.php
Normal file
@ -0,0 +1,16 @@
|
||||
<?php
|
||||
if (session_status() == PHP_SESSION_NONE) {
|
||||
session_start();
|
||||
}
|
||||
|
||||
function is_logged_in() {
|
||||
return isset($_SESSION['user_id']);
|
||||
}
|
||||
|
||||
function require_login() {
|
||||
if (!is_logged_in()) {
|
||||
header('Location: login.php');
|
||||
exit;
|
||||
}
|
||||
}
|
||||
?>
|
||||
8
includes/footer.php
Normal file
8
includes/footer.php
Normal file
@ -0,0 +1,8 @@
|
||||
|
||||
</main>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/js/bootstrap.bundle.min.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
79
includes/header.php
Normal file
79
includes/header.php
Normal file
@ -0,0 +1,79 @@
|
||||
<?php require_once 'includes/auth.php'; ?>
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>SinarKasihMart</title>
|
||||
<meta name="description" content="Aplikasi kasir dan inventori untuk toko kelontong SinarKasihMart.">
|
||||
<meta name="keywords" content="pos, kasir, inventory, point of sale, toko kelontong, minimarket, sembako, SinarKasihMart, Built with Flatlogic Generator">
|
||||
<meta property="og:title" content="SinarKasihMart">
|
||||
<meta property="og:description" content="Aplikasi kasir dan inventori untuk toko kelontong SinarKasihMart.">
|
||||
<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 href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.min.css" rel="stylesheet">
|
||||
<link rel="stylesheet" href="assets/css/custom.css">
|
||||
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/js/bootstrap.bundle.min.js"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<nav class="navbar navbar-expand-lg navbar-dark" style="background-color: #0d6efd;">
|
||||
<div class="container-fluid">
|
||||
<a class="navbar-brand" href="index.php">
|
||||
<i class="bi bi-cart-fill"></i>
|
||||
SinarKasihMart
|
||||
</a>
|
||||
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNav" aria-controls="navbarNav" aria-expanded="false" aria-label="Toggle navigation">
|
||||
<span class="navbar-toggler-icon"></span>
|
||||
</button>
|
||||
<div class="collapse navbar-collapse" id="navbarNav">
|
||||
<?php if (is_logged_in()): ?>
|
||||
<ul class="navbar-nav me-auto mb-2 mb-lg-0">
|
||||
<li class="nav-item">
|
||||
<a class="nav-link <?php echo basename($_SERVER['PHP_SELF']) == 'catalog.php' ? 'active' : ''; ?>" href="catalog.php"><i class="bi bi-shop"></i> Catalog</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link <?php echo basename($_SERVER['PHP_SELF']) == 'index.php' ? 'active' : ''; ?>" href="index.php"><i class="bi bi-speedometer2"></i> Dashboard</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link <?php echo basename($_SERVER['PHP_SELF']) == 'products.php' ? 'active' : ''; ?>" href="products.php"><i class="bi bi-box-seam"></i> Products</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link <?php echo basename($_SERVER['PHP_SELF']) == 'pos.php' ? 'active' : ''; ?>" href="pos.php"><i class="bi bi-cash-register"></i> POS</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link <?php echo basename($_SERVER['PHP_SELF']) == 'reports.php' ? 'active' : ''; ?>" href="reports.php"><i class="bi bi-file-earmark-bar-graph"></i> Reports</a>
|
||||
</li>
|
||||
</ul>
|
||||
<?php endif; ?>
|
||||
<ul class="navbar-nav ms-auto">
|
||||
<?php if (is_logged_in()): ?>
|
||||
<li class="nav-item dropdown">
|
||||
<a class="nav-link dropdown-toggle" href="#" id="navbarDropdown" role="button" data-bs-toggle="dropdown" aria-expanded="false">
|
||||
<i class="bi bi-person-circle"></i> <?php echo htmlspecialchars($_SESSION['username']); ?>
|
||||
</a>
|
||||
<ul class="dropdown-menu dropdown-menu-end" aria-labelledby="navbarDropdown">
|
||||
<li><a class="dropdown-item" href="logout.php">Logout</a></li>
|
||||
</ul>
|
||||
</li>
|
||||
<?php else: ?>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link <?php echo basename($_SERVER['PHP_SELF']) == 'login.php' ? 'active' : ''; ?>" href="login.php">Login</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link <?php echo basename($_SERVER['PHP_SELF']) == 'register.php' ? 'active' : ''; ?>" href="register.php">Register</a>
|
||||
</li>
|
||||
<?php endif; ?>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<div class="container-fluid">
|
||||
<div class="row">
|
||||
<main class="col-md-12 ms-sm-auto col-lg-12 px-md-4">
|
||||
351
index.php
351
index.php
@ -1,150 +1,209 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
@ini_set('display_errors', '1');
|
||||
@error_reporting(E_ALL);
|
||||
@date_default_timezone_set('UTC');
|
||||
require_once __DIR__ . '/includes/auth.php';
|
||||
require_login();
|
||||
|
||||
require_once __DIR__ . '/db/config.php';
|
||||
|
||||
// Run migrations on first load
|
||||
if (session_status() == PHP_SESSION_NONE) {
|
||||
session_start();
|
||||
}
|
||||
if (!isset($_SESSION['migrated'])) {
|
||||
run_migrations();
|
||||
$_SESSION['migrated'] = true;
|
||||
}
|
||||
|
||||
require_once __DIR__ . '/includes/header.php';
|
||||
|
||||
// Fetch some stats for the dashboard
|
||||
$total_products = db()->query('SELECT COUNT(*) FROM products')->fetchColumn();
|
||||
|
||||
// Today's Sales
|
||||
$today = date('Y-m-d');
|
||||
$stmt_today = db()->prepare("SELECT COUNT(*) as num_transactions, SUM(total_amount) as total_sales FROM sales WHERE DATE(sale_date) = ?");
|
||||
$stmt_today->execute([$today]);
|
||||
$today_sales = $stmt_today->fetch(PDO::FETCH_ASSOC);
|
||||
|
||||
// All-Time Sales
|
||||
$stmt_all_time = db()->query("SELECT COUNT(*) as num_transactions, SUM(total_amount) as total_sales FROM sales");
|
||||
$all_time_sales = $stmt_all_time->fetch(PDO::FETCH_ASSOC);
|
||||
|
||||
|
||||
// Low Stock
|
||||
$low_stock_threshold = 5;
|
||||
$stmt_low_stock = db()->prepare('SELECT COUNT(*) FROM products WHERE stock <= ?');
|
||||
$stmt_low_stock->execute([$low_stock_threshold]);
|
||||
$low_stock_count = $stmt_low_stock->fetchColumn();
|
||||
|
||||
$phpVersion = PHP_VERSION;
|
||||
$now = date('Y-m-d H:i:s');
|
||||
?>
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<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>
|
||||
<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 class="d-flex justify-content-between flex-wrap flex-md-nowrap align-items-center pt-3 pb-2 mb-3 border-bottom">
|
||||
<h1 class="h2">Dashboard</h1>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-3">
|
||||
<div class="card text-white bg-primary mb-3">
|
||||
<div class="card-header">Total Products</div>
|
||||
<div class="card-body">
|
||||
<h5 class="card-title"><?php echo $total_products; ?></h5>
|
||||
<p class="card-text">items in inventory.</p>
|
||||
<a href="products.php" class="text-white">View details →</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
<footer>
|
||||
Page updated: <?= htmlspecialchars($now) ?> (UTC)
|
||||
</footer>
|
||||
</body>
|
||||
</html>
|
||||
<div class="col-md-3">
|
||||
<div class="card text-white bg-success mb-3">
|
||||
<div class="card-header">Today's Sales</div>
|
||||
<div class="card-body">
|
||||
<h5 class="card-title">Rp <?php echo number_format($today_sales['total_sales'] ?? 0, 2); ?></h5>
|
||||
<p class="card-text">from <?php echo $today_sales['num_transactions'] ?? 0; ?> transactions.</p>
|
||||
<a href="reports.php?start_date=<?php echo $today; ?>&end_date=<?php echo $today; ?>" class="text-white">View details →</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<div class="card text-white bg-info mb-3">
|
||||
<div class="card-header">All-Time Sales</div>
|
||||
<div class="card-body">
|
||||
<h5 class="card-title">Rp <?php echo number_format($all_time_sales['total_sales'] ?? 0, 2); ?></h5>
|
||||
<p class="card-text">from <?php echo $all_time_sales['num_transactions'] ?? 0; ?> transactions.</p>
|
||||
<a href="reports.php" class="text-white">View details →</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<div class="card text-white bg-warning mb-3">
|
||||
<div class="card-header">Low Stock Alerts</div>
|
||||
<div class="card-body">
|
||||
<h5 class="card-title"><?php echo $low_stock_count; ?></h5>
|
||||
<p class="card-text">items need restocking.</p>
|
||||
<a href="products.php?filter_low_stock=1" class="text-white">View details →</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-8">
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
Sales Trend (Last 7 Days)
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<canvas id="salesChart"></canvas>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
Best-Selling Products
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<canvas id="bestSellingChart"></canvas>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function () {
|
||||
// Sales Trend Chart
|
||||
fetch('_get_sales_data.php')
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
const salesCtx = document.getElementById('salesChart').getContext('2d');
|
||||
new Chart(salesCtx, {
|
||||
type: 'line',
|
||||
data: {
|
||||
labels: data.labels,
|
||||
datasets: [{
|
||||
label: 'Total Sales (Rp)',
|
||||
data: data.data,
|
||||
backgroundColor: 'rgba(13, 110, 253, 0.2)',
|
||||
borderColor: 'rgba(13, 110, 253, 1)',
|
||||
borderWidth: 1,
|
||||
fill: true,
|
||||
tension: 0.4
|
||||
}]
|
||||
},
|
||||
options: {
|
||||
scales: {
|
||||
y: {
|
||||
beginAtZero: true,
|
||||
ticks: {
|
||||
callback: function(value) {
|
||||
return 'Rp ' + value.toLocaleString();
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
plugins: {
|
||||
tooltip: {
|
||||
callbacks: {
|
||||
label: function(context) {
|
||||
let label = context.dataset.label || '';
|
||||
if (label) {
|
||||
label += ': ';
|
||||
}
|
||||
if (context.parsed.y !== null) {
|
||||
label += 'Rp ' + context.parsed.y.toLocaleString();
|
||||
}
|
||||
return label;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Best-Selling Products Chart
|
||||
fetch('_get_best_selling_products.php')
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
const bestSellingCtx = document.getElementById('bestSellingChart').getContext('2d');
|
||||
new Chart(bestSellingCtx, {
|
||||
type: 'bar',
|
||||
data: {
|
||||
labels: data.map(p => p.name),
|
||||
datasets: [{
|
||||
label: 'Quantity Sold',
|
||||
data: data.map(p => p.total_quantity),
|
||||
backgroundColor: [
|
||||
'rgba(255, 99, 132, 0.5)',
|
||||
'rgba(54, 162, 235, 0.5)',
|
||||
'rgba(255, 206, 86, 0.5)',
|
||||
'rgba(75, 192, 192, 0.5)',
|
||||
'rgba(153, 102, 255, 0.5)'
|
||||
],
|
||||
borderColor: [
|
||||
'rgba(255, 99, 132, 1)',
|
||||
'rgba(54, 162, 235, 1)',
|
||||
'rgba(255, 206, 86, 1)',
|
||||
'rgba(75, 192, 192, 1)',
|
||||
'rgba(153, 102, 255, 1)'
|
||||
],
|
||||
borderWidth: 1
|
||||
}]
|
||||
},
|
||||
options: {
|
||||
indexAxis: 'y',
|
||||
scales: {
|
||||
x: {
|
||||
beginAtZero: true
|
||||
}
|
||||
},
|
||||
plugins: {
|
||||
legend: {
|
||||
display: false
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
<?php require_once __DIR__ . '/includes/footer.php'; ?>
|
||||
55
login.php
Normal file
55
login.php
Normal file
@ -0,0 +1,55 @@
|
||||
<?php
|
||||
require_once __DIR__ . '/includes/header.php';
|
||||
?>
|
||||
|
||||
<div class="container mt-5">
|
||||
<div class="row justify-content-center">
|
||||
<div class="col-md-6">
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h2>Login</h2>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<form id="login-form">
|
||||
<div class="form-group">
|
||||
<label for="username">Username</label>
|
||||
<input type="text" class="form-control" id="username" name="username" required>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="password">Password</label>
|
||||
<input type="password" class="form-control" id="password" name="password" required>
|
||||
</div>
|
||||
<button type="submit" class="btn btn-primary">Login</button>
|
||||
</form>
|
||||
<div id="login-message" class="mt-3"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
$(document).ready(function() {
|
||||
$('#login-form').on('submit', function(e) {
|
||||
e.preventDefault();
|
||||
|
||||
$.ajax({
|
||||
url: '_handle_login.php',
|
||||
type: 'POST',
|
||||
data: $(this).serialize(),
|
||||
dataType: 'json',
|
||||
success: function(response) {
|
||||
if (response.success) {
|
||||
window.location.href = 'index.php';
|
||||
} else {
|
||||
$('#login-message').html('<div class="alert alert-danger">' + response.message + '</div>');
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
<?php
|
||||
require_once 'includes/footer.php';
|
||||
?>
|
||||
7
logout.php
Normal file
7
logout.php
Normal file
@ -0,0 +1,7 @@
|
||||
<?php
|
||||
session_start();
|
||||
session_unset();
|
||||
session_destroy();
|
||||
header('Location: login.php');
|
||||
exit;
|
||||
?>
|
||||
196
pos.php
Normal file
196
pos.php
Normal file
@ -0,0 +1,196 @@
|
||||
<?php
|
||||
require_once 'includes/auth.php';
|
||||
require_login();
|
||||
|
||||
require_once 'db/config.php';
|
||||
require_once __DIR__ . '/includes/header.php';
|
||||
|
||||
try {
|
||||
$stmt = db()->query("SELECT id, name, price, stock FROM products ORDER BY name ASC");
|
||||
$products = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||
} catch (PDOException $e) {
|
||||
die("Error fetching products: " . $e->getMessage());
|
||||
}
|
||||
?>
|
||||
|
||||
<div class="container-fluid mt-4">
|
||||
<div class="row">
|
||||
<!-- Product Selection -->
|
||||
<div class="col-md-7">
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h5 class="card-title mb-0">Products</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="row" id="product-grid">
|
||||
<?php if (empty($products)): ?>
|
||||
<div class="col-12">
|
||||
<div class="alert alert-info">No products found. Please <a href="product_add.php">add a product</a> first.</div>
|
||||
</div>
|
||||
<?php else: ?>
|
||||
<?php foreach ($products as $product): ?>
|
||||
<div class="col-md-4 col-lg-3 mb-3">
|
||||
<div class="card product-card h-100" onclick='addToCart(<?php echo htmlspecialchars(json_encode($product)); ?>)'>
|
||||
<div class="card-body text-center">
|
||||
<h6 class="card-title small"><?php echo htmlspecialchars($product['name']); ?></h6>
|
||||
<p class="card-text small fw-bold">IDR <?php echo number_format($product['price'], 2); ?></p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<?php endforeach; ?>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Cart -->
|
||||
<div class="col-md-5">
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h5 class="card-title mb-0">Cart</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div id="cart-items" class="mb-3">
|
||||
<p class="text-muted text-center">Cart is empty</p>
|
||||
</div>
|
||||
<hr>
|
||||
<div class="d-flex justify-content-between align-items-center mb-2">
|
||||
<h6 class="mb-0">Subtotal</h6>
|
||||
<h6 class="mb-0" id="cart-subtotal">IDR 0.00</h6>
|
||||
</div>
|
||||
<div class="d-flex justify-content-between align-items-center mb-3">
|
||||
<h5 class="mb-0">Total</h5>
|
||||
<h5 class="mb-0" id="cart-total">IDR 0.00</h5>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="payment-method" class="form-label">Payment Method</label>
|
||||
<select id="payment-method" class="form-select">
|
||||
<option>Cash</option>
|
||||
<option>Card</option>
|
||||
<option>QRIS</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="d-grid">
|
||||
<button class="btn btn-success btn-lg" id="checkout-btn" disabled>Checkout</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
const cart = [];
|
||||
|
||||
function formatCurrency(amount) {
|
||||
return new Intl.NumberFormat('id-ID', { style: 'currency', currency: 'IDR', minimumFractionDigits: 2 }).format(amount);
|
||||
}
|
||||
|
||||
function findCartItem(productId) {
|
||||
return cart.find(item => item.id === productId);
|
||||
}
|
||||
|
||||
function addToCart(product) {
|
||||
let itemInCart = findCartItem(product.id);
|
||||
|
||||
if (itemInCart) {
|
||||
itemInCart.quantity++;
|
||||
} else {
|
||||
cart.push({ id: product.id, name: product.name, price: product.price, quantity: 1 });
|
||||
}
|
||||
renderCart();
|
||||
}
|
||||
|
||||
function updateQuantity(productId, change) {
|
||||
let itemInCart = findCartItem(productId);
|
||||
if (itemInCart) {
|
||||
itemInCart.quantity += change;
|
||||
if (itemInCart.quantity <= 0) {
|
||||
const itemIndex = cart.findIndex(item => item.id === productId);
|
||||
cart.splice(itemIndex, 1);
|
||||
}
|
||||
}
|
||||
renderCart();
|
||||
}
|
||||
|
||||
function renderCart() {
|
||||
const cartItemsContainer = document.getElementById('cart-items');
|
||||
const subtotalEl = document.getElementById('cart-subtotal');
|
||||
const totalEl = document.getElementById('cart-total');
|
||||
const checkoutBtn = document.getElementById('checkout-btn');
|
||||
|
||||
if (cart.length === 0) {
|
||||
cartItemsContainer.innerHTML = '<p class="text-muted text-center">Cart is empty</p>';
|
||||
subtotalEl.textContent = formatCurrency(0);
|
||||
totalEl.textContent = formatCurrency(0);
|
||||
checkoutBtn.disabled = true;
|
||||
return;
|
||||
}
|
||||
|
||||
cartItemsContainer.innerHTML = cart.map(item => `
|
||||
<div class="d-flex justify-content-between align-items-center mb-2">
|
||||
<div>
|
||||
<h6 class="mb-0 small">${item.name}</h6>
|
||||
<p class="mb-0 text-muted small">${formatCurrency(item.price)}</p>
|
||||
</div>
|
||||
<div class="d-flex align-items-center">
|
||||
<button class="btn btn-sm btn-outline-secondary" onclick="updateQuantity(${item.id}, -1)">-</button>
|
||||
<span class="mx-2">${item.quantity}</span>
|
||||
<button class="btn btn-sm btn-outline-secondary" onclick="updateQuantity(${item.id}, 1)">+</button>
|
||||
</div>
|
||||
<h6 class="mb-0 small">${formatCurrency(item.price * item.quantity)}</h6>
|
||||
</div>
|
||||
`).join('');
|
||||
|
||||
const subtotal = cart.reduce((sum, item) => sum + (item.price * item.quantity), 0);
|
||||
|
||||
subtotalEl.textContent = formatCurrency(subtotal);
|
||||
totalEl.textContent = formatCurrency(subtotal); // Assuming no tax for now
|
||||
checkoutBtn.disabled = false;
|
||||
}
|
||||
|
||||
async function handleCheckout() {
|
||||
const paymentMethod = document.getElementById('payment-method').value;
|
||||
const checkoutBtn = document.getElementById('checkout-btn');
|
||||
checkoutBtn.disabled = true;
|
||||
checkoutBtn.textContent = 'Processing...';
|
||||
|
||||
try {
|
||||
const response = await fetch('_handle_checkout.php', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({ cart: cart, payment_method: paymentMethod })
|
||||
});
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
if (result.success) {
|
||||
const cartContainer = document.querySelector('.col-md-5 .card-body');
|
||||
cartContainer.innerHTML = `
|
||||
<div class="text-center">
|
||||
<h5 class="text-success">Checkout Successful!</h5>
|
||||
<p>Transaction ID: ${result.transaction_id}</p>
|
||||
<a href="receipt.php?sale_id=${result.transaction_id}" target="_blank" class="btn btn-primary">Print Receipt</a>
|
||||
<button onclick="window.location.reload()" class="btn btn-secondary">New Sale</button>
|
||||
</div>
|
||||
`;
|
||||
} else {
|
||||
alert(`Error: ${result.message}`);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Checkout error:', error);
|
||||
alert('An unexpected error occurred during checkout.');
|
||||
} finally {
|
||||
checkoutBtn.disabled = false;
|
||||
checkoutBtn.textContent = 'Checkout';
|
||||
}
|
||||
}
|
||||
|
||||
document.getElementById('checkout-btn').addEventListener('click', handleCheckout);
|
||||
|
||||
</script>
|
||||
|
||||
<?php require_once 'includes/footer.php'; ?>
|
||||
41
product_add.php
Normal file
41
product_add.php
Normal file
@ -0,0 +1,41 @@
|
||||
<?php
|
||||
require_once __DIR__ . '/includes/auth.php';
|
||||
require_login();
|
||||
|
||||
require_once __DIR__ . '/includes/header.php';
|
||||
?>
|
||||
|
||||
<div class="d-flex justify-content-between flex-wrap flex-md-nowrap align-items-center pt-3 pb-2 mb-3 border-bottom">
|
||||
<h1 class="h2">Add New Product</h1>
|
||||
</div>
|
||||
|
||||
<form action="_handle_add_product.php" method="POST">
|
||||
<div class="row">
|
||||
<div class="col-md-6 mb-3">
|
||||
<label for="name" class="form-label">Product Name</label>
|
||||
<input type="text" class="form-control" id="name" name="name" required>
|
||||
</div>
|
||||
<div class="col-md-6 mb-3">
|
||||
<label for="sku" class="form-label">SKU (Stock Keeping Unit)</label>
|
||||
<input type="text" class="form-control" id="sku" name="sku">
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-md-6 mb-3">
|
||||
<label for="category" class="form-label">Category</label>
|
||||
<input type="text" class="form-control" id="category" name="category">
|
||||
</div>
|
||||
<div class="col-md-3 mb-3">
|
||||
<label for="price" class="form-label">Price (Rp)</label>
|
||||
<input type="number" class="form-control" id="price" name="price" step="0.01" required>
|
||||
</div>
|
||||
<div class="col-md-3 mb-3">
|
||||
<label for="stock" class="form-label">Initial Stock</label>
|
||||
<input type="number" class="form-control" id="stock" name="stock" value="0" required>
|
||||
</div>
|
||||
</div>
|
||||
<button type="submit" class="btn btn-primary">Save Product</button>
|
||||
<a href="products.php" class="btn btn-secondary">Cancel</a>
|
||||
</form>
|
||||
|
||||
<?php require_once __DIR__ . '/includes/footer.php'; ?>
|
||||
63
product_edit.php
Normal file
63
product_edit.php
Normal file
@ -0,0 +1,63 @@
|
||||
<?php
|
||||
require_once __DIR__ . '/includes/auth.php';
|
||||
require_login();
|
||||
|
||||
require_once __DIR__ . '/db/config.php';
|
||||
require_once __DIR__ . '/includes/header.php';
|
||||
|
||||
// Check if ID is provided
|
||||
if (!isset($_GET['id']) || !is_numeric($_GET['id'])) {
|
||||
// Redirect or show an error
|
||||
header('Location: products.php');
|
||||
exit;
|
||||
}
|
||||
|
||||
$id = $_GET['id'];
|
||||
|
||||
// Fetch the product
|
||||
$stmt = db()->prepare('SELECT * FROM products WHERE id = ?');
|
||||
$stmt->execute([$id]);
|
||||
$product = $stmt->fetch();
|
||||
|
||||
if (!$product) {
|
||||
// Redirect or show an error
|
||||
header('Location: products.php');
|
||||
exit;
|
||||
}
|
||||
?>
|
||||
|
||||
<div class="d-flex justify-content-between flex-wrap flex-md-nowrap align-items-center pt-3 pb-2 mb-3 border-bottom">
|
||||
<h1 class="h2">Edit Product</h1>
|
||||
</div>
|
||||
|
||||
<form action="_handle_edit_product.php" method="POST">
|
||||
<input type="hidden" name="id" value="<?php echo $product['id']; ?>">
|
||||
<div class="row">
|
||||
<div class="col-md-6 mb-3">
|
||||
<label for="name" class="form-label">Product Name</label>
|
||||
<input type="text" class="form-control" id="name" name="name" value="<?php echo htmlspecialchars($product['name']); ?>" required>
|
||||
</div>
|
||||
<div class="col-md-6 mb-3">
|
||||
<label for="sku" class="form-label">SKU (Stock Keeping Unit)</label>
|
||||
<input type="text" class="form-control" id="sku" name="sku" value="<?php echo htmlspecialchars($product['sku']); ?>">
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-md-6 mb-3">
|
||||
<label for="category" class="form-label">Category</label>
|
||||
<input type="text" class="form-control" id="category" name="category" value="<?php echo htmlspecialchars($product['category']); ?>">
|
||||
</div>
|
||||
<div class="col-md-3 mb-3">
|
||||
<label for="price" class="form-label">Price (Rp)</label>
|
||||
<input type="number" class="form-control" id="price" name="price" step="0.01" value="<?php echo htmlspecialchars($product['price']); ?>" required>
|
||||
</div>
|
||||
<div class="col-md-3 mb-3">
|
||||
<label for="stock" class="form-label">Stock</label>
|
||||
<input type="number" class="form-control" id="stock" name="stock" value="<?php echo htmlspecialchars($product['stock']); ?>" required>
|
||||
</div>
|
||||
</div>
|
||||
<button type="submit" class="btn btn-primary">Update Product</button>
|
||||
<a href="products.php" class="btn btn-secondary">Cancel</a>
|
||||
</form>
|
||||
|
||||
<?php require_once __DIR__ . '/includes/footer.php'; ?>
|
||||
70
products.php
Normal file
70
products.php
Normal file
@ -0,0 +1,70 @@
|
||||
<?php
|
||||
require_once __DIR__ . '/includes/auth.php';
|
||||
require_login();
|
||||
|
||||
require_once __DIR__ . '/db/config.php';
|
||||
require_once __DIR__ . '/includes/header.php';
|
||||
|
||||
$low_stock_threshold = 5;
|
||||
$filter_low_stock = isset($_GET['filter_low_stock']);
|
||||
|
||||
$page_title = "Products";
|
||||
$query = 'SELECT * FROM products ORDER BY created_at DESC';
|
||||
|
||||
if ($filter_low_stock) {
|
||||
$page_title = "Low Stock Products (<= {$low_stock_threshold} items)";
|
||||
$query = "SELECT * FROM products WHERE stock <= {$low_stock_threshold} ORDER BY stock ASC";
|
||||
}
|
||||
|
||||
$stmt = db()->query($query);
|
||||
$products = $stmt->fetchAll();
|
||||
|
||||
?>
|
||||
|
||||
<div class="d-flex justify-content-between flex-wrap flex-md-nowrap align-items-center pt-3 pb-2 mb-3 border-bottom">
|
||||
<h1 class="h2"><?php echo $page_title; ?></h1>
|
||||
<div class="btn-toolbar mb-2 mb-md-0">
|
||||
<a href="product_add.php" class="btn btn-sm btn-primary">
|
||||
<i class="bi bi-plus-lg"></i>
|
||||
Add New Product
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="table-responsive">
|
||||
<table class="table table-striped table-sm">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>SKU</th>
|
||||
<th>Name</th>
|
||||
<th>Category</th>
|
||||
<th>Price</th>
|
||||
<th>Stock</th>
|
||||
<th>Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php if (empty($products)): ?>
|
||||
<tr>
|
||||
<td colspan="6" class="text-center">No products found. <a href="product_add.php">Add one!</a></td>
|
||||
</tr>
|
||||
<?php else: ?>
|
||||
<?php foreach ($products as $product): ?>
|
||||
<tr>
|
||||
<td><?php echo htmlspecialchars($product['sku']); ?></td>
|
||||
<td><?php echo htmlspecialchars($product['name']); ?></td>
|
||||
<td><?php echo htmlspecialchars($product['category']); ?></td>
|
||||
<td>Rp <?php echo number_format($product['price'], 2); ?></td>
|
||||
<td><?php echo htmlspecialchars($product['stock']); ?></td>
|
||||
<td>
|
||||
<a href="product_edit.php?id=<?php echo $product['id']; ?>" class="btn btn-sm btn-outline-secondary"><i class="bi bi-pencil-square"></i></a>
|
||||
<a href="_handle_delete_product.php?id=<?php echo $product['id']; ?>" class="btn btn-sm btn-outline-danger" onclick="return confirm('Are you sure you want to delete this product?');"><i class="bi bi-trash"></i></a>
|
||||
</td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
<?php endif; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<?php require_once __DIR__ . '/includes/footer.php'; ?>
|
||||
134
receipt.php
Normal file
134
receipt.php
Normal file
@ -0,0 +1,134 @@
|
||||
<?php
|
||||
require_once 'includes/auth.php';
|
||||
require_login();
|
||||
|
||||
require_once 'db/config.php';
|
||||
|
||||
if (!isset($_GET['sale_id']) || !is_numeric($_GET['sale_id'])) {
|
||||
die('Invalid Sale ID.');
|
||||
}
|
||||
|
||||
$sale_id = $_GET['sale_id'];
|
||||
$pdo = db();
|
||||
|
||||
// Get sale details
|
||||
$sale_stmt = $pdo->prepare("SELECT * FROM sales WHERE id = ?");
|
||||
$sale_stmt->execute([$sale_id]);
|
||||
$sale = $sale_stmt->fetch(PDO::FETCH_ASSOC);
|
||||
|
||||
if (!$sale) {
|
||||
die('Sale not found.');
|
||||
}
|
||||
|
||||
// Get sale items
|
||||
$items_stmt = $pdo->prepare(
|
||||
"SELECT si.*, p.name as product_name
|
||||
FROM sale_items si
|
||||
JOIN products p ON si.product_id = p.id
|
||||
WHERE si.sale_id = ?"
|
||||
);
|
||||
$items_stmt->execute([$sale_id]);
|
||||
$items = $items_stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||
|
||||
?>
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Receipt - Sale #<?php echo htmlspecialchars($sale['id']); ?></title>
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css" rel="stylesheet">
|
||||
<style>
|
||||
body {
|
||||
background-color: #f8f9fa;
|
||||
font-family: 'Courier New', Courier, monospace;
|
||||
}
|
||||
.receipt-container {
|
||||
max-width: 400px;
|
||||
margin: 2rem auto;
|
||||
padding: 2rem;
|
||||
background-color: #fff;
|
||||
border: 1px dashed #ccc;
|
||||
}
|
||||
.receipt-header {
|
||||
text-align: center;
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
.receipt-header h2 {
|
||||
margin: 0;
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
.receipt-item {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
.receipt-total {
|
||||
font-weight: bold;
|
||||
border-top: 1px dashed #000;
|
||||
margin-top: 1rem;
|
||||
padding-top: 1rem;
|
||||
}
|
||||
@media print {
|
||||
body {
|
||||
background-color: #fff;
|
||||
}
|
||||
.receipt-container {
|
||||
margin: 0;
|
||||
max-width: 100%;
|
||||
border: none;
|
||||
box-shadow: none;
|
||||
}
|
||||
.no-print {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="receipt-container">
|
||||
<div class="receipt-header">
|
||||
<h2>Your Store Name</h2>
|
||||
<p>123 Main Street, Anytown, USA</p>
|
||||
<p>Date: <?php echo htmlspecialchars(date("m/d/Y H:i", strtotime($sale['sale_date']))); ?></p>
|
||||
<p>Receipt #: <?php echo htmlspecialchars($sale['id']); ?></p>
|
||||
</div>
|
||||
|
||||
<div class="receipt-body">
|
||||
<div class="receipt-item fw-bold">
|
||||
<span>Item</span>
|
||||
<span>Price</span>
|
||||
</div>
|
||||
<?php foreach ($items as $item): ?>
|
||||
<div class="receipt-item">
|
||||
<span>
|
||||
<?php echo htmlspecialchars($item['product_name']); ?><br>
|
||||
<small>(<?php echo htmlspecialchars($item['quantity']); ?> @ $<?php echo htmlspecialchars(number_format($item['price'], 2)); ?>)</small>
|
||||
</span>
|
||||
<span>$<?php echo htmlspecialchars(number_format($item['quantity'] * $item['price'], 2)); ?></span>
|
||||
</div>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
|
||||
<div class="receipt-footer">
|
||||
<div class="receipt-item receipt-total">
|
||||
<span>Total</span>
|
||||
<span>$<?php echo htmlspecialchars(number_format($sale['total_amount'], 2)); ?></span>
|
||||
</div>
|
||||
<div class="receipt-item">
|
||||
<span>Payment Method</span>
|
||||
<span><?php echo htmlspecialchars($sale['payment_method']); ?></span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="text-center mt-4">
|
||||
<p>Thank you for your business!</p>
|
||||
</div>
|
||||
|
||||
<div class="text-center mt-4 no-print">
|
||||
<button class="btn btn-primary" onclick="window.print();">Print Receipt</button>
|
||||
<a href="pos.php" class="btn btn-secondary">New Sale</a>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
70
register.php
Normal file
70
register.php
Normal file
@ -0,0 +1,70 @@
|
||||
<?php
|
||||
require_once __DIR__ . '/includes/header.php';
|
||||
?>
|
||||
|
||||
<div class="container mt-5">
|
||||
<div class="row justify-content-center">
|
||||
<div class="col-md-6">
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h2>Register</h2>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<form id="register-form">
|
||||
<div class="form-group">
|
||||
<label for="username">Username</label>
|
||||
<input type="text" class="form-control" id="username" name="username" required>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="password">Password</label>
|
||||
<input type="password" class="form-control" id="password" name="password" required>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="confirm_password">Confirm Password</label>
|
||||
<input type="password" class="form-control" id="confirm_password" name="confirm_password" required>
|
||||
</div>
|
||||
<button type="submit" class="btn btn-primary">Register</button>
|
||||
</form>
|
||||
<div id="register-message" class="mt-3"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
$(document).ready(function() {
|
||||
$('#register-form').on('submit', function(e) {
|
||||
e.preventDefault();
|
||||
|
||||
let password = $('#password').val();
|
||||
let confirm_password = $('#confirm_password').val();
|
||||
|
||||
if (password !== confirm_password) {
|
||||
$('#register-message').html('<div class="alert alert-danger">Passwords do not match.</div>');
|
||||
return;
|
||||
}
|
||||
|
||||
$.ajax({
|
||||
url: '_handle_register.php',
|
||||
type: 'POST',
|
||||
data: $(this).serialize(),
|
||||
dataType: 'json',
|
||||
success: function(response) {
|
||||
if (response.success) {
|
||||
$('#register-message').html('<div class="alert alert-success">' + response.message + '</div>');
|
||||
setTimeout(function() {
|
||||
window.location.href = 'login.php';
|
||||
}, 2000);
|
||||
} else {
|
||||
$('#register-message').html('<div class="alert alert-danger">' + response.message + '</div>');
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
<?php
|
||||
require_once 'includes/footer.php';
|
||||
?>
|
||||
192
reports.php
Normal file
192
reports.php
Normal file
@ -0,0 +1,192 @@
|
||||
<?php
|
||||
require_once 'includes/auth.php';
|
||||
require_login();
|
||||
|
||||
require_once __DIR__ . '/includes/header.php';
|
||||
require_once 'db/config.php';
|
||||
|
||||
$pdo = db();
|
||||
|
||||
// Get distinct payment methods for the filter dropdown
|
||||
$payment_methods_stmt = $pdo->query("SELECT DISTINCT payment_method FROM sales ORDER BY payment_method");
|
||||
$payment_methods = $payment_methods_stmt->fetchAll(PDO::FETCH_COLUMN);
|
||||
|
||||
// Filter logic
|
||||
$start_date = $_GET['start_date'] ?? '';
|
||||
$end_date = $_GET['end_date'] ?? '';
|
||||
$payment_method = $_GET['payment_method'] ?? '';
|
||||
|
||||
$sql = "SELECT * FROM sales";
|
||||
$conditions = [];
|
||||
$params = [];
|
||||
|
||||
if ($start_date) {
|
||||
$conditions[] = "sale_date >= ?";
|
||||
$params[] = $start_date . ' 00:00:00';
|
||||
}
|
||||
if ($end_date) {
|
||||
$conditions[] = "sale_date <= ?";
|
||||
$params[] = $end_date . ' 23:59:59';
|
||||
}
|
||||
if ($payment_method) {
|
||||
$conditions[] = "payment_method = ?";
|
||||
$params[] = $payment_method;
|
||||
}
|
||||
|
||||
if (count($conditions) > 0) {
|
||||
$sql .= " WHERE " . implode(' AND ', $conditions);
|
||||
}
|
||||
|
||||
$sql .= " ORDER BY sale_date DESC";
|
||||
|
||||
$sales_stmt = $pdo->prepare($sql);
|
||||
$sales_stmt->execute($params);
|
||||
$sales = $sales_stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||
?>
|
||||
|
||||
<div class="container-fluid">
|
||||
<h1 class="mt-4">Sales Reports</h1>
|
||||
|
||||
<div class="card mb-4">
|
||||
<div class="card-header">
|
||||
<i class="fas fa-filter me-1"></i>
|
||||
Filter Sales
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<form action="reports.php" method="GET" class="row g-3">
|
||||
<div class="col-md-4">
|
||||
<label for="start_date" class="form-label">Start Date</label>
|
||||
<input type="date" class="form-control" id="start_date" name="start_date" value="<?php echo htmlspecialchars($start_date); ?>">
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<label for="end_date" class="form-label">End Date</label>
|
||||
<input type="date" class="form-control" id="end_date" name="end_date" value="<?php echo htmlspecialchars($end_date); ?>">
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<label for="payment_method" class="form-label">Payment Method</label>
|
||||
<select id="payment_method" name="payment_method" class="form-select">
|
||||
<option value="">All</option>
|
||||
<?php foreach ($payment_methods as $pm): ?>
|
||||
<option value="<?php echo htmlspecialchars($pm); ?>" <?php echo ($payment_method === $pm) ? 'selected' : ''; ?>>
|
||||
<?php echo htmlspecialchars($pm); ?>
|
||||
</option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-12">
|
||||
<button type="submit" class="btn btn-primary">Filter</button>
|
||||
<a href="reports.php" class="btn btn-secondary">Clear</a>
|
||||
<a href="_export_sales_report.php?start_date=<?php echo $start_date; ?>&end_date=<?php echo $end_date; ?>&payment_method=<?php echo $payment_method; ?>" class="btn btn-success">Export to CSV</a>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card mb-4">
|
||||
<div class="card-header">
|
||||
<i class="fas fa-table me-1"></i>
|
||||
Filtered Sales
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<table id="datatablesSimple" class="table table-striped">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>ID</th>
|
||||
<th>Date</th>
|
||||
<th>Total Amount</th>
|
||||
<th>Payment Method</th>
|
||||
<th>Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php if (count($sales) > 0): ?>
|
||||
<?php foreach ($sales as $sale): ?>
|
||||
<tr>
|
||||
<td><?php echo htmlspecialchars($sale['id']); ?></td>
|
||||
<td><?php echo htmlspecialchars($sale['sale_date']); ?></td>
|
||||
<td>$<?php echo htmlspecialchars(number_format($sale['total_amount'], 2)); ?></td>
|
||||
<td><?php echo htmlspecialchars($sale['payment_method']); ?></td>
|
||||
<td>
|
||||
<button class="btn btn-sm btn-primary" data-bs-toggle="modal" data-bs-target="#saleItemsModal" data-sale-id="<?php echo $sale['id']; ?>">
|
||||
View Items
|
||||
</button>
|
||||
<a href="receipt.php?sale_id=<?php echo $sale['id']; ?>" target="_blank" class="btn btn-sm btn-secondary">
|
||||
<i class="fas fa-print"></i> Receipt
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
<?php else: ?>
|
||||
<tr>
|
||||
<td colspan="5" class="text-center">No sales found matching your criteria.</td>
|
||||
</tr>
|
||||
<?php endif; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Modal -->
|
||||
<div class="modal fade" id="saleItemsModal" tabindex="-1" aria-labelledby="saleItemsModalLabel" aria-hidden="true">
|
||||
<div class="modal-dialog modal-lg">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title" id="saleItemsModalLabel">Sale Items</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<table class="table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Product</th>
|
||||
<th>Quantity</th>
|
||||
<th>Price</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="saleItemsTbody">
|
||||
<!-- Items will be loaded here via JavaScript -->
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function () {
|
||||
const saleItemsModal = document.getElementById('saleItemsModal');
|
||||
saleItemsModal.addEventListener('show.bs.modal', function (event) {
|
||||
const button = event.relatedTarget;
|
||||
const saleId = button.getAttribute('data-sale-id');
|
||||
const modalBody = saleItemsModal.querySelector('#saleItemsTbody');
|
||||
modalBody.innerHTML = '<tr><td colspan="3">Loading...</td></tr>';
|
||||
|
||||
fetch('_get_sale_items.php?sale_id=' + saleId)
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
let html = '';
|
||||
if(data.error){
|
||||
html = `<tr><td colspan="3">${data.error}</td></tr>`;
|
||||
} else {
|
||||
data.forEach(item => {
|
||||
html += `
|
||||
<tr>
|
||||
<td>${item.product_name}</td>
|
||||
<td>${item.quantity}</td>
|
||||
<td>$${parseFloat(item.price).toFixed(2)}</td>
|
||||
</tr>
|
||||
`;
|
||||
});
|
||||
}
|
||||
modalBody.innerHTML = html;
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Error fetching sale items:', error);
|
||||
modalBody.innerHTML = '<tr><td colspan="3">Error loading items.</td></tr>';
|
||||
});
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
<?php require_once 'includes/footer.php'; ?>
|
||||
Loading…
x
Reference in New Issue
Block a user