Login Cookie extend
This commit is contained in:
parent
780403843c
commit
6c37a34f69
539
api.php
539
api.php
@ -1,49 +1,86 @@
|
||||
<?php
|
||||
/**
|
||||
* TELVERO BACKOFFICE - API PROXY (V8.1 - LOGGING RESTORED)
|
||||
*/
|
||||
// 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();
|
||||
ini_set('display_errors', 0);
|
||||
error_reporting(E_ALL);
|
||||
}
|
||||
|
||||
// 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;
|
||||
use Mollie\Api\MollieApiClient;
|
||||
|
||||
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'])); }
|
||||
|
||||
function writeLog($action, $details) {
|
||||
global $db;
|
||||
$user = $_SESSION['user'] ?? 'system';
|
||||
$stmt = $db->prepare("INSERT INTO sales_logs (username, action, details, created_at) VALUES (?, ?, ?, NOW())");
|
||||
$stmt->bind_param("sss", $user, $action, $details);
|
||||
$stmt->execute();
|
||||
}
|
||||
|
||||
|
||||
$action = $_GET['action'] ?? '';
|
||||
|
||||
// --- AUTH ---
|
||||
// 6. LOGIN ACTION
|
||||
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", $input['username']);
|
||||
$stmt->bind_param("s", $username);
|
||||
$stmt->execute();
|
||||
$res = $stmt->get_result()->fetch_assoc();
|
||||
|
||||
if ($res && password_verify($input['password'], $res['password'])) {
|
||||
$_SESSION['user'] = $input['username'];
|
||||
$_SESSION['user'] = $username;
|
||||
$_SESSION['full_name'] = $res['full_name'];
|
||||
writeLog('LOGIN', 'Gebruiker ingelogd');
|
||||
session_write_close();
|
||||
|
||||
// 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']);
|
||||
@ -51,117 +88,417 @@ if ($action === 'login') {
|
||||
exit;
|
||||
}
|
||||
|
||||
if (!isset($_SESSION['user']) && $action !== 'login') {
|
||||
http_response_code(403); exit;
|
||||
}
|
||||
|
||||
$woocommerce = new Client($_ENV['WC_URL'], $_ENV['WC_KEY'], $_ENV['WC_SECRET'], ['version' => 'wc/v3', 'verify_ssl' => false, 'timeout' => 400]);
|
||||
|
||||
// --- HELPERS ---
|
||||
if ($action === 'get_payment_methods') {
|
||||
try {
|
||||
$gateways = $woocommerce->get('payment_gateways');
|
||||
$output = [];
|
||||
foreach ($gateways as $gw) {
|
||||
if (str_contains($gw->id, 'applepay') || str_contains($gw->id, 'googlepay')) continue;
|
||||
if ($gw->enabled && (str_contains($gw->id, 'mollie') || str_contains($gw->id, 'riverty') || str_contains($gw->id, 'klarna'))) {
|
||||
$output[] = ['id' => $gw->id, 'title' => $gw->method_title];
|
||||
}
|
||||
}
|
||||
echo json_encode($output);
|
||||
} catch (Exception $e) { echo json_encode([]); }
|
||||
// 7. SECURITY GATE (The ONLY one needed)
|
||||
if (!isset($_SESSION['user'])) {
|
||||
http_response_code(403);
|
||||
echo json_encode(['error' => 'Not authenticated']);
|
||||
exit;
|
||||
}
|
||||
|
||||
if ($action === 'get_products') {
|
||||
try {
|
||||
$products = $woocommerce->get('products', ['status' => 'publish', 'per_page' => 100]);
|
||||
$enriched = [];
|
||||
foreach ($products as $product) {
|
||||
$p = (array)$product;
|
||||
$p['variation_details'] = ($product->type === 'variable') ? (array)$woocommerce->get("products/{$product->id}/variations", ['per_page' => 50]) : [];
|
||||
$enriched[] = $p;
|
||||
}
|
||||
echo json_encode($enriched);
|
||||
} catch (Exception $e) { echo json_encode([]); }
|
||||
// 8. LOGOUT
|
||||
if ($action === 'logout') {
|
||||
session_destroy();
|
||||
setcookie('telvero_remember', '', time() - 3600, '/');
|
||||
echo json_encode(['success' => true]);
|
||||
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 ---
|
||||
if ($action === 'postcode_check') {
|
||||
$postcode = str_replace(' ', '', $_GET['postcode']);
|
||||
$url = "https://postcode.tech/api/v1/postcode?postcode={$postcode}&number=" . $_GET['number'];
|
||||
$ch = curl_init($url); curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
|
||||
$ch = curl_init($url);
|
||||
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
|
||||
curl_setopt($ch, CURLOPT_HTTPHEADER, ["Authorization: Bearer " . $_ENV['POSTCODE_TECH_KEY']]);
|
||||
echo curl_exec($ch); exit;
|
||||
echo curl_exec($ch);
|
||||
exit;
|
||||
}
|
||||
|
||||
// --- CREATE ORDER (V8.1 - MET LOGGING) ---
|
||||
// --- GET PRODUCTS ---
|
||||
// --- 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([]);
|
||||
}
|
||||
exit;
|
||||
}
|
||||
|
||||
|
||||
// --- 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';
|
||||
$shipping_incl_tax = (float)($input['shipping_total'] ?? 0);
|
||||
$wc_gateway_id = $input['payment_method'];
|
||||
$mollie_method = str_replace(['mollie_wc_gateway_', 'rve_'], '', $wc_gateway_id);
|
||||
|
||||
// 1. ACCOUNT SYNC
|
||||
$existing_customers = $woocommerce->get('customers', ['email' => $email]);
|
||||
$input['customer_id'] = !empty($existing_customers) ? $existing_customers[0]->id : 0;
|
||||
$input['payment_method'] = 'cod';
|
||||
$input['payment_method_title'] = 'Sales Panel Order';
|
||||
$input['status'] = 'on-hold';
|
||||
|
||||
// 2. SHIPPING TAX FIX
|
||||
if ($shipping_incl_tax > 0) {
|
||||
$shipping_ex_tax = $shipping_incl_tax / 1.21;
|
||||
$input['shipping_lines'] = [['method_id' => 'flat_rate', 'method_title' => 'Verzendkosten', 'total' => number_format($shipping_ex_tax, 4, '.', '')]];
|
||||
$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['payment_method'] = $wc_gateway_id;
|
||||
$input['customer_note'] = "Agent: {$_SESSION['user']} | Mediacode: $mediacode";
|
||||
|
||||
// 3. ATTRIBUTION METADATA
|
||||
// --- 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 AANMAKEN
|
||||
$order = $woocommerce->post('orders', $input);
|
||||
|
||||
// 4. TERMIJN CHECK
|
||||
if (in_array($mollie_method, ['in3', 'klarna', 'klarnapaylater', 'klarnasliceit', 'riverty']) && (float)$order->total < 100.00) {
|
||||
$woocommerce->delete("orders/{$order->id}", ['force' => true]);
|
||||
throw new Exception("Termijnbetaling pas vanaf €100,-");
|
||||
}
|
||||
$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();
|
||||
|
||||
// 5. MOLLIE PAYMENT
|
||||
$mollie = new MollieApiClient();
|
||||
$mollie->setApiKey($_ENV['MOLLIE_KEY']);
|
||||
|
||||
$paymentData = [
|
||||
"amount" => ["currency" => "EUR", "value" => number_format((float)$order->total, 2, '.', '')],
|
||||
"description" => "Order #{$order->id} [$mediacode]",
|
||||
"redirectUrl" => $_ENV['WC_URL'] . "/checkout/order-received/{$order->id}/?key={$order->order_key}&order_id={$order->id}&utm_source=SalesPanel&utm_campaign=" . urlencode($mediacode),
|
||||
"webhookUrl" => $_ENV['WC_URL'] . "/wc-api/{$wc_gateway_id}/?key={$order->order_key}&order_id={$order->id}",
|
||||
"method" => $mollie_method,
|
||||
"metadata" => ["order_id" => (string)$order->id, "mediacode" => $mediacode]
|
||||
];
|
||||
|
||||
if (in_array($mollie_method, ['in3', 'klarna', 'klarnapaylater', 'klarnasliceit', 'riverty'])) {
|
||||
if ($mollie_method === 'riverty') $paymentData["captureMode"] = "manual";
|
||||
$paymentData["billingAddress"] = ["givenName" => $input['billing']['first_name'], "familyName" => $input['billing']['last_name'], "email" => $input['billing']['email'], "streetAndNumber" => $input['billing']['address_1'], "city" => $input['billing']['city'], "postalCode" => $input['billing']['postcode'], "country" => "NL"];
|
||||
$paymentData["lines"] = [["name" => "Bestelling #" . $order->id, "quantity" => 1, "unitPrice" => ["currency" => "EUR", "value" => number_format((float)$order->total, 2, '.', '')], "totalAmount" => ["currency" => "EUR", "value" => number_format((float)$order->total, 2, '.', '')], "vatRate" => "21.00", "vatAmount" => ["currency" => "EUR", "value" => number_format((float)$order->total_tax, 2, '.', '')]]];
|
||||
}
|
||||
|
||||
$payment = $mollie->payments->create($paymentData);
|
||||
|
||||
// 7. FINISH ORDER
|
||||
$woocommerce->put("orders/{$order->id}", ['meta_data' => [['key' => '_mollie_payment_id', 'value' => $payment->id], ['key' => '_transaction_id', 'value' => $payment->id]]]);
|
||||
$woocommerce->post("orders/{$order->id}/notes", ['note' => "Voltooi de betaling of keur de incasso z.s.m. goed via de volgende link: <br/> " . $payment->getCheckoutUrl(), 'customer_note' => true]);
|
||||
writeLog('ORDER_CREATED', "Order #{$order->id} voor {$input['billing']['email']}");
|
||||
|
||||
echo json_encode(['payment_url' => $payment->getCheckoutUrl()]);
|
||||
echo json_encode(['success' => true, 'order_id' => $order->id, 'total' => $order->total]);
|
||||
} catch (Exception $e) {
|
||||
writeLog('ERROR', $e->getMessage());
|
||||
http_response_code(422); echo json_encode(['error' => $e->getMessage()]);
|
||||
}
|
||||
exit;
|
||||
|
||||
257
index.html
257
index.html
@ -2,19 +2,23 @@
|
||||
<html lang="nl">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Telvero Sales Panel</title>
|
||||
<title>Telvero Sales Panel V9</title>
|
||||
<script src="https://cdn.tailwindcss.com"></script>
|
||||
<script src="https://unpkg.com/alpinejs" defer></script>
|
||||
<style>[x-cloak] { display: none !important; } .custom-scrollbar::-webkit-scrollbar { width: 4px; } .custom-scrollbar::-webkit-scrollbar-thumb { background: #334155; border-radius: 10px; }</style>
|
||||
</head>
|
||||
<body class="bg-slate-100 min-h-screen font-sans" x-data="salesApp()">
|
||||
|
||||
<template x-if="!isLoggedIn">
|
||||
<body class="bg-slate-100 min-h-screen font-sans" x-data="salesApp()" x-init="init()">
|
||||
<template x-if="isLoading">
|
||||
<div class="fixed inset-0 bg-slate-900 flex items-center justify-center z-[60]">
|
||||
<div class="text-white font-black animate-pulse">LAAD SESSIE...</div>
|
||||
</div>
|
||||
</template>
|
||||
<template x-if="!isLoggedIn && !isLoading">
|
||||
<div class="fixed inset-0 bg-slate-900 flex items-center justify-center p-4 z-50">
|
||||
<div class="bg-white p-10 rounded-[2.5rem] shadow-2xl w-full max-w-md text-center border-t-8 border-blue-600">
|
||||
<h2 class="text-3xl font-black mb-8 italic tracking-tighter text-slate-800">TELVERO <span class="text-blue-600">LOGIN</span></h2>
|
||||
<div class="space-y-4">
|
||||
<input type="text" x-model="loginForm.username" placeholder="Gebruikersnaam" class="w-full border p-4 rounded-2xl outline-none focus:border-blue-500 bg-slate-50">
|
||||
<input type="text" x-model="loginForm.username" placeholder="Gebruikersnaam" class="w-full border p-4 rounded-2xl outline-none focus:border-blue-500 bg-slate-50 font-bold">
|
||||
<input type="password" x-model="loginForm.password" @keyup.enter="doLogin()" placeholder="Wachtwoord" class="w-full border p-4 rounded-2xl outline-none focus:border-blue-500 bg-slate-50">
|
||||
<button @click="doLogin()" class="w-full bg-blue-600 text-white p-5 rounded-2xl font-black shadow-lg hover:bg-blue-700 transition uppercase tracking-widest text-sm">Inloggen</button>
|
||||
</div>
|
||||
@ -22,7 +26,7 @@
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<div x-show="isLoggedIn" x-cloak class="max-w-[1440px] mx-auto p-6">
|
||||
<div x-show="isLoggedIn && !isLoading" x-cloak class="max-w-[1440px] mx-auto p-6">
|
||||
<header class="flex justify-between items-center mb-8 bg-white p-6 rounded-3xl shadow-sm border-b-4 border-blue-600">
|
||||
<h1 class="text-2xl font-black italic tracking-tighter">TELVERO <span class="text-blue-600">PANEL</span></h1>
|
||||
<div class="flex items-center gap-6 text-sm font-bold text-slate-400">
|
||||
@ -39,6 +43,8 @@
|
||||
<option value="">-- KIES MEDIACODE --</option>
|
||||
<option value="Telvero - Net5">Telvero - Net5</option>
|
||||
<option value="Telvero - SBS9">Telvero - SBS9</option>
|
||||
<option value="Klantenservice">Klantenservice</option>
|
||||
<option value="Website">Website</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="space-y-4">
|
||||
@ -53,13 +59,13 @@
|
||||
</div>
|
||||
<input type="text" x-model="form.street" placeholder="Straat" class="w-full border p-3 rounded-xl bg-slate-100 font-bold text-xs" readonly>
|
||||
<input type="text" x-model="form.city" placeholder="Stad" class="w-full border p-3 rounded-xl bg-slate-100 font-bold text-xs" readonly>
|
||||
<input type="tel" x-model="form.phone" placeholder="Telefoon (06...)" class="border p-3 rounded-xl w-full focus:border-blue-500 outline-none">
|
||||
<input type="tel" x-model="form.phone" placeholder="Telefoonnummer" class="border p-3 rounded-xl w-full outline-none focus:border-blue-500">
|
||||
<input type="email" x-model="form.email" placeholder="E-mail (Verplicht)" class="border-2 border-amber-300 p-3 rounded-xl w-full outline-none focus:border-amber-500 shadow-sm">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-span-12 lg:col-span-5 bg-white p-8 rounded-[2rem] shadow-sm border border-slate-200">
|
||||
<h2 class="font-bold mb-4 text-slate-400 uppercase text-[10px] tracking-widest border-b pb-2 text-center italic">Product Selecteren</h2>
|
||||
<h2 class="font-bold mb-4 text-slate-400 uppercase text-[10px] tracking-widest border-b pb-2 text-center italic">Producten</h2>
|
||||
<select x-model="selectedProductId" @change="selectProduct()" class="w-full border-2 border-slate-100 p-5 rounded-2xl font-black text-slate-700 mb-6 bg-slate-50 outline-none focus:border-blue-500 shadow-sm">
|
||||
<option value="">-- Kies Hoofdproduct --</option>
|
||||
<template x-for="p in products" :key="p.id">
|
||||
@ -67,7 +73,7 @@
|
||||
</template>
|
||||
</select>
|
||||
|
||||
<div x-show="variations.length > 0" x-cloak class="mb-8 p-6 bg-blue-50 rounded-3xl border border-blue-100 shadow-inner text-center">
|
||||
<div x-show="variations.length > 0" x-cloak class="mb-8 p-6 bg-blue-50 rounded-3xl border border-blue-100 shadow-inner">
|
||||
<select x-model="selectedVariationId" @change="selectVariation()" class="w-full border-2 border-white p-4 rounded-2xl font-bold bg-white text-slate-700 shadow-sm outline-none">
|
||||
<option value="">-- Kies Optie --</option>
|
||||
<template x-for="v in variations" :key="v.id">
|
||||
@ -76,76 +82,104 @@
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="mt-8 border-t pt-6 text-center">
|
||||
<h2 class="font-bold mb-4 text-slate-400 uppercase text-[10px] tracking-widest italic">Extra Product Toevoegen</h2>
|
||||
<div class="mt-8 border-t pt-6">
|
||||
<h2 class="font-bold mb-4 text-slate-400 uppercase text-[10px] tracking-widest italic text-center">Extra's</h2>
|
||||
<div class="flex gap-2">
|
||||
<select x-model="extraProductId" class="flex-1 border-2 border-slate-100 p-3 rounded-xl font-bold text-xs bg-slate-50 outline-none focus:border-green-500">
|
||||
<option value="">-- Kies extra product --</option>
|
||||
<option value="">-- Voeg product toe --</option>
|
||||
<template x-for="p in products" :key="'extra-'+p.id">
|
||||
<option :value="p.id" x-text="p.name + ' (€' + p.price + ')'"></option>
|
||||
</template>
|
||||
</select>
|
||||
<button @click="addExtraItem()" class="bg-green-600 text-white px-6 py-3 rounded-xl font-black text-[10px] uppercase shadow-md hover:bg-green-700 transition active:scale-95">Toevoegen</button>
|
||||
<button @click="addExtraItem()" class="bg-green-600 text-white px-6 py-3 rounded-xl font-black text-[10px] uppercase shadow-md hover:bg-green-700 transition active:scale-95">Add</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div x-show="upsellOptions.length > 0" x-cloak class="mt-8 space-y-3">
|
||||
<p class="text-[10px] font-black text-red-500 uppercase tracking-widest italic px-2 text-center">Upsell Suggesties</p>
|
||||
<template x-for="u in upsellOptions" :key="u.id">
|
||||
<div x-show="recommendedOptions.length > 0" x-cloak class="mt-8 space-y-3">
|
||||
<p class="text-[10px] font-black text-red-500 uppercase tracking-widest italic px-2 text-center">Aanbevolen</p>
|
||||
|
||||
<template x-for="u in recommendedOptions" :key="u.id">
|
||||
<div class="flex items-center justify-between p-4 border rounded-2xl bg-slate-50 hover:bg-white transition-all shadow-sm">
|
||||
<span class="text-xs font-bold text-slate-700" x-text="u.name + ' (€' + u.price + ')'"></span>
|
||||
<button @click="toggleUpsell(u)" :class="isInCart(u.id) ? 'bg-red-500' : 'bg-green-600'" class="text-white px-6 py-2 rounded-xl text-[10px] font-black uppercase shadow-md" x-text="isInCart(u.id) ? 'Verwijder' : 'Voeg toe'"></button>
|
||||
<button
|
||||
@click="toggleUpsell(u)"
|
||||
:class="isInCart(u.id) ? 'bg-red-500' : 'bg-green-600'"
|
||||
class="text-white px-6 py-2 rounded-xl text-[10px] font-black uppercase shadow-md"
|
||||
x-text="isInCart(u.id) ? 'Verwijder' : 'Voeg toe'">
|
||||
</button>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-span-12 lg:col-span-3">
|
||||
<div class="bg-slate-900 text-white p-8 rounded-[2.5rem] shadow-2xl sticky top-6 border border-slate-800">
|
||||
<h2 class="font-bold mb-6 border-b border-slate-800 pb-2 text-[10px] uppercase text-slate-500 tracking-widest italic text-center">Overzicht</h2>
|
||||
<div class="space-y-4 mb-6 min-h-[80px] max-h-[250px] overflow-y-auto pr-2 custom-scrollbar">
|
||||
<div class="bg-slate-900 text-white p-8 rounded-[2.5rem] shadow-2xl sticky top-6 border border-slate-800 transition-all duration-500">
|
||||
|
||||
<template x-if="orderComplete">
|
||||
<div class="text-center py-4">
|
||||
<div class="bg-green-500/20 text-green-400 w-16 h-16 rounded-full flex items-center justify-center mx-auto mb-6 border-2 border-green-500/30 animate-bounce">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-8 w-8" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="3" d="M5 13l4 4L19 7" /></svg>
|
||||
</div>
|
||||
<h2 class="text-xl font-black mb-2 uppercase tracking-tighter text-white">Order Gelukt!</h2>
|
||||
<p class="text-slate-400 text-[10px] mb-8 italic uppercase tracking-widest">Klant is opgeslagen</p>
|
||||
|
||||
<div class="bg-slate-800/50 rounded-2xl p-6 mb-8 text-left border border-slate-700/50 space-y-4">
|
||||
<div class="flex justify-between border-b border-slate-700/50 pb-2">
|
||||
<span class="text-[9px] text-slate-500 font-black uppercase">Order ID</span>
|
||||
<span class="text-sm font-black text-blue-400" x-text="'#' + lastOrder.id"></span>
|
||||
</div>
|
||||
<div class="flex justify-between border-b border-slate-700/50 pb-2">
|
||||
<span class="text-[9px] text-slate-500 font-black uppercase">Klant</span>
|
||||
<span class="text-sm font-bold text-slate-200" x-text="lastOrder.name"></span>
|
||||
</div>
|
||||
<div class="flex justify-between">
|
||||
<span class="text-[9px] text-slate-500 font-black uppercase">Bedrag</span>
|
||||
<span class="text-xl font-black text-green-400" x-text="'€' + lastOrder.total"></span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button @click="resetForNewOrder()" class="w-full bg-blue-600 text-white p-5 rounded-2xl font-black shadow-lg hover:bg-blue-500 transition uppercase tracking-widest text-xs active:scale-95">
|
||||
Start Nieuwe Order
|
||||
</button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template x-if="!orderComplete">
|
||||
<div>
|
||||
<h2 class="font-bold mb-6 border-b border-slate-800 pb-2 text-[10px] uppercase text-slate-500 tracking-widest italic text-center">Samenvatting</h2>
|
||||
|
||||
<div class="space-y-4 mb-6 min-h-[100px] max-h-[300px] overflow-y-auto pr-2 custom-scrollbar">
|
||||
<template x-for="(item, index) in cart" :key="index">
|
||||
<div class="flex justify-between items-center group">
|
||||
<div class="flex flex-col flex-1">
|
||||
<span x-text="item.name" class="text-[11px] font-medium leading-tight text-slate-300"></span>
|
||||
<span x-text="'€' + item.price" class="text-[11px] font-black text-blue-400"></span>
|
||||
</div>
|
||||
<button @click="removeFromCart(index)" class="text-slate-600 hover:text-red-500">
|
||||
<button @click="removeFromCart(index)" class="text-slate-600 hover:text-red-500 transition">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16" /></svg>
|
||||
</button>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
<div class="mb-6 pt-4 border-t border-slate-800 text-center">
|
||||
<label class="text-[10px] text-slate-500 uppercase font-black mb-2 block italic tracking-widest">Verzendkosten (€)</label>
|
||||
<input type="number" x-model="shipping" step="0.01" min="0" class="w-full bg-slate-800 border border-slate-700 rounded-xl p-2 text-center text-sm font-bold text-blue-400 outline-none focus:border-blue-500">
|
||||
<div class="mb-6 pt-4 border-t border-slate-800 flex justify-between items-center">
|
||||
<label class="text-[10px] text-slate-500 uppercase font-black italic">Verzendkosten</label>
|
||||
<input type="number" x-model="shipping" step="0.01" class="w-20 bg-slate-800 border border-slate-700 rounded-lg p-1 text-right text-xs font-bold text-blue-400 outline-none">
|
||||
</div>
|
||||
|
||||
<div class="mb-8 pt-4 border-t border-slate-800 text-center">
|
||||
<p class="text-[10px] text-slate-500 uppercase font-black mb-3 italic tracking-widest">Betaalwijze</p>
|
||||
<div class="space-y-2 max-h-[180px] overflow-y-auto pr-2 custom-scrollbar">
|
||||
<template x-for="method in filteredPaymentMethods" :key="method.id">
|
||||
<button @click="payment_method = method.id"
|
||||
:class="payment_method === method.id ? 'bg-blue-600 border-blue-400 ring-2 ring-blue-500/50' : 'bg-slate-800 border-slate-700 opacity-60'"
|
||||
class="w-full text-left p-3 rounded-2xl border flex items-center gap-3 transition-all duration-200">
|
||||
<img :src="method.image" class="w-7 h-7 rounded bg-white p-1">
|
||||
<span class="text-[10px] font-black uppercase text-white tracking-tighter" x-text="method.title"></span>
|
||||
</button>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex justify-between items-center mb-8 pt-4 border-t border-slate-800 text-right">
|
||||
<div class="flex justify-between items-center mb-8 pt-4 border-t border-slate-800">
|
||||
<span class="text-[10px] text-slate-500 font-black uppercase">Totaal</span>
|
||||
<span class="text-3xl font-black text-green-400" x-text="'€' + total"></span>
|
||||
</div>
|
||||
|
||||
<button @click="submitOrder()"
|
||||
:disabled="submitting || !form.email || !meta.mediacode || cart.length === 0"
|
||||
class="w-full bg-blue-600 hover:bg-blue-500 p-6 rounded-2xl font-black text-lg shadow-xl disabled:opacity-20 uppercase tracking-tighter transition active:scale-95 shadow-blue-500/20">
|
||||
<span x-text="submitting ? 'PROCESSING...' : 'ORDER BEVESTIGEN'"></span>
|
||||
class="w-full bg-blue-600 hover:bg-blue-500 p-6 rounded-2xl font-black text-lg shadow-xl disabled:opacity-20 uppercase tracking-tighter transition active:scale-95">
|
||||
<span x-text="submitting ? 'BEZIG...' : 'BEVESTIGEN'"></span>
|
||||
</button>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -153,49 +187,62 @@
|
||||
<script>
|
||||
function salesApp() {
|
||||
return {
|
||||
isLoggedIn: false, currentUser: '', loginForm: { username: '', password: '' },
|
||||
products: [], paymentMethods: [], upsellOptions: [], cart: [], activeProduct: null,
|
||||
selectedProductId: '', selectedVariationId: '', variations: [], payment_method: '',
|
||||
extraProductId: '', shipping: '9.95',
|
||||
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: [],
|
||||
|
||||
// THIS RUNS AUTOMATICALLY ON PAGE LOAD
|
||||
async init() {
|
||||
this.isLoading = true;
|
||||
try {
|
||||
const res = await fetch('api.php?action=get_products');
|
||||
if (res.ok) {
|
||||
this.products = await res.json();
|
||||
this.isLoggedIn = true;
|
||||
this.currentUser = localStorage.getItem('telvero_user') || 'Agent';
|
||||
}
|
||||
} catch (e) {
|
||||
console.error("Session check failed");
|
||||
} finally {
|
||||
this.isLoading = false; // Now we know for sure
|
||||
}
|
||||
},
|
||||
|
||||
async doLogin() {
|
||||
const res = await fetch('api.php?action=login', { method: 'POST', body: JSON.stringify(this.loginForm) });
|
||||
const res = await fetch('api.php?action=login', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(this.loginForm)
|
||||
});
|
||||
if(res.ok) {
|
||||
const data = await res.json();
|
||||
this.isLoggedIn = true; this.currentUser = data.user;
|
||||
await this.initData();
|
||||
} else { alert("Login mislukt"); }
|
||||
},
|
||||
|
||||
async initData() {
|
||||
const [pRes, mRes] = await Promise.all([
|
||||
fetch('api.php?action=get_products'),
|
||||
fetch('api.php?action=get_payment_methods')
|
||||
]);
|
||||
this.products = await pRes.json();
|
||||
let methods = await mRes.json();
|
||||
this.paymentMethods = methods.map(m => {
|
||||
let iconKey = m.id.replace('mollie_wc_gateway_', '');
|
||||
if (m.id.includes('riverty')) iconKey = 'riverty';
|
||||
if (m.id.includes('klarna')) iconKey = 'klarna';
|
||||
if (m.id.includes('in3')) iconKey = 'in3';
|
||||
return { ...m, image: `https://www.mollie.com/external/icons/payment-methods/${iconKey}.svg` };
|
||||
});
|
||||
this.payment_method = 'mollie_wc_gateway_ideal';
|
||||
},
|
||||
|
||||
get filteredPaymentMethods() {
|
||||
return this.paymentMethods.filter(method => {
|
||||
const isTermin = method.id.includes('in3') || method.id.includes('klarna') || method.id.includes('riverty');
|
||||
if (isTermin && parseFloat(this.total) < 100) {
|
||||
if (this.payment_method === method.id) this.payment_method = 'mollie_wc_gateway_ideal';
|
||||
return false;
|
||||
this.isLoggedIn = true;
|
||||
this.currentUser = data.user;
|
||||
localStorage.setItem('telvero_user', data.user);
|
||||
await this.init(); // This loads the products and stabilizes the session
|
||||
} else {
|
||||
alert("Login mislukt");
|
||||
}
|
||||
return true;
|
||||
});
|
||||
},
|
||||
|
||||
async doLogout() {
|
||||
await fetch('api.php?action=logout');
|
||||
localStorage.removeItem('telvero_user'); // Clean up local storage
|
||||
location.reload();
|
||||
},
|
||||
|
||||
selectProduct() {
|
||||
@ -205,7 +252,7 @@
|
||||
this.cart = []; this.selectedVariationId = '';
|
||||
if (p.type !== 'variable') {
|
||||
this.cart.push({ id: parseInt(p.id), name: p.name, price: p.price });
|
||||
this.loadUpsells(p);
|
||||
this.loadRecommendations(p);
|
||||
}
|
||||
},
|
||||
|
||||
@ -223,23 +270,35 @@
|
||||
this.extraProductId = '';
|
||||
},
|
||||
|
||||
loadUpsells(product) {
|
||||
this.upsellOptions = [];
|
||||
if (product.upsell_ids && product.upsell_ids.length > 0) {
|
||||
const idsToFind = product.upsell_ids.map(id => parseInt(id));
|
||||
this.upsellOptions = this.products.filter(p => idsToFind.includes(parseInt(p.id)));
|
||||
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 }); }
|
||||
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);
|
||||
@ -251,20 +310,38 @@
|
||||
async submitOrder() {
|
||||
this.submitting = true;
|
||||
const payload = {
|
||||
payment_method: this.payment_method, mediacode_internal: this.meta.mediacode,
|
||||
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 },
|
||||
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.payment_url) { alert("Succes!"); this.cart = []; this.selectedProductId = ''; this.shipping = '9.95'; this.form = { initials: '', lastname: '', postcode: '', houseno: '', suffix: '', street: '', city: '', email: '', phone: '' }; }
|
||||
else { alert("Fout: " + result.error); }
|
||||
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;
|
||||
},
|
||||
async doLogout() { await fetch('api.php?action=logout'); location.reload(); },
|
||||
|
||||
resetForNewOrder() {
|
||||
this.cart = []; this.selectedProductId = ''; this.selectedVariationId = ''; this.shipping = '8.95';
|
||||
this.form = { initials: '', lastname: '', postcode: '', houseno: '', suffix: '', street: '', city: '', email: '', phone: '' };
|
||||
this.orderComplete = false;
|
||||
},
|
||||
|
||||
|
||||
async lookupAddress() {
|
||||
if (this.form.postcode.length >= 6 && this.form.houseno) {
|
||||
const res = await fetch(`api.php?action=postcode_check&postcode=${this.form.postcode}&number=${this.form.houseno}`);
|
||||
|
||||
223
logs.php
223
logs.php
@ -1,6 +1,6 @@
|
||||
<?php
|
||||
/**
|
||||
* TELVERO BACKOFFICE - API PROXY (V8.1 - LOGGING RESTORED)
|
||||
* TELVERO LOGS DASHBOARD (V9.5 - DEPRECATED FIX)
|
||||
*/
|
||||
session_start();
|
||||
ini_set('display_errors', 0);
|
||||
@ -13,151 +13,94 @@ if (file_exists(__DIR__ . '/.env')) {
|
||||
$dotenv->load();
|
||||
}
|
||||
|
||||
use Automattic\WooCommerce\Client;
|
||||
use Mollie\Api\MollieApiClient;
|
||||
|
||||
header('Content-Type: application/json');
|
||||
if (!isset($_SESSION['user'])) {
|
||||
die("Toegang geweigerd. Log eerst in.");
|
||||
}
|
||||
|
||||
$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'])); }
|
||||
|
||||
$action = $_GET['action'] ?? '';
|
||||
// 1. Totalen van vandaag
|
||||
$today_stats = $db->query("SELECT COUNT(id) as total_orders, IFNULL(SUM(amount), 0) as total_revenue FROM sales_logs WHERE action_type = 'order_created' AND DATE(created_at) = CURDATE()")->fetch_assoc();
|
||||
|
||||
// --- AUTH ---
|
||||
if ($action === 'login') {
|
||||
$input = json_decode(file_get_contents('php://input'), true);
|
||||
$stmt = $db->prepare("SELECT password, full_name FROM sales_users WHERE username = ?");
|
||||
$stmt->bind_param("s", $input['username']);
|
||||
$stmt->execute();
|
||||
$res = $stmt->get_result()->fetch_assoc();
|
||||
if ($res && password_verify($input['password'], $res['password'])) {
|
||||
$_SESSION['user'] = $input['username'];
|
||||
$_SESSION['full_name'] = $res['full_name'];
|
||||
session_write_close();
|
||||
echo json_encode(['success' => true, 'user' => $res['full_name']]);
|
||||
} else {
|
||||
http_response_code(401); echo json_encode(['error' => 'Login mislukt']);
|
||||
}
|
||||
exit;
|
||||
}
|
||||
// 2. Performance per agent (username)
|
||||
$agent_stats = $db->query("SELECT username, COUNT(id) as orders, IFNULL(SUM(amount), 0) as revenue FROM sales_logs WHERE action_type = 'order_created' AND DATE(created_at) = CURDATE() GROUP BY username ORDER BY orders DESC");
|
||||
|
||||
if (!isset($_SESSION['user']) && $action !== 'login') {
|
||||
http_response_code(403); exit;
|
||||
}
|
||||
// 3. Laatste 50 logs
|
||||
$logs = $db->query("SELECT * FROM sales_logs ORDER BY created_at DESC LIMIT 50");
|
||||
?>
|
||||
<!DOCTYPE html>
|
||||
<html lang="nl">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Telvero Logs</title>
|
||||
<script src="https://cdn.tailwindcss.com"></script>
|
||||
</head>
|
||||
<body class="bg-slate-50 p-8 font-sans text-slate-900">
|
||||
|
||||
$woocommerce = new Client($_ENV['WC_URL'], $_ENV['WC_KEY'], $_ENV['WC_SECRET'], ['version' => 'wc/v3', 'verify_ssl' => false, 'timeout' => 400]);
|
||||
<div class="max-w-6xl mx-auto">
|
||||
<header class="flex justify-between items-center mb-10">
|
||||
<h1 class="text-2xl font-black italic uppercase">TELVERO <span class="text-blue-600">LOGS</span></h1>
|
||||
<a href="index.html" class="bg-white border px-6 py-2 rounded-2xl text-[10px] font-black uppercase tracking-widest hover:bg-slate-50 transition">Panel</a>
|
||||
</header>
|
||||
|
||||
// --- HELPERS ---
|
||||
if ($action === 'get_payment_methods') {
|
||||
try {
|
||||
$gateways = $woocommerce->get('payment_gateways');
|
||||
$output = [];
|
||||
foreach ($gateways as $gw) {
|
||||
if (str_contains($gw->id, 'applepay') || str_contains($gw->id, 'googlepay')) continue;
|
||||
if ($gw->enabled && (str_contains($gw->id, 'mollie') || str_contains($gw->id, 'riverty') || str_contains($gw->id, 'klarna'))) {
|
||||
$output[] = ['id' => $gw->id, 'title' => $gw->method_title];
|
||||
}
|
||||
}
|
||||
echo json_encode($output);
|
||||
} catch (Exception $e) { echo json_encode([]); }
|
||||
exit;
|
||||
}
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-8 mb-10">
|
||||
<div class="bg-white p-8 rounded-[2.5rem] shadow-sm border-b-8 border-emerald-500">
|
||||
<p class="text-[10px] font-black text-slate-400 uppercase tracking-widest mb-1">Omzet Vandaag</p>
|
||||
<p class="text-4xl font-black italic">€<?php echo number_format((float)($today_stats['total_revenue'] ?? 0), 2, ',', '.'); ?></p>
|
||||
</div>
|
||||
<div class="bg-white p-8 rounded-[2.5rem] shadow-sm border-b-8 border-blue-600">
|
||||
<p class="text-[10px] font-black text-slate-400 uppercase tracking-widest mb-1">Orders Vandaag</p>
|
||||
<p class="text-4xl font-black italic"><?php echo $today_stats['total_orders']; ?></p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
if ($action === 'get_products') {
|
||||
try {
|
||||
$products = $woocommerce->get('products', ['status' => 'publish', 'per_page' => 100]);
|
||||
$enriched = [];
|
||||
foreach ($products as $product) {
|
||||
$p = (array)$product;
|
||||
$p['variation_details'] = ($product->type === 'variable') ? (array)$woocommerce->get("products/{$product->id}/variations", ['per_page' => 50]) : [];
|
||||
$enriched[] = $p;
|
||||
}
|
||||
echo json_encode($enriched);
|
||||
} catch (Exception $e) { echo json_encode([]); }
|
||||
exit;
|
||||
}
|
||||
<div class="grid grid-cols-12 gap-8">
|
||||
<div class="col-span-12 lg:col-span-4">
|
||||
<div class="bg-white p-8 rounded-[2.5rem] shadow-sm h-full">
|
||||
<h2 class="text-[10px] font-black uppercase tracking-widest mb-6 border-b pb-4 text-slate-400 italic">Agent Ranking</h2>
|
||||
<div class="space-y-4">
|
||||
<?php while($row = $agent_stats->fetch_assoc()): ?>
|
||||
<div class="flex justify-between items-center bg-slate-50 p-4 rounded-3xl border border-slate-100">
|
||||
<span class="text-xs font-black uppercase italic"><?php echo htmlspecialchars($row['username']); ?></span>
|
||||
<span class="bg-white px-4 py-1 rounded-full border text-[10px] font-black"><?php echo $row['orders']; ?> orders</span>
|
||||
</div>
|
||||
<?php endwhile; ?>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
if ($action === 'postcode_check') {
|
||||
$postcode = str_replace(' ', '', $_GET['postcode']);
|
||||
$url = "https://postcode.tech/api/v1/postcode?postcode={$postcode}&number=" . $_GET['number'];
|
||||
$ch = curl_init($url); curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
|
||||
curl_setopt($ch, CURLOPT_HTTPHEADER, ["Authorization: Bearer " . $_ENV['POSTCODE_TECH_KEY']]);
|
||||
echo curl_exec($ch); exit;
|
||||
}
|
||||
|
||||
// --- CREATE ORDER (V8.1 - MET LOGGING) ---
|
||||
if ($action === 'create_order') {
|
||||
$input = json_decode(file_get_contents('php://input'), true);
|
||||
try {
|
||||
$email = $input['billing']['email'];
|
||||
$mediacode = $input['mediacode_internal'] ?? 'Geen';
|
||||
$shipping_incl_tax = (float)($input['shipping_total'] ?? 0);
|
||||
$wc_gateway_id = $input['payment_method'];
|
||||
$mollie_method = str_replace(['mollie_wc_gateway_', 'rve_'], '', $wc_gateway_id);
|
||||
|
||||
// 1. ACCOUNT SYNC
|
||||
$existing_customers = $woocommerce->get('customers', ['email' => $email]);
|
||||
$input['customer_id'] = !empty($existing_customers) ? $existing_customers[0]->id : 0;
|
||||
|
||||
// 2. SHIPPING TAX FIX
|
||||
if ($shipping_incl_tax > 0) {
|
||||
$shipping_ex_tax = $shipping_incl_tax / 1.21;
|
||||
$input['shipping_lines'] = [['method_id' => 'flat_rate', 'method_title' => 'Verzendkosten', 'total' => number_format($shipping_ex_tax, 4, '.', '')]];
|
||||
}
|
||||
|
||||
$input['payment_method'] = $wc_gateway_id;
|
||||
$input['customer_note'] = "Agent: {$_SESSION['user']} | Mediacode: $mediacode";
|
||||
|
||||
// 3. 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];
|
||||
|
||||
// ORDER AANMAKEN
|
||||
$order = $woocommerce->post('orders', $input);
|
||||
|
||||
// 4. TERMIJN CHECK
|
||||
if (in_array($mollie_method, ['in3', 'klarna', 'klarnapaylater', 'klarnasliceit', 'riverty']) && (float)$order->total < 100.00) {
|
||||
$woocommerce->delete("orders/{$order->id}", ['force' => true]);
|
||||
throw new Exception("Termijnbetaling pas vanaf €100,-");
|
||||
}
|
||||
|
||||
// 5. MOLLIE PAYMENT
|
||||
$mollie = new MollieApiClient();
|
||||
$mollie->setApiKey($_ENV['MOLLIE_KEY']);
|
||||
|
||||
$paymentData = [
|
||||
"amount" => ["currency" => "EUR", "value" => number_format((float)$order->total, 2, '.', '')],
|
||||
"description" => "Order #{$order->id} [$mediacode]",
|
||||
"redirectUrl" => $_ENV['WC_URL'] . "/checkout/order-received/{$order->id}/?key={$order->order_key}&order_id={$order->id}&utm_source=SalesPanel&utm_campaign=" . urlencode($mediacode),
|
||||
"webhookUrl" => $_ENV['WC_URL'] . "/wc-api/{$wc_gateway_id}/?key={$order->order_key}&order_id={$order->id}",
|
||||
"method" => $mollie_method,
|
||||
"metadata" => ["order_id" => (string)$order->id, "mediacode" => $mediacode]
|
||||
];
|
||||
|
||||
if (in_array($mollie_method, ['in3', 'klarna', 'klarnapaylater', 'klarnasliceit', 'riverty'])) {
|
||||
if ($mollie_method === 'riverty') $paymentData["captureMode"] = "manual";
|
||||
$paymentData["billingAddress"] = ["givenName" => $input['billing']['first_name'], "familyName" => $input['billing']['last_name'], "email" => $input['billing']['email'], "streetAndNumber" => $input['billing']['address_1'], "city" => $input['billing']['city'], "postalCode" => $input['billing']['postcode'], "country" => "NL"];
|
||||
$paymentData["lines"] = [["name" => "Bestelling #" . $order->id, "quantity" => 1, "unitPrice" => ["currency" => "EUR", "value" => number_format((float)$order->total, 2, '.', '')], "totalAmount" => ["currency" => "EUR", "value" => number_format((float)$order->total, 2, '.', '')], "vatRate" => "21.00", "vatAmount" => ["currency" => "EUR", "value" => number_format((float)$order->total_tax, 2, '.', '')]]];
|
||||
}
|
||||
|
||||
$payment = $mollie->payments->create($paymentData);
|
||||
|
||||
// 6. LOGGING NAAR DB (HERSTELD)
|
||||
$log_stmt = $db->prepare("INSERT INTO sales_logs (agent_user, order_id, amount, mediacode, customer_email) VALUES (?, ?, ?, ?, ?)");
|
||||
$log_stmt->bind_param("sidss", $_SESSION['user'], $order->id, $order->total, $mediacode, $email);
|
||||
$log_stmt->execute();
|
||||
|
||||
// 7. FINISH ORDER
|
||||
$woocommerce->put("orders/{$order->id}", ['meta_data' => [['key' => '_mollie_payment_id', 'value' => $payment->id], ['key' => '_transaction_id', 'value' => $payment->id]]]);
|
||||
$woocommerce->post("orders/{$order->id}/notes", ['note' => "Betaallink gegenereerd: " . $payment->getCheckoutUrl(), 'customer_note' => true]);
|
||||
|
||||
echo json_encode(['payment_url' => $payment->getCheckoutUrl()]);
|
||||
} catch (Exception $e) {
|
||||
http_response_code(422); echo json_encode(['error' => $e->getMessage()]);
|
||||
}
|
||||
exit;
|
||||
}
|
||||
|
||||
if ($action === 'logout') { session_destroy(); echo json_encode(['success' => true]); exit; }
|
||||
<div class="col-span-12 lg:col-span-8">
|
||||
<div class="bg-white rounded-[2.5rem] shadow-sm overflow-hidden border border-slate-100">
|
||||
<table class="w-full text-left">
|
||||
<thead class="bg-slate-900 text-white">
|
||||
<tr>
|
||||
<th class="p-5 text-[9px] font-black uppercase tracking-widest opacity-60">Tijd</th>
|
||||
<th class="p-5 text-[9px] font-black uppercase tracking-widest opacity-60">Agent</th>
|
||||
<th class="p-5 text-[9px] font-black uppercase tracking-widest opacity-60">Inhoud</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="divide-y divide-slate-100">
|
||||
<?php while($log = $logs->fetch_assoc()): ?>
|
||||
<tr class="hover:bg-slate-50 transition-colors">
|
||||
<td class="p-5 text-[10px] font-bold text-slate-400"><?php echo date('H:i', strtotime($log['created_at'])); ?></td>
|
||||
<td class="p-5 text-xs font-black uppercase italic"><?php echo htmlspecialchars($log['username']); ?></td>
|
||||
<td class="p-5 text-[11px] font-bold">
|
||||
<?php if($log['action_type'] == 'order_created'): ?>
|
||||
<span class="text-emerald-500 font-black">ORDER #<?php echo $log['order_id']; ?></span>
|
||||
<span class="text-slate-400 mx-1">|</span> €<?php echo number_format((float)($log['amount'] ?? 0), 2, ',', '.'); ?>
|
||||
<span class="text-slate-300 ml-2 italic text-[9px]"><?php echo $log['mediacode']; ?></span>
|
||||
<?php else: ?>
|
||||
<span class="text-blue-500 uppercase font-black"><?php echo str_replace('_', ' ', $log['action_type']); ?></span>
|
||||
<span class="text-slate-400 italic font-normal ml-2"><?php echo htmlspecialchars($log['details']); ?></span>
|
||||
<?php endif; ?>
|
||||
</td>
|
||||
</tr>
|
||||
<?php endwhile; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
Loading…
x
Reference in New Issue
Block a user