code refactor
This commit is contained in:
parent
751ebcd438
commit
80e38ef7df
555
api.php
555
api.php
@ -1,555 +1,68 @@
|
||||
<?php
|
||||
// 1. SESSION CONFIGURATION (MUST BE AT THE VERY TOP)
|
||||
$now = time();
|
||||
$midnight_timestamp = strtotime('tomorrow midnight') - 1;
|
||||
$duration = $midnight_timestamp - $now;
|
||||
/**
|
||||
* API Router - Main entry point for all API requests
|
||||
*
|
||||
* This file acts as a router that dispatches requests to the appropriate handlers.
|
||||
*/
|
||||
|
||||
ini_set('session.gc_maxlifetime', $duration);
|
||||
ini_set('session.cookie_lifetime', $duration);
|
||||
// Load bootstrap (session, WordPress, autoload, env)
|
||||
require_once __DIR__ . '/api/bootstrap.php';
|
||||
|
||||
session_set_cookie_params([
|
||||
'lifetime' => $duration,
|
||||
'path' => '/',
|
||||
'secure' => isset($_SERVER['HTTPS']),
|
||||
'httponly' => true,
|
||||
'samesite' => 'Lax'
|
||||
]);
|
||||
// Load configuration (database, WooCommerce)
|
||||
require_once __DIR__ . '/api/config.php';
|
||||
|
||||
if (session_status() === PHP_SESSION_NONE) {
|
||||
session_start();
|
||||
}
|
||||
// Load authentication middleware
|
||||
require_once __DIR__ . '/api/middleware/auth.php';
|
||||
|
||||
// 2. RECOVERY LOGIC (Check cookie before WP loads)
|
||||
if (!isset($_SESSION['user']) && isset($_COOKIE['telvero_remember'])) {
|
||||
$decoded = json_decode(base64_decode($_COOKIE['telvero_remember']), true);
|
||||
if ($decoded && $decoded['expires'] > time()) {
|
||||
$_SESSION['user'] = $decoded['user'];
|
||||
$_SESSION['full_name'] = $decoded['full_name'];
|
||||
}
|
||||
}
|
||||
|
||||
// 3. CAPTURE DATA FOR WP PROTECTION
|
||||
$cap_user = $_SESSION['user'] ?? null;
|
||||
$cap_name = $_SESSION['full_name'] ?? null;
|
||||
|
||||
// 4. LOAD WORDPRESS
|
||||
$wp_load = __DIR__ . '/wp-load.php';
|
||||
if (!file_exists($wp_load)) { $wp_load = dirname(__DIR__) . '/wp-load.php'; }
|
||||
if (file_exists($wp_load)) { require_once $wp_load; }
|
||||
|
||||
// 5. RESTORE DATA
|
||||
if ($cap_user && !isset($_SESSION['user'])) {
|
||||
$_SESSION['user'] = $cap_user;
|
||||
$_SESSION['full_name'] = $cap_name;
|
||||
}
|
||||
|
||||
// 6. REST OF THE BOOTSTRAP
|
||||
require __DIR__ . '/vendor/autoload.php';
|
||||
if (file_exists(__DIR__ . '/.env')) {
|
||||
$dotenv = Dotenv\Dotenv::createImmutable(__DIR__);
|
||||
$dotenv->load();
|
||||
}
|
||||
|
||||
use Automattic\WooCommerce\Client;
|
||||
// Set JSON content type for all responses
|
||||
header('Content-Type: application/json');
|
||||
|
||||
$db = new mysqli($_ENV['DB_HOST'], $_ENV['DB_USER'], $_ENV['DB_PASS'], $_ENV['DB_NAME']);
|
||||
if ($db->connect_error) { die(json_encode(['error' => 'Database connectie mislukt'])); }
|
||||
|
||||
// Get the requested action
|
||||
$action = $_GET['action'] ?? '';
|
||||
|
||||
// 6. LOGIN ACTION
|
||||
// Route: Login (no auth required)
|
||||
if ($action === 'login') {
|
||||
$input = json_decode(file_get_contents('php://input'), true);
|
||||
$username = $input['username'] ?? '';
|
||||
$stmt = $db->prepare("SELECT password, full_name FROM sales_users WHERE username = ?");
|
||||
$stmt->bind_param("s", $username);
|
||||
$stmt->execute();
|
||||
$res = $stmt->get_result()->fetch_assoc();
|
||||
|
||||
if ($res && password_verify($input['password'], $res['password'])) {
|
||||
$_SESSION['user'] = $username;
|
||||
$_SESSION['full_name'] = $res['full_name'];
|
||||
|
||||
// Recovery cookie payload
|
||||
$cookie_payload = base64_encode(json_encode([
|
||||
'user' => $username,
|
||||
'full_name' => $res['full_name'],
|
||||
'expires' => $midnight_timestamp
|
||||
]));
|
||||
|
||||
// Use the explicit $midnight_timestamp variable
|
||||
setcookie('telvero_remember', $cookie_payload, $midnight_timestamp, '/', '', isset($_SERVER['HTTPS']), true);
|
||||
|
||||
echo json_encode(['success' => true, 'user' => $res['full_name']]);
|
||||
} else {
|
||||
http_response_code(401); echo json_encode(['error' => 'Login mislukt']);
|
||||
}
|
||||
$db = getDatabase();
|
||||
handleLogin($db);
|
||||
exit;
|
||||
}
|
||||
|
||||
// 7. SESSION CHECK (lightweight, before security gate)
|
||||
// Route: Check Session (no auth required)
|
||||
if ($action === 'check_session') {
|
||||
if (isset($_SESSION['user'])) {
|
||||
echo json_encode(['authenticated' => true, 'user' => $_SESSION['full_name'] ?? $_SESSION['user']]);
|
||||
} else {
|
||||
echo json_encode(['authenticated' => false]);
|
||||
}
|
||||
handleCheckSession();
|
||||
exit;
|
||||
}
|
||||
|
||||
// 8. SECURITY GATE (The ONLY one needed)
|
||||
if (!isset($_SESSION['user'])) {
|
||||
http_response_code(403);
|
||||
echo json_encode(['error' => 'Not authenticated']);
|
||||
exit;
|
||||
}
|
||||
// Security Gate - All routes below require authentication
|
||||
requireAuth();
|
||||
|
||||
// 8. LOGOUT
|
||||
// Route: Logout
|
||||
if ($action === 'logout') {
|
||||
session_destroy();
|
||||
setcookie('telvero_remember', '', time() - 3600, '/');
|
||||
echo json_encode(['success' => true]);
|
||||
handleLogout();
|
||||
exit;
|
||||
}
|
||||
|
||||
$woocommerce = new Client($_ENV['WC_URL'], $_ENV['WC_KEY'], $_ENV['WC_SECRET'], ['version' => 'wc/v3', 'verify_ssl' => false]);
|
||||
|
||||
function ss_get_upsellwp_recommended_product_ids($trigger_product_id) {
|
||||
if (!function_exists('get_option')) {
|
||||
return [];
|
||||
}
|
||||
|
||||
global $wpdb;
|
||||
|
||||
$trigger_product_id = (int) $trigger_product_id;
|
||||
if ($trigger_product_id <= 0) return [];
|
||||
|
||||
// 1) Vind "campaign posts" waar in meta_value ergens dit product_id voorkomt.
|
||||
// We weten niet exact welke meta_key UpsellWP gebruikt, dus zoeken we breed op meta_key LIKE '%upsellwp%'.
|
||||
// Daarnaast zoeken we op bekende patronen in serialized/json/plain.
|
||||
$like_json = '%' . $wpdb->esc_like('"' . $trigger_product_id . '"') . '%';
|
||||
$like_plain = '%' . $wpdb->esc_like((string) $trigger_product_id) . '%';
|
||||
$like_serial = '%' . $wpdb->esc_like('i:' . $trigger_product_id . ';') . '%';
|
||||
|
||||
$campaign_post_ids = $wpdb->get_col(
|
||||
$wpdb->prepare(
|
||||
"
|
||||
SELECT DISTINCT post_id
|
||||
FROM {$wpdb->postmeta}
|
||||
WHERE meta_key LIKE %s
|
||||
AND (
|
||||
meta_value LIKE %s
|
||||
OR meta_value LIKE %s
|
||||
OR meta_value LIKE %s
|
||||
)
|
||||
",
|
||||
'%upsellwp%',
|
||||
$like_json,
|
||||
$like_serial,
|
||||
$like_plain
|
||||
)
|
||||
);
|
||||
|
||||
if (empty($campaign_post_ids)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
// 2) Verzamel uit die campaigns alle product IDs die genoemd worden (offers / bundles / buy-more-save-more tiers).
|
||||
$recommended = [];
|
||||
|
||||
foreach ($campaign_post_ids as $cid) {
|
||||
$cid = (int) $cid;
|
||||
|
||||
$rows = $wpdb->get_results(
|
||||
$wpdb->prepare(
|
||||
"
|
||||
SELECT meta_key, meta_value
|
||||
FROM {$wpdb->postmeta}
|
||||
WHERE post_id = %d
|
||||
",
|
||||
$cid
|
||||
)
|
||||
);
|
||||
|
||||
foreach ($rows as $r) {
|
||||
$key = (string) $r->meta_key;
|
||||
|
||||
// Kleine bias naar keys die logisch product/offers bevatten
|
||||
// maar alsnog niet te streng, want add-ons gebruiken soms andere keys.
|
||||
if (!preg_match('/product|products|offer|offers|bundle|bmsm|tier|rule|condition|trigger|cart|recommend/i', $key)
|
||||
&& stripos($key, 'upsellwp') === false
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$val = (string) $r->meta_value;
|
||||
|
||||
// Extract alle integers uit json/serialized/plain
|
||||
if (preg_match_all('/\b\d+\b/', $val, $m)) {
|
||||
foreach ($m[0] as $id) {
|
||||
$id = (int) $id;
|
||||
if ($id > 0) $recommended[] = $id;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 3) Opschonen: unieke IDs, trigger zelf eruit, en alleen echte WC producten (best effort).
|
||||
$recommended = array_values(array_unique(array_filter($recommended)));
|
||||
$recommended = array_values(array_diff($recommended, [$trigger_product_id]));
|
||||
|
||||
// Filter op bestaande producten (scheelt false positives zoals campaign IDs)
|
||||
$recommended = array_values(array_filter($recommended, function($id){
|
||||
return get_post_type($id) === 'product' || get_post_type($id) === 'product_variation';
|
||||
}));
|
||||
|
||||
return $recommended;
|
||||
}
|
||||
|
||||
/**
|
||||
* Build a map: product_id => related product ids from CUW (UpsellWP) campaigns.
|
||||
* Uses {$wpdb->prefix}cuw_campaigns table (as in your screenshot).
|
||||
*/
|
||||
function ss_cuw_build_product_map() {
|
||||
if (!function_exists('get_post_type')) return [];
|
||||
|
||||
global $wpdb;
|
||||
|
||||
$table = $wpdb->prefix . 'cuw_campaigns';
|
||||
// Safety: table might not exist
|
||||
$exists = $wpdb->get_var( $wpdb->prepare("SHOW TABLES LIKE %s", $table) );
|
||||
if ($exists !== $table) return [];
|
||||
|
||||
$rows = $wpdb->get_results("
|
||||
SELECT id, type, filters, data
|
||||
FROM {$table}
|
||||
WHERE enabled = 1
|
||||
ORDER BY priority DESC, id DESC
|
||||
");
|
||||
|
||||
if (empty($rows)) return [];
|
||||
|
||||
$map = [];
|
||||
|
||||
foreach ($rows as $row) {
|
||||
$filters = json_decode((string) $row->filters, true);
|
||||
if (!is_array($filters)) continue;
|
||||
|
||||
// Extract product lists from filters:
|
||||
// Example (your screenshot):
|
||||
// {"relation":"or","176...":{"type":"products","method":"in_list","values":["1831"]}}
|
||||
$filterProductLists = [];
|
||||
|
||||
foreach ($filters as $k => $v) {
|
||||
if ($k === 'relation') continue;
|
||||
if (!is_array($v)) continue;
|
||||
|
||||
$type = $v['type'] ?? '';
|
||||
$method = $v['method'] ?? '';
|
||||
$values = $v['values'] ?? [];
|
||||
|
||||
if ($type !== 'products') continue;
|
||||
if (!in_array($method, ['in_list', 'in', 'include', 'equals'], true)) continue;
|
||||
if (!is_array($values)) continue;
|
||||
|
||||
$ids = array_values(array_unique(array_map('intval', $values)));
|
||||
$ids = array_values(array_filter($ids));
|
||||
|
||||
if (!empty($ids)) {
|
||||
$filterProductLists[] = $ids;
|
||||
}
|
||||
}
|
||||
|
||||
if (empty($filterProductLists)) continue;
|
||||
|
||||
// For each list: if a product is in that list, relate it to the other products in that list
|
||||
foreach ($filterProductLists as $list) {
|
||||
foreach ($list as $pid) {
|
||||
$related = array_values(array_diff($list, [$pid]));
|
||||
if (empty($related)) continue;
|
||||
|
||||
// Keep only real Woo products
|
||||
$related = array_values(array_filter($related, function($id){
|
||||
$pt = get_post_type($id);
|
||||
return $pt === 'product' || $pt === 'product_variation';
|
||||
}));
|
||||
|
||||
if (empty($related)) continue;
|
||||
|
||||
if (!isset($map[$pid])) $map[$pid] = [];
|
||||
$map[$pid] = array_values(array_unique(array_merge($map[$pid], $related)));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $map;
|
||||
}
|
||||
|
||||
function ss_cuw_build_deals_map() {
|
||||
global $wpdb;
|
||||
|
||||
$table = $wpdb->prefix . 'cuw_campaigns';
|
||||
|
||||
$exists = $wpdb->get_var( $wpdb->prepare("SHOW TABLES LIKE %s", $table) );
|
||||
if ($exists !== $table) return [];
|
||||
|
||||
$rows = $wpdb->get_results("
|
||||
SELECT id, type, title, priority, filters, data
|
||||
FROM {$table}
|
||||
WHERE enabled = 1
|
||||
ORDER BY priority DESC, id DESC
|
||||
");
|
||||
|
||||
if (empty($rows)) return [];
|
||||
|
||||
$map = [];
|
||||
|
||||
foreach ($rows as $row) {
|
||||
$type = (string) $row->type;
|
||||
if ($type !== 'buy_more_save_more') {
|
||||
continue;
|
||||
}
|
||||
|
||||
$filters = json_decode((string) $row->filters, true);
|
||||
$data = json_decode((string) $row->data, true);
|
||||
|
||||
if (!is_array($filters) || !is_array($data)) continue;
|
||||
|
||||
// 1) Product IDs uit filters halen (zoals jouw screenshot: type=products, method=in_list, values=[...])
|
||||
$product_ids = [];
|
||||
|
||||
foreach ($filters as $k => $v) {
|
||||
if ($k === 'relation') continue;
|
||||
if (!is_array($v)) continue;
|
||||
|
||||
$f_type = $v['type'] ?? '';
|
||||
$f_method = $v['method'] ?? '';
|
||||
$values = $v['values'] ?? [];
|
||||
|
||||
if ($f_type !== 'products') continue;
|
||||
if (!in_array($f_method, ['in_list', 'in', 'include', 'equals'], true)) continue;
|
||||
if (!is_array($values)) continue;
|
||||
|
||||
foreach ($values as $pid) {
|
||||
$pid = (int) $pid;
|
||||
if ($pid > 0) $product_ids[] = $pid;
|
||||
}
|
||||
}
|
||||
|
||||
$product_ids = array_values(array_unique(array_filter($product_ids)));
|
||||
if (empty($product_ids)) continue;
|
||||
|
||||
// 2) Deal details uit data halen
|
||||
$cta_text = $data['template']['cta_text'] ?? '';
|
||||
$display_location = $data['display_location'] ?? ($data['template']['display_location'] ?? '');
|
||||
|
||||
$discounts = $data['discounts'] ?? [];
|
||||
if (!is_array($discounts) || empty($discounts)) continue;
|
||||
|
||||
foreach ($discounts as $disc) {
|
||||
if (!is_array($disc)) continue;
|
||||
|
||||
$qty = isset($disc['qty']) ? (int) $disc['qty'] : 0;
|
||||
$dType = isset($disc['type']) ? (string) $disc['type'] : '';
|
||||
$value = isset($disc['value']) ? (float) $disc['value'] : 0.0;
|
||||
|
||||
if ($qty <= 0 || $dType === '' || $value <= 0) continue;
|
||||
|
||||
$deal = [
|
||||
'campaign_id' => (int) $row->id,
|
||||
'campaign_type' => $type,
|
||||
'title' => (string) $row->title,
|
||||
'priority' => (int) $row->priority,
|
||||
'qty' => $qty,
|
||||
'discount_type' => $dType, // fixed_price
|
||||
'value' => $value, // 10 (korting op 2e in jouw geval)
|
||||
'cta_text' => (string) $cta_text,
|
||||
'display_location' => (string) $display_location,
|
||||
];
|
||||
|
||||
foreach ($product_ids as $pid) {
|
||||
if (!isset($map[$pid])) $map[$pid] = [];
|
||||
$map[$pid][] = $deal;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $map;
|
||||
}
|
||||
|
||||
|
||||
// --- POSTCODE CHECK ---
|
||||
// Route: Postcode Check
|
||||
if ($action === 'postcode_check') {
|
||||
try {
|
||||
$postcode = str_replace(' ', '', $_GET['postcode'] ?? '');
|
||||
$number = $_GET['number'] ?? '';
|
||||
|
||||
if (empty($postcode) || empty($number)) {
|
||||
http_response_code(400);
|
||||
echo json_encode(['error' => 'Postcode en huisnummer zijn verplicht']);
|
||||
require_once __DIR__ . '/api/actions/postcode.php';
|
||||
handlePostcodeCheck();
|
||||
exit;
|
||||
}
|
||||
|
||||
$url = "https://postcode.tech/api/v1/postcode?postcode={$postcode}&number={$number}";
|
||||
$ch = curl_init($url);
|
||||
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
|
||||
curl_setopt($ch, CURLOPT_HTTPHEADER, ["Authorization: Bearer " . $_ENV['POSTCODE_TECH_KEY']]);
|
||||
curl_setopt($ch, CURLOPT_TIMEOUT, 10);
|
||||
curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 5);
|
||||
|
||||
$response = curl_exec($ch);
|
||||
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
||||
$curlError = curl_error($ch);
|
||||
curl_close($ch);
|
||||
|
||||
if ($curlError || $httpCode >= 500 ) {
|
||||
http_response_code(503);
|
||||
echo json_encode(['error' => 'Postcode service niet bereikbaar, vul straat en woonplaats zelf in', 'details' => $curlError]);
|
||||
exit;
|
||||
}
|
||||
|
||||
if ($httpCode >= 400) {
|
||||
http_response_code($httpCode);
|
||||
$decoded = json_decode($response, true);
|
||||
if ($decoded && isset($decoded['error'])) {
|
||||
echo json_encode(['error' => $decoded['error']]);
|
||||
} else {
|
||||
echo json_encode(['error' => 'Postcode niet gevonden of ongeldige invoer, vul straat en woonplaats zelf in']);
|
||||
}
|
||||
exit;
|
||||
}
|
||||
|
||||
echo $response;
|
||||
} catch (Exception $e) {
|
||||
http_response_code(500);
|
||||
echo json_encode(['error' => 'Er is een fout opgetreden bij het ophalen van adresgegevens, vul straat en woonplaats zelf in']);
|
||||
}
|
||||
exit;
|
||||
}
|
||||
|
||||
// --- GET PRODUCTS ---
|
||||
// --- GET PRODUCTS ---
|
||||
// Route: Get Products
|
||||
if ($action === 'get_products') {
|
||||
try {
|
||||
$products = $woocommerce->get('products', ['status' => 'publish', 'per_page' => 100]);
|
||||
$cuw_map = ss_cuw_build_product_map();
|
||||
|
||||
$enriched = [];
|
||||
|
||||
foreach ($products as $product) {
|
||||
|
||||
// variations (bestaand)
|
||||
$variation_details = ($product->type === 'variable')
|
||||
? (array) $woocommerce->get("products/{$product->id}/variations", ['per_page' => 50])
|
||||
: [];
|
||||
|
||||
// wpupsell IDs uit meta_data halen (heuristisch, werkt bij veel plugins)
|
||||
$wpupsell_ids = [];
|
||||
if (!empty($product->meta_data) && is_array($product->meta_data)) {
|
||||
foreach ($product->meta_data as $md) {
|
||||
$key = isset($md->key) ? (string) $md->key : '';
|
||||
if ($key === '' || !preg_match('/wp\s*upsell|wpupsell|upsell\s*campaign/i', $key)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$val = $md->value ?? null;
|
||||
|
||||
// value kan array/object/string zijn
|
||||
if (is_array($val)) {
|
||||
$flat = json_encode($val);
|
||||
if (preg_match_all('/\b\d+\b/', $flat, $m)) {
|
||||
foreach ($m[0] as $id) $wpupsell_ids[] = (int) $id;
|
||||
}
|
||||
} elseif (is_object($val)) {
|
||||
$flat = json_encode($val);
|
||||
if (preg_match_all('/\b\d+\b/', $flat, $m)) {
|
||||
foreach ($m[0] as $id) $wpupsell_ids[] = (int) $id;
|
||||
}
|
||||
} else {
|
||||
$flat = (string) $val;
|
||||
if (preg_match_all('/\b\d+\b/', $flat, $m)) {
|
||||
foreach ($m[0] as $id) $wpupsell_ids[] = (int) $id;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// upsell + cross-sell + wpupsell bundelen
|
||||
$upsell_ids = !empty($product->upsell_ids) ? array_map('intval', (array) $product->upsell_ids) : [];
|
||||
$cross_sell_ids = !empty($product->cross_sell_ids) ? array_map('intval', (array) $product->cross_sell_ids) : [];
|
||||
|
||||
$cuw_ids = $cuw_map[(int)$product->id] ?? [];
|
||||
|
||||
$recommended_ids = array_values(array_unique(array_filter(array_merge(
|
||||
$upsell_ids,
|
||||
$cross_sell_ids,
|
||||
$cuw_ids
|
||||
))));
|
||||
|
||||
// product naar array + velden toevoegen
|
||||
$p = (array) $product;
|
||||
$p['variation_details'] = $variation_details;
|
||||
|
||||
// expliciet meesturen voor frontend
|
||||
$p['cuw_ids'] = $cuw_ids;
|
||||
$p['recommended_ids'] = $recommended_ids;
|
||||
|
||||
$enriched[] = $p;
|
||||
}
|
||||
|
||||
echo json_encode($enriched);
|
||||
} catch (Exception $e) {
|
||||
echo json_encode([]);
|
||||
}
|
||||
require_once __DIR__ . '/api/actions/products.php';
|
||||
handleGetProducts();
|
||||
exit;
|
||||
}
|
||||
|
||||
|
||||
// --- CREATE ORDER ---
|
||||
// Route: Create Order
|
||||
if ($action === 'create_order') {
|
||||
$input = json_decode(file_get_contents('php://input'), true);
|
||||
try {
|
||||
$email = $input['billing']['email'];
|
||||
$mediacode = $input['mediacode_internal'] ?? 'Geen';
|
||||
|
||||
$input['payment_method'] = 'cod';
|
||||
$input['payment_method_title'] = 'Sales Panel Order';
|
||||
$input['status'] = 'on-hold';
|
||||
|
||||
$existing = $woocommerce->get('customers', ['email' => $email]);
|
||||
$input['customer_id'] = !empty($existing) ? $existing[0]->id : 0;
|
||||
|
||||
$shipping_incl = (float)($input['shipping_total'] ?? 0);
|
||||
if ($shipping_incl > 0) {
|
||||
$input['shipping_lines'] = [[
|
||||
'method_id' => 'flat_rate',
|
||||
'method_title' => 'Verzendkosten',
|
||||
'total' => number_format($shipping_incl / 1.21, 4, '.', '')
|
||||
]];
|
||||
}
|
||||
|
||||
$input['customer_note'] = "Agent: {$_SESSION['user']} | Mediacode: $mediacode";
|
||||
|
||||
// --- ATTRIBUTION DATA (GEFIXTE SYNTAX) ---
|
||||
$input['meta_data'][] = ['key' => '_wc_order_attribution_source_type', 'value' => 'utm'];
|
||||
$input['meta_data'][] = ['key' => '_wc_order_attribution_utm_source', 'value' => 'SalesPanel'];
|
||||
$input['meta_data'][] = ['key' => '_wc_order_attribution_utm_campaign', 'value' => $mediacode];
|
||||
$input['meta_data'][] = ['key' => 'Mediacode', 'value' => $mediacode];
|
||||
$input['meta_data'][] = ['key' => 'Bron', 'value' => 'SalesPanel'];
|
||||
|
||||
$order = $woocommerce->post('orders', $input);
|
||||
|
||||
$action_type = 'order_created';
|
||||
$log_stmt = $db->prepare("INSERT INTO sales_logs (username, action_type, order_id, amount, mediacode, customer_email, created_at) VALUES (?, ?, ?, ?, ?, ?, NOW())");
|
||||
$log_stmt->bind_param("ssidss", $_SESSION['user'], $action_type, $order->id, $order->total, $mediacode, $email);
|
||||
$log_stmt->execute();
|
||||
|
||||
echo json_encode(['success' => true, 'order_id' => $order->id, 'total' => $order->total]);
|
||||
} catch (Exception $e) {
|
||||
http_response_code(422); echo json_encode(['error' => $e->getMessage()]);
|
||||
}
|
||||
require_once __DIR__ . '/api/actions/orders.php';
|
||||
handleCreateOrder();
|
||||
exit;
|
||||
}
|
||||
|
||||
if ($action === 'logout') { session_destroy(); echo json_encode(['success' => true]); exit; }
|
||||
// Unknown action
|
||||
http_response_code(400);
|
||||
echo json_encode(['error' => 'Unknown action']);
|
||||
|
||||
79
api/actions/orders.php
Normal file
79
api/actions/orders.php
Normal file
@ -0,0 +1,79 @@
|
||||
<?php
|
||||
/**
|
||||
* Orders Action - Create orders via WooCommerce
|
||||
*/
|
||||
|
||||
/**
|
||||
* Handle create_order action
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
function handleCreateOrder(): void
|
||||
{
|
||||
$input = json_decode(file_get_contents('php://input'), true);
|
||||
|
||||
try {
|
||||
$woocommerce = getWooCommerce();
|
||||
$db = getDatabase();
|
||||
|
||||
$email = $input['billing']['email'];
|
||||
$mediacode = $input['mediacode_internal'] ?? 'Geen';
|
||||
|
||||
$input['payment_method'] = 'cod';
|
||||
$input['payment_method_title'] = 'Sales Panel Order';
|
||||
$input['status'] = 'on-hold';
|
||||
|
||||
// Check for existing customer
|
||||
$existing = $woocommerce->get('customers', ['email' => $email]);
|
||||
$input['customer_id'] = !empty($existing) ? $existing[0]->id : 0;
|
||||
|
||||
// Handle shipping costs
|
||||
$shipping_incl = (float)($input['shipping_total'] ?? 0);
|
||||
if ($shipping_incl > 0) {
|
||||
$input['shipping_lines'] = [[
|
||||
'method_id' => 'flat_rate',
|
||||
'method_title' => 'Verzendkosten',
|
||||
'total' => number_format($shipping_incl / 1.21, 4, '.', '')
|
||||
]];
|
||||
}
|
||||
|
||||
// Add customer note with agent info
|
||||
$input['customer_note'] = "Agent: {$_SESSION['user']} | Mediacode: $mediacode";
|
||||
|
||||
// Add attribution metadata
|
||||
$input['meta_data'][] = ['key' => '_wc_order_attribution_source_type', 'value' => 'utm'];
|
||||
$input['meta_data'][] = ['key' => '_wc_order_attribution_utm_source', 'value' => 'SalesPanel'];
|
||||
$input['meta_data'][] = ['key' => '_wc_order_attribution_utm_campaign', 'value' => $mediacode];
|
||||
$input['meta_data'][] = ['key' => 'Mediacode', 'value' => $mediacode];
|
||||
$input['meta_data'][] = ['key' => 'Bron', 'value' => 'SalesPanel'];
|
||||
|
||||
// Create the order
|
||||
$order = $woocommerce->post('orders', $input);
|
||||
|
||||
// Log the order
|
||||
$action_type = 'order_created';
|
||||
$log_stmt = $db->prepare(
|
||||
"INSERT INTO sales_logs (username, action_type, order_id, amount, mediacode, customer_email, created_at)
|
||||
VALUES (?, ?, ?, ?, ?, ?, NOW())"
|
||||
);
|
||||
$log_stmt->bind_param(
|
||||
"ssidss",
|
||||
$_SESSION['user'],
|
||||
$action_type,
|
||||
$order->id,
|
||||
$order->total,
|
||||
$mediacode,
|
||||
$email
|
||||
);
|
||||
$log_stmt->execute();
|
||||
|
||||
echo json_encode([
|
||||
'success' => true,
|
||||
'order_id' => $order->id,
|
||||
'total' => $order->total
|
||||
]);
|
||||
} catch (Exception $e) {
|
||||
http_response_code(422);
|
||||
echo json_encode(['error' => $e->getMessage()]);
|
||||
}
|
||||
}
|
||||
38
api/actions/postcode.php
Normal file
38
api/actions/postcode.php
Normal file
@ -0,0 +1,38 @@
|
||||
<?php
|
||||
/**
|
||||
* Postcode Action - Handle postcode lookup requests
|
||||
*/
|
||||
|
||||
require_once __DIR__ . '/../services/PostcodeService.php';
|
||||
|
||||
/**
|
||||
* Handle postcode_check action
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
function handlePostcodeCheck(): void
|
||||
{
|
||||
try {
|
||||
$postcode = $_GET['postcode'] ?? '';
|
||||
$number = $_GET['number'] ?? '';
|
||||
|
||||
$result = PostcodeService::lookup($postcode, $number);
|
||||
|
||||
if (!$result['success']) {
|
||||
http_response_code($result['http_code']);
|
||||
$response = ['error' => $result['error']];
|
||||
if (isset($result['details'])) {
|
||||
$response['details'] = $result['details'];
|
||||
}
|
||||
echo json_encode($response);
|
||||
return;
|
||||
}
|
||||
|
||||
echo json_encode($result['data']);
|
||||
} catch (Exception $e) {
|
||||
http_response_code(500);
|
||||
echo json_encode([
|
||||
'error' => 'Er is een fout opgetreden bij het ophalen van adresgegevens, vul straat en woonplaats zelf in'
|
||||
]);
|
||||
}
|
||||
}
|
||||
57
api/actions/products.php
Normal file
57
api/actions/products.php
Normal file
@ -0,0 +1,57 @@
|
||||
<?php
|
||||
/**
|
||||
* Products Action - Get products with enriched data
|
||||
*/
|
||||
|
||||
require_once __DIR__ . '/../services/UpsellService.php';
|
||||
|
||||
/**
|
||||
* Handle get_products action
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
function handleGetProducts(): void
|
||||
{
|
||||
try {
|
||||
$woocommerce = getWooCommerce();
|
||||
$products = $woocommerce->get('products', ['status' => 'publish', 'per_page' => 100]);
|
||||
$cuw_map = UpsellService::buildProductMap();
|
||||
|
||||
$enriched = [];
|
||||
|
||||
foreach ($products as $product) {
|
||||
// Get variations for variable products
|
||||
$variation_details = ($product->type === 'variable')
|
||||
? (array) $woocommerce->get("products/{$product->id}/variations", ['per_page' => 50])
|
||||
: [];
|
||||
|
||||
// Combine upsell + cross-sell + CUW IDs
|
||||
$upsell_ids = !empty($product->upsell_ids)
|
||||
? array_map('intval', (array) $product->upsell_ids)
|
||||
: [];
|
||||
$cross_sell_ids = !empty($product->cross_sell_ids)
|
||||
? array_map('intval', (array) $product->cross_sell_ids)
|
||||
: [];
|
||||
|
||||
$cuw_ids = $cuw_map[(int)$product->id] ?? [];
|
||||
|
||||
$recommended_ids = array_values(array_unique(array_filter(array_merge(
|
||||
$upsell_ids,
|
||||
$cross_sell_ids,
|
||||
$cuw_ids
|
||||
))));
|
||||
|
||||
// Convert product to array and add fields
|
||||
$p = (array) $product;
|
||||
$p['variation_details'] = $variation_details;
|
||||
$p['cuw_ids'] = $cuw_ids;
|
||||
$p['recommended_ids'] = $recommended_ids;
|
||||
|
||||
$enriched[] = $p;
|
||||
}
|
||||
|
||||
echo json_encode($enriched);
|
||||
} catch (Exception $e) {
|
||||
echo json_encode([]);
|
||||
}
|
||||
}
|
||||
64
api/bootstrap.php
Normal file
64
api/bootstrap.php
Normal file
@ -0,0 +1,64 @@
|
||||
<?php
|
||||
/**
|
||||
* Bootstrap file - Session configuration and WordPress loading
|
||||
*/
|
||||
|
||||
// 1. SESSION CONFIGURATION (MUST BE AT THE VERY TOP)
|
||||
$now = time();
|
||||
$midnight_timestamp = strtotime('tomorrow midnight') - 1;
|
||||
$duration = $midnight_timestamp - $now;
|
||||
|
||||
ini_set('session.gc_maxlifetime', $duration);
|
||||
ini_set('session.cookie_lifetime', $duration);
|
||||
|
||||
session_set_cookie_params([
|
||||
'lifetime' => $duration,
|
||||
'path' => '/',
|
||||
'secure' => isset($_SERVER['HTTPS']),
|
||||
'httponly' => true,
|
||||
'samesite' => 'Lax'
|
||||
]);
|
||||
|
||||
if (session_status() === PHP_SESSION_NONE) {
|
||||
session_start();
|
||||
}
|
||||
|
||||
// 2. RECOVERY LOGIC (Check cookie before WP loads)
|
||||
if (!isset($_SESSION['user']) && isset($_COOKIE['telvero_remember'])) {
|
||||
$decoded = json_decode(base64_decode($_COOKIE['telvero_remember']), true);
|
||||
if ($decoded && $decoded['expires'] > time()) {
|
||||
$_SESSION['user'] = $decoded['user'];
|
||||
$_SESSION['full_name'] = $decoded['full_name'];
|
||||
}
|
||||
}
|
||||
|
||||
// 3. CAPTURE DATA FOR WP PROTECTION
|
||||
$cap_user = $_SESSION['user'] ?? null;
|
||||
$cap_name = $_SESSION['full_name'] ?? null;
|
||||
|
||||
// 4. LOAD WORDPRESS
|
||||
$wp_load = dirname(__DIR__) . '/wp-load.php';
|
||||
if (!file_exists($wp_load)) {
|
||||
$wp_load = dirname(dirname(__DIR__)) . '/wp-load.php';
|
||||
}
|
||||
if (file_exists($wp_load)) {
|
||||
require_once $wp_load;
|
||||
}
|
||||
|
||||
// 5. RESTORE DATA
|
||||
if ($cap_user && !isset($_SESSION['user'])) {
|
||||
$_SESSION['user'] = $cap_user;
|
||||
$_SESSION['full_name'] = $cap_name;
|
||||
}
|
||||
|
||||
// 6. LOAD COMPOSER AUTOLOAD
|
||||
require dirname(__DIR__) . '/vendor/autoload.php';
|
||||
|
||||
// 7. LOAD ENVIRONMENT VARIABLES
|
||||
if (file_exists(dirname(__DIR__) . '/.env')) {
|
||||
$dotenv = Dotenv\Dotenv::createImmutable(dirname(__DIR__));
|
||||
$dotenv->load();
|
||||
}
|
||||
|
||||
// Export midnight timestamp for use in other files
|
||||
define('MIDNIGHT_TIMESTAMP', $midnight_timestamp);
|
||||
54
api/config.php
Normal file
54
api/config.php
Normal file
@ -0,0 +1,54 @@
|
||||
<?php
|
||||
/**
|
||||
* Configuration file - Database connection and WooCommerce client
|
||||
*/
|
||||
|
||||
use Automattic\WooCommerce\Client;
|
||||
|
||||
/**
|
||||
* Get database connection
|
||||
* @return mysqli
|
||||
*/
|
||||
function getDatabase(): mysqli
|
||||
{
|
||||
static $db = null;
|
||||
|
||||
if ($db === null) {
|
||||
$db = new mysqli(
|
||||
$_ENV['DB_HOST'],
|
||||
$_ENV['DB_USER'],
|
||||
$_ENV['DB_PASS'],
|
||||
$_ENV['DB_NAME']
|
||||
);
|
||||
|
||||
if ($db->connect_error) {
|
||||
header('Content-Type: application/json');
|
||||
http_response_code(500);
|
||||
die(json_encode(['error' => 'Database connectie mislukt']));
|
||||
}
|
||||
|
||||
$db->set_charset('utf8mb4');
|
||||
}
|
||||
|
||||
return $db;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get WooCommerce client
|
||||
* @return Client
|
||||
*/
|
||||
function getWooCommerce(): Client
|
||||
{
|
||||
static $woocommerce = null;
|
||||
|
||||
if ($woocommerce === null) {
|
||||
$woocommerce = new Client(
|
||||
$_ENV['WC_URL'],
|
||||
$_ENV['WC_KEY'],
|
||||
$_ENV['WC_SECRET'],
|
||||
['version' => 'wc/v3', 'verify_ssl' => false]
|
||||
);
|
||||
}
|
||||
|
||||
return $woocommerce;
|
||||
}
|
||||
88
api/middleware/auth.php
Normal file
88
api/middleware/auth.php
Normal file
@ -0,0 +1,88 @@
|
||||
<?php
|
||||
/**
|
||||
* Authentication middleware and handlers
|
||||
*/
|
||||
|
||||
/**
|
||||
* Handle login action
|
||||
* @param mysqli $db Database connection
|
||||
* @return void
|
||||
*/
|
||||
function handleLogin(mysqli $db): void
|
||||
{
|
||||
$input = json_decode(file_get_contents('php://input'), true);
|
||||
$username = $input['username'] ?? '';
|
||||
|
||||
$stmt = $db->prepare("SELECT password, full_name FROM sales_users WHERE username = ?");
|
||||
$stmt->bind_param("s", $username);
|
||||
$stmt->execute();
|
||||
$res = $stmt->get_result()->fetch_assoc();
|
||||
|
||||
if ($res && password_verify($input['password'], $res['password'])) {
|
||||
$_SESSION['user'] = $username;
|
||||
$_SESSION['full_name'] = $res['full_name'];
|
||||
|
||||
// Recovery cookie payload
|
||||
$cookie_payload = base64_encode(json_encode([
|
||||
'user' => $username,
|
||||
'full_name' => $res['full_name'],
|
||||
'expires' => MIDNIGHT_TIMESTAMP
|
||||
]));
|
||||
|
||||
setcookie('telvero_remember', $cookie_payload, MIDNIGHT_TIMESTAMP, '/', '', isset($_SERVER['HTTPS']), true);
|
||||
|
||||
echo json_encode(['success' => true, 'user' => $res['full_name']]);
|
||||
} else {
|
||||
http_response_code(401);
|
||||
echo json_encode(['error' => 'Login mislukt']);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle session check action
|
||||
* @return void
|
||||
*/
|
||||
function handleCheckSession(): void
|
||||
{
|
||||
if (isset($_SESSION['user'])) {
|
||||
echo json_encode([
|
||||
'authenticated' => true,
|
||||
'user' => $_SESSION['full_name'] ?? $_SESSION['user']
|
||||
]);
|
||||
} else {
|
||||
echo json_encode(['authenticated' => false]);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle logout action
|
||||
* @return void
|
||||
*/
|
||||
function handleLogout(): void
|
||||
{
|
||||
session_destroy();
|
||||
setcookie('telvero_remember', '', time() - 3600, '/');
|
||||
echo json_encode(['success' => true]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if user is authenticated
|
||||
* @return bool
|
||||
*/
|
||||
function isAuthenticated(): bool
|
||||
{
|
||||
return isset($_SESSION['user']);
|
||||
}
|
||||
|
||||
/**
|
||||
* Require authentication - exits if not authenticated
|
||||
* @return void
|
||||
*/
|
||||
function requireAuth(): void
|
||||
{
|
||||
if (!isAuthenticated()) {
|
||||
http_response_code(403);
|
||||
echo json_encode(['error' => 'Not authenticated']);
|
||||
exit;
|
||||
}
|
||||
}
|
||||
72
api/services/PostcodeService.php
Normal file
72
api/services/PostcodeService.php
Normal file
@ -0,0 +1,72 @@
|
||||
<?php
|
||||
/**
|
||||
* Postcode Service - Handles postcode lookup via postcode.tech API
|
||||
*/
|
||||
|
||||
class PostcodeService
|
||||
{
|
||||
/**
|
||||
* Lookup address by postcode and house number
|
||||
*
|
||||
* @param string $postcode
|
||||
* @param string $number
|
||||
* @return array
|
||||
*/
|
||||
public static function lookup(string $postcode, string $number): array
|
||||
{
|
||||
$postcode = str_replace(' ', '', $postcode);
|
||||
|
||||
if (empty($postcode) || empty($number)) {
|
||||
return [
|
||||
'success' => false,
|
||||
'error' => 'Postcode en huisnummer zijn verplicht',
|
||||
'http_code' => 400
|
||||
];
|
||||
}
|
||||
|
||||
$url = "https://postcode.tech/api/v1/postcode?postcode={$postcode}&number={$number}";
|
||||
|
||||
$ch = curl_init($url);
|
||||
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
|
||||
curl_setopt($ch, CURLOPT_HTTPHEADER, [
|
||||
"Authorization: Bearer " . $_ENV['POSTCODE_TECH_KEY']
|
||||
]);
|
||||
curl_setopt($ch, CURLOPT_TIMEOUT, 10);
|
||||
curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 5);
|
||||
|
||||
$response = curl_exec($ch);
|
||||
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
||||
$curlError = curl_error($ch);
|
||||
curl_close($ch);
|
||||
|
||||
if ($curlError || $httpCode >= 500) {
|
||||
return [
|
||||
'success' => false,
|
||||
'error' => 'Postcode service niet bereikbaar, vul straat en woonplaats zelf in',
|
||||
'details' => $curlError,
|
||||
'http_code' => 503
|
||||
];
|
||||
}
|
||||
|
||||
if ($httpCode >= 400) {
|
||||
$decoded = json_decode($response, true);
|
||||
$errorMessage = ($decoded && isset($decoded['error']))
|
||||
? $decoded['error']
|
||||
: 'Postcode niet gevonden of ongeldige invoer, vul straat en woonplaats zelf in';
|
||||
|
||||
return [
|
||||
'success' => false,
|
||||
'error' => $errorMessage,
|
||||
'http_code' => $httpCode
|
||||
];
|
||||
}
|
||||
|
||||
$data = json_decode($response, true);
|
||||
|
||||
return [
|
||||
'success' => true,
|
||||
'data' => $data,
|
||||
'http_code' => 200
|
||||
];
|
||||
}
|
||||
}
|
||||
263
api/services/UpsellService.php
Normal file
263
api/services/UpsellService.php
Normal file
@ -0,0 +1,263 @@
|
||||
<?php
|
||||
/**
|
||||
* Upsell Service - Handles CUW (UpsellWP) campaign data
|
||||
*/
|
||||
|
||||
class UpsellService
|
||||
{
|
||||
/**
|
||||
* Build a map: product_id => related product ids from CUW (UpsellWP) campaigns.
|
||||
* Uses {$wpdb->prefix}cuw_campaigns table.
|
||||
*
|
||||
* @return array<int, int[]>
|
||||
*/
|
||||
public static function buildProductMap(): array
|
||||
{
|
||||
if (!function_exists('get_post_type')) {
|
||||
return [];
|
||||
}
|
||||
|
||||
global $wpdb;
|
||||
|
||||
$table = $wpdb->prefix . 'cuw_campaigns';
|
||||
|
||||
// Safety: table might not exist
|
||||
$exists = $wpdb->get_var($wpdb->prepare("SHOW TABLES LIKE %s", $table));
|
||||
if ($exists !== $table) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$rows = $wpdb->get_results("
|
||||
SELECT id, type, filters, data
|
||||
FROM {$table}
|
||||
WHERE enabled = 1
|
||||
ORDER BY priority DESC, id DESC
|
||||
");
|
||||
|
||||
if (empty($rows)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$map = [];
|
||||
|
||||
foreach ($rows as $row) {
|
||||
$filters = json_decode((string) $row->filters, true);
|
||||
if (!is_array($filters)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$filterProductLists = self::extractProductListsFromFilters($filters);
|
||||
|
||||
if (empty($filterProductLists)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// For each list: if a product is in that list, relate it to the other products in that list
|
||||
foreach ($filterProductLists as $list) {
|
||||
foreach ($list as $pid) {
|
||||
$related = array_values(array_diff($list, [$pid]));
|
||||
if (empty($related)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Keep only real Woo products
|
||||
$related = array_values(array_filter($related, function ($id) {
|
||||
$pt = get_post_type($id);
|
||||
return $pt === 'product' || $pt === 'product_variation';
|
||||
}));
|
||||
|
||||
if (empty($related)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!isset($map[$pid])) {
|
||||
$map[$pid] = [];
|
||||
}
|
||||
$map[$pid] = array_values(array_unique(array_merge($map[$pid], $related)));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $map;
|
||||
}
|
||||
|
||||
/**
|
||||
* Build deals map for buy_more_save_more campaigns
|
||||
*
|
||||
* @return array<int, array>
|
||||
*/
|
||||
public static function buildDealsMap(): array
|
||||
{
|
||||
global $wpdb;
|
||||
|
||||
$table = $wpdb->prefix . 'cuw_campaigns';
|
||||
|
||||
$exists = $wpdb->get_var($wpdb->prepare("SHOW TABLES LIKE %s", $table));
|
||||
if ($exists !== $table) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$rows = $wpdb->get_results("
|
||||
SELECT id, type, title, priority, filters, data
|
||||
FROM {$table}
|
||||
WHERE enabled = 1
|
||||
ORDER BY priority DESC, id DESC
|
||||
");
|
||||
|
||||
if (empty($rows)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$map = [];
|
||||
|
||||
foreach ($rows as $row) {
|
||||
$type = (string) $row->type;
|
||||
if ($type !== 'buy_more_save_more') {
|
||||
continue;
|
||||
}
|
||||
|
||||
$filters = json_decode((string) $row->filters, true);
|
||||
$data = json_decode((string) $row->data, true);
|
||||
|
||||
if (!is_array($filters) || !is_array($data)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$product_ids = self::extractProductIdsFromFilters($filters);
|
||||
|
||||
if (empty($product_ids)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Deal details from data
|
||||
$cta_text = $data['template']['cta_text'] ?? '';
|
||||
$display_location = $data['display_location'] ?? ($data['template']['display_location'] ?? '');
|
||||
|
||||
$discounts = $data['discounts'] ?? [];
|
||||
if (!is_array($discounts) || empty($discounts)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
foreach ($discounts as $disc) {
|
||||
if (!is_array($disc)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$qty = isset($disc['qty']) ? (int) $disc['qty'] : 0;
|
||||
$dType = isset($disc['type']) ? (string) $disc['type'] : '';
|
||||
$value = isset($disc['value']) ? (float) $disc['value'] : 0.0;
|
||||
|
||||
if ($qty <= 0 || $dType === '' || $value <= 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$deal = [
|
||||
'campaign_id' => (int) $row->id,
|
||||
'campaign_type' => $type,
|
||||
'title' => (string) $row->title,
|
||||
'priority' => (int) $row->priority,
|
||||
'qty' => $qty,
|
||||
'discount_type' => $dType,
|
||||
'value' => $value,
|
||||
'cta_text' => (string) $cta_text,
|
||||
'display_location' => (string) $display_location,
|
||||
];
|
||||
|
||||
foreach ($product_ids as $pid) {
|
||||
if (!isset($map[$pid])) {
|
||||
$map[$pid] = [];
|
||||
}
|
||||
$map[$pid][] = $deal;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $map;
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract product lists from campaign filters
|
||||
*
|
||||
* @param array $filters
|
||||
* @return array<int[]>
|
||||
*/
|
||||
private static function extractProductListsFromFilters(array $filters): array
|
||||
{
|
||||
$filterProductLists = [];
|
||||
|
||||
foreach ($filters as $k => $v) {
|
||||
if ($k === 'relation') {
|
||||
continue;
|
||||
}
|
||||
if (!is_array($v)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$type = $v['type'] ?? '';
|
||||
$method = $v['method'] ?? '';
|
||||
$values = $v['values'] ?? [];
|
||||
|
||||
if ($type !== 'products') {
|
||||
continue;
|
||||
}
|
||||
if (!in_array($method, ['in_list', 'in', 'include', 'equals'], true)) {
|
||||
continue;
|
||||
}
|
||||
if (!is_array($values)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$ids = array_values(array_unique(array_map('intval', $values)));
|
||||
$ids = array_values(array_filter($ids));
|
||||
|
||||
if (!empty($ids)) {
|
||||
$filterProductLists[] = $ids;
|
||||
}
|
||||
}
|
||||
|
||||
return $filterProductLists;
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract product IDs from campaign filters
|
||||
*
|
||||
* @param array $filters
|
||||
* @return int[]
|
||||
*/
|
||||
private static function extractProductIdsFromFilters(array $filters): array
|
||||
{
|
||||
$product_ids = [];
|
||||
|
||||
foreach ($filters as $k => $v) {
|
||||
if ($k === 'relation') {
|
||||
continue;
|
||||
}
|
||||
if (!is_array($v)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$f_type = $v['type'] ?? '';
|
||||
$f_method = $v['method'] ?? '';
|
||||
$values = $v['values'] ?? [];
|
||||
|
||||
if ($f_type !== 'products') {
|
||||
continue;
|
||||
}
|
||||
if (!in_array($f_method, ['in_list', 'in', 'include', 'equals'], true)) {
|
||||
continue;
|
||||
}
|
||||
if (!is_array($values)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
foreach ($values as $pid) {
|
||||
$pid = (int) $pid;
|
||||
if ($pid > 0) {
|
||||
$product_ids[] = $pid;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return array_values(array_unique(array_filter($product_ids)));
|
||||
}
|
||||
}
|
||||
226
index.html
226
index.html
@ -20,6 +20,12 @@
|
||||
border-radius: 10px;
|
||||
}
|
||||
</style>
|
||||
<!-- Load JavaScript modules -->
|
||||
<script src="js/services/api.js"></script>
|
||||
<script src="js/components/cart.js"></script>
|
||||
<script src="js/components/products.js"></script>
|
||||
<script src="js/components/forms.js"></script>
|
||||
<script src="js/app.js"></script>
|
||||
</head>
|
||||
|
||||
<body class="bg-slate-100 min-h-screen font-sans" x-data="salesApp()">
|
||||
@ -305,226 +311,6 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
function salesApp() {
|
||||
return {
|
||||
isLoggedIn: false,
|
||||
isLoading: true,
|
||||
currentUser: '',
|
||||
loginForm: { username: '', password: '' },
|
||||
products: [],
|
||||
cart: [],
|
||||
activeProduct: null,
|
||||
selectedProductId: '',
|
||||
selectedVariationId: '',
|
||||
variations: [],
|
||||
extraProductId: '',
|
||||
shipping: '8.95',
|
||||
submitting: false,
|
||||
orderComplete: false,
|
||||
lastOrder: { id: '', name: '', total: '' },
|
||||
form: { initials: '', lastname: '', postcode: '', houseno: '', suffix: '', street: '', city: '', email: '', phone: '' },
|
||||
meta: { mediacode: '' },
|
||||
recommendedOptions: [],
|
||||
addressError: '',
|
||||
productSearch: '',
|
||||
extraProductSearch: '',
|
||||
|
||||
get filteredProducts() {
|
||||
let filtered = this.products;
|
||||
if (this.productSearch.trim()) {
|
||||
const search = this.productSearch.toLowerCase().trim();
|
||||
filtered = this.products.filter(p => p.name.toLowerCase().includes(search));
|
||||
}
|
||||
return filtered;
|
||||
},
|
||||
|
||||
get filteredExtraProducts() {
|
||||
let filtered = this.products;
|
||||
if (this.extraProductSearch.trim()) {
|
||||
const search = this.extraProductSearch.toLowerCase().trim();
|
||||
filtered = this.products.filter(p => p.name.toLowerCase().includes(search));
|
||||
}
|
||||
return filtered;
|
||||
},
|
||||
|
||||
// THIS RUNS AUTOMATICALLY ON PAGE LOAD
|
||||
async init() {
|
||||
this.isLoading = true;
|
||||
try {
|
||||
// Lichte sessie-check (geen producten laden)
|
||||
const res = await fetch('api.php?action=check_session');
|
||||
const data = await res.json();
|
||||
if (data.authenticated) {
|
||||
this.currentUser = data.user || localStorage.getItem('telvero_user') || 'Agent';
|
||||
// Producten laden en pas daarna isLoggedIn zetten
|
||||
await this.loadProducts();
|
||||
this.isLoggedIn = true;
|
||||
}
|
||||
} catch (e) {
|
||||
console.error("Session check failed");
|
||||
} finally {
|
||||
this.isLoading = false;
|
||||
}
|
||||
},
|
||||
|
||||
async doLogin() {
|
||||
const res = await fetch('api.php?action=login', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(this.loginForm)
|
||||
});
|
||||
if (res.ok) {
|
||||
const data = await res.json();
|
||||
this.currentUser = data.user;
|
||||
localStorage.setItem('telvero_user', data.user);
|
||||
// Producten laden en pas daarna isLoggedIn zetten
|
||||
await this.loadProducts();
|
||||
this.isLoggedIn = true;
|
||||
} else {
|
||||
alert("Login mislukt");
|
||||
}
|
||||
},
|
||||
|
||||
async loadProducts() {
|
||||
try {
|
||||
const res = await fetch('api.php?action=get_products');
|
||||
if (res.ok) {
|
||||
const data = await res.json();
|
||||
// Sort products alphabetically by name
|
||||
this.products = data.sort((a, b) => a.name.localeCompare(b.name, 'nl'));
|
||||
}
|
||||
} catch (e) {
|
||||
console.error("Failed to load products");
|
||||
}
|
||||
},
|
||||
|
||||
async doLogout() {
|
||||
await fetch('api.php?action=logout');
|
||||
localStorage.removeItem('telvero_user'); // Clean up local storage
|
||||
location.reload();
|
||||
},
|
||||
|
||||
selectProduct() {
|
||||
const p = this.products.find(x => x.id == this.selectedProductId);
|
||||
if (!p) return;
|
||||
this.activeProduct = p; this.variations = p.variation_details || [];
|
||||
this.cart = []; this.selectedVariationId = '';
|
||||
if (p.type !== 'variable') {
|
||||
this.cart.push({ id: parseInt(p.id), name: p.name, price: p.price });
|
||||
this.loadRecommendations(p);
|
||||
}
|
||||
},
|
||||
|
||||
selectVariation() {
|
||||
const v = this.variations.find(x => x.id == this.selectedVariationId);
|
||||
if (!v) return;
|
||||
this.cart = [{ id: parseInt(this.activeProduct.id), variation_id: parseInt(v.id), name: this.activeProduct.name + ' - ' + this.getVarName(v), price: v.price }];
|
||||
this.loadUpsells(this.activeProduct);
|
||||
},
|
||||
|
||||
addExtraItem() {
|
||||
const p = this.products.find(x => x.id == this.extraProductId);
|
||||
if (!p) return;
|
||||
this.cart.push({ id: parseInt(p.id), name: p.name, price: p.price });
|
||||
this.extraProductId = '';
|
||||
},
|
||||
|
||||
loadRecommendations(product) {
|
||||
this.recommendedOptions = [];
|
||||
|
||||
// voorkeur: gecombineerde lijst vanuit API
|
||||
let ids = [];
|
||||
if (product.recommended_ids && product.recommended_ids.length) {
|
||||
ids = product.recommended_ids.map(id => parseInt(id));
|
||||
} else {
|
||||
// fallback: combineer Woo upsells + cross-sells
|
||||
const ups = (product.upsell_ids || []).map(id => parseInt(id));
|
||||
const crs = (product.cross_sell_ids || []).map(id => parseInt(id));
|
||||
ids = [...ups, ...crs];
|
||||
}
|
||||
|
||||
ids = [...new Set(ids)].filter(Boolean);
|
||||
|
||||
// filter op producten die we al in de products-lijst hebben
|
||||
this.recommendedOptions = this.products.filter(p => ids.includes(parseInt(p.id)));
|
||||
},
|
||||
|
||||
|
||||
removeFromCart(index) { this.cart.splice(index, 1); },
|
||||
getVarName(v) { return v.attributes.map(a => a.option).join(' '); },
|
||||
toggleUpsell(u) {
|
||||
const idx = this.cart.findIndex(i => parseInt(i.id) === parseInt(u.id));
|
||||
if (idx > -1) this.cart.splice(idx, 1);
|
||||
else this.cart.push({ id: parseInt(u.id), name: u.name, price: u.price });
|
||||
},
|
||||
isInCart(id) { return this.cart.some(i => parseInt(i.id) === parseInt(id)); },
|
||||
get total() {
|
||||
const itemsTotal = this.cart.reduce((sum, item) => sum + parseFloat(item.price), 0);
|
||||
return (itemsTotal + (parseFloat(this.shipping) || 0)).toFixed(2);
|
||||
},
|
||||
|
||||
formatInitials() { let v = this.form.initials.replace(/[^a-z]/gi, '').toUpperCase(); this.form.initials = v.split('').join('.') + (v ? '.' : ''); },
|
||||
formatLastname() { this.form.lastname = this.form.lastname.charAt(0).toUpperCase() + this.form.lastname.slice(1); },
|
||||
|
||||
async submitOrder() {
|
||||
this.submitting = true;
|
||||
const payload = {
|
||||
mediacode_internal: this.meta.mediacode,
|
||||
shipping_total: this.shipping,
|
||||
billing: {
|
||||
first_name: this.form.initials, last_name: this.form.lastname,
|
||||
address_1: (this.form.street + ' ' + this.form.houseno + ' ' + (this.form.suffix || '')).trim(),
|
||||
city: this.form.city, postcode: this.form.postcode, country: 'NL', email: this.form.email, phone: this.form.phone
|
||||
},
|
||||
shipping: {
|
||||
first_name: this.form.initials, last_name: this.form.lastname,
|
||||
address_1: (this.form.street + ' ' + this.form.houseno + ' ' + (this.form.suffix || '')).trim(),
|
||||
city: this.form.city, postcode: this.form.postcode, country: 'NL', email: this.form.email, phone: this.form.phone
|
||||
},
|
||||
line_items: this.cart.map(i => ({ product_id: i.id, variation_id: i.variation_id || 0, quantity: 1 }))
|
||||
};
|
||||
try {
|
||||
const res = await fetch('api.php?action=create_order', { method: 'POST', body: JSON.stringify(payload) });
|
||||
const result = await res.json();
|
||||
if (result.success) {
|
||||
this.lastOrder = { id: result.order_id, name: this.form.initials + ' ' + this.form.lastname, total: result.total };
|
||||
this.orderComplete = true;
|
||||
} else { alert("Fout: " + result.error); }
|
||||
} catch (e) { alert("Systeemfout"); }
|
||||
this.submitting = false;
|
||||
},
|
||||
|
||||
resetForNewOrder() {
|
||||
this.cart = []; this.selectedProductId = ''; this.selectedVariationId = ''; this.shipping = '8.95';
|
||||
this.form = { initials: '', lastname: '', postcode: '', houseno: '', suffix: '', street: '', city: '', email: '', phone: '' };
|
||||
this.addressError = '';
|
||||
this.orderComplete = false;
|
||||
},
|
||||
|
||||
|
||||
async lookupAddress() {
|
||||
this.addressError = '';
|
||||
if (this.form.postcode.length >= 6 && this.form.houseno) {
|
||||
try {
|
||||
const res = await fetch(`api.php?action=postcode_check&postcode=${this.form.postcode}&number=${this.form.houseno}`);
|
||||
const data = await res.json();
|
||||
if (data.street) {
|
||||
this.form.street = data.street.toUpperCase();
|
||||
this.form.city = data.city.toUpperCase();
|
||||
} else if (data.error) {
|
||||
this.addressError = data.error;
|
||||
} else {
|
||||
this.addressError = 'Adres niet gevonden';
|
||||
}
|
||||
} catch (e) {
|
||||
this.addressError = 'Fout bij ophalen adres';
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
124
js/app.js
Normal file
124
js/app.js
Normal file
@ -0,0 +1,124 @@
|
||||
/**
|
||||
* Main Application - Alpine.js Sales Panel App
|
||||
*
|
||||
* This file combines all components into the main Alpine.js application.
|
||||
*/
|
||||
|
||||
function salesApp() {
|
||||
return {
|
||||
// Authentication state
|
||||
isLoggedIn: false,
|
||||
isLoading: true,
|
||||
currentUser: '',
|
||||
loginForm: { username: '', password: '' },
|
||||
|
||||
// Order state
|
||||
submitting: false,
|
||||
orderComplete: false,
|
||||
lastOrder: { id: '', name: '', total: '' },
|
||||
|
||||
// Include state from components
|
||||
...CartComponent.getInitialState(),
|
||||
...ProductsComponent.getInitialState(),
|
||||
...FormsComponent.getInitialState(),
|
||||
|
||||
// Computed properties
|
||||
get total() {
|
||||
return CartComponent.getComputed().total.call(this);
|
||||
},
|
||||
|
||||
get filteredProducts() {
|
||||
return ProductsComponent.getComputed().filteredProducts.call(this);
|
||||
},
|
||||
|
||||
get filteredExtraProducts() {
|
||||
return ProductsComponent.getComputed().filteredExtraProducts.call(this);
|
||||
},
|
||||
|
||||
/**
|
||||
* Initialize the application
|
||||
* Runs automatically on page load
|
||||
*/
|
||||
async init() {
|
||||
this.isLoading = true;
|
||||
try {
|
||||
const data = await ApiService.checkSession();
|
||||
if (data.authenticated) {
|
||||
this.currentUser = data.user || localStorage.getItem('telvero_user') || 'Agent';
|
||||
await this.loadProducts();
|
||||
this.isLoggedIn = true;
|
||||
}
|
||||
} catch (e) {
|
||||
console.error("Session check failed");
|
||||
} finally {
|
||||
this.isLoading = false;
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Handle user login
|
||||
*/
|
||||
async doLogin() {
|
||||
const result = await ApiService.login(this.loginForm.username, this.loginForm.password);
|
||||
if (result.ok) {
|
||||
this.currentUser = result.data.user;
|
||||
localStorage.setItem('telvero_user', result.data.user);
|
||||
await this.loadProducts();
|
||||
this.isLoggedIn = true;
|
||||
} else {
|
||||
alert("Login mislukt");
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Handle user logout
|
||||
*/
|
||||
async doLogout() {
|
||||
await ApiService.logout();
|
||||
localStorage.removeItem('telvero_user');
|
||||
location.reload();
|
||||
},
|
||||
|
||||
/**
|
||||
* Submit order to API
|
||||
*/
|
||||
async submitOrder() {
|
||||
this.submitting = true;
|
||||
|
||||
try {
|
||||
const payload = this.buildOrderPayload();
|
||||
const result = await ApiService.createOrder(payload);
|
||||
|
||||
if (result.success) {
|
||||
this.lastOrder = {
|
||||
id: result.order_id,
|
||||
name: this.form.initials + ' ' + this.form.lastname,
|
||||
total: result.total
|
||||
};
|
||||
this.orderComplete = true;
|
||||
} else {
|
||||
alert("Fout: " + result.error);
|
||||
}
|
||||
} catch (e) {
|
||||
alert("Systeemfout");
|
||||
}
|
||||
|
||||
this.submitting = false;
|
||||
},
|
||||
|
||||
/**
|
||||
* Reset everything for a new order
|
||||
*/
|
||||
resetForNewOrder() {
|
||||
this.resetCart();
|
||||
this.resetProducts();
|
||||
this.resetForm();
|
||||
this.orderComplete = false;
|
||||
},
|
||||
|
||||
// Include methods from components
|
||||
...CartComponent.getMethods(),
|
||||
...ProductsComponent.getMethods(),
|
||||
...FormsComponent.getMethods()
|
||||
};
|
||||
}
|
||||
102
js/components/cart.js
Normal file
102
js/components/cart.js
Normal file
@ -0,0 +1,102 @@
|
||||
/**
|
||||
* Cart Component - Handles shopping cart functionality
|
||||
*/
|
||||
const CartComponent = {
|
||||
/**
|
||||
* Initialize cart state
|
||||
* @returns {Object}
|
||||
*/
|
||||
getInitialState() {
|
||||
return {
|
||||
cart: [],
|
||||
shipping: '8.95'
|
||||
};
|
||||
},
|
||||
|
||||
/**
|
||||
* Get cart methods for Alpine.js component
|
||||
* @returns {Object}
|
||||
*/
|
||||
getMethods() {
|
||||
return {
|
||||
/**
|
||||
* Add item to cart
|
||||
* @param {Object} item - Item with id, name, price, and optional variation_id
|
||||
*/
|
||||
addToCart(item) {
|
||||
this.cart.push({
|
||||
id: parseInt(item.id),
|
||||
variation_id: item.variation_id || 0,
|
||||
name: item.name,
|
||||
price: item.price
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Remove item from cart by index
|
||||
* @param {number} index
|
||||
*/
|
||||
removeFromCart(index) {
|
||||
this.cart.splice(index, 1);
|
||||
},
|
||||
|
||||
/**
|
||||
* Check if product is in cart
|
||||
* @param {number} id
|
||||
* @returns {boolean}
|
||||
*/
|
||||
isInCart(id) {
|
||||
return this.cart.some(i => parseInt(i.id) === parseInt(id));
|
||||
},
|
||||
|
||||
/**
|
||||
* Toggle upsell product in cart
|
||||
* @param {Object} product
|
||||
*/
|
||||
toggleUpsell(product) {
|
||||
const idx = this.cart.findIndex(i => parseInt(i.id) === parseInt(product.id));
|
||||
if (idx > -1) {
|
||||
this.cart.splice(idx, 1);
|
||||
} else {
|
||||
this.cart.push({
|
||||
id: parseInt(product.id),
|
||||
name: product.name,
|
||||
price: product.price
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Clear the cart
|
||||
*/
|
||||
clearCart() {
|
||||
this.cart = [];
|
||||
},
|
||||
|
||||
/**
|
||||
* Reset cart and shipping for new order
|
||||
*/
|
||||
resetCart() {
|
||||
this.cart = [];
|
||||
this.shipping = '8.95';
|
||||
}
|
||||
};
|
||||
},
|
||||
|
||||
/**
|
||||
* Get computed properties for Alpine.js component
|
||||
* @returns {Object}
|
||||
*/
|
||||
getComputed() {
|
||||
return {
|
||||
/**
|
||||
* Calculate total including shipping
|
||||
* @returns {string}
|
||||
*/
|
||||
total() {
|
||||
const itemsTotal = this.cart.reduce((sum, item) => sum + parseFloat(item.price), 0);
|
||||
return (itemsTotal + (parseFloat(this.shipping) || 0)).toFixed(2);
|
||||
}
|
||||
};
|
||||
}
|
||||
};
|
||||
139
js/components/forms.js
Normal file
139
js/components/forms.js
Normal file
@ -0,0 +1,139 @@
|
||||
/**
|
||||
* Forms Component - Handles form data and validation
|
||||
*/
|
||||
const FormsComponent = {
|
||||
/**
|
||||
* Initialize forms state
|
||||
* @returns {Object}
|
||||
*/
|
||||
getInitialState() {
|
||||
return {
|
||||
form: {
|
||||
initials: '',
|
||||
lastname: '',
|
||||
postcode: '',
|
||||
houseno: '',
|
||||
suffix: '',
|
||||
street: '',
|
||||
city: '',
|
||||
email: '',
|
||||
phone: ''
|
||||
},
|
||||
meta: {
|
||||
mediacode: ''
|
||||
},
|
||||
addressError: ''
|
||||
};
|
||||
},
|
||||
|
||||
/**
|
||||
* Get forms methods for Alpine.js component
|
||||
* @returns {Object}
|
||||
*/
|
||||
getMethods() {
|
||||
return {
|
||||
/**
|
||||
* Format initials with dots (e.g., "JK" -> "J.K.")
|
||||
*/
|
||||
formatInitials() {
|
||||
let v = this.form.initials.replace(/[^a-z]/gi, '').toUpperCase();
|
||||
this.form.initials = v.split('').join('.') + (v ? '.' : '');
|
||||
},
|
||||
|
||||
/**
|
||||
* Capitalize first letter of lastname
|
||||
*/
|
||||
formatLastname() {
|
||||
this.form.lastname = this.form.lastname.charAt(0).toUpperCase() + this.form.lastname.slice(1);
|
||||
},
|
||||
|
||||
/**
|
||||
* Lookup address by postcode and house number
|
||||
*/
|
||||
async lookupAddress() {
|
||||
this.addressError = '';
|
||||
|
||||
if (this.form.postcode.length >= 6 && this.form.houseno) {
|
||||
try {
|
||||
const data = await ApiService.lookupPostcode(this.form.postcode, this.form.houseno);
|
||||
|
||||
if (data.street) {
|
||||
this.form.street = data.street.toUpperCase();
|
||||
this.form.city = data.city.toUpperCase();
|
||||
} else if (data.error) {
|
||||
this.addressError = data.error;
|
||||
} else {
|
||||
this.addressError = 'Adres niet gevonden';
|
||||
}
|
||||
} catch (e) {
|
||||
this.addressError = 'Fout bij ophalen adres';
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Reset form for new order
|
||||
*/
|
||||
resetForm() {
|
||||
this.form = {
|
||||
initials: '',
|
||||
lastname: '',
|
||||
postcode: '',
|
||||
houseno: '',
|
||||
suffix: '',
|
||||
street: '',
|
||||
city: '',
|
||||
email: '',
|
||||
phone: ''
|
||||
};
|
||||
this.addressError = '';
|
||||
},
|
||||
|
||||
/**
|
||||
* Build order payload from form data
|
||||
* @returns {Object}
|
||||
*/
|
||||
buildOrderPayload() {
|
||||
const address = (this.form.street + ' ' + this.form.houseno + ' ' + (this.form.suffix || '')).trim();
|
||||
|
||||
return {
|
||||
mediacode_internal: this.meta.mediacode,
|
||||
shipping_total: this.shipping,
|
||||
billing: {
|
||||
first_name: this.form.initials,
|
||||
last_name: this.form.lastname,
|
||||
address_1: address,
|
||||
city: this.form.city,
|
||||
postcode: this.form.postcode,
|
||||
country: 'NL',
|
||||
email: this.form.email,
|
||||
phone: this.form.phone
|
||||
},
|
||||
shipping: {
|
||||
first_name: this.form.initials,
|
||||
last_name: this.form.lastname,
|
||||
address_1: address,
|
||||
city: this.form.city,
|
||||
postcode: this.form.postcode,
|
||||
country: 'NL',
|
||||
email: this.form.email,
|
||||
phone: this.form.phone
|
||||
},
|
||||
line_items: this.cart.map(i => ({
|
||||
product_id: i.id,
|
||||
variation_id: i.variation_id || 0,
|
||||
quantity: 1
|
||||
}))
|
||||
};
|
||||
},
|
||||
|
||||
/**
|
||||
* Validate form before submission
|
||||
* @returns {boolean}
|
||||
*/
|
||||
isFormValid() {
|
||||
return this.form.email && this.meta.mediacode && this.cart.length > 0;
|
||||
}
|
||||
};
|
||||
}
|
||||
};
|
||||
175
js/components/products.js
Normal file
175
js/components/products.js
Normal file
@ -0,0 +1,175 @@
|
||||
/**
|
||||
* Products Component - Handles product selection and recommendations
|
||||
*/
|
||||
const ProductsComponent = {
|
||||
/**
|
||||
* Initialize products state
|
||||
* @returns {Object}
|
||||
*/
|
||||
getInitialState() {
|
||||
return {
|
||||
products: [],
|
||||
activeProduct: null,
|
||||
selectedProductId: '',
|
||||
selectedVariationId: '',
|
||||
variations: [],
|
||||
extraProductId: '',
|
||||
recommendedOptions: [],
|
||||
productSearch: '',
|
||||
extraProductSearch: ''
|
||||
};
|
||||
},
|
||||
|
||||
/**
|
||||
* Get products methods for Alpine.js component
|
||||
* @returns {Object}
|
||||
*/
|
||||
getMethods() {
|
||||
return {
|
||||
/**
|
||||
* Load products from API
|
||||
*/
|
||||
async loadProducts() {
|
||||
try {
|
||||
const data = await ApiService.getProducts();
|
||||
// Sort products alphabetically by name
|
||||
this.products = data.sort((a, b) => a.name.localeCompare(b.name, 'nl'));
|
||||
} catch (e) {
|
||||
console.error("Failed to load products");
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Handle main product selection
|
||||
*/
|
||||
selectProduct() {
|
||||
const p = this.products.find(x => x.id == this.selectedProductId);
|
||||
if (!p) return;
|
||||
|
||||
this.activeProduct = p;
|
||||
this.variations = p.variation_details || [];
|
||||
this.cart = [];
|
||||
this.selectedVariationId = '';
|
||||
|
||||
if (p.type !== 'variable') {
|
||||
this.cart.push({
|
||||
id: parseInt(p.id),
|
||||
name: p.name,
|
||||
price: p.price
|
||||
});
|
||||
this.loadRecommendations(p);
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Handle variation selection
|
||||
*/
|
||||
selectVariation() {
|
||||
const v = this.variations.find(x => x.id == this.selectedVariationId);
|
||||
if (!v) return;
|
||||
|
||||
this.cart = [{
|
||||
id: parseInt(this.activeProduct.id),
|
||||
variation_id: parseInt(v.id),
|
||||
name: this.activeProduct.name + ' - ' + this.getVarName(v),
|
||||
price: v.price
|
||||
}];
|
||||
|
||||
this.loadRecommendations(this.activeProduct);
|
||||
},
|
||||
|
||||
/**
|
||||
* Add extra product to cart
|
||||
*/
|
||||
addExtraItem() {
|
||||
const p = this.products.find(x => x.id == this.extraProductId);
|
||||
if (!p) return;
|
||||
|
||||
this.cart.push({
|
||||
id: parseInt(p.id),
|
||||
name: p.name,
|
||||
price: p.price
|
||||
});
|
||||
this.extraProductId = '';
|
||||
},
|
||||
|
||||
/**
|
||||
* Load product recommendations
|
||||
* @param {Object} product
|
||||
*/
|
||||
loadRecommendations(product) {
|
||||
this.recommendedOptions = [];
|
||||
|
||||
// Prefer combined list from API
|
||||
let ids = [];
|
||||
if (product.recommended_ids && product.recommended_ids.length) {
|
||||
ids = product.recommended_ids.map(id => parseInt(id));
|
||||
} else {
|
||||
// Fallback: combine Woo upsells + cross-sells
|
||||
const ups = (product.upsell_ids || []).map(id => parseInt(id));
|
||||
const crs = (product.cross_sell_ids || []).map(id => parseInt(id));
|
||||
ids = [...ups, ...crs];
|
||||
}
|
||||
|
||||
ids = [...new Set(ids)].filter(Boolean);
|
||||
|
||||
// Filter to products we have in our products list
|
||||
this.recommendedOptions = this.products.filter(p => ids.includes(parseInt(p.id)));
|
||||
},
|
||||
|
||||
/**
|
||||
* Get variation name from attributes
|
||||
* @param {Object} v - Variation object
|
||||
* @returns {string}
|
||||
*/
|
||||
getVarName(v) {
|
||||
return v.attributes.map(a => a.option).join(' ');
|
||||
},
|
||||
|
||||
/**
|
||||
* Reset product selection for new order
|
||||
*/
|
||||
resetProducts() {
|
||||
this.selectedProductId = '';
|
||||
this.selectedVariationId = '';
|
||||
this.activeProduct = null;
|
||||
this.variations = [];
|
||||
this.recommendedOptions = [];
|
||||
}
|
||||
};
|
||||
},
|
||||
|
||||
/**
|
||||
* Get computed properties for Alpine.js component
|
||||
* @returns {Object}
|
||||
*/
|
||||
getComputed() {
|
||||
return {
|
||||
/**
|
||||
* Filter products by search term
|
||||
* @returns {Array}
|
||||
*/
|
||||
filteredProducts() {
|
||||
let filtered = this.products;
|
||||
if (this.productSearch.trim()) {
|
||||
const search = this.productSearch.toLowerCase().trim();
|
||||
filtered = this.products.filter(p => p.name.toLowerCase().includes(search));
|
||||
}
|
||||
return filtered;
|
||||
},
|
||||
|
||||
/**
|
||||
* Filter extra products by search term
|
||||
* @returns {Array}
|
||||
*/
|
||||
filteredExtraProducts() {
|
||||
let filtered = this.products;
|
||||
if (this.extraProductSearch.trim()) {
|
||||
const search = this.extraProductSearch.toLowerCase().trim();
|
||||
filtered = this.products.filter(p => p.name.toLowerCase().includes(search));
|
||||
}
|
||||
return filtered;
|
||||
}
|
||||
};
|
||||
}
|
||||
};
|
||||
79
js/services/api.js
Normal file
79
js/services/api.js
Normal file
@ -0,0 +1,79 @@
|
||||
/**
|
||||
* API Service - Handles all API communication
|
||||
*/
|
||||
const ApiService = {
|
||||
/**
|
||||
* Base URL for API calls
|
||||
*/
|
||||
baseUrl: 'api.php',
|
||||
|
||||
/**
|
||||
* Check if user session is valid
|
||||
* @returns {Promise<{authenticated: boolean, user?: string}>}
|
||||
*/
|
||||
async checkSession() {
|
||||
const res = await fetch(`${this.baseUrl}?action=check_session`);
|
||||
return res.json();
|
||||
},
|
||||
|
||||
/**
|
||||
* Login user
|
||||
* @param {string} username
|
||||
* @param {string} password
|
||||
* @returns {Promise<{success?: boolean, user?: string, error?: string}>}
|
||||
*/
|
||||
async login(username, password) {
|
||||
const res = await fetch(`${this.baseUrl}?action=login`, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ username, password })
|
||||
});
|
||||
return { ok: res.ok, data: await res.json() };
|
||||
},
|
||||
|
||||
/**
|
||||
* Logout user
|
||||
* @returns {Promise<{success: boolean}>}
|
||||
*/
|
||||
async logout() {
|
||||
const res = await fetch(`${this.baseUrl}?action=logout`);
|
||||
return res.json();
|
||||
},
|
||||
|
||||
/**
|
||||
* Get all products
|
||||
* @returns {Promise<Array>}
|
||||
*/
|
||||
async getProducts() {
|
||||
const res = await fetch(`${this.baseUrl}?action=get_products`);
|
||||
if (res.ok) {
|
||||
return res.json();
|
||||
}
|
||||
return [];
|
||||
},
|
||||
|
||||
/**
|
||||
* Lookup address by postcode and house number
|
||||
* @param {string} postcode
|
||||
* @param {string} number
|
||||
* @returns {Promise<{street?: string, city?: string, error?: string}>}
|
||||
*/
|
||||
async lookupPostcode(postcode, number) {
|
||||
const res = await fetch(
|
||||
`${this.baseUrl}?action=postcode_check&postcode=${encodeURIComponent(postcode)}&number=${encodeURIComponent(number)}`
|
||||
);
|
||||
return res.json();
|
||||
},
|
||||
|
||||
/**
|
||||
* Create a new order
|
||||
* @param {Object} orderData
|
||||
* @returns {Promise<{success?: boolean, order_id?: number, total?: string, error?: string}>}
|
||||
*/
|
||||
async createOrder(orderData) {
|
||||
const res = await fetch(`${this.baseUrl}?action=create_order`, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(orderData)
|
||||
});
|
||||
return res.json();
|
||||
}
|
||||
};
|
||||
Loading…
x
Reference in New Issue
Block a user