diff --git a/public/index.php b/public/index.php
index b11557f..849a664 100644
--- a/public/index.php
+++ b/public/index.php
@@ -75,7 +75,7 @@ if (str_starts_with(haystack: $route, needle: '/.well-known/lnurlp/')) {
}
// Use this controller for routes that include a model ID
-if (preg_match('/^\/(address(?:\/edit|\/delete)?|transaction|user|order|quote|product|subscription|cart|admin\/categories\/edit)\/([\w-]+)$/', $route, $matches)) {
+if (preg_match('/^\/(address(?:\/edit|\/delete)?|transaction|user|order|quote|product|subscription|cart|admin\/categories\/edit|admin\/products\/edit)\/([\w-]+)$/', $route, $matches)) {
[$full, $type, $id] = $matches;
$controller = [
'address/edit' => fn($id) => address::edit($defaults, $id),
@@ -88,6 +88,7 @@ if (preg_match('/^\/(address(?:\/edit|\/delete)?|transaction|user|order|quote|pr
'subscription' => fn($id) => subscriptions::view($id),
'cart' => fn($id) => cart::index($id),
'admin/categories/edit' => fn($id) => $defaults['is_admin'] ? admin::categories_edit($defaults, $id) : lost::index($defaults),
+ 'admin/products/edit' => fn($id) => $defaults['is_admin'] ? admin::products_edit($defaults, $id) : lost::index($defaults),
];
if (isset($controller[$type])) {
@@ -114,6 +115,7 @@ if (preg_match('/^\/(address(?:\/edit|\/delete)?|transaction|user|order|quote|pr
'/admin' => $defaults['is_admin'] ? admin::index($defaults) : lost::index($defaults),
'/admin/users' => $defaults['is_admin'] ? admin::users($defaults) : lost::index($defaults),
'/admin/products' => $defaults['is_admin'] ? admin::products($defaults) : lost::index($defaults),
+ '/admin/products/add' => $defaults['is_admin'] ? admin::products_add($defaults) : lost::index($defaults),
'/admin/orders' => $defaults['is_admin'] ? admin::orders($defaults) : lost::index($defaults),
'/admin/emails' => $defaults['is_admin'] ? admin::emails($defaults) : lost::index($defaults),
'/admin/categories' => $defaults['is_admin'] ? admin::categories($defaults) : lost::index($defaults),
diff --git a/src/controllers/admin.php b/src/controllers/admin.php
index bd8a793..c5a6a4f 100644
--- a/src/controllers/admin.php
+++ b/src/controllers/admin.php
@@ -3,6 +3,7 @@ namespace app\controllers;
use app\models\categories;
use app\models\emails;
+use app\models\products;
use app\models\transactions;
use app\models\users;
@@ -27,6 +28,7 @@ class admin
echo $GLOBALS['twig']->render('lib/pages/index.twig', array_merge($defaults, [
'child_template' => 'admin/users.twig',
'page_title' => 'Users',
+ 'users' => users::getUsers(20),
'breadcrumbs' => [
[
'url' => '/admin',
@@ -46,6 +48,7 @@ class admin
'child_template' => 'admin/products/index.twig',
'page_title' => 'Products',
'categories' => categories::getTree(),
+ 'products' => products::get(20),
'breadcrumbs' => [
[
'url' => '/admin',
@@ -59,6 +62,84 @@ class admin
]));
}
+ public static function products_add($defaults)
+ {
+ if ($_SERVER['REQUEST_METHOD'] == 'POST') {
+ $_SESSION['last_post'] = $_POST;
+ $valid = products::validateProductData($_POST);
+ if (! empty($valid['errors'])) {
+ $_SESSION['error'] = implode(' ', $valid['errors']);
+ header('Location: /admin/products/add');
+ exit;
+ }
+ products::add($valid);
+ $_SESSION['success'] = 'Product added successfully.';
+ unset($_SESSION['last_post']);
+ header('Location: /admin/products/add');
+ exit;
+ }
+ echo $GLOBALS['twig']->render('lib/pages/index.twig', array_merge($defaults, [
+ 'child_template' => 'admin/products/add.twig',
+ 'page_title' => 'Add a Product',
+ 'categories' => categories::getOptions(),
+ 'breadcrumbs' => [
+ [
+ 'url' => '/admin',
+ 'title' => 'Admin',
+ ],
+ [
+ 'url' => '/admin/products',
+ 'title' => 'Products',
+ ],
+ [
+ 'url' => '/admin/products/add',
+ 'title' => 'Add Product',
+ ],
+ ],
+ ]));
+ }
+
+ public static function products_edit($defaults, $id)
+ {
+ if ($_SERVER['REQUEST_METHOD'] == 'POST') {
+ $_SESSION['last_post'] = $_POST;
+ $valid = products::validateProductData($_POST);
+
+ if (! empty($valid['errors'])) {
+ $_SESSION['error'] = implode(' ', $valid['errors']);
+ header("Location: /admin/products/edit/$id");
+ exit;
+ }
+ products::update($id, $valid);
+ $_SESSION['success'] = 'Product updated successfully.';
+ unset($_SESSION['last_post']);
+ header("Location: /admin/products/edit/$id");
+ exit;
+ }
+ $product = products::getById($id);
+
+ echo $GLOBALS['twig']->render('lib/pages/index.twig', array_merge($defaults, [
+ 'child_template' => 'admin/products/edit.twig',
+ 'page_title' => 'Edit Product',
+ 'product' => $product,
+ 'categories' => categories::getOptions(),
+ 'breadcrumbs' => [
+ [
+ 'url' => '/admin',
+ 'title' => 'Admin',
+ ],
+ [
+ 'url' => '/admin/products',
+ 'title' => 'Products',
+ ],
+ [
+ 'url' => "/admin/products/edit/$id",
+ 'title' => 'Edit Product',
+ ],
+ ],
+ ]));
+ }
+
public static function categories($defaults)
{
echo $GLOBALS['twig']->render('lib/pages/index.twig', array_merge($defaults, [
diff --git a/src/models/addresses.php b/src/models/addresses.php
index cefcd90..06bc148 100644
--- a/src/models/addresses.php
+++ b/src/models/addresses.php
@@ -9,7 +9,7 @@ class addresses
{
app::$db->exec("CREATE TABLE IF NOT EXISTS addresses (
id INTEGER PRIMARY KEY AUTOINCREMENT,
- user_id INTEGER,
+ user_id INTEGER REFERENCES users(id),
name TEXT NOT NULL,
company TEXT,
addressLine1 TEXT NOT NULL,
@@ -17,8 +17,7 @@ class addresses
city TEXT NOT NULL,
state TEXT NOT NULL,
zip TEXT NOT NULL,
- phone TEXT,
- FOREIGN KEY (user_id) REFERENCES users(id)
+ phone TEXT
)");
}
diff --git a/src/models/cart_items.php b/src/models/cart_items.php
index edbaef1..8d4f829 100644
--- a/src/models/cart_items.php
+++ b/src/models/cart_items.php
@@ -9,12 +9,10 @@ class cart_items
{
app::$db->exec("CREATE TABLE IF NOT EXISTS cart_items (
id INTEGER PRIMARY KEY AUTOINCREMENT,
- cart_id INTEGER NOT NULL,
- product_id INTEGER NOT NULL,
+ cart_id INTEGER NOT NULL REFERENCES carts(id) ON DELETE CASCADE,
+ product_id INTEGER NOT NULL REFERENCES products(id),
quantity INTEGER NOT NULL CHECK(quantity > 0),
- added_at DATETIME DEFAULT CURRENT_TIMESTAMP,
- FOREIGN KEY (cart_id) REFERENCES carts(id) ON DELETE CASCADE,
- FOREIGN KEY (product_id) REFERENCES products(id)
+ added_at DATETIME DEFAULT CURRENT_TIMESTAMP
);");
}
diff --git a/src/models/carts.php b/src/models/carts.php
index 7bf304d..84eb24e 100644
--- a/src/models/carts.php
+++ b/src/models/carts.php
@@ -9,10 +9,9 @@ class carts
{
app::$db->exec("CREATE TABLE IF NOT EXISTS carts (
id INTEGER PRIMARY KEY AUTOINCREMENT,
- user_id INTEGER NOT NULL,
+ user_id INTEGER NOT NULL REFERENCES users(id),
short_id TEXT UNIQUE NOT NULL,
- created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
- FOREIGN KEY (user_id) REFERENCES users(id)
+ created_at DATETIME DEFAULT CURRENT_TIMESTAMP
);");
}
diff --git a/src/models/categories.php b/src/models/categories.php
index 5d30a96..45c7f18 100644
--- a/src/models/categories.php
+++ b/src/models/categories.php
@@ -9,10 +9,9 @@ class categories
{
app::$db->exec("CREATE TABLE IF NOT EXISTS categories (
id INTEGER PRIMARY KEY AUTOINCREMENT,
- parent_id INTEGER,
+ parent_id INTEGER REFERENCES categories(id),
label TEXT UNIQUE NOT NULL,
- value TEXT UNIQUE NOT NULL,
- FOREIGN KEY (parent_id) REFERENCES categories(id)
+ value TEXT UNIQUE NOT NULL
);");
}
diff --git a/src/models/emails.php b/src/models/emails.php
index b39f3ed..818eab0 100644
--- a/src/models/emails.php
+++ b/src/models/emails.php
@@ -11,15 +11,14 @@ class emails
{
app::$db->exec("CREATE TABLE IF NOT EXISTS emails (
id INTEGER PRIMARY KEY AUTOINCREMENT,
- user_id INTEGER,
+ user_id INTEGER REFERENCES users(id),
from_email TEXT NOT NULL,
from_name TEXT NOT NULL,
to_email TEXT NOT NULL,
subject TEXT NOT NULL,
message TEXT NOT NULL,
html_message TEXT NOT NULL,
- created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
- FOREIGN KEY (user_id) REFERENCES users(id)
+ created_at DATETIME DEFAULT CURRENT_TIMESTAMP
);");
}
diff --git a/src/models/invoices.php b/src/models/invoices.php
index 3d98880..661b939 100644
--- a/src/models/invoices.php
+++ b/src/models/invoices.php
@@ -10,15 +10,13 @@ class invoices
{
$query = "CREATE TABLE IF NOT EXISTS invoices (
id INTEGER PRIMARY KEY AUTOINCREMENT,
- user_id INTEGER NOT NULL,
- order_id INTEGER,
+ user_id INTEGER NOT NULL REFERENCES users(id),
+ order_id INTEGER REFERENCES orders(id),
invoice TEXT NOT NULL,
verify TEXT NOT NULL,
amount_msats REAL NOT NULL,
expiry_date DATETIME NOT NULL,
- settled BOOLEAN DEFAULT FALSE,
- FOREIGN KEY(user_id) REFERENCES users(id),
- FOREIGN KEY(order_id) REFERENCES orders(id)
+ settled BOOLEAN DEFAULT FALSE
)";
app::$db->exec($query);
}
diff --git a/src/models/order_items.php b/src/models/order_items.php
index 3b71a78..4992dec 100644
--- a/src/models/order_items.php
+++ b/src/models/order_items.php
@@ -9,12 +9,10 @@ class order_items
{
app::$db->exec("CREATE TABLE IF NOT EXISTS order_items (
id INTEGER PRIMARY KEY AUTOINCREMENT,
- order_id INTEGER NOT NULL,
- product_id INTEGER NOT NULL,
+ order_id INTEGER NOT NULL REFERENCES orders(id) ON DELETE CASCADE,
+ product_id INTEGER NOT NULL REFERENCES products(id),
quantity INTEGER NOT NULL CHECK(quantity > 0),
- added_at DATETIME DEFAULT CURRENT_TIMESTAMP,
- FOREIGN KEY (order_id) REFERENCES orders(id) ON DELETE CASCADE,
- FOREIGN KEY (product_id) REFERENCES products(id)
+ added_at DATETIME DEFAULT CURRENT_TIMESTAMP
);");
}
diff --git a/src/models/orders.php b/src/models/orders.php
index 931ba7e..c417fa6 100644
--- a/src/models/orders.php
+++ b/src/models/orders.php
@@ -14,12 +14,11 @@ class orders
{
app::$db->exec("CREATE TABLE IF NOT EXISTS orders (
id INTEGER PRIMARY KEY AUTOINCREMENT,
- user_id INTEGER NOT NULL,
+ user_id INTEGER NOT NULL REFERENCES users(id),
value_sats INTEGER NOT NULL CHECK(value_sats >= 0),
value_cents INTEGER NOT NULL CHECK(value_cents >= 0),
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
- status TEXT CHECK(status IN ('" . implode("', '", self::STATUSES) . "')) NOT NULL DEFAULT 'PENDING',
- FOREIGN KEY (user_id) REFERENCES users(id)
+ status TEXT CHECK(status IN ('" . implode("', '", self::STATUSES) . "')) NOT NULL DEFAULT 'PENDING'
);");
}
diff --git a/src/models/products.php b/src/models/products.php
index 6bcdb23..71f757a 100644
--- a/src/models/products.php
+++ b/src/models/products.php
@@ -7,64 +7,295 @@ class products
{
public static function init()
{
+ // max 12 images and 12 specification key/value pairs allowed
+ $imageColumns = '';
+ $specColumns = '';
+ for ($i = 0; $i < 12; $i++) {
+ $imageColumns .= "image_url_$i TEXT" . ($i === 0 ? " NOT NULL" : "") . ", ";
+ $specColumns .= "spec_key_$i TEXT, spec_val_$i TEXT, ";
+ }
+ $imageColumns = rtrim($imageColumns, ', ');
+ $specColumns = rtrim($specColumns, ', ');
+
app::$db->exec("CREATE TABLE IF NOT EXISTS products (
id INTEGER PRIMARY KEY AUTOINCREMENT,
+ category_id INTEGER REFERENCES categories(id),
title TEXT NOT NULL,
- desc TEXT,
- stock_qty INTEGER NOT NULL DEFAULT 0 CHECK(stock_qty >= 0),
- specs_json TEXT,
- sats_price INTEGER NOT NULL DEFAULT 0 CHECK(sats_price >= 0),
- cents_price INTEGER NOT NULL DEFAULT 0 CHECK(cents_price >= 0),
- digital BOOLEAN NOT NULL DEFAULT 0,
- subscription BOOLEAN NOT NULL DEFAULT 0,
- image_url_0 TEXT,
- image_url_1 TEXT,
- image_url_2 TEXT,
- image_url_3 TEXT,
- image_url_4 TEXT,
- image_url_5 TEXT,
- image_url_6 TEXT,
- image_url_7 TEXT,
- image_url_8 TEXT,
- image_url_9 TEXT,
- image_url_10 TEXT,
- image_url_11 TEXT
+ description TEXT,
+ stock_qty INTEGER,
+ is_unlimited BOOLEAN,
+ is_enabled BOOLEAN,
+ is_quote_only BOOLEAN,
+ price_sats INTEGER,
+ sats_back_percent INTEGER,
+ is_accepting_sats BOOLEAN,
+ is_sats_back_only BOOLEAN,
+ price_cents INTEGER,
+ cents_back_percent INTEGER,
+ is_accepting_cents BOOLEAN,
+ is_cents_back_only BOOLEAN,
+ is_digital BOOLEAN,
+ oz REAL,
+ lbs REAL,
+ length REAL,
+ width REAL,
+ height REAL,
+ is_subscription BOOLEAN,
+ is_subscription_only BOOLEAN,
+ subscription_frequency TEXT,
+ $imageColumns,
+ $specColumns
)");
}
- public static function add($title, $desc, $stock_qty, $specs_json, $sats_price, $cents_price, $digital, $subscription, $images)
+ public static function validateProductData($data)
{
+ $error_messages = [];
+ $title = $data['title'] ?? null;
+ $description = $data['description'] ?? null;
+ $price_sats = $data['price_sats'] ?? null;
+ $price_cents = $data['price_cents'] ?? null;
+ $stock_qty = $data['stock_qty'] ?? null;
+ $is_digital = $data['is_digital'] ?? false;
+ $is_subscription = $data['is_subscription'] ?? false;
+ $is_subscription_only = $data['is_subscription_only'] ?? false;
+ $is_quote_only = $data['is_quote_only'] ?? false;
+ $oz = $data['oz'] ?? null;
+ $lbs = $data['lbs'] ?? null;
+ $length = $data['length'] ?? null;
+ $width = $data['width'] ?? null;
+ $height = $data['height'] ?? null;
+ $category_id = $data['category_id'] ?? null;
+ $is_unlimited = $data['is_unlimited'] ?? false;
+ $is_sats_back_only = $data['is_sats_back_only'] ?? false;
+ $is_cents_back_only = $data['is_cents_back_only'] ?? false;
+ $is_enabled = $data['is_enabled'] ?? false;
+ $is_accepting_sats = $data['is_accepting_sats'] ?? false;
+ $is_accepting_cents = $data['is_accepting_cents'] ?? false;
+ $subscription_frequency = $data['subscription_frequency'] ?? null;
+ $images = [];
+ for ($i = 0; $i <= 11; $i++) {
+ $images[] = $data['image_url_' . $i] ?? null;
+ }
+ $specs = [];
+ for ($i = 0; $i <= 11; $i++) {
+ $key = $_POST['spec_key_' . $i] ?? null;
+ $val = $_POST['spec_val_' . $i] ?? null;
+ if ($key !== null || $val !== null) {
+ if ($key === null || $val === null) {
+ $error_messages[] = 'Each specification key must have a corresponding value.';
+ }
+ $specs[] = ['key' => $key, 'value' => $val];
+ }
+ }
+
+ if (! $title) {
+ $error_messages[] = 'Title is a required field.';
+ }
+ if (! $description) {
+ $error_messages[] = 'Description is a required field.';
+ }
+ if (! $category_id) {
+ $error_messages[] = 'Category is a required field.';
+ }
+ if (! $images[0]) {
+ $error_messages[] = 'The first image URL is a required field.';
+ }
+ if (! $is_unlimited && ! $stock_qty) {
+ $error_messages[] = 'Quantity is required unless the product is marked as unlimited.';
+ }
+
+ if (! $is_quote_only) {
+ if ($price_sats === null && $price_cents === null) {
+ $error_messages[] = 'Either price in sats or price in cents must be set. They can be zero.';
+ }
+ if ($is_cents_back_only && $is_sats_back_only) {
+ $error_messages[] = 'A product cannot be both cents back only and sats back only.';
+ }
+ }
+
+ if (! $is_digital) {
+ if ($oz === null || $lbs === null || $length === null || $width === null || $height === null) {
+ $error_messages[] = 'For non-digital products, weight (oz and lbs), length, width, and height are required fields.';
+ }
+ }
+ if ($is_subscription_only && ! $is_subscription) {
+ $error_messages[] = 'If the product is subscription only, it must also enable subscriptions.';
+ }
+
+ return [
+ 'errors' => $error_messages,
+ 'title' => $title,
+ 'description' => $description,
+ 'category_id' => $category_id,
+ 'images' => $images,
+ 'specs' => $specs,
+ 'stock_qty' => $stock_qty,
+ 'price_sats' => $price_sats,
+ 'price_cents' => $price_cents,
+ 'is_digital' => $is_digital,
+ 'is_subscription' => $is_subscription,
+ 'is_subscription_only' => $is_subscription_only,
+ 'is_enabled' => $is_enabled,
+ 'is_unlimited' => $is_unlimited,
+ 'is_accepting_sats' => $is_accepting_sats,
+ 'is_accepting_cents' => $is_accepting_cents,
+ 'is_sats_back_only' => $is_sats_back_only,
+ 'is_cents_back_only' => $is_cents_back_only,
+ 'subscription_frequency' => $subscription_frequency,
+ 'is_quote_only' => $is_quote_only,
+ 'oz' => $oz,
+ 'lbs' => $lbs,
+ 'length' => $length,
+ 'width' => $width,
+ 'height' => $height,
+ ];
+ }
+
+ public static function add($productData)
+ {
+ $imageColumns = '';
+ $imageValues = '';
+ $specColumns = '';
+ $specValues = '';
+
+ for ($i = 0; $i < 12; $i++) {
+ $imageColumns .= "image_url_$i, ";
+ $imageValues .= ":image_url_$i, ";
+ $specColumns .= "spec_key_$i, spec_val_$i, ";
+ $specValues .= ":spec_key_$i, :spec_val_$i, ";
+ }
+
+ $imageColumns = rtrim($imageColumns, ', ');
+ $imageValues = rtrim($imageValues, ', ');
+ $specColumns = rtrim($specColumns, ', ');
+ $specValues = rtrim($specValues, ', ');
+
$stmt = app::$db->prepare("INSERT INTO products (
- title, desc, stock_qty, specs_json, sats_price, cents_price, digital, subscription,
- image_url_0, image_url_1, image_url_2, image_url_3, image_url_4, image_url_5,
- image_url_6, image_url_7, image_url_8, image_url_9, image_url_10, image_url_11
+ title, description, stock_qty, price_sats, price_cents, is_digital, is_subscription, is_subscription_only, is_enabled, is_unlimited, is_accepting_sats, is_accepting_cents, is_sats_back_only, is_cents_back_only, subscription_frequency, is_quote_only, category_id, oz, lbs, length, width, height,
+ $imageColumns, $specColumns
) VALUES (
- :title, :desc, :stock_qty, :specs_json, :sats_price, :cents_price, :digital, :subscription,
- :image_url_0, :image_url_1, :image_url_2, :image_url_3, :image_url_4, :image_url_5,
- :image_url_6, :image_url_7, :image_url_8, :image_url_9, :image_url_10, :image_url_11
+ :title, :description, :stock_qty, :price_sats, :price_cents, :is_digital, :is_subscription, :is_subscription_only, :is_enabled, :is_unlimited, :is_accepting_sats, :is_accepting_cents, :is_sats_back_only, :is_cents_back_only, :subscription_frequency, :is_quote_only, :category_id, :oz, :lbs, :length, :width, :height,
+ $imageValues, $specValues
)");
- $stmt->execute([
- ':title' => $title,
- ':desc' => $desc,
- ':stock_qty' => $stock_qty,
- ':specs_json' => $specs_json,
- ':sats_price' => $sats_price,
- ':cents_price' => $cents_price,
- ':digital' => (int) $digital,
- ':subscription' => (int) $subscription,
- ':image_url_0' => $images[0] ?? null,
- ':image_url_1' => $images[1] ?? null,
- ':image_url_2' => $images[2] ?? null,
- ':image_url_3' => $images[3] ?? null,
- ':image_url_4' => $images[4] ?? null,
- ':image_url_5' => $images[5] ?? null,
- ':image_url_6' => $images[6] ?? null,
- ':image_url_7' => $images[7] ?? null,
- ':image_url_8' => $images[8] ?? null,
- ':image_url_9' => $images[9] ?? null,
- ':image_url_10' => $images[10] ?? null,
- ':image_url_11' => $images[11] ?? null,
- ]);
+ $stmt->bindValue(':title', $productData['title']);
+ $stmt->bindValue(':description', $productData['description']);
+ $stmt->bindValue(':stock_qty', $productData['stock_qty']);
+ $stmt->bindValue(':price_sats', $productData['price_sats']);
+ $stmt->bindValue(':price_cents', $productData['price_cents']);
+ $stmt->bindValue(':is_digital', (bool) $productData['is_digital'], \PDO::PARAM_BOOL);
+ $stmt->bindValue(':is_subscription', (bool) $productData['is_subscription'], \PDO::PARAM_BOOL);
+ $stmt->bindValue(':is_subscription_only', (bool) $productData['is_subscription_only'], \PDO::PARAM_BOOL);
+ $stmt->bindValue(':is_enabled', (bool) $productData['is_enabled'], \PDO::PARAM_BOOL);
+ $stmt->bindValue(':is_unlimited', (bool) $productData['is_unlimited'], \PDO::PARAM_BOOL);
+ $stmt->bindValue(':is_accepting_sats', (bool) $productData['is_accepting_sats'], \PDO::PARAM_BOOL);
+ $stmt->bindValue(':is_accepting_cents', (bool) $productData['is_accepting_cents'], \PDO::PARAM_BOOL);
+ $stmt->bindValue(':is_sats_back_only', (bool) $productData['is_sats_back_only'], \PDO::PARAM_BOOL);
+ $stmt->bindValue(':is_cents_back_only', (bool) $productData['is_cents_back_only'], \PDO::PARAM_BOOL);
+ $stmt->bindValue(':subscription_frequency', $productData['subscription_frequency']);
+ $stmt->bindValue(':is_quote_only', (bool) $productData['is_quote_only'], \PDO::PARAM_BOOL);
+ $stmt->bindValue(':category_id', $productData['category_id'], \PDO::PARAM_INT);
+ $stmt->bindValue(':oz', $productData['oz']);
+ $stmt->bindValue(':lbs', $productData['lbs']);
+ $stmt->bindValue(':length', $productData['length']);
+ $stmt->bindValue(':width', $productData['width']);
+ $stmt->bindValue(':height', $productData['height']);
+
+ for ($i = 0; $i < 12; $i++) {
+ $stmt->bindValue(":image_url_$i", $productData['images'][$i] ?? null);
+ $stmt->bindValue(":spec_key_$i", $productData['specs'][$i]['key'] ?? null);
+ $stmt->bindValue(":spec_val_$i", $productData['specs'][$i]['value'] ?? null);
+ }
+
+ $stmt->execute();
+ }
+
+ public static function update($id, $productData)
+ {
+ $imageColumns = '';
+ $specColumns = '';
+
+ for ($i = 0; $i < 12; $i++) {
+ $imageColumns .= "image_url_$i = :image_url_$i, ";
+ $specColumns .= "spec_key_$i = :spec_key_$i, spec_val_$i = :spec_val_$i, ";
+ }
+
+ $imageColumns = rtrim($imageColumns, ', ');
+ $specColumns = rtrim($specColumns, ', ');
+
+ $stmt = app::$db->prepare("UPDATE products SET
+ title = :title,
+ description = :description,
+ stock_qty = :stock_qty,
+ price_sats = :price_sats,
+ price_cents = :price_cents,
+ is_digital = :is_digital,
+ is_subscription = :is_subscription,
+ is_subscription_only = :is_subscription_only,
+ is_enabled = :is_enabled,
+ is_unlimited = :is_unlimited,
+ is_accepting_sats = :is_accepting_sats,
+ is_accepting_cents = :is_accepting_cents,
+ is_sats_back_only = :is_sats_back_only,
+ is_cents_back_only = :is_cents_back_only,
+ subscription_frequency = :subscription_frequency,
+ is_quote_only = :is_quote_only,
+ category_id = :category_id,
+ oz = :oz,
+ lbs = :lbs,
+ length = :length,
+ width = :width,
+ height = :height,
+ $imageColumns,
+ $specColumns
+ WHERE id = :id");
+
+ $stmt->bindValue(':id', (int) $id, \PDO::PARAM_INT);
+ $stmt->bindValue(':title', $productData['title']);
+ $stmt->bindValue(':description', $productData['description']);
+ $stmt->bindValue(':stock_qty', $productData['stock_qty']);
+ $stmt->bindValue(':price_sats', $productData['price_sats']);
+ $stmt->bindValue(':price_cents', $productData['price_cents']);
+ $stmt->bindValue(':is_digital', (bool) $productData['is_digital'], \PDO::PARAM_BOOL);
+ $stmt->bindValue(':is_subscription', (bool) $productData['is_subscription'], \PDO::PARAM_BOOL);
+ $stmt->bindValue(':is_subscription_only', (bool) $productData['is_subscription_only'], \PDO::PARAM_BOOL);
+ $stmt->bindValue(':is_enabled', (bool) $productData['is_enabled'], \PDO::PARAM_BOOL);
+ $stmt->bindValue(':is_unlimited', (bool) $productData['is_unlimited'], \PDO::PARAM_BOOL);
+ $stmt->bindValue(':is_accepting_sats', (bool) $productData['is_accepting_sats'], \PDO::PARAM_BOOL);
+ $stmt->bindValue(':is_accepting_cents', (bool) $productData['is_accepting_cents'], \PDO::PARAM_BOOL);
+ $stmt->bindValue(':is_sats_back_only', (bool) $productData['is_sats_back_only'], \PDO::PARAM_BOOL);
+ $stmt->bindValue(':is_cents_back_only', (bool) $productData['is_cents_back_only'], \PDO::PARAM_BOOL);
+ $stmt->bindValue(':subscription_frequency', $productData['subscription_frequency']);
+ $stmt->bindValue(':is_quote_only', (bool) $productData['is_quote_only'], \PDO::PARAM_BOOL);
+ $stmt->bindValue(':category_id', $productData['category_id'], \PDO::PARAM_INT);
+ $stmt->bindValue(':oz', $productData['oz']);
+ $stmt->bindValue(':lbs', $productData['lbs']);
+ $stmt->bindValue(':length', $productData['length']);
+ $stmt->bindValue(':width', $productData['width']);
+ $stmt->bindValue(':height', $productData['height']);
+
+ for ($i = 0; $i < 12; $i++) {
+ $stmt->bindValue(":image_url_$i", $productData['images'][$i] ?? null);
+ $stmt->bindValue(":spec_key_$i", $productData['specs'][$i]['key'] ?? null);
+ $stmt->bindValue(":spec_val_$i", $productData['specs'][$i]['value'] ?? null);
+ }
+
+ $stmt->execute();
+ }
+
+ public static function get($limit)
+ {
+ $stmt = app::$db->prepare("SELECT * FROM products LIMIT :limit");
+ $stmt->bindValue(':limit', (int) $limit, \PDO::PARAM_INT);
+ $stmt->execute();
+ return $stmt->fetchAll(\PDO::FETCH_ASSOC);
+ }
+ public static function getById($id)
+ {
+ $stmt = app::$db->prepare("SELECT * FROM products WHERE id = :id");
+ $stmt->bindValue(':id', (int) $id, \PDO::PARAM_INT);
+ $stmt->execute();
+ return $stmt->fetch(\PDO::FETCH_ASSOC);
}
}
diff --git a/src/models/quote_items.php b/src/models/quote_items.php
index af78494..4cbc54a 100644
--- a/src/models/quote_items.php
+++ b/src/models/quote_items.php
@@ -9,13 +9,11 @@ class quote_items
{
app::$db->exec("CREATE TABLE IF NOT EXISTS quote_items (
id INTEGER PRIMARY KEY AUTOINCREMENT,
- quote_id INTEGER NOT NULL,
- product_id INTEGER NOT NULL,
+ quote_id INTEGER NOT NULL REFERENCES quotes(id) ON DELETE CASCADE,
+ product_id INTEGER NOT NULL REFERENCES products(id),
quantity INTEGER NOT NULL CHECK(quantity > 0),
price REAL NOT NULL CHECK(price >= 0),
- added_at DATETIME DEFAULT CURRENT_TIMESTAMP,
- FOREIGN KEY (quote_id) REFERENCES quotes(id) ON DELETE CASCADE,
- FOREIGN KEY (product_id) REFERENCES products(id)
+ added_at DATETIME DEFAULT CURRENT_TIMESTAMP
);");
}
diff --git a/src/models/quotes.php b/src/models/quotes.php
index 8ee2dac..cf6db31 100644
--- a/src/models/quotes.php
+++ b/src/models/quotes.php
@@ -14,10 +14,9 @@ class quotes
{
app::$db->exec("CREATE TABLE IF NOT EXISTS quotes (
id INTEGER PRIMARY KEY AUTOINCREMENT,
- user_id INTEGER NOT NULL,
+ user_id INTEGER NOT NULL REFERENCES users(id),
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
- status TEXT CHECK(status IN ('" . implode("', '", self::STATUSES) . "')) NOT NULL DEFAULT 'DRAFT',
- FOREIGN KEY (user_id) REFERENCES users(id)
+ status TEXT CHECK(status IN ('" . implode("', '", self::STATUSES) . "')) NOT NULL DEFAULT 'DRAFT'
);");
}
diff --git a/src/models/subscriptions.php b/src/models/subscriptions.php
index 236779f..65f64a9 100644
--- a/src/models/subscriptions.php
+++ b/src/models/subscriptions.php
@@ -17,15 +17,13 @@ class subscriptions
{
app::$db->exec("CREATE TABLE IF NOT EXISTS subscriptions (
subscription_id INTEGER PRIMARY KEY AUTOINCREMENT,
- user_id INTEGER NOT NULL,
- product_id INTEGER NOT NULL,
+ user_id INTEGER NOT NULL REFERENCES users(id),
+ product_id INTEGER NOT NULL REFERENCES products(id),
start_date DATETIME NOT NULL,
renews_at DATETIME NOT NULL,
status TEXT CHECK(status IN ('" . implode("', '", self::STATUS) . "')) NOT NULL DEFAULT 'COMPLETED',
state TEXT CHECK(state IN ('" . implode("', '", self::STATES) . "')) NOT NULL DEFAULT 'TRIAL',
- invoice_date DATETIME NOT NULL,
- FOREIGN KEY (user_id) REFERENCES users(id),
- FOREIGN KEY (product_id) REFERENCES products(id)
+ invoice_date DATETIME NOT NULL
);");
}
diff --git a/src/models/transactions.php b/src/models/transactions.php
index e557abd..8a4d7a0 100644
--- a/src/models/transactions.php
+++ b/src/models/transactions.php
@@ -12,12 +12,11 @@ class transactions
$typesList = "'" . implode("', '", self::TYPES) . "'";
app::$db->exec("CREATE TABLE IF NOT EXISTS transactions (
id INTEGER PRIMARY KEY AUTOINCREMENT,
- user_id INTEGER NOT NULL,
+ user_id INTEGER NOT NULL REFERENCES users(id),
type TEXT CHECK(type IN ($typesList)) NOT NULL,
cents INTEGER DEFAULT 0,
sats INTEGER DEFAULT 0,
- date TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
- FOREIGN KEY (user_id) REFERENCES users(id)
+ date TIMESTAMP DEFAULT CURRENT_TIMESTAMP
)");
}
diff --git a/src/models/user_settings.php b/src/models/user_settings.php
index c0a67c3..84b3c35 100644
--- a/src/models/user_settings.php
+++ b/src/models/user_settings.php
@@ -9,12 +9,11 @@ class user_settings
{
app::$db->exec("CREATE TABLE IF NOT EXISTS user_settings (
id INTEGER PRIMARY KEY AUTOINCREMENT,
- user_id INTEGER NOT NULL,
+ user_id INTEGER NOT NULL REFERENCES users(id),
opt_in_promotional BOOLEAN NOT NULL,
opt_in_subscription BOOLEAN DEFAULT TRUE,
opt_in_order BOOLEAN DEFAULT TRUE,
- dark_theme BOOLEAN NOT NULL,
- FOREIGN KEY (user_id) REFERENCES users(id)
+ dark_theme BOOLEAN NOT NULL
);");
}
diff --git a/src/models/users.php b/src/models/users.php
index 6a25fd2..80dd060 100644
--- a/src/models/users.php
+++ b/src/models/users.php
@@ -14,8 +14,8 @@ class users
app::$db->exec("CREATE TABLE IF NOT EXISTS users (
id INTEGER PRIMARY KEY AUTOINCREMENT,
email TEXT UNIQUE,
- shipping_address_id INTEGER,
- billing_address_id INTEGER,
+ shipping_address_id INTEGER REFERENCES addresses(id),
+ billing_address_id INTEGER REFERENCES addresses(id),
lifetime_spend INTEGER DEFAULT 0,
lifetime_orders INTEGER DEFAULT 0,
verified BOOLEAN NOT NULL,
@@ -29,6 +29,7 @@ class users
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
)");
}
+
public static function setDefaultShipping($user_id, $shipping_address_id)
{
$query = "UPDATE users SET shipping_address_id = :shipping_address_id WHERE id = :user_id";
@@ -179,4 +180,13 @@ class users
$stmt->execute();
return $stmt->fetch(\PDO::FETCH_ASSOC);
}
+
+ public static function getUsers($n)
+ {
+ $query = "SELECT * FROM users LIMIT :limit";
+ $stmt = app::$db->prepare($query);
+ $stmt->bindParam(':limit', $n, \PDO::PARAM_INT);
+ $stmt->execute();
+ return $stmt->fetchAll(\PDO::FETCH_ASSOC);
+ }
}
diff --git a/src/views/admin/products/add.twig b/src/views/admin/products/add.twig
new file mode 100644
index 0000000..710f5e1
--- /dev/null
+++ b/src/views/admin/products/add.twig
@@ -0,0 +1,9 @@
+
- Category ID - | -- Title - | -- Slug - | -|||||||
---|---|---|---|---|---|---|---|---|---|
- {{ category.id }} - | -- {% if depth > 0 %} - {% for i in 1..depth %} - - - {% endfor %} - {% endif %} - {{ category.label }} - | -- {{ category.value }} - | ++ Product ID + | ++ Title + | ++ Description + | ++ Stock Quantity + | ++ Price (Sats) + | ++ Price (Cents) + | ++ Action + |
+ {{ product.id }} + | ++ {{ product.title }} + | ++ {{ product.description }} + | ++ {{ product.stock_qty }} + | ++ {{ product.price_sats }} + | ++ {{ product.price_cents }} + | ++ + Edit + + | +|||
+ No products found. + | +|||||||||
- No categories yet. - | -
+ User ID + | ++ Name + | ++ Email + | ++ Verified + | ++ Action + | +
---|---|---|---|---|
+ {{ user.id }} + | ++ {{ user.name }} + | ++ {{ user.email }} + | ++ {{ user.verified ? 'Yes' : 'No' }} + | ++ + Edit + + | +
+ No users found. + | +
+ Non-digital products require shipping information. +
+{% include 'lib/inputs/toggle.twig' with { + name: 'is_digital', + label: 'Digital Product?', + on: product.is_digital +} %} ++ Provide any relevant specifications such as brand, material, model, year, color, and more. +
++ {{ desc }} +
+{% endif %} \ No newline at end of file diff --git a/src/views/lib/inputs/select.twig b/src/views/lib/inputs/select.twig index 65a697b..415e6b4 100644 --- a/src/views/lib/inputs/select.twig +++ b/src/views/lib/inputs/select.twig @@ -1,6 +1,16 @@ +{% if desc is defined %} ++ {{ desc }} +
+{% endif %}