Add PayPal webhook handler and marketing styles
- Implemented PayPal webhook for handling payment notifications, including signature verification and transaction updates. - Created invoice generation and license management for software upgrades upon successful payment. - Added comprehensive logging for debugging purposes. - Introduced new CSS styles for the marketing file management system, including layout, toolbar, breadcrumb navigation, search filters, and file management UI components.
This commit is contained in:
@@ -1,103 +0,0 @@
|
|||||||
# Payment Integration Implementation Summary
|
|
||||||
|
|
||||||
## Overview
|
|
||||||
Complete payment integration for software upgrades using existing ecommerce infrastructure (transaction API, invoice API, PHPMailer, DomPDF).
|
|
||||||
|
|
||||||
## New Files to Create
|
|
||||||
|
|
||||||
### 1. `/webhook_mollie.php` (Root directory)
|
|
||||||
**Purpose**: Mollie webhook handler specifically for software upgrades
|
|
||||||
**Based on**: Existing webhook.php from commerce product
|
|
||||||
**Key features**:
|
|
||||||
- ✅ Uses `/v2/transactions/` API for status updates (consistent with commerce)
|
|
||||||
- ✅ Uses `/v2/invoice/` API for invoice generation
|
|
||||||
- ✅ Creates PDF invoice with DomPDF
|
|
||||||
- ✅ Sends email via PHPMailer
|
|
||||||
- ✅ Creates software license
|
|
||||||
- ✅ Multi-language support
|
|
||||||
- ✅ Sends to bookkeeping if configured
|
|
||||||
|
|
||||||
**Webhook URL**: `https://yourdomain.com/webhook_mollie.php`
|
|
||||||
|
|
||||||
### 2. `/api/v2/post/payment.php`
|
|
||||||
**Purpose**: Create Mollie payment for software upgrade
|
|
||||||
**Input**: serial_number, version_id, user_data
|
|
||||||
**Output**: {checkout_url, payment_id}
|
|
||||||
**Security**: Server-side price calculation
|
|
||||||
|
|
||||||
### 3. `/api/v2/get/payment.php`
|
|
||||||
**Purpose**: Retrieve payment status
|
|
||||||
**Input**: ?payment_id=xxx
|
|
||||||
**Output**: {payment_id, payment_status, serial_number, equipment_id, ...}
|
|
||||||
|
|
||||||
## Modified Files
|
|
||||||
|
|
||||||
### 1. `/assets/functions.php`
|
|
||||||
**Add new functions**:
|
|
||||||
- `generateUniqueLicenseKey()` - Generate unique license keys
|
|
||||||
- `generateSoftwareInvoice($invoice_data, $order_id, $language)` - Generate HTML invoice for software upgrades
|
|
||||||
- Based on existing `generateInvoice()` function
|
|
||||||
- Custom template for software licenses
|
|
||||||
- Shows: Device serial number, software version, license key, expiry date
|
|
||||||
- Returns: [$html_content, $customer_email, $order_id]
|
|
||||||
|
|
||||||
### 2. `/assets/softwaretool.js`
|
|
||||||
**Modify**:
|
|
||||||
- `processPayment()` - Call `/v2/post/payment` API
|
|
||||||
- `downloadAndInstallSoftware()` - Add serial number verification
|
|
||||||
|
|
||||||
### 3. `/softwaretool.php`
|
|
||||||
**Add**: Payment return detection (`?payment_id=xxx`)
|
|
||||||
|
|
||||||
## Database
|
|
||||||
**No changes needed** - Uses existing:
|
|
||||||
- `transactions` table (txn_id, payment_status, payment_amount, etc.)
|
|
||||||
- `transactions_items` table (item_id, item_options with JSON)
|
|
||||||
|
|
||||||
## Payment Status Codes (Matching Commerce System)
|
|
||||||
- `0` = Pending
|
|
||||||
- `1` = Paid
|
|
||||||
- `101` = Open/Pending (Mollie)
|
|
||||||
- `102` = Failed
|
|
||||||
- `103` = Expired
|
|
||||||
- `999` = Canceled
|
|
||||||
|
|
||||||
## Implementation Order
|
|
||||||
|
|
||||||
1. ✅ Add Mollie constants to config.php
|
|
||||||
2. Create helper functions in functions.php:
|
|
||||||
- `generateUniqueLicenseKey()`
|
|
||||||
- `generateSoftwareInvoice()`
|
|
||||||
3. Create `/api/v2/post/payment.php`
|
|
||||||
4. Create `/api/v2/get/payment.php`
|
|
||||||
5. Create `/webhook_mollie.php`
|
|
||||||
6. Modify frontend JavaScript
|
|
||||||
7. Modify softwaretool.php
|
|
||||||
8. Test in DEBUG mode
|
|
||||||
9. Test with Mollie sandbox
|
|
||||||
10. Deploy to production
|
|
||||||
|
|
||||||
## Key Benefits
|
|
||||||
|
|
||||||
1. **Consistent with ecommerce** - Uses same API structure
|
|
||||||
2. **Professional invoices** - PDF generation + email delivery
|
|
||||||
3. **Complete audit trail** - Transactions + invoices + licenses
|
|
||||||
4. **Multi-language** - Invoice language based on customer country
|
|
||||||
5. **Bookkeeping integration** - Auto-send to bookkeeping email
|
|
||||||
6. **Refund handling** - Webhook detects refunds (TODO: disable license)
|
|
||||||
|
|
||||||
## Invoice Email Template
|
|
||||||
|
|
||||||
The email will include:
|
|
||||||
- Subject: "Software Upgrade - Invoice: [ORDER_ID]"
|
|
||||||
- HTML body with invoice details
|
|
||||||
- PDF attachment (Invoice_[ORDER_ID].pdf)
|
|
||||||
- Sent to customer email + bookkeeping (if configured)
|
|
||||||
|
|
||||||
**Invoice contains:**
|
|
||||||
- Customer details (name, address, email)
|
|
||||||
- Order ID / Transaction ID
|
|
||||||
- Software upgrade details (version, device serial number)
|
|
||||||
- License key + expiry date (2099-12-31)
|
|
||||||
- Price breakdown
|
|
||||||
- Company information
|
|
||||||
@@ -1,825 +0,0 @@
|
|||||||
# Plan: Payment Flow with Redirect for Software Upgrade Tool
|
|
||||||
|
|
||||||
## User Request
|
|
||||||
Design the payment flow for software upgrades using Mollie (payment provider) with the following requirements:
|
|
||||||
1. User initiates paid upgrade
|
|
||||||
2. System redirects to Mollie for payment
|
|
||||||
3. After successful payment, Mollie redirects back to software tool
|
|
||||||
4. System creates license connected to serialnumber
|
|
||||||
5. Download and upload to device starts automatically
|
|
||||||
|
|
||||||
## Key Challenge
|
|
||||||
**User Experience**: How to resume the upgrade flow after payment redirect, ensuring seamless transition from payment completion back to automatic download/upload.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Current System Analysis
|
|
||||||
|
|
||||||
### Existing Infrastructure
|
|
||||||
✅ **Transactions Table** - Ready for payment tracking (txn_id, payment_status, payment_amount)
|
|
||||||
✅ **Licenses Table** - Has transaction_id field for linking (currently unused)
|
|
||||||
✅ **Payment Modal UI** - Frontend form exists in softwaretool.js (lines 455-572)
|
|
||||||
❌ **Payment Provider Integration** - No Mollie/Stripe/PayPal implementation exists
|
|
||||||
❌ **Webhook Handlers** - No callback endpoints implemented
|
|
||||||
❌ **Redirect Handling** - No return_url/cancel_url pattern
|
|
||||||
❌ **License Auto-creation** - No logic to create licenses after successful payment
|
|
||||||
❌ **Payment Session State** - No state persistence across redirect cycle
|
|
||||||
|
|
||||||
### Current Payment Flow (Simulated)
|
|
||||||
```
|
|
||||||
softwaretool.js:
|
|
||||||
1. User clicks "Purchase & Install" → showPaymentModal()
|
|
||||||
2. User fills form → processPayment()
|
|
||||||
3. [SIMULATED 2-second delay - no actual payment]
|
|
||||||
4. downloadAndInstallSoftware() → triggers upload.js
|
|
||||||
```
|
|
||||||
|
|
||||||
**Problem**: Step 3 will become a redirect to Mollie, breaking the flow and losing all state.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## User's Preferred Flow (APPROVED)
|
|
||||||
|
|
||||||
The user wants a simpler, more elegant approach:
|
|
||||||
|
|
||||||
1. **Payment creates license** - Mollie webhook creates license linked to serial number
|
|
||||||
2. **Return to software tool** - User redirected back with upgrade information in URL
|
|
||||||
3. **Reconnect device** - User connects device (may be different device!)
|
|
||||||
4. **Re-check software options** - System calls `software_update` API again
|
|
||||||
5. **License automatically applied** - Paid upgrade now shows as FREE (license found)
|
|
||||||
6. **Install button changes** - "Purchase & Install" becomes "Install Now" (free)
|
|
||||||
7. **User proceeds** - Click install to download and upload
|
|
||||||
|
|
||||||
### Key Benefits
|
|
||||||
- ✅ No complex state management needed
|
|
||||||
- ✅ Existing license checking logic handles everything
|
|
||||||
- ✅ User can connect different device (license is separate)
|
|
||||||
- ✅ Clean separation: payment → license → upgrade check
|
|
||||||
- ✅ Works with existing `software_update.php` license validation (lines 274-311)
|
|
||||||
|
|
||||||
### Critical Security Check
|
|
||||||
**IMPORTANT**: Before starting upload, verify serial number matches the one from payment.
|
|
||||||
- Store `serial_number` in payment session/URL
|
|
||||||
- When user returns and reconnects device, compare:
|
|
||||||
- `serialnumber_from_payment` vs `serialnumber_from_device`
|
|
||||||
- If mismatch: Show warning "Different device detected - license applied to original device (SN: XXXXX)"
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Proposed Solution Architecture
|
|
||||||
|
|
||||||
### Database Changes
|
|
||||||
**No new tables needed** - Use existing `transactions` and `transactions_items` tables
|
|
||||||
|
|
||||||
**`transactions` table fields:**
|
|
||||||
- `txn_id` (varchar 255, UNIQUE) - Store Mollie payment_id here
|
|
||||||
- `payment_status` (int 11) - Payment status code (need to define: 0=pending, 1=paid, 2=failed, 3=canceled, etc.)
|
|
||||||
- `payment_amount` (decimal 7,2) - Price
|
|
||||||
- `payer_email` (varchar 255) - Customer email
|
|
||||||
- `first_name`, `last_name` - Customer name
|
|
||||||
- `address_*` fields - Customer address
|
|
||||||
- `account_id` (varchar 255) - Can store serial_number here or user account
|
|
||||||
- `payment_method` (int 11) - Payment method ID
|
|
||||||
- `created`, `updated` - Timestamps
|
|
||||||
|
|
||||||
**`transactions_items` table fields:**
|
|
||||||
- `txn_id` (varchar 255) - Links to transactions.txn_id
|
|
||||||
- `item_id` (int 11) - Store version_id (products_software_versions.rowID)
|
|
||||||
- `item_price` (decimal 7,2) - Software version price
|
|
||||||
- `item_quantity` (int 11) - Always 1 for software upgrades
|
|
||||||
- `item_options` (varchar 255) - Store JSON with: `{"serial_number": "22110095", "equipment_id": 123, "hw_version": "r08"}`
|
|
||||||
- `created`, `updated` - Timestamps
|
|
||||||
|
|
||||||
**Payment Status Codes** (matching existing webhook.php):
|
|
||||||
- `0` = Pending (initial state, before Mollie call)
|
|
||||||
- `1` = Paid (payment successful)
|
|
||||||
- `101` = Open/Pending (Mollie isPending or isOpen)
|
|
||||||
- `102` = Failed (Mollie isFailed)
|
|
||||||
- `103` = Expired (Mollie isExpired)
|
|
||||||
- `999` = Canceled (Mollie isCanceled)
|
|
||||||
|
|
||||||
### API Endpoints Needed (Following Standard Structure)
|
|
||||||
1. **POST /api/v2/post/payment.php** - Initiates Mollie payment (create action)
|
|
||||||
2. **GET /api/v2/get/payment.php** - Retrieves payment status and details
|
|
||||||
3. **NEW `webhook_mollie.php`** - Separate webhook for software upgrades (based on webhook.php structure, but simplified for this use case)
|
|
||||||
|
|
||||||
### Simplified Flow Diagram
|
|
||||||
```
|
|
||||||
[User] → [Select Paid Upgrade] → [Payment Modal]
|
|
||||||
↓
|
|
||||||
processPayment() calls POST /v2/post/payment
|
|
||||||
- Store pending payment in DB
|
|
||||||
- Call Mollie API: create payment
|
|
||||||
- Get checkout URL
|
|
||||||
- Redirect user to Mollie
|
|
||||||
↓
|
|
||||||
User pays at Mollie ←→ [Mollie Payment Page]
|
|
||||||
↓
|
|
||||||
┌───────────────────────┴───────────────────────┐
|
|
||||||
↓ ↓
|
|
||||||
[Mollie redirects user back] [Mollie webhook fires asynchronously]
|
|
||||||
softwaretool.php?payment_id={payment_id} NEW webhook_mollie.php receives POST
|
|
||||||
- Calls GET /v2/get/payment?payment_id=X - Fetches payment from Mollie API
|
|
||||||
- Shows status message - Updates transaction status (1=paid)
|
|
||||||
- Display device connection button - Creates license in products_software_licenses
|
|
||||||
- Updates equipment.sw_version_license
|
|
||||||
↓
|
|
||||||
[User clicks "Connect Device"]
|
|
||||||
↓
|
|
||||||
connectDeviceForSoftware()
|
|
||||||
- User connects device (may be different device!)
|
|
||||||
- Read SN, FW, HW from device
|
|
||||||
↓
|
|
||||||
checkSoftwareAvailability() → calls /v2/software_update
|
|
||||||
- Existing license validation (lines 274-311) finds license
|
|
||||||
- Paid upgrade now shows price = 0.00
|
|
||||||
- Button text changes: "Purchase & Install" → "Install Now"
|
|
||||||
↓
|
|
||||||
[User clicks "Install Now"]
|
|
||||||
↓
|
|
||||||
selectUpgrade(option) → sees price = 0, skips payment modal
|
|
||||||
↓
|
|
||||||
downloadAndInstallSoftware()
|
|
||||||
- CRITICAL: Verify serial number matches payment
|
|
||||||
- If mismatch: Show warning but allow (license already applied)
|
|
||||||
- Download firmware
|
|
||||||
- Trigger upload.js
|
|
||||||
```
|
|
||||||
|
|
||||||
### Key Design Decisions
|
|
||||||
|
|
||||||
**1. Leverage Existing License Logic**
|
|
||||||
- No need to manually check licenses in frontend
|
|
||||||
- `software_update.php` lines 274-311 already handle this perfectly
|
|
||||||
- When license exists and is valid, price automatically becomes 0.00
|
|
||||||
- Frontend just needs to check `if (price === 0)` to show different button
|
|
||||||
|
|
||||||
**2. Minimal State Management**
|
|
||||||
- Store only essential data in `transactions` and `transactions_items`
|
|
||||||
- URL parameters carry context back (payment_id)
|
|
||||||
- No need to persist entire upgrade state
|
|
||||||
- User reconnects device = fresh state from device
|
|
||||||
|
|
||||||
**3. Serial Number Verification**
|
|
||||||
- Store `serial_number` in `transactions_items.item_options` JSON
|
|
||||||
- After return, when user reconnects device, compare:
|
|
||||||
- `serialnumber_from_payment` (from item_options JSON)
|
|
||||||
- `deviceSerialNumber` (from connected device)
|
|
||||||
- If mismatch: Show warning "Different device detected. License was applied to device SN: XXXXX"
|
|
||||||
- Allow upload to proceed (license is already created for original SN)
|
|
||||||
|
|
||||||
**4. Separate Webhook for Software Upgrades**
|
|
||||||
- Create new `webhook_mollie.php` based on structure from existing webhook.php
|
|
||||||
- Specifically designed for software upgrade payments (no invoice generation needed)
|
|
||||||
- Simplified logic: Just update transaction status and create license
|
|
||||||
- Webhook URL: `https://site.com/webhook_mollie.php`
|
|
||||||
- Webhook is authoritative for license creation
|
|
||||||
- Return URL handler just shows status message
|
|
||||||
- Race condition safe: user may see "payment successful" before webhook fires
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Implementation Plan
|
|
||||||
|
|
||||||
### Phase 1: Database & Payment Infrastructure
|
|
||||||
|
|
||||||
**1.1 Database Table - No Changes Needed**
|
|
||||||
```
|
|
||||||
The existing transactions and transactions_items tables will be used.
|
|
||||||
No schema modifications required.
|
|
||||||
```
|
|
||||||
|
|
||||||
**1.2 Create `/api/v2/post/payment.php`**
|
|
||||||
```php
|
|
||||||
<?php
|
|
||||||
defined($security_key) or exit;
|
|
||||||
|
|
||||||
// POST endpoint for payment creation
|
|
||||||
// Input (JSON): serial_number, version_id, user_data (name, email, address)
|
|
||||||
// Output (JSON): {checkout_url: "https://mollie.com/...", payment_id: "tr_xxx"}
|
|
||||||
|
|
||||||
//Connect to DB
|
|
||||||
$pdo = dbConnect($dbname);
|
|
||||||
|
|
||||||
//CONTENT FROM API (POST)
|
|
||||||
$post_content = json_decode($input, true);
|
|
||||||
|
|
||||||
// SECURITY: Never trust price/currency from frontend!
|
|
||||||
// Steps:
|
|
||||||
1. Validate inputs (serial_number, version_id, user_data)
|
|
||||||
2. SERVER-SIDE: Calculate actual price using software_update logic:
|
|
||||||
a. Get equipment data from serial_number
|
|
||||||
b. Get version data from version_id
|
|
||||||
c. Check upgrade path pricing (same logic as software_update.php lines 237-253)
|
|
||||||
d. Check license validity (same logic as software_update.php lines 274-311)
|
|
||||||
e. Calculate FINAL price server-side
|
|
||||||
3. Verify price > 0 (free upgrades shouldn't reach payment API)
|
|
||||||
4. Call Mollie API FIRST to get payment_id:
|
|
||||||
$mollie->payments->create([
|
|
||||||
'amount' => ['currency' => 'EUR', 'value' => $final_price],
|
|
||||||
'description' => 'Software upgrade to version X',
|
|
||||||
'redirectUrl' => 'https://site.com/softwaretool.php?payment_return=1&payment_id={payment_id}',
|
|
||||||
'webhookUrl' => 'https://site.com/webhook_mollie.php', // NEW webhook for software upgrades
|
|
||||||
'metadata' => ['order_id' => $mollie_payment_id] // for compatibility
|
|
||||||
])
|
|
||||||
5. Store transaction in DB with Mollie payment_id:
|
|
||||||
INSERT INTO transactions (txn_id, payment_amount, payment_status, payer_email, first_name, last_name, address_*, account_id, ...)
|
|
||||||
VALUES ($mollie_payment_id, $final_price, 0, ...) -- 0 = pending
|
|
||||||
6. Store transaction item:
|
|
||||||
INSERT INTO transactions_items (txn_id, item_id, item_price, item_quantity, item_options, ...)
|
|
||||||
VALUES ($mollie_payment_id, $version_id, $final_price, 1, '{"serial_number":"...", "equipment_id":...}', ...)
|
|
||||||
7. Return JSON: {checkout_url: $mollie_checkout_url, payment_id: $mollie_payment_id}
|
|
||||||
```
|
|
||||||
|
|
||||||
**1.3 Create `/api/v2/get/payment.php`**
|
|
||||||
```php
|
|
||||||
<?php
|
|
||||||
defined($security_key) or exit;
|
|
||||||
|
|
||||||
// GET endpoint for payment status retrieval
|
|
||||||
// Input (URL): ?payment_id=tr_xxx
|
|
||||||
// Output (JSON): {payment_id, serial_number, version_id, payment_status, price, currency, user_data}
|
|
||||||
|
|
||||||
//Connect to DB
|
|
||||||
$pdo = dbConnect($dbname);
|
|
||||||
|
|
||||||
//NEW ARRAY
|
|
||||||
$criterias = [];
|
|
||||||
|
|
||||||
//Check for $_GET variables
|
|
||||||
if(isset($get_content) && $get_content!=''){
|
|
||||||
$requests = explode("&", $get_content);
|
|
||||||
foreach ($requests as $y){
|
|
||||||
$v = explode("=", $y);
|
|
||||||
$criterias[$v[0]] = $v[1];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Steps:
|
|
||||||
1. Validate payment_id from URL
|
|
||||||
2. Fetch transaction: SELECT * FROM transactions WHERE txn_id = ?
|
|
||||||
3. Fetch transaction item: SELECT * FROM transactions_items WHERE txn_id = ?
|
|
||||||
4. Parse item_options JSON to get serial_number, equipment_id
|
|
||||||
5. Return JSON with payment details:
|
|
||||||
{
|
|
||||||
"payment_id": txn_id,
|
|
||||||
"payment_status": payment_status, // 0=pending, 1=paid, 2=failed, 3=canceled
|
|
||||||
"payment_amount": payment_amount,
|
|
||||||
"serial_number": from item_options JSON,
|
|
||||||
"equipment_id": from item_options JSON,
|
|
||||||
"version_id": item_id,
|
|
||||||
"payer_email": payer_email,
|
|
||||||
"customer_name": first_name + " " + last_name
|
|
||||||
}
|
|
||||||
6. If not found, return error
|
|
||||||
```
|
|
||||||
|
|
||||||
**1.4 Create NEW `webhook_mollie.php`**
|
|
||||||
```php
|
|
||||||
<?php
|
|
||||||
// NEW FILE - Webhook for software upgrade payments
|
|
||||||
// Based on structure from existing webhook.php from commerce product
|
|
||||||
// Uses existing transaction API + invoice API + email system
|
|
||||||
|
|
||||||
require_once 'assets/config.php';
|
|
||||||
require_once 'assets/functions.php';
|
|
||||||
|
|
||||||
//+++++++++++++++++++++++++++++++++++++++++++++++++++++
|
|
||||||
//LOGIN TO API (same as commerce webhook.php)
|
|
||||||
//+++++++++++++++++++++++++++++++++++++++++++++++++++++
|
|
||||||
$data = json_encode(array("clientID" => clientID, "clientsecret" => clientsecret), JSON_UNESCAPED_UNICODE);
|
|
||||||
$responses = ioAPIv2('/v2/authorization', $data,'');
|
|
||||||
//Decode Payload
|
|
||||||
if (!empty($responses)){$responses = json_decode($responses,true);}else{$responses = '400';}
|
|
||||||
$clientsecret = $responses['token'];
|
|
||||||
|
|
||||||
//+++++++++++++++++++++++++++++++++++++++++++++++++++++
|
|
||||||
// BASEURL is required for invoice template
|
|
||||||
//+++++++++++++++++++++++++++++++++++++++++++++++++++++
|
|
||||||
$base_url = 'https://'.$_SERVER['SERVER_NAME'].'/';
|
|
||||||
define('base_url', $base_url);
|
|
||||||
|
|
||||||
try {
|
|
||||||
//+++++++++++++++++++++++++++++++++++++++++++++++++++++
|
|
||||||
// Initialize the Mollie API library
|
|
||||||
//+++++++++++++++++++++++++++++++++++++++++++++++++++++
|
|
||||||
require "initialize.php"; // Mollie initialization (from commerce webhook)
|
|
||||||
|
|
||||||
//+++++++++++++++++++++++++++++++++++++++++++++++++++++
|
|
||||||
//Retrieve the payment's current state
|
|
||||||
//+++++++++++++++++++++++++++++++++++++++++++++++++++++
|
|
||||||
$payment = $mollie->payments->get($_POST["id"]);
|
|
||||||
$orderId = $payment->metadata->order_id;
|
|
||||||
|
|
||||||
//+++++++++++++++++++++++++++++++++++++++++++++++++++++
|
|
||||||
// Update the transaction using existing API
|
|
||||||
//+++++++++++++++++++++++++++++++++++++++++++++++++++++
|
|
||||||
|
|
||||||
if ($payment->isPaid() && !$payment->hasRefunds() && !$payment->hasChargebacks()) {
|
|
||||||
//+++++++++++++++++++++++++++++++++++++++++++++++++++++
|
|
||||||
// PAID - Update transaction status via API
|
|
||||||
//+++++++++++++++++++++++++++++++++++++++++++++++++++++
|
|
||||||
$payload = json_encode(array("txn_id" => $orderId, "payment_status" => 1), JSON_UNESCAPED_UNICODE);
|
|
||||||
$transaction = ioAPIv2('/v2/transactions/',$payload,$clientsecret);
|
|
||||||
$transaction = json_decode($transaction,true);
|
|
||||||
|
|
||||||
if ($transaction !== null && !empty($transaction)) {
|
|
||||||
if(count($transaction) > 0) {
|
|
||||||
|
|
||||||
//+++++++++++++++++++++++++++++++++++++++++++++++++++++
|
|
||||||
// CREATE LICENSE for software upgrade
|
|
||||||
//+++++++++++++++++++++++++++++++++++++++++++++++++++++
|
|
||||||
$pdo = dbConnect($dbname);
|
|
||||||
|
|
||||||
// Fetch transaction items to find software upgrade
|
|
||||||
$sql = 'SELECT * FROM transactions_items WHERE txn_id = ?';
|
|
||||||
$stmt = $pdo->prepare($sql);
|
|
||||||
$stmt->execute([$orderId]);
|
|
||||||
$items = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
|
||||||
|
|
||||||
foreach ($items as $item) {
|
|
||||||
if (!empty($item['item_options'])) {
|
|
||||||
$options = json_decode($item['item_options'], true);
|
|
||||||
|
|
||||||
// Check if this is a software upgrade (has serial_number and equipment_id)
|
|
||||||
if (isset($options['serial_number']) && isset($options['equipment_id'])) {
|
|
||||||
|
|
||||||
// Check if license already exists for this transaction
|
|
||||||
$sql = 'SELECT rowID FROM products_software_licenses WHERE transaction_id = ?';
|
|
||||||
$stmt = $pdo->prepare($sql);
|
|
||||||
$stmt->execute([$orderId]);
|
|
||||||
$existing_license = $stmt->fetch(PDO::FETCH_ASSOC);
|
|
||||||
|
|
||||||
if (!$existing_license) {
|
|
||||||
// Generate unique license key
|
|
||||||
$license_key = generateUniqueLicenseKey();
|
|
||||||
|
|
||||||
// Create license
|
|
||||||
$sql = 'INSERT INTO products_software_licenses
|
|
||||||
(license_key, equipment_id, license_type, status, start_at, expires_at, transaction_id, created, createdby)
|
|
||||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)';
|
|
||||||
$stmt = $pdo->prepare($sql);
|
|
||||||
$stmt->execute([
|
|
||||||
$license_key,
|
|
||||||
$options['equipment_id'],
|
|
||||||
'upgrade',
|
|
||||||
1, // active
|
|
||||||
date('Y-m-d H:i:s'),
|
|
||||||
'2099-12-31 23:59:59', // effectively permanent
|
|
||||||
$orderId,
|
|
||||||
date('Y-m-d H:i:s'),
|
|
||||||
'webhook' // created by webhook
|
|
||||||
]);
|
|
||||||
|
|
||||||
// Update equipment.sw_version_license
|
|
||||||
$sql = 'UPDATE equipment SET sw_version_license = ? WHERE rowID = ?';
|
|
||||||
$stmt = $pdo->prepare($sql);
|
|
||||||
$stmt->execute([$license_key, $options['equipment_id']]);
|
|
||||||
|
|
||||||
error_log("Webhook: License created for equipment_id: " . $options['equipment_id'] . ", license_key: " . $license_key);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
//+++++++++++++++++++++++++++++++++++++++++++++++++++++
|
|
||||||
//Generate INVOICE RECORD via API
|
|
||||||
//+++++++++++++++++++++++++++++++++++++++++++++++++++++
|
|
||||||
$payload = json_encode(array("txn_id" => $transaction['transaction_id']), JSON_UNESCAPED_UNICODE);
|
|
||||||
$invoice = ioAPIv2('/v2/invoice/',$payload,$clientsecret);
|
|
||||||
$invoice = json_decode($invoice,true);
|
|
||||||
|
|
||||||
if ($invoice !== null && !empty($invoice)) {
|
|
||||||
//+++++++++++++++++++++++++++++++++++++++++++++++++++++
|
|
||||||
//Generate INVOICE PDF and send email
|
|
||||||
//+++++++++++++++++++++++++++++++++++++++++++++++++++++
|
|
||||||
$invoice_cust = ioAPIv2('/v2/invoice/list=invoice&id='.$invoice['invoice_id'],'',$clientsecret);
|
|
||||||
$invoice_cust = json_decode($invoice_cust,true);
|
|
||||||
|
|
||||||
// Determine invoice language
|
|
||||||
if (!empty($invoice_cust['customer']['language'])) {
|
|
||||||
$invoice_language = strtoupper($invoice_cust['customer']['language']);
|
|
||||||
} elseif (!empty($invoice_cust['customer']['country']) && isset($available_languages[strtoupper($invoice_cust['customer']['country'])])) {
|
|
||||||
$invoice_language = $available_languages[strtoupper($invoice_cust['customer']['country'])];
|
|
||||||
} else {
|
|
||||||
$invoice_language = 'US'; // Default fallback
|
|
||||||
}
|
|
||||||
|
|
||||||
// Generate invoice HTML (using custom template for software upgrades)
|
|
||||||
list($data,$customer_email,$order_id) = generateSoftwareInvoice($invoice_cust,$orderId,$invoice_language);
|
|
||||||
|
|
||||||
//CREATE PDF using DomPDF
|
|
||||||
$dompdf->loadHtml($data);
|
|
||||||
$dompdf->setPaper('A4', 'portrait');
|
|
||||||
$dompdf->render();
|
|
||||||
$subject = ($invoice_software_subject ?? 'Software Upgrade - Invoice: ').$order_id;
|
|
||||||
$attachment = $dompdf->output();
|
|
||||||
|
|
||||||
//+++++++++++++++++++++++++++++++++++++++++++++++++++++
|
|
||||||
//Send email via PHPMailer
|
|
||||||
//+++++++++++++++++++++++++++++++++++++++++++++++++++++
|
|
||||||
send_mail_by_PHPMailer($customer_email, $subject, $data, $attachment, $subject);
|
|
||||||
|
|
||||||
if(invoice_bookkeeping){
|
|
||||||
send_mail_by_PHPMailer(email_bookkeeping, $subject, $data, $attachment, $subject);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
} elseif ($payment->isOpen()) {
|
|
||||||
// OPEN/PENDING (101)
|
|
||||||
$payload = json_encode(array("txn_id" => $orderId, "payment_status" => 101), JSON_UNESCAPED_UNICODE);
|
|
||||||
$transaction = ioAPIv2('/v2/transactions/',$payload,$clientsecret);
|
|
||||||
|
|
||||||
} elseif ($payment->isPending()) {
|
|
||||||
// PENDING (101)
|
|
||||||
$payload = json_encode(array("txn_id" => $orderId, "payment_status" => 101), JSON_UNESCAPED_UNICODE);
|
|
||||||
$transaction = ioAPIv2('/v2/transactions/',$payload,$clientsecret);
|
|
||||||
|
|
||||||
} elseif ($payment->isFailed()) {
|
|
||||||
// FAILED (102)
|
|
||||||
$payload = json_encode(array("txn_id" => $orderId, "payment_status" => 102), JSON_UNESCAPED_UNICODE);
|
|
||||||
$transaction = ioAPIv2('/v2/transactions/',$payload,$clientsecret);
|
|
||||||
|
|
||||||
} elseif ($payment->isExpired()) {
|
|
||||||
// EXPIRED (103)
|
|
||||||
$payload = json_encode(array("txn_id" => $orderId, "payment_status" => 103), JSON_UNESCAPED_UNICODE);
|
|
||||||
$transaction = ioAPIv2('/v2/transactions/',$payload,$clientsecret);
|
|
||||||
|
|
||||||
} elseif ($payment->isCanceled()) {
|
|
||||||
// CANCELED (999)
|
|
||||||
$payload = json_encode(array("txn_id" => $orderId, "payment_status" => 999), JSON_UNESCAPED_UNICODE);
|
|
||||||
$transaction = ioAPIv2('/v2/transactions/',$payload,$clientsecret);
|
|
||||||
|
|
||||||
} elseif ($payment->hasRefunds()) {
|
|
||||||
// REFUNDED (1 + refund flag)
|
|
||||||
$payload = json_encode(array("txn_id" => $orderId, "payment_status" => 1), JSON_UNESCAPED_UNICODE);
|
|
||||||
$transaction = ioAPIv2('/v2/transactions/',$payload,$clientsecret);
|
|
||||||
// TODO: Disable license on refund
|
|
||||||
}
|
|
||||||
|
|
||||||
} catch (\Mollie\Api\Exceptions\ApiException $e) {
|
|
||||||
error_log("Webhook API call failed: " . htmlspecialchars($e->getMessage()));
|
|
||||||
http_response_code(500);
|
|
||||||
echo "API call failed: " . htmlspecialchars($e->getMessage());
|
|
||||||
} catch (Exception $e) {
|
|
||||||
error_log("Webhook error: " . htmlspecialchars($e->getMessage()));
|
|
||||||
http_response_code(500);
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
**Key Features (matching commerce webhook.php):**
|
|
||||||
- ✅ Uses `/v2/transactions/` API for status updates
|
|
||||||
- ✅ Uses `/v2/invoice/` API for invoice generation
|
|
||||||
- ✅ Generates PDF invoice with DomPDF
|
|
||||||
- ✅ Sends email via PHPMailer
|
|
||||||
- ✅ Creates license for software upgrade
|
|
||||||
- ✅ Uses same payment status codes (0, 1, 101, 102, 103, 999)
|
|
||||||
- ✅ Handles refunds (TODO: disable license)
|
|
||||||
- ✅ Multi-language invoice support
|
|
||||||
- ✅ Sends to bookkeeping if configured
|
|
||||||
|
|
||||||
### Phase 2: Frontend Integration
|
|
||||||
|
|
||||||
**2.1 Modify `processPayment()` in softwaretool.js (lines 574-608)**
|
|
||||||
```javascript
|
|
||||||
async function processPayment(paymentData, option, modal) {
|
|
||||||
try {
|
|
||||||
progressBar("10", "Processing payment...", "#04AA6D");
|
|
||||||
|
|
||||||
// SECURITY: Only send serial_number and version_id
|
|
||||||
// Server will calculate the price to prevent tampering
|
|
||||||
const response = await fetch(link + "/v2/post/payment", {
|
|
||||||
method: "POST",
|
|
||||||
headers: {
|
|
||||||
"Content-Type": "application/json",
|
|
||||||
"Authorization": "Bearer " + document.getElementById("servicetoken").textContent
|
|
||||||
},
|
|
||||||
body: JSON.stringify({
|
|
||||||
serial_number: deviceSerialNumber,
|
|
||||||
version_id: option.version_id,
|
|
||||||
user_data: paymentData // name, email, address only
|
|
||||||
// REMOVED: price, currency - server calculates these
|
|
||||||
})
|
|
||||||
});
|
|
||||||
|
|
||||||
const result = await response.json();
|
|
||||||
|
|
||||||
if (result.checkout_url) {
|
|
||||||
await logCommunication(`Redirecting to payment provider`, 'sent');
|
|
||||||
// Redirect to Mollie checkout
|
|
||||||
window.location.href = result.checkout_url;
|
|
||||||
} else {
|
|
||||||
throw new Error(result.error || "Failed to create payment");
|
|
||||||
}
|
|
||||||
|
|
||||||
} catch (error) {
|
|
||||||
await logCommunication(`Payment error: ${error.message}`, 'error');
|
|
||||||
progressBar("0", "Payment failed: " + error.message, "#ff6666");
|
|
||||||
alert("Payment failed: " + error.message);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
**2.2 Remove equipment_id tracking - NOT NEEDED**
|
|
||||||
```javascript
|
|
||||||
// SECURITY: We don't need to track equipment_id in frontend
|
|
||||||
// The server will look it up from serial_number in the payment/create API
|
|
||||||
// This prevents tampering with equipment_id
|
|
||||||
```
|
|
||||||
|
|
||||||
**2.3 Add Serial Number Verification in `downloadAndInstallSoftware()` (lines 610-699)**
|
|
||||||
```javascript
|
|
||||||
async function downloadAndInstallSoftware(option) {
|
|
||||||
// Check if we're returning from payment
|
|
||||||
const urlParams = new URLSearchParams(window.location.search);
|
|
||||||
const paymentId = urlParams.get('payment_id');
|
|
||||||
|
|
||||||
if (paymentId) {
|
|
||||||
// Verify serial number matches payment using GET /v2/get/payment
|
|
||||||
const response = await fetch(link + `/v2/get/payment?payment_id=${paymentId}`, {
|
|
||||||
method: "GET",
|
|
||||||
headers: {
|
|
||||||
"Authorization": "Bearer " + document.getElementById("servicetoken").textContent
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
const paymentData = await response.json();
|
|
||||||
|
|
||||||
if (paymentData.serial_number !== deviceSerialNumber) {
|
|
||||||
const confirmed = confirm(
|
|
||||||
`WARNING: Different device detected!\n\n` +
|
|
||||||
`License was created for device: ${paymentData.serial_number}\n` +
|
|
||||||
`Currently connected device: ${deviceSerialNumber}\n\n` +
|
|
||||||
`The license is already applied to the original device. ` +
|
|
||||||
`Do you want to continue with this device anyway?`
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!confirmed) {
|
|
||||||
progressBar("0", "Upload canceled by user", "#ff6666");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Continue with existing download logic...
|
|
||||||
selectedSoftwareUrl = option.source;
|
|
||||||
// ... rest of function unchanged
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
**Note**: Serial number verification uses existing GET /v2/get/payment endpoint (no separate verify endpoint needed)
|
|
||||||
|
|
||||||
### Phase 3: Return URL Handling
|
|
||||||
|
|
||||||
**3.1 Modify `softwaretool.php` to detect return from payment**
|
|
||||||
```php
|
|
||||||
// Add near top of softwaretool.php (after includes, before $view)
|
|
||||||
<?php
|
|
||||||
$payment_return = isset($_GET['payment_id']) ? $_GET['payment_id'] : null;
|
|
||||||
|
|
||||||
if ($payment_return) {
|
|
||||||
// Optionally fetch payment status via GET /v2/get/payment
|
|
||||||
// and show appropriate message banner at top of page
|
|
||||||
// "Payment successful! Please reconnect your device to continue."
|
|
||||||
// User will then click "Connect Device" button
|
|
||||||
// After connection, checkSoftwareAvailability() will run
|
|
||||||
// License will be found via existing logic, price will be 0.00
|
|
||||||
}
|
|
||||||
?>
|
|
||||||
```
|
|
||||||
|
|
||||||
**3.2 Optional: Auto-trigger device connection after payment return**
|
|
||||||
```javascript
|
|
||||||
// In softwaretool.js, check URL on page load
|
|
||||||
window.addEventListener('DOMContentLoaded', function() {
|
|
||||||
const urlParams = new URLSearchParams(window.location.search);
|
|
||||||
if (urlParams.has('payment_id')) {
|
|
||||||
// Show message: "Payment successful! Please reconnect your device."
|
|
||||||
// Optionally auto-show device connection UI
|
|
||||||
}
|
|
||||||
});
|
|
||||||
```
|
|
||||||
|
|
||||||
### Phase 4: Testing Strategy
|
|
||||||
|
|
||||||
**4.1 DEBUG Mode Testing (Complete Simulation)**
|
|
||||||
```php
|
|
||||||
// In /api/v2/post/payment.php, check if DEBUG mode
|
|
||||||
if (defined('debug') && debug) {
|
|
||||||
// FULL SIMULATION: No Mollie API connection, no device connection
|
|
||||||
$fake_payment_id = 'DEBUG_' . uniqid();
|
|
||||||
|
|
||||||
// 1. Store transaction with status 0 (pending)
|
|
||||||
$sql = 'INSERT INTO transactions
|
|
||||||
(txn_id, payment_amount, payment_status, payer_email, first_name, last_name,
|
|
||||||
address_street, address_city, address_state, address_zip, address_country, account_id)
|
|
||||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)';
|
|
||||||
$stmt = $pdo->prepare($sql);
|
|
||||||
$stmt->execute([
|
|
||||||
$fake_payment_id,
|
|
||||||
$final_price,
|
|
||||||
0, // 0 = pending
|
|
||||||
$post_content['user_data']['email'],
|
|
||||||
$post_content['user_data']['first_name'] ?? '',
|
|
||||||
$post_content['user_data']['last_name'] ?? '',
|
|
||||||
$post_content['user_data']['address_street'] ?? '',
|
|
||||||
$post_content['user_data']['address_city'] ?? '',
|
|
||||||
$post_content['user_data']['address_state'] ?? '',
|
|
||||||
$post_content['user_data']['address_zip'] ?? '',
|
|
||||||
$post_content['user_data']['address_country'] ?? '',
|
|
||||||
$post_content['serial_number'] // store serial number in account_id
|
|
||||||
]);
|
|
||||||
|
|
||||||
// 2. Store transaction item
|
|
||||||
$item_options = json_encode([
|
|
||||||
'serial_number' => $post_content['serial_number'],
|
|
||||||
'equipment_id' => $equipment_id,
|
|
||||||
'hw_version' => $hw_version
|
|
||||||
]);
|
|
||||||
$sql = 'INSERT INTO transactions_items
|
|
||||||
(txn_id, item_id, item_price, item_quantity, item_options)
|
|
||||||
VALUES (?, ?, ?, ?, ?)';
|
|
||||||
$stmt = $pdo->prepare($sql);
|
|
||||||
$stmt->execute([
|
|
||||||
$fake_payment_id,
|
|
||||||
$post_content['version_id'],
|
|
||||||
$final_price,
|
|
||||||
1,
|
|
||||||
$item_options
|
|
||||||
]);
|
|
||||||
|
|
||||||
// 3. Immediately simulate webhook success (update status to paid + create license)
|
|
||||||
$sql = 'UPDATE transactions SET payment_status = 1 WHERE txn_id = ?'; // 1 = paid
|
|
||||||
$stmt = $pdo->prepare($sql);
|
|
||||||
$stmt->execute([$fake_payment_id]);
|
|
||||||
|
|
||||||
// 4. Create license
|
|
||||||
$license_key = generateUniqueLicenseKey();
|
|
||||||
$sql = 'INSERT INTO products_software_licenses
|
|
||||||
(license_key, equipment_id, license_type, status, start_at, expires_at, transaction_id, created, createdby)
|
|
||||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)';
|
|
||||||
$stmt = $pdo->prepare($sql);
|
|
||||||
$stmt->execute([
|
|
||||||
$license_key,
|
|
||||||
$equipment_id,
|
|
||||||
'upgrade',
|
|
||||||
1,
|
|
||||||
date('Y-m-d H:i:s'),
|
|
||||||
'2099-12-31 23:59:59',
|
|
||||||
$fake_payment_id,
|
|
||||||
date('Y-m-d H:i:s'),
|
|
||||||
$username
|
|
||||||
]);
|
|
||||||
|
|
||||||
// 5. Update equipment.sw_version_license
|
|
||||||
$sql = 'UPDATE equipment SET sw_version_license = ? WHERE rowID = ?';
|
|
||||||
$stmt = $pdo->prepare($sql);
|
|
||||||
$stmt->execute([$license_key, $equipment_id]);
|
|
||||||
|
|
||||||
// 6. Return fake checkout URL that redirects immediately
|
|
||||||
$messages = [
|
|
||||||
'checkout_url' => 'https://'.$_SERVER['SERVER_NAME'].'/softwaretool.php?payment_return=1&payment_id=' . $fake_payment_id,
|
|
||||||
'payment_id' => $fake_payment_id
|
|
||||||
];
|
|
||||||
echo json_encode($messages);
|
|
||||||
exit;
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
**Note**: In DEBUG mode, the entire payment + license creation flow is simulated without:
|
|
||||||
- Calling Mollie API
|
|
||||||
- Requiring physical device connection (works with DEBUG mode mock device data in softwaretool.js)
|
|
||||||
|
|
||||||
**4.2 Mollie Sandbox Testing**
|
|
||||||
1. Use Mollie test API key
|
|
||||||
2. Test successful payment flow
|
|
||||||
3. Test failed payment flow
|
|
||||||
4. Test canceled payment flow
|
|
||||||
5. Test webhook delivery
|
|
||||||
6. Test license creation
|
|
||||||
|
|
||||||
**4.3 Serial Number Mismatch Testing**
|
|
||||||
1. Complete payment with device A (SN: 22110095)
|
|
||||||
2. Disconnect device A
|
|
||||||
3. Connect device B (different SN)
|
|
||||||
4. Verify warning appears
|
|
||||||
5. Verify license was created for device A
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Critical Files to Modify
|
|
||||||
|
|
||||||
### New Files
|
|
||||||
- `/api/v2/post/payment.php` - Payment creation (POST)
|
|
||||||
- `/api/v2/get/payment.php` - Payment status retrieval (GET)
|
|
||||||
- `/webhook_mollie.php` - Mollie webhook handler for software upgrades (based on existing webhook.php structure)
|
|
||||||
- `generateSoftwareInvoice()` function in `/assets/functions.php` - Invoice template for software upgrades
|
|
||||||
|
|
||||||
### Modified Files
|
|
||||||
- `/assets/softwaretool.js`:
|
|
||||||
- `processPayment()` (lines 574-608) - Call POST /v2/post/payment instead of simulation
|
|
||||||
- `downloadAndInstallSoftware()` (lines 610-699) - Add serial number verification using GET /v2/get/payment
|
|
||||||
- Add payment return detection on page load (optional)
|
|
||||||
- `/softwaretool.php`:
|
|
||||||
- Add payment return URL detection (check for ?payment_id=X)
|
|
||||||
- Optionally show success message banner after payment
|
|
||||||
- `/api/v2/get/software_update.php`:
|
|
||||||
- **No changes needed** (existing license logic at lines 274-311 works perfectly!)
|
|
||||||
|
|
||||||
### Database & Helper Functions
|
|
||||||
- No new tables needed (using existing `transactions` and `transactions_items`)
|
|
||||||
- Add helper function `generateUniqueLicenseKey()` in `assets/functions.php`
|
|
||||||
- Payment status codes already defined in existing webhook.php (0, 1, 101, 102, 103, 999)
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Security Architecture Summary
|
|
||||||
|
|
||||||
### ✅ **SECURE APPROACH: Server-Side Price Validation**
|
|
||||||
|
|
||||||
**Frontend sends:**
|
|
||||||
- `serial_number` (from connected device)
|
|
||||||
- `version_id` (which version they want)
|
|
||||||
- `user_data` (name, email, address)
|
|
||||||
|
|
||||||
**Backend does:**
|
|
||||||
1. Look up equipment from `serial_number`
|
|
||||||
2. Look up version from `version_id`
|
|
||||||
3. **Calculate actual price using same logic as software_update.php**:
|
|
||||||
- Check upgrade path pricing (lines 244-260)
|
|
||||||
- Check if license exists and reduces price (lines 274-311)
|
|
||||||
- Get final server-calculated price
|
|
||||||
4. Verify price > 0 (reject free upgrades)
|
|
||||||
5. Create Mollie payment with **SERVER-CALCULATED price**
|
|
||||||
6. Store pending payment with correct price
|
|
||||||
|
|
||||||
### ❌ **INSECURE APPROACH: Never Do This**
|
|
||||||
```javascript
|
|
||||||
// WRONG - User can modify price in browser console!
|
|
||||||
body: JSON.stringify({
|
|
||||||
serial_number: deviceSerialNumber,
|
|
||||||
version_id: option.version_id,
|
|
||||||
price: 0.01, // <-- Tampered from 49.99!
|
|
||||||
currency: "EUR"
|
|
||||||
})
|
|
||||||
```
|
|
||||||
|
|
||||||
**Why this is dangerous:**
|
|
||||||
- User can open browser console
|
|
||||||
- Change `option.price = 0.01` before payment
|
|
||||||
- Backend trusts this value = user pays 1 cent for €49.99 upgrade
|
|
||||||
|
|
||||||
### ✅ **CORRECT APPROACH**
|
|
||||||
```javascript
|
|
||||||
// SECURE - Only send identifiers, server calculates price
|
|
||||||
body: JSON.stringify({
|
|
||||||
serial_number: deviceSerialNumber, // Who is buying
|
|
||||||
version_id: option.version_id, // What they want
|
|
||||||
user_data: paymentData // Customer info
|
|
||||||
// NO PRICE - server calculates it!
|
|
||||||
})
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Configuration & Requirements (USER CONFIRMED)
|
|
||||||
|
|
||||||
1. ✅ **Mollie API Credentials**: User has Mollie info - will be added as constants in `config.php`
|
|
||||||
- `MOLLIE_API_KEY_TEST` (for sandbox)
|
|
||||||
- `MOLLIE_API_KEY_LIVE` (for production)
|
|
||||||
2. ✅ **License Duration**: `expires_at = '2099-12-31 23:59:59'` (effectively permanent until 2099)
|
|
||||||
3. ✅ **Multiple Devices**: One license per device (license linked to specific equipment_id)
|
|
||||||
4. ✅ **DEBUG Mode**: Full payment process simulation without Mollie connection AND without device connection
|
|
||||||
5. ✅ **Transaction Logging**: Use existing ecommerce transaction APIs:
|
|
||||||
- `transactions` table - main transaction record
|
|
||||||
- `transaction_items` table - line items (software upgrade details)
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Next Steps After Plan Approval
|
|
||||||
|
|
||||||
1. ✅ Add Mollie constants to `config.php`:
|
|
||||||
```php
|
|
||||||
define('MOLLIE_API_KEY_TEST', 'test_xxxxx'); // User will provide
|
|
||||||
define('MOLLIE_API_KEY_LIVE', 'live_xxxxx'); // User will provide
|
|
||||||
```
|
|
||||||
2. ✅ Mollie PHP SDK already installed (used by existing webhook.php)
|
|
||||||
3. ✅ No database changes needed (using existing `transactions` and `transactions_items` tables)
|
|
||||||
4. Create helper functions in `assets/functions.php`:
|
|
||||||
- `generateUniqueLicenseKey()` - Generate unique license keys
|
|
||||||
- `generateSoftwareInvoice()` - Generate HTML invoice for software upgrades (based on existing generateInvoice())
|
|
||||||
5. ✅ Payment status codes already defined in existing webhook.php
|
|
||||||
6. Implement NEW backend files:
|
|
||||||
- `/api/v2/post/payment.php` (with DEBUG mode support)
|
|
||||||
- `/api/v2/get/payment.php`
|
|
||||||
- `/webhook_mollie.php` (based on existing webhook.php structure)
|
|
||||||
8. Modify frontend JavaScript in `/assets/softwaretool.js`:
|
|
||||||
- Update `processPayment()` to call POST /v2/post/payment
|
|
||||||
- Add serial number verification in `downloadAndInstallSoftware()`
|
|
||||||
9. Modify `/softwaretool.php` to detect payment return
|
|
||||||
10. Test in DEBUG mode (full simulation without Mollie or device)
|
|
||||||
11. Test with Mollie sandbox
|
|
||||||
12. Deploy to production
|
|
||||||
|
|
||||||
3
api.php
3
api.php
@@ -154,7 +154,8 @@ if($is_jwt_valid && str_contains($version, 'v')) {
|
|||||||
// First check if endPoint is fileUpload
|
// First check if endPoint is fileUpload
|
||||||
//------------------------------------------
|
//------------------------------------------
|
||||||
$fileUploadEndpoints = [
|
$fileUploadEndpoints = [
|
||||||
'media_upload'
|
'media_upload',
|
||||||
|
'marketing_upload'
|
||||||
];
|
];
|
||||||
|
|
||||||
$isFileUploadEndpoint = in_array($collection, $fileUploadEndpoints);
|
$isFileUploadEndpoint = in_array($collection, $fileUploadEndpoints);
|
||||||
|
|||||||
BIN
api/.DS_Store
vendored
BIN
api/.DS_Store
vendored
Binary file not shown.
BIN
api/v1/.DS_Store
vendored
BIN
api/v1/.DS_Store
vendored
Binary file not shown.
BIN
api/v2/.DS_Store
vendored
BIN
api/v2/.DS_Store
vendored
Binary file not shown.
116
api/v2/get/equipment_history.php
Normal file
116
api/v2/get/equipment_history.php
Normal file
@@ -0,0 +1,116 @@
|
|||||||
|
<?php
|
||||||
|
defined($security_key) or exit;
|
||||||
|
|
||||||
|
// Database connection
|
||||||
|
$pdo = dbConnect($dbname);
|
||||||
|
|
||||||
|
// ============================================
|
||||||
|
// Input Validation & Sanitization
|
||||||
|
// ============================================
|
||||||
|
|
||||||
|
$filters = [
|
||||||
|
'serialnumber' => isset($_GET['serialnumber']) ? trim($_GET['serialnumber']) : null,
|
||||||
|
'type' => isset($_GET['type']) ? trim($_GET['type']) : null,
|
||||||
|
'start' => isset($_GET['start']) ? trim($_GET['start']) : date("Y-m-d", strtotime("-270 days")),
|
||||||
|
'end' => isset($_GET['end']) ? trim($_GET['end']) : date("Y-m-d", strtotime("+1 days"))
|
||||||
|
];
|
||||||
|
|
||||||
|
// ============================================
|
||||||
|
// Build Query with Prepared Statements
|
||||||
|
// ============================================
|
||||||
|
|
||||||
|
$whereClauses = [];
|
||||||
|
$params = [];
|
||||||
|
|
||||||
|
// Serial Number Filter
|
||||||
|
if ($filters['serialnumber']) {
|
||||||
|
$whereClauses[] = 'h.description LIKE :serialnumber';
|
||||||
|
$params[':serialnumber'] = "%historycreated%SN%:" . $filters['serialnumber'] . "%";
|
||||||
|
$whereClauses[] = 'h.type != :excluded_type';
|
||||||
|
$params[':excluded_type'] = 'SRIncluded';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Type Filter
|
||||||
|
if ($filters['type']) {
|
||||||
|
if ($filters['type'] === 'latest') {
|
||||||
|
// Get only the latest record per equipment
|
||||||
|
if ($filters['serialnumber']) {
|
||||||
|
$whereClauses[] = 'h.rowID IN (
|
||||||
|
SELECT MAX(h2.rowID)
|
||||||
|
FROM equipment_history h2
|
||||||
|
GROUP BY h2.equipmentid
|
||||||
|
)';
|
||||||
|
} else {
|
||||||
|
$whereClauses[] = "h.description LIKE '%historycreated%'";
|
||||||
|
$whereClauses[] = 'h.rowID IN (
|
||||||
|
SELECT MAX(h2.rowID)
|
||||||
|
FROM equipment_history h2
|
||||||
|
WHERE h2.description LIKE :history_created
|
||||||
|
GROUP BY h2.equipmentid
|
||||||
|
)';
|
||||||
|
$params[':history_created'] = '%historycreated%';
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Specific type filter
|
||||||
|
$whereClauses[] = 'h.type = :type';
|
||||||
|
$params[':type'] = $filters['type'];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Default filter if no other filters applied
|
||||||
|
if (empty($whereClauses)) {
|
||||||
|
$whereClauses[] = "h.description LIKE '%historycreated%'";
|
||||||
|
}
|
||||||
|
|
||||||
|
// Date Range Filter
|
||||||
|
$whereClauses[] = 'h.created BETWEEN :start_date AND :end_date';
|
||||||
|
$params[':start_date'] = $filters['start'];
|
||||||
|
$params[':end_date'] = $filters['end'];
|
||||||
|
|
||||||
|
// ============================================
|
||||||
|
// Execute Query
|
||||||
|
// ============================================
|
||||||
|
|
||||||
|
$whereClause = 'WHERE ' . implode(' AND ', $whereClauses);
|
||||||
|
$sql = "SELECT h.rowID, h.description
|
||||||
|
FROM equipment_history h
|
||||||
|
$whereClause
|
||||||
|
ORDER BY h.created DESC";
|
||||||
|
|
||||||
|
try {
|
||||||
|
$stmt = $pdo->prepare($sql);
|
||||||
|
$stmt->execute($params);
|
||||||
|
$messages = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||||
|
|
||||||
|
// ============================================
|
||||||
|
// Format Response
|
||||||
|
// ============================================
|
||||||
|
|
||||||
|
$results = [];
|
||||||
|
foreach ($messages as $message) {
|
||||||
|
$record = json_decode($message['description'], true);
|
||||||
|
|
||||||
|
// Handle JSON decode errors
|
||||||
|
if (json_last_error() !== JSON_ERROR_NONE) {
|
||||||
|
continue; // Skip invalid JSON
|
||||||
|
}
|
||||||
|
|
||||||
|
$record['historyID'] = (int)$message['rowID'];
|
||||||
|
$results[] = $record;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set proper headers
|
||||||
|
header('Content-Type: application/json; charset=utf-8');
|
||||||
|
echo json_encode($results, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE);
|
||||||
|
|
||||||
|
} catch (PDOException $e) {
|
||||||
|
// Log error (don't expose to client in production)
|
||||||
|
error_log("Database error: " . $e->getMessage());
|
||||||
|
|
||||||
|
//header('Content-Type: application/json; charset=utf-8', true, 500);
|
||||||
|
echo json_encode([
|
||||||
|
'error' => 'An error occurred while processing your request'
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
?>
|
||||||
@@ -275,7 +275,7 @@ else {
|
|||||||
}
|
}
|
||||||
|
|
||||||
//SQL for Paging
|
//SQL for Paging
|
||||||
$sql = 'SELECT e.rowID as equipmentID, e.*, p.productcode, p.productname, p.product_media from equipment e LEFT JOIN products p ON e.productrowid = p.rowID '.$whereclause.' ORDER BY '.$sort.' LIMIT :page,:num_products';
|
$sql = 'SELECT e.rowID as equipmentID, e.*, p.productcode, p.productname, p.product_media, psl.starts_at,psl.expires_at,psl.status as license_status from equipment e LEFT JOIN products p ON e.productrowid = p.rowID LEFT JOIN products_software_licenses psl ON e.sw_version_license = psl.license_key '.$whereclause.' ORDER BY '.$sort.' LIMIT :page,:num_products';
|
||||||
}
|
}
|
||||||
|
|
||||||
$stmt = $pdo->prepare($sql);
|
$stmt = $pdo->prepare($sql);
|
||||||
|
|||||||
155
api/v2/get/marketing_files.php
Normal file
155
api/v2/get/marketing_files.php
Normal file
@@ -0,0 +1,155 @@
|
|||||||
|
<?php
|
||||||
|
defined($security_key) or exit;
|
||||||
|
|
||||||
|
//------------------------------------------
|
||||||
|
// Marketing Files
|
||||||
|
//------------------------------------------
|
||||||
|
|
||||||
|
//Connect to DB
|
||||||
|
$pdo = dbConnect($dbname);
|
||||||
|
|
||||||
|
//SoldTo is empty
|
||||||
|
if (empty($partner->soldto) || $partner->soldto == ''){$soldto_search = '%';} else {$soldto_search = '-%';}
|
||||||
|
|
||||||
|
//default whereclause
|
||||||
|
$whereclause = '';
|
||||||
|
|
||||||
|
// For testing, disable account hierarchy filtering
|
||||||
|
// list($whereclause,$condition) = getWhereclauselvl2("",$permission,$partner,'get');
|
||||||
|
|
||||||
|
//NEW ARRAY
|
||||||
|
$criterias = [];
|
||||||
|
$clause = '';
|
||||||
|
|
||||||
|
//Check for $_GET variables and build up clause
|
||||||
|
if(isset($get_content) && $get_content!=''){
|
||||||
|
//GET VARIABLES FROM URL
|
||||||
|
$requests = explode("&", $get_content);
|
||||||
|
//Check for keys and values
|
||||||
|
foreach ($requests as $y){
|
||||||
|
$v = explode("=", $y);
|
||||||
|
//INCLUDE VARIABLES IN ARRAY
|
||||||
|
$criterias[$v[0]] = $v[1];
|
||||||
|
|
||||||
|
if ($v[0] == 'page' || $v[0] =='p' || $v[0] =='totals' || $v[0] =='list' || $v[0] == 'action' || $v[0] =='success_msg' || $v[0] == '_t'){
|
||||||
|
//do nothing
|
||||||
|
}
|
||||||
|
elseif ($v[0] == 'folder_id') {
|
||||||
|
if ($v[1] === 'null' || $v[1] === '') {
|
||||||
|
$clause .= ' AND folder_id IS NULL';
|
||||||
|
} else {
|
||||||
|
$clause .= ' AND folder_id = :folder_id';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
elseif ($v[0] == 'search') {
|
||||||
|
$clause .= ' AND (title LIKE :search OR original_filename LIKE :search)';
|
||||||
|
}
|
||||||
|
elseif ($v[0] == 'tag') {
|
||||||
|
$clause .= ' AND EXISTS (SELECT 1 FROM marketing_file_tags ft JOIN marketing_tags t ON ft.tag_id = t.id WHERE ft.file_id = mf.id AND t.tag_name = :tag)';
|
||||||
|
}
|
||||||
|
elseif ($v[0] == 'file_type') {
|
||||||
|
$clause .= ' AND file_type = :file_type';
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
// Ignore unknown parameters
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if ($whereclause == '' && $clause !=''){
|
||||||
|
$whereclause = 'WHERE '.substr($clause, 4);
|
||||||
|
} else {
|
||||||
|
$whereclause .= $clause;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//Set page
|
||||||
|
$pagina = 1;
|
||||||
|
if(isset($criterias['p']) && $criterias['p'] !='') {
|
||||||
|
$pagina = $criterias['p'];
|
||||||
|
}
|
||||||
|
|
||||||
|
//Set limit
|
||||||
|
$limit = 50;
|
||||||
|
if(isset($criterias['limit']) && $criterias['limit'] !='') {
|
||||||
|
$limit = intval($criterias['limit']);
|
||||||
|
}
|
||||||
|
$offset = ($pagina - 1) * $limit;
|
||||||
|
|
||||||
|
//check for totals call
|
||||||
|
if(isset($criterias['totals'])){
|
||||||
|
$sql = 'SELECT COUNT(*) as found FROM marketing_files mf '.$whereclause.' ';
|
||||||
|
$stmt = $pdo->prepare($sql);
|
||||||
|
|
||||||
|
// Bind parameters
|
||||||
|
if (!empty($criterias)) {
|
||||||
|
foreach ($criterias as $key => $value) {
|
||||||
|
if ($key !== 'totals' && $key !== 'page' && $key !== 'p' && $key !== 'limit' && $key !== 'action') {
|
||||||
|
if ($key == 'search') {
|
||||||
|
$stmt->bindValue(':'.$key, '%'.$value.'%');
|
||||||
|
} elseif ($key == 'folder_id' && ($value === 'null' || $value === '')) {
|
||||||
|
continue;
|
||||||
|
} else {
|
||||||
|
$stmt->bindValue(':'.$key, $value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$stmt->execute();
|
||||||
|
$found = $stmt->fetchColumn();
|
||||||
|
echo $found;
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Main query
|
||||||
|
$sql = "SELECT
|
||||||
|
mf.*,
|
||||||
|
GROUP_CONCAT(mt.tag_name) as tags
|
||||||
|
FROM marketing_files mf
|
||||||
|
LEFT JOIN marketing_file_tags mft ON mf.id = mft.file_id
|
||||||
|
LEFT JOIN marketing_tags mt ON mft.tag_id = mt.id
|
||||||
|
" . $whereclause . "
|
||||||
|
GROUP BY mf.id
|
||||||
|
ORDER BY mf.created DESC
|
||||||
|
LIMIT " . $limit . " OFFSET " . $offset;
|
||||||
|
|
||||||
|
$stmt = $pdo->prepare($sql);
|
||||||
|
|
||||||
|
// Bind parameters
|
||||||
|
if (!empty($criterias)) {
|
||||||
|
foreach ($criterias as $key => $value) {
|
||||||
|
if ($key !== 'totals' && $key !== 'page' && $key !== 'p' && $key !== 'limit') {
|
||||||
|
if ($key == 'search') {
|
||||||
|
$stmt->bindValue(':'.$key, '%'.$value.'%');
|
||||||
|
} elseif ($key == 'folder_id' && ($value === 'null' || $value === '')) {
|
||||||
|
continue;
|
||||||
|
} else {
|
||||||
|
$stmt->bindValue(':'.$key, $value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$stmt->execute();
|
||||||
|
$marketing_files = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||||
|
|
||||||
|
// Process each file
|
||||||
|
foreach ($marketing_files as &$file) {
|
||||||
|
// Process tags
|
||||||
|
$file['tags'] = $file['tags'] ? explode(',', $file['tags']) : [];
|
||||||
|
|
||||||
|
// Format file size
|
||||||
|
$bytes = $file['file_size'];
|
||||||
|
if ($bytes >= 1073741824) {
|
||||||
|
$file['file_size_formatted'] = number_format($bytes / 1073741824, 2) . ' GB';
|
||||||
|
} elseif ($bytes >= 1048576) {
|
||||||
|
$file['file_size_formatted'] = number_format($bytes / 1048576, 2) . ' MB';
|
||||||
|
} elseif ($bytes >= 1024) {
|
||||||
|
$file['file_size_formatted'] = number_format($bytes / 1024, 2) . ' KB';
|
||||||
|
} else {
|
||||||
|
$file['file_size_formatted'] = $bytes . ' B';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Return result
|
||||||
|
echo json_encode($marketing_files, JSON_UNESCAPED_UNICODE);
|
||||||
|
exit;
|
||||||
172
api/v2/get/marketing_folders.php
Normal file
172
api/v2/get/marketing_folders.php
Normal file
@@ -0,0 +1,172 @@
|
|||||||
|
<?php
|
||||||
|
defined($security_key) or exit;
|
||||||
|
ini_set('display_errors', '1');
|
||||||
|
ini_set('display_startup_errors', '1');
|
||||||
|
error_reporting(E_ALL);
|
||||||
|
//------------------------------------------
|
||||||
|
// Marketing Folders
|
||||||
|
//------------------------------------------
|
||||||
|
|
||||||
|
//Connect to DB
|
||||||
|
$pdo = dbConnect($dbname);
|
||||||
|
|
||||||
|
// Function to build hierarchical tree structure
|
||||||
|
function buildFolderTree($folders, $parentId = null) {
|
||||||
|
$tree = [];
|
||||||
|
|
||||||
|
foreach ($folders as $folder) {
|
||||||
|
if ($folder['parent_id'] == $parentId) {
|
||||||
|
$children = buildFolderTree($folders, $folder['id']);
|
||||||
|
$folder['children'] = $children; // Always include children array, even if empty
|
||||||
|
$tree[] = $folder;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $tree;
|
||||||
|
}
|
||||||
|
|
||||||
|
//SoldTo is empty
|
||||||
|
if (empty($partner->soldto) || $partner->soldto == ''){$soldto_search = '%';} else {$soldto_search = '-%';}
|
||||||
|
|
||||||
|
//default whereclause
|
||||||
|
$whereclause = '';
|
||||||
|
|
||||||
|
list($whereclause,$condition) = getWhereclauselvl2('',$permission,$partner,'get');
|
||||||
|
|
||||||
|
//NEW ARRAY
|
||||||
|
$criterias = [];
|
||||||
|
$clause = '';
|
||||||
|
|
||||||
|
//Check for $_GET variables and build up clause
|
||||||
|
if(isset($get_content) && $get_content!=''){
|
||||||
|
//GET VARIABLES FROM URL
|
||||||
|
$requests = explode("&", $get_content);
|
||||||
|
//Check for keys and values
|
||||||
|
foreach ($requests as $y){
|
||||||
|
$v = explode("=", $y);
|
||||||
|
//INCLUDE VARIABLES IN ARRAY
|
||||||
|
$criterias[$v[0]] = $v[1];
|
||||||
|
|
||||||
|
if ($v[0] == 'page' || $v[0] =='p' || $v[0] =='totals' || $v[0] =='list' || $v[0] =='success_msg' || $v[0] == 'action' || $v[0] == 'tree'){
|
||||||
|
//do nothing - these are not SQL parameters
|
||||||
|
}
|
||||||
|
elseif ($v[0] == 'parent_id') {
|
||||||
|
if ($v[1] === 'null' || $v[1] === '') {
|
||||||
|
$clause .= ' AND parent_id IS NULL';
|
||||||
|
} else {
|
||||||
|
$clause .= ' AND parent_id = :parent_id';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
elseif ($v[0] == 'search') {
|
||||||
|
$clause .= ' AND (folder_name LIKE :search OR description LIKE :search)';
|
||||||
|
}
|
||||||
|
else {//create clause
|
||||||
|
$clause .= ' AND '.$v[0].' = :'.$v[0];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if ($whereclause == '' && $clause !=''){
|
||||||
|
$whereclause = 'WHERE '.substr($clause, 4);
|
||||||
|
} else {
|
||||||
|
$whereclause .= $clause;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//Define Query
|
||||||
|
if(isset($criterias['totals']) && $criterias['totals'] ==''){
|
||||||
|
//Request for total rows
|
||||||
|
$sql = 'SELECT count(*) as count FROM marketing_folders '.$whereclause.'';
|
||||||
|
}
|
||||||
|
elseif (isset($criterias['list']) && $criterias['list'] =='') {
|
||||||
|
//SQL for list (no paging)
|
||||||
|
$sql = "SELECT
|
||||||
|
mf.*,
|
||||||
|
(SELECT COUNT(*) FROM marketing_files WHERE folder_id = mf.id) as file_count,
|
||||||
|
(SELECT COUNT(*) FROM marketing_folders WHERE parent_id = mf.id) as subfolder_count,
|
||||||
|
CASE
|
||||||
|
WHEN mf.parent_id IS NOT NULL THEN
|
||||||
|
(SELECT folder_name FROM marketing_folders WHERE id = mf.parent_id)
|
||||||
|
ELSE NULL
|
||||||
|
END as parent_folder_name
|
||||||
|
FROM marketing_folders mf
|
||||||
|
" . $whereclause . "
|
||||||
|
ORDER BY mf.folder_name ASC";
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
//SQL for paging
|
||||||
|
$sql = "SELECT
|
||||||
|
mf.*,
|
||||||
|
(SELECT COUNT(*) FROM marketing_files WHERE folder_id = mf.id) as file_count,
|
||||||
|
(SELECT COUNT(*) FROM marketing_folders WHERE parent_id = mf.id) as subfolder_count,
|
||||||
|
CASE
|
||||||
|
WHEN mf.parent_id IS NOT NULL THEN
|
||||||
|
(SELECT folder_name FROM marketing_folders WHERE id = mf.parent_id)
|
||||||
|
ELSE NULL
|
||||||
|
END as parent_folder_name
|
||||||
|
FROM marketing_folders mf
|
||||||
|
" . $whereclause . "
|
||||||
|
ORDER BY mf.folder_name ASC
|
||||||
|
LIMIT :page,:num_folders";
|
||||||
|
}
|
||||||
|
|
||||||
|
$stmt = $pdo->prepare($sql);
|
||||||
|
|
||||||
|
//Bind to query
|
||||||
|
if (str_contains($whereclause, ':condition')){
|
||||||
|
$stmt->bindValue('condition', $condition, PDO::PARAM_STR);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!empty($criterias)){
|
||||||
|
foreach ($criterias as $key => $value){
|
||||||
|
$key_condition = ':'.$key;
|
||||||
|
if (str_contains($whereclause, $key_condition)){
|
||||||
|
if ($key == 'search'){
|
||||||
|
$search_value = '%'.$value.'%';
|
||||||
|
$stmt->bindValue($key, $search_value, PDO::PARAM_STR);
|
||||||
|
}
|
||||||
|
elseif ($key == 'parent_id' && ($value === 'null' || $value === '')) {
|
||||||
|
// Skip binding for NULL parent_id
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
$stmt->bindValue($key, $value, PDO::PARAM_STR);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//Add paging details
|
||||||
|
if(isset($criterias['totals']) && $criterias['totals']==''){
|
||||||
|
$stmt->execute();
|
||||||
|
$messages = $stmt->fetch();
|
||||||
|
$messages = $messages[0];
|
||||||
|
}
|
||||||
|
elseif(isset($criterias['list']) && $criterias['list']==''){
|
||||||
|
//Execute Query
|
||||||
|
$stmt->execute();
|
||||||
|
//Get results
|
||||||
|
$messages = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
$current_page = isset($criterias['p']) && is_numeric($criterias['p']) ? (int)$criterias['p'] : 1;
|
||||||
|
$stmt->bindValue('page', ($current_page - 1) * $page_rows_folders, PDO::PARAM_INT);
|
||||||
|
$stmt->bindValue('num_folders', $page_rows_folders, PDO::PARAM_INT);
|
||||||
|
|
||||||
|
//Execute Query
|
||||||
|
$stmt->execute();
|
||||||
|
//Get results
|
||||||
|
$messages = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if tree structure is requested
|
||||||
|
if (isset($criterias['tree']) && isset($messages) && is_array($messages)) {
|
||||||
|
// Build hierarchical tree structure
|
||||||
|
$messages = buildFolderTree($messages);
|
||||||
|
}
|
||||||
|
|
||||||
|
//------------------------------------------
|
||||||
|
//JSON_ENCODE
|
||||||
|
//------------------------------------------
|
||||||
|
$messages = json_encode($messages, JSON_UNESCAPED_UNICODE);
|
||||||
|
|
||||||
|
//Send results
|
||||||
|
echo $messages;
|
||||||
115
api/v2/get/marketing_tags.php
Normal file
115
api/v2/get/marketing_tags.php
Normal file
@@ -0,0 +1,115 @@
|
|||||||
|
<?php
|
||||||
|
defined($security_key) or exit;
|
||||||
|
|
||||||
|
//------------------------------------------
|
||||||
|
// Marketing Tags
|
||||||
|
//------------------------------------------
|
||||||
|
|
||||||
|
//Connect to DB
|
||||||
|
$pdo = dbConnect($dbname);
|
||||||
|
|
||||||
|
//SoldTo is empty
|
||||||
|
if (empty($partner->soldto) || $partner->soldto == ''){$soldto_search = '%';} else {$soldto_search = '-%';}
|
||||||
|
|
||||||
|
//default whereclause
|
||||||
|
$whereclause = '';
|
||||||
|
|
||||||
|
// Tags are global, so no account hierarchy filtering
|
||||||
|
// list($whereclause,$condition) = getWhereclauselvl2("",$permission,$partner,'get');
|
||||||
|
|
||||||
|
//NEW ARRAY
|
||||||
|
$criterias = [];
|
||||||
|
$clause = '';
|
||||||
|
|
||||||
|
//Check for $_GET variables and build up clause
|
||||||
|
if(isset($get_content) && $get_content!=''){
|
||||||
|
//GET VARIABLES FROM URL
|
||||||
|
$requests = explode("&", $get_content);
|
||||||
|
//Check for keys and values
|
||||||
|
foreach ($requests as $y){
|
||||||
|
$v = explode("=", $y);
|
||||||
|
//INCLUDE VARIABLES IN ARRAY
|
||||||
|
$criterias[$v[0]] = $v[1];
|
||||||
|
|
||||||
|
if ($v[0] == 'page' || $v[0] =='p' || $v[0] =='totals' || $v[0] =='list' || $v[0] =='success_msg' || $v[0] == 'action'){
|
||||||
|
//do nothing
|
||||||
|
}
|
||||||
|
elseif ($v[0] == 'search') {
|
||||||
|
$clause .= ' AND tag_name LIKE :search';
|
||||||
|
}
|
||||||
|
elseif ($v[0] == 'used_only') {
|
||||||
|
if ($v[1] === 'true') {
|
||||||
|
$clause .= ' AND id IN (SELECT DISTINCT tag_id FROM marketing_file_tags)';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {//create clause
|
||||||
|
$clause .= ' AND '.$v[0].' = :'.$v[0];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if ($whereclause == '' && $clause !=''){
|
||||||
|
$whereclause = 'WHERE '.substr($clause, 4);
|
||||||
|
} else {
|
||||||
|
$whereclause .= $clause;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//Set page
|
||||||
|
$pagina = 1;
|
||||||
|
if(isset($criterias['p']) && $criterias['p'] !='') {
|
||||||
|
$pagina = $criterias['p'];
|
||||||
|
}
|
||||||
|
|
||||||
|
//check for totals call
|
||||||
|
if(isset($criterias['totals'])){
|
||||||
|
$sql = 'SELECT COUNT(*) as found FROM marketing_tags mt '.$whereclause.' ';
|
||||||
|
$stmt = $pdo->prepare($sql);
|
||||||
|
|
||||||
|
// Bind parameters
|
||||||
|
if (!empty($criterias)) {
|
||||||
|
foreach ($criterias as $key => $value) {
|
||||||
|
if ($key !== 'totals' && $key !== 'page' && $key !== 'p' && $key !== 'used_only') {
|
||||||
|
if ($key == 'search') {
|
||||||
|
$stmt->bindValue(':'.$key, '%'.$value.'%');
|
||||||
|
} else {
|
||||||
|
$stmt->bindValue(':'.$key, $value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$stmt->execute();
|
||||||
|
$found = $stmt->fetchColumn();
|
||||||
|
echo $found;
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Main query
|
||||||
|
$sql = "SELECT
|
||||||
|
mt.*,
|
||||||
|
COUNT(mft.file_id) as usage_count
|
||||||
|
FROM marketing_tags mt
|
||||||
|
LEFT JOIN marketing_file_tags mft ON mt.id = mft.tag_id
|
||||||
|
" . $whereclause . "
|
||||||
|
GROUP BY mt.id
|
||||||
|
ORDER BY mt.tag_name ASC";
|
||||||
|
|
||||||
|
$stmt = $pdo->prepare($sql);
|
||||||
|
|
||||||
|
// Bind parameters
|
||||||
|
if (!empty($criterias)) {
|
||||||
|
foreach ($criterias as $key => $value) {
|
||||||
|
if ($key !== 'totals' && $key !== 'page' && $key !== 'p' && $key !== 'used_only') {
|
||||||
|
if ($key == 'search') {
|
||||||
|
$stmt->bindValue(':'.$key, '%'.$value.'%');
|
||||||
|
} else {
|
||||||
|
$stmt->bindValue(':'.$key, $value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$stmt->execute();
|
||||||
|
$marketing_tags = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||||
|
|
||||||
|
// Return result
|
||||||
|
echo json_encode($marketing_tags, JSON_UNESCAPED_UNICODE);
|
||||||
41
api/v2/get/service.php
Normal file
41
api/v2/get/service.php
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
<?php
|
||||||
|
defined($security_key) or exit;
|
||||||
|
|
||||||
|
//------------------------------------------
|
||||||
|
// Application related calls
|
||||||
|
//------------------------------------------
|
||||||
|
$request = explode('/', trim($_SERVER['PATH_INFO'],'/'));
|
||||||
|
$action = $request[2] ?? '';
|
||||||
|
|
||||||
|
|
||||||
|
if ($action == 'init'){
|
||||||
|
include './settings/systemservicetool_init.php';
|
||||||
|
echo json_encode($init);
|
||||||
|
}
|
||||||
|
elseif ($action == 'questions' && (isset($_GET['type']) && $_GET['type'] != '')){
|
||||||
|
|
||||||
|
include './settings/systemservicetool.php';
|
||||||
|
|
||||||
|
//build questions
|
||||||
|
switch ($_GET['type']) {
|
||||||
|
case 'visual':
|
||||||
|
$arrayQuestions = $arrayQuestions_visual;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'final':
|
||||||
|
$arrayQuestions = $arrayQuestions_finalize;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'cartest':
|
||||||
|
include './settings/systemcartest.php';
|
||||||
|
$arrayQuestions = $arrayQuestions_cartest;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
//Return JSON
|
||||||
|
echo json_encode($arrayQuestions);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
http_response_code(400);
|
||||||
|
}
|
||||||
|
|
||||||
|
?>
|
||||||
@@ -62,6 +62,7 @@ if (isset($criterias['sn']) && $criterias['sn'] != ''){
|
|||||||
e.sw_version as current_sw_version,
|
e.sw_version as current_sw_version,
|
||||||
e.hw_version,
|
e.hw_version,
|
||||||
e.sw_version_license,
|
e.sw_version_license,
|
||||||
|
e.sw_version_upgrade,
|
||||||
e.rowID as equipment_rowid
|
e.rowID as equipment_rowid
|
||||||
FROM equipment e
|
FROM equipment e
|
||||||
JOIN products p ON e.productrowid = p.rowID
|
JOIN products p ON e.productrowid = p.rowID
|
||||||
@@ -78,6 +79,7 @@ if (isset($criterias['sn']) && $criterias['sn'] != ''){
|
|||||||
$current_sw_version = $equipment_data['current_sw_version'];
|
$current_sw_version = $equipment_data['current_sw_version'];
|
||||||
$hw_version = $equipment_data['hw_version'];
|
$hw_version = $equipment_data['hw_version'];
|
||||||
$sw_version_license = $equipment_data['sw_version_license'];
|
$sw_version_license = $equipment_data['sw_version_license'];
|
||||||
|
$sw_version_upgrade = $equipment_data['sw_version_upgrade'];
|
||||||
$equipment_rowid = $equipment_data['equipment_rowid'];
|
$equipment_rowid = $equipment_data['equipment_rowid'];
|
||||||
|
|
||||||
if (debug) {
|
if (debug) {
|
||||||
@@ -85,7 +87,8 @@ if (isset($criterias['sn']) && $criterias['sn'] != ''){
|
|||||||
'product_rowid' => $product_rowid,
|
'product_rowid' => $product_rowid,
|
||||||
'productcode' => $productcode,
|
'productcode' => $productcode,
|
||||||
'current_sw_version_raw' => $current_sw_version,
|
'current_sw_version_raw' => $current_sw_version,
|
||||||
'hw_version' => $hw_version
|
'hw_version' => $hw_version,
|
||||||
|
'sw_version_upgrade' => $sw_version_upgrade
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -119,6 +122,77 @@ if (isset($criterias['sn']) && $criterias['sn'] != ''){
|
|||||||
exit;
|
exit;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Check if sw_version_upgrade is set - this overrides normal availability check
|
||||||
|
if (!empty($sw_version_upgrade)) {
|
||||||
|
if (debug) {
|
||||||
|
$debug['sw_version_upgrade_check'] = [
|
||||||
|
'sw_version_upgrade_id' => $sw_version_upgrade,
|
||||||
|
'checking_override' => true
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if this version exists and is active
|
||||||
|
$sql = 'SELECT
|
||||||
|
psv.rowID as version_id,
|
||||||
|
psv.version,
|
||||||
|
psv.name,
|
||||||
|
psv.description,
|
||||||
|
psv.mandatory,
|
||||||
|
psv.latest,
|
||||||
|
psv.hw_version,
|
||||||
|
psv.file_path,
|
||||||
|
psv.status
|
||||||
|
FROM products_software_versions psv
|
||||||
|
WHERE psv.rowID = ?';
|
||||||
|
$stmt = $pdo->prepare($sql);
|
||||||
|
$stmt->execute([$sw_version_upgrade]);
|
||||||
|
$upgrade_version = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||||
|
|
||||||
|
if ($upgrade_version && $upgrade_version['status'] == 1) {
|
||||||
|
// Valid override found - check if different from current version
|
||||||
|
$normalized_upgrade_version = strtolower(ltrim($upgrade_version['version'], '0'));
|
||||||
|
|
||||||
|
if (debug) {
|
||||||
|
$debug['sw_version_upgrade_check']['found_version'] = [
|
||||||
|
'version' => $upgrade_version['version'],
|
||||||
|
'name' => $upgrade_version['name'],
|
||||||
|
'normalized' => $normalized_upgrade_version,
|
||||||
|
'status' => $upgrade_version['status'],
|
||||||
|
'is_different_from_current' => ($current_sw_version != $normalized_upgrade_version)
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($current_sw_version && $normalized_upgrade_version == $current_sw_version) {
|
||||||
|
// Override version is same as current - no upgrade available
|
||||||
|
$software_available = "no";
|
||||||
|
if (debug) {
|
||||||
|
$debug['sw_version_upgrade_check']['decision'] = 'Override version is same as current version';
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Override version is different - upgrade is available
|
||||||
|
$software_available = "yes";
|
||||||
|
if (debug) {
|
||||||
|
$debug['sw_version_upgrade_check']['decision'] = 'Override version is available';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$messages = ["software_available" => $software_available];
|
||||||
|
|
||||||
|
if (debug) {
|
||||||
|
debuglog(json_encode($debug));
|
||||||
|
}
|
||||||
|
|
||||||
|
echo json_encode($messages, JSON_UNESCAPED_UNICODE);
|
||||||
|
exit;
|
||||||
|
} else {
|
||||||
|
// Override version not found or inactive - fall back to standard check
|
||||||
|
if (debug) {
|
||||||
|
$debug['sw_version_upgrade_check']['found_version'] = $upgrade_version ? 'found but inactive' : 'not found';
|
||||||
|
$debug['sw_version_upgrade_check']['decision'] = 'Falling back to standard check';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
//GET ALL ACTIVE SOFTWARE ASSIGNMENTS for this product with matching HW version
|
//GET ALL ACTIVE SOFTWARE ASSIGNMENTS for this product with matching HW version
|
||||||
$sql = 'SELECT
|
$sql = 'SELECT
|
||||||
psv.rowID as version_id,
|
psv.rowID as version_id,
|
||||||
|
|||||||
@@ -61,6 +61,7 @@ if (isset($criterias['sn']) && $criterias['sn'] != ''){
|
|||||||
e.sw_version as current_sw_version,
|
e.sw_version as current_sw_version,
|
||||||
e.hw_version,
|
e.hw_version,
|
||||||
e.sw_version_license,
|
e.sw_version_license,
|
||||||
|
e.sw_version_upgrade,
|
||||||
e.rowID as equipment_rowid
|
e.rowID as equipment_rowid
|
||||||
FROM equipment e
|
FROM equipment e
|
||||||
JOIN products p ON e.productrowid = p.rowID
|
JOIN products p ON e.productrowid = p.rowID
|
||||||
@@ -77,6 +78,7 @@ if (isset($criterias['sn']) && $criterias['sn'] != ''){
|
|||||||
$current_sw_version = $equipment_data['current_sw_version'];
|
$current_sw_version = $equipment_data['current_sw_version'];
|
||||||
$hw_version = $equipment_data['hw_version'];
|
$hw_version = $equipment_data['hw_version'];
|
||||||
$sw_version_license = $equipment_data['sw_version_license'];
|
$sw_version_license = $equipment_data['sw_version_license'];
|
||||||
|
$sw_version_upgrade = $equipment_data['sw_version_upgrade'];
|
||||||
$equipment_rowid = $equipment_data['equipment_rowid'];
|
$equipment_rowid = $equipment_data['equipment_rowid'];
|
||||||
|
|
||||||
if (debug) {
|
if (debug) {
|
||||||
@@ -85,7 +87,8 @@ if (isset($criterias['sn']) && $criterias['sn'] != ''){
|
|||||||
'productcode' => $productcode,
|
'productcode' => $productcode,
|
||||||
'current_sw_version_raw' => $current_sw_version,
|
'current_sw_version_raw' => $current_sw_version,
|
||||||
'hw_version' => $hw_version,
|
'hw_version' => $hw_version,
|
||||||
'sw_version_license' => $sw_version_license
|
'sw_version_license' => $sw_version_license,
|
||||||
|
'sw_version_upgrade' => $sw_version_upgrade
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -119,6 +122,95 @@ if (isset($criterias['sn']) && $criterias['sn'] != ''){
|
|||||||
exit;
|
exit;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Check if sw_version_upgrade is set - this overrides normal availability check
|
||||||
|
if (!empty($sw_version_upgrade)) {
|
||||||
|
if (debug) {
|
||||||
|
$debug['sw_version_upgrade_check'] = [
|
||||||
|
'sw_version_upgrade_id' => $sw_version_upgrade,
|
||||||
|
'checking_override' => true
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if this version exists and is active
|
||||||
|
$sql = 'SELECT
|
||||||
|
psv.rowID as version_id,
|
||||||
|
psv.version,
|
||||||
|
psv.name,
|
||||||
|
psv.description,
|
||||||
|
psv.mandatory,
|
||||||
|
psv.latest,
|
||||||
|
psv.hw_version,
|
||||||
|
psv.file_path,
|
||||||
|
psv.status
|
||||||
|
FROM products_software_versions psv
|
||||||
|
WHERE psv.rowID = ?';
|
||||||
|
$stmt = $pdo->prepare($sql);
|
||||||
|
$stmt->execute([$sw_version_upgrade]);
|
||||||
|
$upgrade_version = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||||
|
|
||||||
|
if ($upgrade_version && $upgrade_version['status'] == 1) {
|
||||||
|
// Valid override found - check if different from current version
|
||||||
|
$normalized_upgrade_version = strtolower(ltrim($upgrade_version['version'], '0'));
|
||||||
|
|
||||||
|
if (debug) {
|
||||||
|
$debug['sw_version_upgrade_check']['found_version'] = [
|
||||||
|
'version' => $upgrade_version['version'],
|
||||||
|
'name' => $upgrade_version['name'],
|
||||||
|
'normalized' => $normalized_upgrade_version,
|
||||||
|
'status' => $upgrade_version['status'],
|
||||||
|
'is_different_from_current' => ($current_sw_version != $normalized_upgrade_version)
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!$current_sw_version || $current_sw_version == '' || $normalized_upgrade_version != $current_sw_version) {
|
||||||
|
// Override version is different from current (or no current) - return only this upgrade
|
||||||
|
$output[] = [
|
||||||
|
"productcode" => $productcode,
|
||||||
|
"name" => $upgrade_version['name'] ?? '',
|
||||||
|
"version" => $upgrade_version['version'],
|
||||||
|
"version_id" => $upgrade_version['version_id'],
|
||||||
|
"description" => $upgrade_version['description'] ?? '',
|
||||||
|
"hw_version" => $upgrade_version['hw_version'] ?? '',
|
||||||
|
"mandatory" => $upgrade_version['mandatory'] ?? '',
|
||||||
|
"latest" => $upgrade_version['latest'] ?? '',
|
||||||
|
"software" => $upgrade_version['file_path'] ?? '',
|
||||||
|
"source" => '',
|
||||||
|
"source_type" => '',
|
||||||
|
"price" => '0.00',
|
||||||
|
"currency" => '',
|
||||||
|
"is_current" => false
|
||||||
|
];
|
||||||
|
|
||||||
|
// Generate download token
|
||||||
|
$download_token = create_download_url_token($criterias['sn'], $upgrade_version['version_id']);
|
||||||
|
$download_url = 'https://'.$_SERVER['SERVER_NAME'].'/api.php/v2/software_download?token='.$download_token;
|
||||||
|
$output[0]['source'] = $download_url;
|
||||||
|
$output[0]['source_type'] = 'token_url';
|
||||||
|
|
||||||
|
if (debug) {
|
||||||
|
$debug['sw_version_upgrade_check']['decision'] = 'Override version returned as only upgrade';
|
||||||
|
$output[0]['_debug'] = $debug;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Override version is same as current - no upgrades
|
||||||
|
if (debug) {
|
||||||
|
$debug['sw_version_upgrade_check']['decision'] = 'Override version is same as current version - no upgrades';
|
||||||
|
$output = ['message' => 'No upgrades available', 'debug' => $debug];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$messages = $output;
|
||||||
|
echo json_encode($messages, JSON_UNESCAPED_UNICODE);
|
||||||
|
exit;
|
||||||
|
} else {
|
||||||
|
// Override version not found or inactive - fall back to standard check
|
||||||
|
if (debug) {
|
||||||
|
$debug['sw_version_upgrade_check']['found_version'] = $upgrade_version ? 'found but inactive' : 'not found';
|
||||||
|
$debug['sw_version_upgrade_check']['decision'] = 'Falling back to standard check';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
//GET ALL ACTIVE SOFTWARE ASSIGNMENTS for this product with matching HW version
|
//GET ALL ACTIVE SOFTWARE ASSIGNMENTS for this product with matching HW version
|
||||||
$sql = 'SELECT
|
$sql = 'SELECT
|
||||||
psv.rowID as version_id,
|
psv.rowID as version_id,
|
||||||
|
|||||||
93
api/v2/post/marketing_delete.php
Normal file
93
api/v2/post/marketing_delete.php
Normal file
@@ -0,0 +1,93 @@
|
|||||||
|
<?php
|
||||||
|
defined($security_key) or exit;
|
||||||
|
|
||||||
|
//------------------------------------------
|
||||||
|
// Marketing Files Delete
|
||||||
|
//------------------------------------------
|
||||||
|
//Connect to DB
|
||||||
|
$pdo = dbConnect($dbname);
|
||||||
|
|
||||||
|
//CONTENT FROM API (POST)
|
||||||
|
$post_content = json_decode($input,true);
|
||||||
|
|
||||||
|
//SoldTo is empty
|
||||||
|
if (empty($partner->soldto) || $partner->soldto == ''){$soldto_search = '%';} else {$soldto_search = '-%';}
|
||||||
|
|
||||||
|
//default whereclause
|
||||||
|
list($whereclause,$condition) = getWhereclauselvl2("",$permission,$partner,'');
|
||||||
|
|
||||||
|
$file_id = $post_content['file_id'] ?? '';
|
||||||
|
|
||||||
|
if (empty($file_id)) {
|
||||||
|
echo json_encode(['error' => 'File ID is required']);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
//QUERY AND VERIFY ALLOWED
|
||||||
|
if (isAllowed('marketing',$profile,$permission,'D') === 1){
|
||||||
|
// Get file information for cleanup
|
||||||
|
$file_sql = 'SELECT * FROM marketing_files WHERE id = ? AND accounthierarchy LIKE ?';
|
||||||
|
$stmt = $pdo->prepare($file_sql);
|
||||||
|
$stmt->execute([$file_id, '%' . $partner->soldto . '%']);
|
||||||
|
$file_info = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||||
|
|
||||||
|
if (!$file_info) {
|
||||||
|
echo json_encode(['error' => 'File not found or access denied']);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
$pdo->beginTransaction();
|
||||||
|
|
||||||
|
// Remove file tags
|
||||||
|
$delete_tags_sql = 'DELETE FROM marketing_file_tags WHERE file_id = ?';
|
||||||
|
$stmt = $pdo->prepare($delete_tags_sql);
|
||||||
|
$stmt->execute([$file_id]);
|
||||||
|
|
||||||
|
// Delete file record
|
||||||
|
$delete_file_sql = 'DELETE FROM marketing_files WHERE id = ? AND accounthierarchy LIKE ?';
|
||||||
|
$stmt = $pdo->prepare($delete_file_sql);
|
||||||
|
$stmt->execute([$file_id, '%' . $partner->soldto . '%']);
|
||||||
|
|
||||||
|
// Delete physical files
|
||||||
|
$base_path = dirname(__FILE__, 4) . "/";
|
||||||
|
$main_file = $base_path . $file_info['file_path'];
|
||||||
|
$thumbnail_file = $file_info['thumbnail_path'] ? $base_path . $file_info['thumbnail_path'] : null;
|
||||||
|
|
||||||
|
$files_deleted = [];
|
||||||
|
$files_failed = [];
|
||||||
|
|
||||||
|
if (file_exists($main_file)) {
|
||||||
|
if (unlink($main_file)) {
|
||||||
|
$files_deleted[] = $file_info['file_path'];
|
||||||
|
} else {
|
||||||
|
$files_failed[] = $file_info['file_path'];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($thumbnail_file && file_exists($thumbnail_file)) {
|
||||||
|
if (unlink($thumbnail_file)) {
|
||||||
|
$files_deleted[] = $file_info['thumbnail_path'];
|
||||||
|
} else {
|
||||||
|
$files_failed[] = $file_info['thumbnail_path'];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$pdo->commit();
|
||||||
|
|
||||||
|
echo json_encode([
|
||||||
|
'success' => true,
|
||||||
|
'message' => 'File deleted successfully',
|
||||||
|
'files_deleted' => $files_deleted,
|
||||||
|
'files_failed' => $files_failed
|
||||||
|
]);
|
||||||
|
|
||||||
|
} catch (Exception $e) {
|
||||||
|
$pdo->rollback();
|
||||||
|
echo json_encode(['error' => 'Failed to delete file: ' . $e->getMessage()]);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
echo json_encode(['error' => 'Insufficient permissions']);
|
||||||
|
}
|
||||||
|
|
||||||
|
?>
|
||||||
105
api/v2/post/marketing_folders.php
Normal file
105
api/v2/post/marketing_folders.php
Normal file
@@ -0,0 +1,105 @@
|
|||||||
|
<?php
|
||||||
|
defined($security_key) or exit;
|
||||||
|
|
||||||
|
//------------------------------------------
|
||||||
|
// Marketing Folders
|
||||||
|
//------------------------------------------
|
||||||
|
//Connect to DB
|
||||||
|
$pdo = dbConnect($dbname);
|
||||||
|
|
||||||
|
//CONTENT FROM API (POST)
|
||||||
|
$post_content = json_decode($input,true);
|
||||||
|
|
||||||
|
//SoldTo is empty
|
||||||
|
if (empty($partner->soldto) || $partner->soldto == ''){$soldto_search = '%';} else {$soldto_search = '-%';}
|
||||||
|
|
||||||
|
//default whereclause
|
||||||
|
list($whereclause,$condition) = getWhereclauselvl2("",$permission,$partner,'');
|
||||||
|
|
||||||
|
//BUILD UP PARTNERHIERARCHY FROM USER
|
||||||
|
$partner_hierarchy = json_encode(array("salesid"=>$partner->salesid,"soldto"=>$partner->soldto), JSON_UNESCAPED_UNICODE);
|
||||||
|
|
||||||
|
$id = $post_content['id'] ?? ''; //check for rowID
|
||||||
|
$command = ($id == '')? 'insert' : 'update'; //IF rowID = empty then INSERT
|
||||||
|
if (isset($post_content['delete'])){$command = 'delete';} //change command to delete
|
||||||
|
$date = date('Y-m-d H:i:s');
|
||||||
|
|
||||||
|
//CREATE EMPTY STRINGS
|
||||||
|
$clause = '';
|
||||||
|
$clause_insert ='';
|
||||||
|
$input_insert = '';
|
||||||
|
|
||||||
|
if ($command == 'update'){
|
||||||
|
$post_content['updatedby'] = $username;
|
||||||
|
$post_content['updated'] = $date;
|
||||||
|
}
|
||||||
|
if ($command == 'insert'){
|
||||||
|
$post_content['createdby'] = $username;
|
||||||
|
$post_content['accounthierarchy'] = $partner_hierarchy;
|
||||||
|
}
|
||||||
|
|
||||||
|
//CREATE NEW ARRAY AND MAP TO CLAUSE
|
||||||
|
if(isset($post_content) && $post_content!=''){
|
||||||
|
foreach ($post_content as $key => $var){
|
||||||
|
if ($key == 'submit' || $key == 'id' || $key == 'delete'){
|
||||||
|
//do nothing
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
// Handle empty parent_id as NULL for foreign key constraint
|
||||||
|
if ($key == 'parent_id' && $var === '') {
|
||||||
|
$var = null;
|
||||||
|
}
|
||||||
|
$criterias[$key] = $var;
|
||||||
|
$clause .= ' , '.$key.' = ?';
|
||||||
|
$clause_insert .= ' , '.$key.'';
|
||||||
|
$input_insert .= ', ?'; // ? for each insert item
|
||||||
|
$execute_input[]= $var; // Build array for input
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//CLEAN UP INPUT
|
||||||
|
$clause = substr($clause, 2); //Clean clause - remove first comma
|
||||||
|
$clause_insert = substr($clause_insert, 2); //Clean clause - remove first comma
|
||||||
|
$input_insert = substr($input_insert, 1); //Clean clause - remove first comma
|
||||||
|
|
||||||
|
//QUERY AND VERIFY ALLOWED
|
||||||
|
if ($command == 'update' && isAllowed('marketing',$profile,$permission,'U') === 1){
|
||||||
|
$sql = 'UPDATE marketing_folders SET '.$clause.' WHERE id = ? '.$whereclause.'';
|
||||||
|
$execute_input[] = $id;
|
||||||
|
$stmt = $pdo->prepare($sql);
|
||||||
|
$stmt->execute($execute_input);
|
||||||
|
echo json_encode(['success' => true, 'message' => 'Folder updated successfully']);
|
||||||
|
}
|
||||||
|
elseif ($command == 'insert' && isAllowed('marketing',$profile,$permission,'C') === 1){
|
||||||
|
$sql = 'INSERT INTO marketing_folders ('.$clause_insert.') VALUES ('.$input_insert.')';
|
||||||
|
$stmt = $pdo->prepare($sql);
|
||||||
|
$stmt->execute($execute_input);
|
||||||
|
$folder_id = $pdo->lastInsertId();
|
||||||
|
echo json_encode(['success' => true, 'rowID' => $folder_id, 'message' => 'Folder created successfully']);
|
||||||
|
}
|
||||||
|
elseif ($command == 'delete' && isAllowed('marketing',$profile,$permission,'D') === 1){
|
||||||
|
// Check if folder has subfolders
|
||||||
|
$subfolder_sql = 'SELECT COUNT(*) as count FROM marketing_folders WHERE parent_id = ? AND accounthierarchy LIKE ?';
|
||||||
|
$stmt = $pdo->prepare($subfolder_sql);
|
||||||
|
$stmt->execute([$id, '%' . $partner->soldto . '%']);
|
||||||
|
$subfolder_count = $stmt->fetch()['count'];
|
||||||
|
|
||||||
|
// Check if folder has files
|
||||||
|
$files_sql = 'SELECT COUNT(*) as count FROM marketing_files WHERE folder_id = ? AND accounthierarchy LIKE ?';
|
||||||
|
$stmt = $pdo->prepare($files_sql);
|
||||||
|
$stmt->execute([$id, '%' . $partner->soldto . '%']);
|
||||||
|
$files_count = $stmt->fetch()['count'];
|
||||||
|
|
||||||
|
if ($subfolder_count > 0 || $files_count > 0) {
|
||||||
|
echo json_encode(['error' => 'Cannot delete folder that contains subfolders or files']);
|
||||||
|
} else {
|
||||||
|
$stmt = $pdo->prepare('DELETE FROM marketing_folders WHERE id = ? '.$whereclause.'');
|
||||||
|
$stmt->execute([ $id ]);
|
||||||
|
echo json_encode(['success' => true, 'message' => 'Folder deleted successfully']);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
echo json_encode(['error' => 'Insufficient permissions or invalid operation']);
|
||||||
|
}
|
||||||
|
|
||||||
|
?>
|
||||||
302
api/v2/post/marketing_upload.php
Normal file
302
api/v2/post/marketing_upload.php
Normal file
@@ -0,0 +1,302 @@
|
|||||||
|
<?php
|
||||||
|
defined($security_key) or exit;
|
||||||
|
|
||||||
|
//------------------------------------------
|
||||||
|
// Marketing Upload
|
||||||
|
//------------------------------------------
|
||||||
|
//Connect to DB
|
||||||
|
$pdo = dbConnect($dbname);
|
||||||
|
|
||||||
|
//SoldTo is empty
|
||||||
|
if (empty($partner->soldto) || $partner->soldto == ''){$soldto_search = '%';} else {$soldto_search = '-%';}
|
||||||
|
|
||||||
|
//default whereclause
|
||||||
|
list($whereclause,$condition) = getWhereclauselvl2("",$permission,$partner,'');
|
||||||
|
|
||||||
|
//BUILD UP PARTNERHIERARCHY FROM USER
|
||||||
|
$partner_hierarchy = $condition;
|
||||||
|
|
||||||
|
//QUERY AND VERIFY ALLOWED
|
||||||
|
if (isAllowed('marketing',$profile,$permission,'C') === 1){
|
||||||
|
if (!isset($_FILES['file'])) {
|
||||||
|
echo json_encode(['success' => false, 'error' => 'No file uploaded']);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
$file = $_FILES['file'];
|
||||||
|
$folder_id = $_POST['folder_id'] ?? '';
|
||||||
|
$tags = isset($_POST['tags']) ? json_decode($_POST['tags'], true) : [];
|
||||||
|
$title = $_POST['title'] ?? pathinfo($file['name'], PATHINFO_FILENAME);
|
||||||
|
|
||||||
|
// Validate file type
|
||||||
|
$allowedTypes = ['jpg', 'jpeg', 'png', 'gif', 'webp', 'pdf', 'doc', 'docx', 'xls', 'xlsx', 'mp4', 'mov', 'avi'];
|
||||||
|
$filename = $file['name'];
|
||||||
|
$ext = strtolower(pathinfo($filename, PATHINFO_EXTENSION));
|
||||||
|
|
||||||
|
if (!in_array($ext, $allowedTypes)) {
|
||||||
|
echo json_encode(['success' => false, 'error' => 'Invalid file type. Allowed: ' . implode(', ', $allowedTypes)]);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
$imageTypes = ['jpg', 'jpeg', 'png', 'gif', 'webp'];
|
||||||
|
$isImage = in_array($ext, $imageTypes);
|
||||||
|
|
||||||
|
// For images over 10MB, automatically compress
|
||||||
|
if ($isImage && $file['size'] > 10000000) {
|
||||||
|
$compressed = compressImage($file['tmp_name'], $ext, 10000000);
|
||||||
|
if ($compressed === false) {
|
||||||
|
echo json_encode(['success' => false, 'error' => 'Failed to compress large image. Please reduce file size manually.']);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
// Update file size after compression
|
||||||
|
$file['size'] = filesize($file['tmp_name']);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Non-images must be under 10MB
|
||||||
|
if (!$isImage && $file['size'] > 10000000) {
|
||||||
|
echo json_encode(['success' => false, 'error' => 'File too large. Maximum size is 10MB.']);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create unique filename
|
||||||
|
$unique_filename = uniqid() . '_' . time() . '.' . $ext;
|
||||||
|
$target_dir = dirname(__FILE__, 4) . "/marketing/uploads/";
|
||||||
|
$target_file = $target_dir . $unique_filename;
|
||||||
|
$logical_path = "marketing/uploads/" . $unique_filename;
|
||||||
|
|
||||||
|
// Ensure upload directory exists
|
||||||
|
if (!file_exists($target_dir)) {
|
||||||
|
mkdir($target_dir, 0755, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (move_uploaded_file($file['tmp_name'], $target_file)) {
|
||||||
|
// Generate thumbnail for images
|
||||||
|
$thumbnail_path = null;
|
||||||
|
if (in_array($ext, ['jpg', 'jpeg', 'png', 'gif', 'webp'])) {
|
||||||
|
$thumb_dir = $target_dir . "thumbs/";
|
||||||
|
if (!file_exists($thumb_dir)) {
|
||||||
|
mkdir($thumb_dir, 0755, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
$thumbnail_file = $thumb_dir . $unique_filename;
|
||||||
|
if (generateThumbnail($target_file, $thumbnail_file, 200, 200)) {
|
||||||
|
$thumbnail_path = "marketing/uploads/thumbs/" . $unique_filename;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Insert into database
|
||||||
|
$insert_sql = 'INSERT INTO `marketing_files` (`title`, `original_filename`, `file_path`, `thumbnail_path`, `file_type`, `file_size`, `folder_id`, `tags`, `createdby`, `accounthierarchy`) VALUES (?,?,?,?,?,?,?,?,?,?)';
|
||||||
|
$stmt = $pdo->prepare($insert_sql);
|
||||||
|
$stmt->execute([
|
||||||
|
$title,
|
||||||
|
$filename,
|
||||||
|
$logical_path,
|
||||||
|
$thumbnail_path,
|
||||||
|
$ext,
|
||||||
|
$file['size'],
|
||||||
|
$folder_id,
|
||||||
|
json_encode($tags),
|
||||||
|
$username,
|
||||||
|
$partner_hierarchy
|
||||||
|
]);
|
||||||
|
|
||||||
|
$file_id = $pdo->lastInsertId();
|
||||||
|
|
||||||
|
// Insert tags into separate table
|
||||||
|
if (!empty($tags)) {
|
||||||
|
$tag_sql = 'INSERT IGNORE INTO `marketing_tags` (`tag_name`) VALUES (?)';
|
||||||
|
$tag_stmt = $pdo->prepare($tag_sql);
|
||||||
|
|
||||||
|
$file_tag_sql = 'INSERT INTO `marketing_file_tags` (`file_id`, `tag_id`) SELECT ?, id FROM marketing_tags WHERE tag_name = ?';
|
||||||
|
$file_tag_stmt = $pdo->prepare($file_tag_sql);
|
||||||
|
|
||||||
|
foreach ($tags as $tag) {
|
||||||
|
$tag_stmt->execute([trim($tag)]);
|
||||||
|
$file_tag_stmt->execute([$file_id, trim($tag)]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
echo json_encode([
|
||||||
|
'success' => true,
|
||||||
|
'file_id' => $file_id,
|
||||||
|
'path' => $logical_path,
|
||||||
|
'thumbnail' => $thumbnail_path,
|
||||||
|
'message' => 'File uploaded successfully'
|
||||||
|
]);
|
||||||
|
|
||||||
|
} else {
|
||||||
|
echo json_encode(['success' => false, 'error' => 'Failed to move uploaded file']);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
echo json_encode(['success' => false, 'error' => 'Insufficient permissions']);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Function to compress large images
|
||||||
|
function compressImage($source, $ext, $maxSize) {
|
||||||
|
$info = @getimagesize($source);
|
||||||
|
if ($info === false) return false;
|
||||||
|
|
||||||
|
$mime = $info['mime'];
|
||||||
|
|
||||||
|
// Load image
|
||||||
|
switch ($mime) {
|
||||||
|
case 'image/jpeg':
|
||||||
|
$image = @imagecreatefromjpeg($source);
|
||||||
|
break;
|
||||||
|
case 'image/png':
|
||||||
|
$image = @imagecreatefrompng($source);
|
||||||
|
break;
|
||||||
|
case 'image/gif':
|
||||||
|
$image = @imagecreatefromgif($source);
|
||||||
|
break;
|
||||||
|
case 'image/webp':
|
||||||
|
$image = @imagecreatefromwebp($source);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($image === false) return false;
|
||||||
|
|
||||||
|
$width = imagesx($image);
|
||||||
|
$height = imagesy($image);
|
||||||
|
|
||||||
|
// Start with 90% quality and reduce dimensions if needed
|
||||||
|
$quality = 90;
|
||||||
|
$scale = 1.0;
|
||||||
|
$tempFile = $source . '.tmp';
|
||||||
|
|
||||||
|
// Try progressive compression
|
||||||
|
while (true) {
|
||||||
|
// Calculate new dimensions
|
||||||
|
$newWidth = (int)($width * $scale);
|
||||||
|
$newHeight = (int)($height * $scale);
|
||||||
|
|
||||||
|
// Create resized image
|
||||||
|
$resized = imagecreatetruecolor($newWidth, $newHeight);
|
||||||
|
|
||||||
|
// Preserve transparency for PNG/GIF
|
||||||
|
if ($mime === 'image/png' || $mime === 'image/gif') {
|
||||||
|
imagealphablending($resized, false);
|
||||||
|
imagesavealpha($resized, true);
|
||||||
|
$transparent = imagecolorallocatealpha($resized, 255, 255, 255, 127);
|
||||||
|
imagefilledrectangle($resized, 0, 0, $newWidth, $newHeight, $transparent);
|
||||||
|
}
|
||||||
|
|
||||||
|
imagecopyresampled($resized, $image, 0, 0, 0, 0, $newWidth, $newHeight, $width, $height);
|
||||||
|
|
||||||
|
// Save with current quality
|
||||||
|
if ($ext === 'jpg' || $ext === 'jpeg') {
|
||||||
|
imagejpeg($resized, $tempFile, $quality);
|
||||||
|
} elseif ($ext === 'png') {
|
||||||
|
// PNG compression level (0-9, where 9 is best compression)
|
||||||
|
$pngQuality = (int)((100 - $quality) / 11);
|
||||||
|
imagepng($resized, $tempFile, $pngQuality);
|
||||||
|
} elseif ($ext === 'webp') {
|
||||||
|
imagewebp($resized, $tempFile, $quality);
|
||||||
|
} else {
|
||||||
|
imagegif($resized, $tempFile);
|
||||||
|
}
|
||||||
|
|
||||||
|
imagedestroy($resized);
|
||||||
|
|
||||||
|
$fileSize = filesize($tempFile);
|
||||||
|
|
||||||
|
// If file is small enough, use it
|
||||||
|
if ($fileSize <= $maxSize) {
|
||||||
|
imagedestroy($image);
|
||||||
|
rename($tempFile, $source);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If we've reduced too much, give up
|
||||||
|
if ($quality < 50 && $scale < 0.5) {
|
||||||
|
imagedestroy($image);
|
||||||
|
@unlink($tempFile);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reduce quality or scale
|
||||||
|
if ($quality > 50) {
|
||||||
|
$quality -= 10;
|
||||||
|
} else {
|
||||||
|
$scale -= 0.1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Function to generate thumbnail
|
||||||
|
function generateThumbnail($source, $destination, $width, $height) {
|
||||||
|
$info = getimagesize($source);
|
||||||
|
if ($info === false) return false;
|
||||||
|
|
||||||
|
$mime = $info['mime'];
|
||||||
|
|
||||||
|
switch ($mime) {
|
||||||
|
case 'image/jpeg':
|
||||||
|
$image = imagecreatefromjpeg($source);
|
||||||
|
break;
|
||||||
|
case 'image/png':
|
||||||
|
$image = imagecreatefrompng($source);
|
||||||
|
break;
|
||||||
|
case 'image/gif':
|
||||||
|
$image = imagecreatefromgif($source);
|
||||||
|
break;
|
||||||
|
case 'image/webp':
|
||||||
|
$image = imagecreatefromwebp($source);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($image === false) return false;
|
||||||
|
|
||||||
|
$original_width = imagesx($image);
|
||||||
|
$original_height = imagesy($image);
|
||||||
|
|
||||||
|
// Calculate aspect ratio
|
||||||
|
$aspect_ratio = $original_width / $original_height;
|
||||||
|
|
||||||
|
if ($width / $height > $aspect_ratio) {
|
||||||
|
$new_width = $height * $aspect_ratio;
|
||||||
|
$new_height = $height;
|
||||||
|
} else {
|
||||||
|
$new_height = $width / $aspect_ratio;
|
||||||
|
$new_width = $width;
|
||||||
|
}
|
||||||
|
|
||||||
|
$thumbnail = imagecreatetruecolor($new_width, $new_height);
|
||||||
|
|
||||||
|
// Preserve transparency
|
||||||
|
imagealphablending($thumbnail, false);
|
||||||
|
imagesavealpha($thumbnail, true);
|
||||||
|
$transparent = imagecolorallocatealpha($thumbnail, 255, 255, 255, 127);
|
||||||
|
imagefilledrectangle($thumbnail, 0, 0, $new_width, $new_height, $transparent);
|
||||||
|
|
||||||
|
imagecopyresampled($thumbnail, $image, 0, 0, 0, 0, $new_width, $new_height, $original_width, $original_height);
|
||||||
|
|
||||||
|
// Save thumbnail
|
||||||
|
switch ($mime) {
|
||||||
|
case 'image/jpeg':
|
||||||
|
$result = imagejpeg($thumbnail, $destination, 85);
|
||||||
|
break;
|
||||||
|
case 'image/png':
|
||||||
|
$result = imagepng($thumbnail, $destination, 8);
|
||||||
|
break;
|
||||||
|
case 'image/gif':
|
||||||
|
$result = imagegif($thumbnail, $destination);
|
||||||
|
break;
|
||||||
|
case 'image/webp':
|
||||||
|
$result = imagewebp($thumbnail, $destination, 85);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
$result = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
imagedestroy($image);
|
||||||
|
imagedestroy($thumbnail);
|
||||||
|
|
||||||
|
return $result;
|
||||||
|
}
|
||||||
|
|
||||||
|
?>
|
||||||
@@ -6,7 +6,7 @@ defined($security_key) or exit;
|
|||||||
//------------------------------------------
|
//------------------------------------------
|
||||||
// Payment Creation (for Software Upgrades)
|
// Payment Creation (for Software Upgrades)
|
||||||
//------------------------------------------
|
//------------------------------------------
|
||||||
// This endpoint creates a Mollie payment and stores transaction data
|
// This endpoint creates a payment (Mollie or PayPal) and stores transaction data
|
||||||
|
|
||||||
//Connect to DB
|
//Connect to DB
|
||||||
$pdo = dbConnect($dbname);
|
$pdo = dbConnect($dbname);
|
||||||
@@ -25,6 +25,8 @@ if (empty($post_content['serial_number']) || empty($post_content['version_id']))
|
|||||||
$serial_number = $post_content['serial_number'];
|
$serial_number = $post_content['serial_number'];
|
||||||
$version_id = $post_content['version_id'];
|
$version_id = $post_content['version_id'];
|
||||||
$user_data = $post_content['user_data'] ?? [];
|
$user_data = $post_content['user_data'] ?? [];
|
||||||
|
// Read payment_provider from top level first, then fallback to user_data
|
||||||
|
$payment_provider = $post_content['payment_provider'] ?? $user_data['payment_provider'] ?? 'mollie';
|
||||||
|
|
||||||
//+++++++++++++++++++++++++++++++++++++++++++++++++++++
|
//+++++++++++++++++++++++++++++++++++++++++++++++++++++
|
||||||
// STEP 1: Get equipment data from serial_number
|
// STEP 1: Get equipment data from serial_number
|
||||||
@@ -137,41 +139,116 @@ if ($final_price <= 0) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
//+++++++++++++++++++++++++++++++++++++++++++++++++++++
|
//+++++++++++++++++++++++++++++++++++++++++++++++++++++
|
||||||
// STEP 6: DEBUG MODE - Log but continue to real Mollie
|
// STEP 6: DEBUG MODE - Log
|
||||||
//+++++++++++++++++++++++++++++++++++++++++++++++++++++
|
//+++++++++++++++++++++++++++++++++++++++++++++++++++++
|
||||||
if (debug) {
|
if (debug) {
|
||||||
debuglog("DEBUG MODE: Creating real Mollie payment for testing");
|
debuglog("DEBUG MODE: Creating $payment_provider payment for testing");
|
||||||
debuglog("DEBUG: Serial Number: $serial_number, Version ID: $version_id, Price: $final_price");
|
debuglog("DEBUG: Serial Number: $serial_number, Version ID: $version_id, Price: $final_price");
|
||||||
}
|
}
|
||||||
|
|
||||||
//+++++++++++++++++++++++++++++++++++++++++++++++++++++
|
//+++++++++++++++++++++++++++++++++++++++++++++++++++++
|
||||||
// STEP 7: Call Mollie API to create payment
|
// STEP 7: Create payment based on provider
|
||||||
//+++++++++++++++++++++++++++++++++++++++++++++++++++++
|
//+++++++++++++++++++++++++++++++++++++++++++++++++++++
|
||||||
try {
|
try {
|
||||||
// Initialize Mollie
|
// Format price (must be string with 2 decimals)
|
||||||
require dirname(__FILE__, 4).'/initialize.php';
|
|
||||||
|
|
||||||
// Format price for Mollie (must be string with 2 decimals)
|
|
||||||
$formatted_price = number_format((float)$final_price, 2, '.', '');
|
$formatted_price = number_format((float)$final_price, 2, '.', '');
|
||||||
|
|
||||||
//+++++++++++++++++++++++++++++++++++++++++++++++++++++
|
//+++++++++++++++++++++++++++++++++++++++++++++++++++++
|
||||||
// STEP 7A: Generate transaction ID BEFORE creating Mollie payment
|
// STEP 7A: Generate transaction ID BEFORE creating payment
|
||||||
//+++++++++++++++++++++++++++++++++++++++++++++++++++++
|
//+++++++++++++++++++++++++++++++++++++++++++++++++++++
|
||||||
// Generate unique transaction ID (same as placeorder.php)
|
|
||||||
$txn_id = strtoupper(uniqid('SC') . substr(md5(mt_rand()), 0, 5));
|
$txn_id = strtoupper(uniqid('SC') . substr(md5(mt_rand()), 0, 5));
|
||||||
|
|
||||||
// Build webhook URL and redirect URL with actual transaction ID
|
// Build URLs
|
||||||
$protocol = 'https';
|
$protocol = 'https';
|
||||||
$hostname = $_SERVER['SERVER_NAME'];
|
$hostname = $_SERVER['SERVER_NAME'];
|
||||||
$path = '/';
|
$path = '/';
|
||||||
$webhook_url = "{$protocol}://{$hostname}{$path}webhook_mollie.php";
|
|
||||||
$redirect_url = "{$protocol}://{$hostname}{$path}?page=softwaretool&payment_return=1&order_id={$txn_id}";
|
$redirect_url = "{$protocol}://{$hostname}{$path}?page=softwaretool&payment_return=1&order_id={$txn_id}";
|
||||||
|
|
||||||
if (debug) {
|
if (debug) {
|
||||||
debuglog("DEBUG: Transaction ID: {$txn_id}");
|
debuglog("DEBUG: Transaction ID: {$txn_id}");
|
||||||
debuglog("DEBUG: redirectUrl being sent to Mollie: " . $redirect_url);
|
debuglog("DEBUG: Redirect URL: " . $redirect_url);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//+++++++++++++++++++++++++++++++++++++++++++++++++++++
|
||||||
|
// Create payment based on selected provider
|
||||||
|
//+++++++++++++++++++++++++++++++++++++++++++++++++++++
|
||||||
|
if ($payment_provider === 'paypal') {
|
||||||
|
//==========================================
|
||||||
|
// PAYPAL PAYMENT CREATION
|
||||||
|
//==========================================
|
||||||
|
$cancel_url = "{$protocol}://{$hostname}{$path}?page=softwaretool&payment_return=cancelled&order_id={$txn_id}";
|
||||||
|
|
||||||
|
// Get PayPal access token
|
||||||
|
$access_token = getPayPalAccessToken();
|
||||||
|
|
||||||
|
// Create PayPal order
|
||||||
|
$order_data = [
|
||||||
|
'intent' => 'CAPTURE',
|
||||||
|
'purchase_units' => [[
|
||||||
|
'custom_id' => $txn_id,
|
||||||
|
'description' => "Software upgrade Order #{$txn_id}",
|
||||||
|
'amount' => [
|
||||||
|
'currency_code' => $final_currency ?: 'EUR',
|
||||||
|
'value' => $formatted_price
|
||||||
|
],
|
||||||
|
'payee' => [
|
||||||
|
'email_address' => email
|
||||||
|
]
|
||||||
|
]],
|
||||||
|
'application_context' => [
|
||||||
|
'return_url' => $redirect_url,
|
||||||
|
'cancel_url' => $cancel_url,
|
||||||
|
'brand_name' => site_name,
|
||||||
|
'user_action' => 'PAY_NOW'
|
||||||
|
]
|
||||||
|
];
|
||||||
|
|
||||||
|
$ch = curl_init(PAYPAL_URL . '/v2/checkout/orders');
|
||||||
|
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
|
||||||
|
curl_setopt($ch, CURLOPT_POST, true);
|
||||||
|
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($order_data));
|
||||||
|
curl_setopt($ch, CURLOPT_HTTPHEADER, [
|
||||||
|
'Content-Type: application/json',
|
||||||
|
'Authorization: Bearer ' . $access_token
|
||||||
|
]);
|
||||||
|
|
||||||
|
$response = curl_exec($ch);
|
||||||
|
$http_code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
||||||
|
curl_close($ch);
|
||||||
|
|
||||||
|
if ($http_code != 200 && $http_code != 201) {
|
||||||
|
debuglog("PayPal API Error: HTTP $http_code - Response: $response");
|
||||||
|
throw new Exception("PayPal order creation failed: HTTP $http_code");
|
||||||
|
}
|
||||||
|
|
||||||
|
$paypal_order = json_decode($response, true);
|
||||||
|
$payment_id = $paypal_order['id'] ?? null;
|
||||||
|
|
||||||
|
// Extract approval URL
|
||||||
|
$checkout_url = '';
|
||||||
|
foreach ($paypal_order['links'] ?? [] as $link) {
|
||||||
|
if ($link['rel'] === 'approve') {
|
||||||
|
$checkout_url = $link['href'];
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!$checkout_url) {
|
||||||
|
throw new Exception("No approval URL received from PayPal");
|
||||||
|
}
|
||||||
|
|
||||||
|
$payment_method_id = 1; // PayPal
|
||||||
|
$payment_metadata = 'paypal_order_id';
|
||||||
|
|
||||||
|
} else {
|
||||||
|
//==========================================
|
||||||
|
// MOLLIE PAYMENT CREATION
|
||||||
|
//==========================================
|
||||||
|
// Initialize Mollie
|
||||||
|
require dirname(__FILE__, 4).'/initialize.php';
|
||||||
|
|
||||||
|
$webhook_url = "{$protocol}://{$hostname}{$path}webhook_mollie.php";
|
||||||
|
|
||||||
// Create payment with Mollie
|
// Create payment with Mollie
|
||||||
$payment = $mollie->payments->create([
|
$payment = $mollie->payments->create([
|
||||||
'amount' => [
|
'amount' => [
|
||||||
@@ -189,15 +266,23 @@ try {
|
|||||||
]
|
]
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$mollie_payment_id = $payment->id;
|
$payment_id = $payment->id;
|
||||||
$checkout_url = $payment->getCheckoutUrl();
|
$checkout_url = $payment->getCheckoutUrl();
|
||||||
|
|
||||||
if (debug) {
|
if (debug) {
|
||||||
debuglog("DEBUG: Mollie payment created successfully");
|
debuglog("DEBUG: Mollie payment created successfully");
|
||||||
debuglog("DEBUG: Payment ID: $mollie_payment_id");
|
debuglog("DEBUG: Payment ID: $payment_id");
|
||||||
debuglog("DEBUG: Redirect URL sent: $redirect_url");
|
debuglog("DEBUG: Redirect URL sent: $redirect_url");
|
||||||
debuglog("DEBUG: Redirect URL from Mollie object: " . $payment->redirectUrl);
|
debuglog("DEBUG: Checkout URL: $checkout_url");
|
||||||
debuglog("DEBUG: Full payment object: " . json_encode($payment));
|
}
|
||||||
|
|
||||||
|
$payment_method_id = 0; // Mollie
|
||||||
|
$payment_metadata = 'mollie_payment_id';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (debug) {
|
||||||
|
debuglog("DEBUG: Payment created via $payment_provider");
|
||||||
|
debuglog("DEBUG: Payment ID: $payment_id");
|
||||||
debuglog("DEBUG: Checkout URL: $checkout_url");
|
debuglog("DEBUG: Checkout URL: $checkout_url");
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -218,7 +303,7 @@ try {
|
|||||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)';
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)';
|
||||||
$stmt = $pdo->prepare($sql);
|
$stmt = $pdo->prepare($sql);
|
||||||
$stmt->execute([
|
$stmt->execute([
|
||||||
$txn_id, // Use generated transaction ID, not Mollie payment ID
|
$txn_id,
|
||||||
$final_price,
|
$final_price,
|
||||||
0, // 0 = pending
|
0, // 0 = pending
|
||||||
$user_data['email'] ?? '',
|
$user_data['email'] ?? '',
|
||||||
@@ -230,7 +315,7 @@ try {
|
|||||||
$user_data['postal'] ?? '',
|
$user_data['postal'] ?? '',
|
||||||
$user_data['country'] ?? '',
|
$user_data['country'] ?? '',
|
||||||
$serial_number,
|
$serial_number,
|
||||||
0, // payment method
|
$payment_method_id, // 0 = Mollie, 1 = PayPal
|
||||||
$partner_product,
|
$partner_product,
|
||||||
date('Y-m-d H:i:s')
|
date('Y-m-d H:i:s')
|
||||||
]);
|
]);
|
||||||
@@ -245,14 +330,14 @@ try {
|
|||||||
'serial_number' => $serial_number,
|
'serial_number' => $serial_number,
|
||||||
'equipment_id' => $equipment_id,
|
'equipment_id' => $equipment_id,
|
||||||
'hw_version' => $hw_version,
|
'hw_version' => $hw_version,
|
||||||
'mollie_payment_id' => $mollie_payment_id // Store Mollie payment ID in options
|
$payment_metadata => $payment_id // Store payment provider ID
|
||||||
], JSON_UNESCAPED_UNICODE);
|
], JSON_UNESCAPED_UNICODE);
|
||||||
|
|
||||||
$sql = 'INSERT INTO transactions_items (txn_id, item_id, item_price, item_quantity, item_options, created)
|
$sql = 'INSERT INTO transactions_items (txn_id, item_id, item_price, item_quantity, item_options, created)
|
||||||
VALUES (?, ?, ?, ?, ?, ?)';
|
VALUES (?, ?, ?, ?, ?, ?)';
|
||||||
$stmt = $pdo->prepare($sql);
|
$stmt = $pdo->prepare($sql);
|
||||||
$stmt->execute([
|
$stmt->execute([
|
||||||
$transaction_id, // Use database transaction ID (not txn_id string, not mollie_payment_id)
|
$transaction_id,
|
||||||
$version_id,
|
$version_id,
|
||||||
$final_price,
|
$final_price,
|
||||||
1,
|
1,
|
||||||
@@ -265,7 +350,7 @@ try {
|
|||||||
//+++++++++++++++++++++++++++++++++++++++++++++++++++++
|
//+++++++++++++++++++++++++++++++++++++++++++++++++++++
|
||||||
$messages = json_encode([
|
$messages = json_encode([
|
||||||
'checkout_url' => $checkout_url,
|
'checkout_url' => $checkout_url,
|
||||||
'payment_id' => $mollie_payment_id
|
'payment_id' => $payment_id
|
||||||
], JSON_UNESCAPED_UNICODE);
|
], JSON_UNESCAPED_UNICODE);
|
||||||
echo $messages;
|
echo $messages;
|
||||||
|
|
||||||
@@ -275,4 +360,27 @@ try {
|
|||||||
exit;
|
exit;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//+++++++++++++++++++++++++++++++++++++++++++++++++++++
|
||||||
|
// Helper function to get PayPal access token
|
||||||
|
//+++++++++++++++++++++++++++++++++++++++++++++++++++++
|
||||||
|
function getPayPalAccessToken() {
|
||||||
|
$ch = curl_init(PAYPAL_URL . '/v1/oauth2/token');
|
||||||
|
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
|
||||||
|
curl_setopt($ch, CURLOPT_POST, true);
|
||||||
|
curl_setopt($ch, CURLOPT_POSTFIELDS, 'grant_type=client_credentials');
|
||||||
|
curl_setopt($ch, CURLOPT_USERPWD, PAYPAL_CLIENT_ID . ':' . PAYPAL_CLIENT_SECRET);
|
||||||
|
curl_setopt($ch, CURLOPT_HTTPHEADER, ['Content-Type: application/x-www-form-urlencoded']);
|
||||||
|
|
||||||
|
$response = curl_exec($ch);
|
||||||
|
$http_code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
||||||
|
curl_close($ch);
|
||||||
|
|
||||||
|
if ($http_code != 200) {
|
||||||
|
throw new Exception("Failed to get PayPal access token: HTTP $http_code");
|
||||||
|
}
|
||||||
|
|
||||||
|
$result = json_decode($response, true);
|
||||||
|
return $result['access_token'] ?? '';
|
||||||
|
}
|
||||||
|
|
||||||
?>
|
?>
|
||||||
|
|||||||
BIN
assets/.DS_Store
vendored
BIN
assets/.DS_Store
vendored
Binary file not shown.
114
assets/database/marketing_install.sql
Normal file
114
assets/database/marketing_install.sql
Normal file
@@ -0,0 +1,114 @@
|
|||||||
|
-- Marketing System Database Tables
|
||||||
|
-- Run this script to create the necessary tables for the marketing file management system
|
||||||
|
--
|
||||||
|
-- Usage: Import this file into your MySQL database or run the commands individually
|
||||||
|
-- Make sure to select the correct database before running these commands
|
||||||
|
|
||||||
|
-- Disable foreign key checks temporarily to avoid constraint errors
|
||||||
|
SET FOREIGN_KEY_CHECKS = 0;
|
||||||
|
|
||||||
|
-- Create marketing_folders table
|
||||||
|
CREATE TABLE IF NOT EXISTS `marketing_folders` (
|
||||||
|
`id` int(11) NOT NULL AUTO_INCREMENT,
|
||||||
|
`folder_name` varchar(255) NOT NULL,
|
||||||
|
`parent_id` int(11) DEFAULT NULL,
|
||||||
|
`description` text DEFAULT NULL,
|
||||||
|
`created` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
`createdby` varchar(100) DEFAULT NULL,
|
||||||
|
`updated` timestamp NULL DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP,
|
||||||
|
`updatedby` varchar(100) DEFAULT NULL,
|
||||||
|
`accounthierarchy` text DEFAULT NULL,
|
||||||
|
PRIMARY KEY (`id`),
|
||||||
|
KEY `parent_id` (`parent_id`),
|
||||||
|
KEY `accounthierarchy_idx` (`accounthierarchy`(100))
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||||
|
|
||||||
|
-- Create marketing_files table
|
||||||
|
CREATE TABLE IF NOT EXISTS `marketing_files` (
|
||||||
|
`id` int(11) NOT NULL AUTO_INCREMENT,
|
||||||
|
`title` varchar(255) NOT NULL,
|
||||||
|
`original_filename` varchar(255) NOT NULL,
|
||||||
|
`file_path` varchar(500) NOT NULL,
|
||||||
|
`thumbnail_path` varchar(500) DEFAULT NULL,
|
||||||
|
`file_type` varchar(10) NOT NULL,
|
||||||
|
`file_size` bigint(20) NOT NULL DEFAULT 0,
|
||||||
|
`folder_id` int(11) DEFAULT NULL,
|
||||||
|
`tags` json DEFAULT NULL,
|
||||||
|
`created` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
`createdby` varchar(100) DEFAULT NULL,
|
||||||
|
`updated` timestamp NULL DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP,
|
||||||
|
`updatedby` varchar(100) DEFAULT NULL,
|
||||||
|
`accounthierarchy` text DEFAULT NULL,
|
||||||
|
PRIMARY KEY (`id`),
|
||||||
|
KEY `folder_id` (`folder_id`),
|
||||||
|
KEY `file_type` (`file_type`),
|
||||||
|
KEY `accounthierarchy_idx` (`accounthierarchy`(100)),
|
||||||
|
KEY `created_idx` (`created`)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||||
|
|
||||||
|
-- Create marketing_tags table
|
||||||
|
CREATE TABLE IF NOT EXISTS `marketing_tags` (
|
||||||
|
`id` int(11) NOT NULL AUTO_INCREMENT,
|
||||||
|
`tag_name` varchar(100) NOT NULL,
|
||||||
|
`created` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
PRIMARY KEY (`id`),
|
||||||
|
UNIQUE KEY `tag_name` (`tag_name`)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||||
|
|
||||||
|
-- Create marketing_file_tags junction table
|
||||||
|
CREATE TABLE IF NOT EXISTS `marketing_file_tags` (
|
||||||
|
`file_id` int(11) NOT NULL,
|
||||||
|
`tag_id` int(11) NOT NULL,
|
||||||
|
PRIMARY KEY (`file_id`, `tag_id`),
|
||||||
|
KEY `file_id` (`file_id`),
|
||||||
|
KEY `tag_id` (`tag_id`)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||||
|
|
||||||
|
-- Add foreign key constraints after all tables are created
|
||||||
|
ALTER TABLE `marketing_folders`
|
||||||
|
ADD CONSTRAINT `fk_marketing_folders_parent`
|
||||||
|
FOREIGN KEY (`parent_id`) REFERENCES `marketing_folders`(`id`) ON DELETE CASCADE;
|
||||||
|
|
||||||
|
ALTER TABLE `marketing_files`
|
||||||
|
ADD CONSTRAINT `fk_marketing_files_folder`
|
||||||
|
FOREIGN KEY (`folder_id`) REFERENCES `marketing_folders`(`id`) ON DELETE SET NULL;
|
||||||
|
|
||||||
|
ALTER TABLE `marketing_file_tags`
|
||||||
|
ADD CONSTRAINT `fk_marketing_file_tags_file`
|
||||||
|
FOREIGN KEY (`file_id`) REFERENCES `marketing_files`(`id`) ON DELETE CASCADE;
|
||||||
|
|
||||||
|
ALTER TABLE `marketing_file_tags`
|
||||||
|
ADD CONSTRAINT `fk_marketing_file_tags_tag`
|
||||||
|
FOREIGN KEY (`tag_id`) REFERENCES `marketing_tags`(`id`) ON DELETE CASCADE;
|
||||||
|
|
||||||
|
-- Re-enable foreign key checks
|
||||||
|
SET FOREIGN_KEY_CHECKS = 1;
|
||||||
|
|
||||||
|
-- Insert some default sample data (optional)
|
||||||
|
-- Uncomment the lines below if you want to start with sample folders and tags
|
||||||
|
|
||||||
|
-- INSERT INTO `marketing_folders` (`folder_name`, `description`, `createdby`) VALUES
|
||||||
|
-- ('Product Brochures', 'Marketing brochures and product information', 'system'),
|
||||||
|
-- ('Technical Specifications', 'Technical documentation and specifications', 'system'),
|
||||||
|
-- ('Images', 'Product images and photos', 'system'),
|
||||||
|
-- ('Videos', 'Product videos and demonstrations', 'system');
|
||||||
|
|
||||||
|
-- INSERT INTO `marketing_tags` (`tag_name`) VALUES
|
||||||
|
-- ('brochure'),
|
||||||
|
-- ('specification'),
|
||||||
|
-- ('manual'),
|
||||||
|
-- ('image'),
|
||||||
|
-- ('video'),
|
||||||
|
-- ('product'),
|
||||||
|
-- ('marketing'),
|
||||||
|
-- ('technical');
|
||||||
|
|
||||||
|
-- Create upload directories (Note: This requires manual creation on file system)
|
||||||
|
-- Create the following directories in your web server:
|
||||||
|
-- - ./marketing/uploads/
|
||||||
|
-- - ./marketing/uploads/thumbs/
|
||||||
|
--
|
||||||
|
-- Linux/macOS commands:
|
||||||
|
-- mkdir -p marketing/uploads/thumbs
|
||||||
|
-- chmod 755 marketing/uploads
|
||||||
|
-- chmod 755 marketing/uploads/thumbs
|
||||||
@@ -1353,6 +1353,47 @@ function ioAPIv2($api_call, $data, $token){
|
|||||||
return $resp;
|
return $resp;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//------------------------------------------
|
||||||
|
// API TO API version 2 File Upload
|
||||||
|
//------------------------------------------
|
||||||
|
function ioAPIv2_FileUpload($api_call, $fileData, $additionalData = [], $token = '') {
|
||||||
|
include dirname(__FILE__,2).'/settings/settings_redirector.php';
|
||||||
|
|
||||||
|
$url = $baseurl . $api_call;
|
||||||
|
|
||||||
|
$curl = curl_init($url);
|
||||||
|
curl_setopt($curl, CURLOPT_URL, $url);
|
||||||
|
curl_setopt($curl, CURLOPT_RETURNTRANSFER, true);
|
||||||
|
curl_setopt($curl, CURLOPT_POST, true);
|
||||||
|
|
||||||
|
// Prepare headers (no Content-Type for multipart uploads)
|
||||||
|
if ($token != '') {
|
||||||
|
$headers = array("Authorization: Bearer $token");
|
||||||
|
} else {
|
||||||
|
$headers = array();
|
||||||
|
}
|
||||||
|
curl_setopt($curl, CURLOPT_HTTPHEADER, $headers);
|
||||||
|
|
||||||
|
// Merge file data with additional data
|
||||||
|
$postData = array_merge($fileData, $additionalData);
|
||||||
|
curl_setopt($curl, CURLOPT_POSTFIELDS, $postData);
|
||||||
|
|
||||||
|
$resp = curl_exec($curl);
|
||||||
|
$http_status = curl_getinfo($curl) ?? '200';
|
||||||
|
curl_close($curl);
|
||||||
|
|
||||||
|
if ($http_status['http_code'] == '403' || $http_status['http_code'] == '400') {
|
||||||
|
$resp = json_encode('NOK');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (debug){
|
||||||
|
$message = $date.';'.$api_call;
|
||||||
|
debuglog($message);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $resp;
|
||||||
|
}
|
||||||
|
|
||||||
//------------------------------------------
|
//------------------------------------------
|
||||||
// DEFINE WHERECLAUSE BASED ON ACCOUNTHIERARCHY ALL
|
// DEFINE WHERECLAUSE BASED ON ACCOUNTHIERARCHY ALL
|
||||||
//------------------------------------------
|
//------------------------------------------
|
||||||
@@ -1491,7 +1532,12 @@ function getProfile($profile, $permission){
|
|||||||
'com_log' => 'U',
|
'com_log' => 'U',
|
||||||
'software_update' => 'R',
|
'software_update' => 'R',
|
||||||
'software_download' => 'R',
|
'software_download' => 'R',
|
||||||
'software_available' => 'R'
|
'software_available' => 'R',
|
||||||
|
'marketing_files' => 'CRUD',
|
||||||
|
'marketing_folders' => 'CRUD',
|
||||||
|
'marketing_tags' => 'CRUD',
|
||||||
|
'marketing_upload' => 'CRUD',
|
||||||
|
'marketing_delete' => 'CRUD'
|
||||||
];
|
];
|
||||||
|
|
||||||
// Group permissions: [granting_page => [collection => allowed_actions_string]]
|
// Group permissions: [granting_page => [collection => allowed_actions_string]]
|
||||||
|
|||||||
BIN
assets/images/.DS_Store
vendored
BIN
assets/images/.DS_Store
vendored
Binary file not shown.
900
assets/marketing.js
Normal file
900
assets/marketing.js
Normal file
@@ -0,0 +1,900 @@
|
|||||||
|
/**
|
||||||
|
* Marketing File Management System
|
||||||
|
* Professional drag-and-drop upload with folder management and tagging
|
||||||
|
*/
|
||||||
|
|
||||||
|
class MarketingFileManager {
|
||||||
|
constructor() {
|
||||||
|
this.currentFolder = '';
|
||||||
|
this.selectedFiles = [];
|
||||||
|
this.uploadQueue = [];
|
||||||
|
this.viewMode = 'grid';
|
||||||
|
this.filters = {
|
||||||
|
search: '',
|
||||||
|
tag: '',
|
||||||
|
fileTypes: []
|
||||||
|
};
|
||||||
|
this.folders = []; // Store folders data
|
||||||
|
this.loadRequestId = 0; // Track the latest load request
|
||||||
|
|
||||||
|
this.init();
|
||||||
|
}
|
||||||
|
|
||||||
|
init() {
|
||||||
|
this.bindEvents();
|
||||||
|
this.loadFolders();
|
||||||
|
this.loadTags();
|
||||||
|
this.loadFiles();
|
||||||
|
this.setupDragAndDrop();
|
||||||
|
}
|
||||||
|
|
||||||
|
bindEvents() {
|
||||||
|
// Upload modal
|
||||||
|
document.getElementById('uploadBtn')?.addEventListener('click', () => {
|
||||||
|
this.showUploadModal();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Create folder modal
|
||||||
|
document.getElementById('createFolderBtn')?.addEventListener('click', () => {
|
||||||
|
this.showFolderModal();
|
||||||
|
});
|
||||||
|
|
||||||
|
// View mode toggle
|
||||||
|
document.getElementById('gridViewBtn')?.addEventListener('click', () => {
|
||||||
|
this.setViewMode('grid');
|
||||||
|
});
|
||||||
|
|
||||||
|
document.getElementById('listViewBtn')?.addEventListener('click', () => {
|
||||||
|
this.setViewMode('list');
|
||||||
|
});
|
||||||
|
|
||||||
|
// Search
|
||||||
|
document.getElementById('searchInput')?.addEventListener('input', (e) => {
|
||||||
|
this.filters.search = e.target.value;
|
||||||
|
this.debounce(this.loadFiles.bind(this), 300)();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Tag filter
|
||||||
|
document.getElementById('tagFilter')?.addEventListener('change', (e) => {
|
||||||
|
this.filters.tag = e.target.value;
|
||||||
|
this.loadFiles();
|
||||||
|
});
|
||||||
|
|
||||||
|
// File type filters
|
||||||
|
document.querySelectorAll('.file-type-filters input[type="checkbox"]').forEach(checkbox => {
|
||||||
|
checkbox.addEventListener('change', () => {
|
||||||
|
this.updateFileTypeFilters();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Modal events
|
||||||
|
this.bindModalEvents();
|
||||||
|
|
||||||
|
// Upload events
|
||||||
|
this.bindUploadEvents();
|
||||||
|
}
|
||||||
|
|
||||||
|
bindModalEvents() {
|
||||||
|
// Close modals
|
||||||
|
document.querySelectorAll('.modal-close, .modal-cancel').forEach(btn => {
|
||||||
|
btn.addEventListener('click', (e) => {
|
||||||
|
this.closeModal(e.target.closest('.modal'));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Create folder
|
||||||
|
document.getElementById('createFolder')?.addEventListener('click', () => {
|
||||||
|
this.createFolder();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Download file
|
||||||
|
document.getElementById('downloadFile')?.addEventListener('click', () => {
|
||||||
|
if (this.selectedFile) {
|
||||||
|
this.downloadFile(this.selectedFile);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Delete file
|
||||||
|
document.getElementById('deleteFile')?.addEventListener('click', () => {
|
||||||
|
if (this.selectedFile) {
|
||||||
|
this.deleteFile(this.selectedFile);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
bindUploadEvents() {
|
||||||
|
const fileInput = document.getElementById('fileInput');
|
||||||
|
const browseBtn = document.getElementById('browseBtn');
|
||||||
|
const startUpload = document.getElementById('startUpload');
|
||||||
|
|
||||||
|
browseBtn?.addEventListener('click', () => {
|
||||||
|
fileInput.click();
|
||||||
|
});
|
||||||
|
|
||||||
|
fileInput?.addEventListener('change', (e) => {
|
||||||
|
this.handleFileSelect(e.target.files);
|
||||||
|
});
|
||||||
|
|
||||||
|
startUpload?.addEventListener('click', () => {
|
||||||
|
this.startUpload();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
setupDragAndDrop() {
|
||||||
|
const uploadArea = document.getElementById('uploadArea');
|
||||||
|
const filesContainer = document.getElementById('filesContainer');
|
||||||
|
|
||||||
|
if (uploadArea) {
|
||||||
|
uploadArea.addEventListener('dragover', this.handleDragOver);
|
||||||
|
uploadArea.addEventListener('drop', (e) => this.handleDrop(e));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (filesContainer) {
|
||||||
|
filesContainer.addEventListener('dragover', this.handleDragOver);
|
||||||
|
filesContainer.addEventListener('drop', (e) => this.handleDrop(e));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
handleDragOver(e) {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
e.currentTarget.classList.add('drag-over');
|
||||||
|
}
|
||||||
|
|
||||||
|
handleDrop(e) {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
e.currentTarget.classList.remove('drag-over');
|
||||||
|
|
||||||
|
const files = e.dataTransfer.files;
|
||||||
|
if (files.length > 0) {
|
||||||
|
this.showUploadModal();
|
||||||
|
this.handleFileSelect(files);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async loadFolders() {
|
||||||
|
try {
|
||||||
|
const response = await fetch('index.php?page=marketing&action=marketing_folders&tree=true', { cache: 'no-store' });
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`HTTP error! status: ${response.status}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const text = await response.text();
|
||||||
|
if (!text || text.trim() === '') {
|
||||||
|
console.warn('Empty response from folders API');
|
||||||
|
this.folders = [];
|
||||||
|
this.renderFolderTree([]);
|
||||||
|
this.populateFolderSelects([]);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = JSON.parse(text);
|
||||||
|
|
||||||
|
this.folders = data || []; // Store the folders data
|
||||||
|
// Always render the folder tree (at minimum shows Root)
|
||||||
|
this.renderFolderTree(this.folders);
|
||||||
|
this.populateFolderSelects(this.folders);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error loading folders:', error);
|
||||||
|
this.folders = [];
|
||||||
|
// Show at least root folder on error
|
||||||
|
this.renderFolderTree([]);
|
||||||
|
this.populateFolderSelects([]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async loadTags() {
|
||||||
|
try {
|
||||||
|
const response = await fetch('index.php?page=marketing&action=marketing_tags&used_only=true', { cache: 'no-store' });
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`HTTP error! status: ${response.status}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const text = await response.text();
|
||||||
|
if (!text || text.trim() === '') {
|
||||||
|
console.warn('Empty response from tags API');
|
||||||
|
this.populateTagFilter([]);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = JSON.parse(text);
|
||||||
|
|
||||||
|
// Always populate tag filter (at minimum shows "All Tags")
|
||||||
|
this.populateTagFilter(data || []);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error loading tags:', error);
|
||||||
|
// Show empty tag filter on error
|
||||||
|
this.populateTagFilter([]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async loadFiles() {
|
||||||
|
const container = document.getElementById('filesContainer');
|
||||||
|
const loading = document.getElementById('loadingIndicator');
|
||||||
|
const emptyState = document.getElementById('emptyState');
|
||||||
|
|
||||||
|
// Increment request ID to invalidate previous requests
|
||||||
|
const requestId = ++this.loadRequestId;
|
||||||
|
|
||||||
|
// Clear container FIRST to prevent showing old files
|
||||||
|
container.innerHTML = '';
|
||||||
|
loading.style.display = 'block';
|
||||||
|
emptyState.style.display = 'none';
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Use proper folder ID (null for root, or the folder ID)
|
||||||
|
const folderId = this.currentFolder ? this.currentFolder : 'null';
|
||||||
|
// Add cache busting to prevent browser caching
|
||||||
|
let url = `index.php?page=marketing&action=marketing_files&folder_id=${folderId}&limit=50&_t=${Date.now()}`;
|
||||||
|
|
||||||
|
if (this.filters.search) {
|
||||||
|
url += `&search=${encodeURIComponent(this.filters.search)}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.filters.tag) {
|
||||||
|
url += `&tag=${encodeURIComponent(this.filters.tag)}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.filters.fileTypes.length > 0) {
|
||||||
|
// API expects individual file_type parameter, so we'll filter client-side for now
|
||||||
|
}
|
||||||
|
|
||||||
|
const response = await fetch(url, { cache: 'no-store' });
|
||||||
|
|
||||||
|
// Ignore response if a newer request was made
|
||||||
|
if (requestId !== this.loadRequestId) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`HTTP error! status: ${response.status}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const text = await response.text();
|
||||||
|
|
||||||
|
if (!text || text.trim() === '') {
|
||||||
|
console.warn('Empty response from files API');
|
||||||
|
emptyState.style.display = 'block';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = JSON.parse(text);
|
||||||
|
|
||||||
|
if (data && data.length > 0) {
|
||||||
|
let files = data;
|
||||||
|
|
||||||
|
// Client-side file type filtering
|
||||||
|
if (this.filters.fileTypes.length > 0) {
|
||||||
|
files = files.filter(file =>
|
||||||
|
this.filters.fileTypes.includes(file.file_type.toLowerCase())
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (files.length === 0) {
|
||||||
|
emptyState.style.display = 'block';
|
||||||
|
} else {
|
||||||
|
this.renderFiles(files);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
emptyState.style.display = 'block';
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error loading files:', error);
|
||||||
|
this.showToast('Error loading files', 'error');
|
||||||
|
} finally {
|
||||||
|
loading.style.display = 'none';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
renderFolderTree(folders, container = null, level = 0) {
|
||||||
|
if (!container) {
|
||||||
|
container = document.getElementById('folderTree');
|
||||||
|
container.innerHTML = '<div class="folder-item root" data-folder=""><i class="fa fa-home"></i> Root</div>';
|
||||||
|
|
||||||
|
// Add click listener to root folder
|
||||||
|
const rootFolder = container.querySelector('.folder-item.root');
|
||||||
|
if (rootFolder) {
|
||||||
|
rootFolder.addEventListener('click', () => {
|
||||||
|
this.selectFolder('');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
folders.forEach(folder => {
|
||||||
|
const folderItem = document.createElement('div');
|
||||||
|
folderItem.className = 'folder-item';
|
||||||
|
folderItem.setAttribute('data-folder', folder.id);
|
||||||
|
folderItem.style.marginLeft = `${level * 20}px`;
|
||||||
|
|
||||||
|
const hasChildren = folder.children && folder.children.length > 0;
|
||||||
|
const expandIcon = hasChildren ? '<i class="fa fa-chevron-right expand-icon"></i>' : '';
|
||||||
|
|
||||||
|
folderItem.innerHTML = `
|
||||||
|
${expandIcon}
|
||||||
|
<i class="fa fa-folder"></i>
|
||||||
|
<span class="folder-name">${this.escapeHtml(folder.folder_name)}</span>
|
||||||
|
<span class="file-count">(${folder.file_count})</span>
|
||||||
|
`;
|
||||||
|
|
||||||
|
folderItem.addEventListener('click', () => {
|
||||||
|
this.selectFolder(folder.id);
|
||||||
|
});
|
||||||
|
|
||||||
|
container.appendChild(folderItem);
|
||||||
|
|
||||||
|
if (hasChildren) {
|
||||||
|
this.renderFolderTree(folder.children, container, level + 1);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
renderFiles(files) {
|
||||||
|
const container = document.getElementById('filesContainer');
|
||||||
|
container.innerHTML = '';
|
||||||
|
|
||||||
|
files.forEach(file => {
|
||||||
|
const fileElement = this.createFileElement(file);
|
||||||
|
container.appendChild(fileElement);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
createFileElement(file) {
|
||||||
|
const fileElement = document.createElement('div');
|
||||||
|
fileElement.className = `file-item ${this.viewMode}-item`;
|
||||||
|
fileElement.setAttribute('data-file-id', file.id);
|
||||||
|
|
||||||
|
const thumbnail = this.getThumbnail(file);
|
||||||
|
const tags = file.tags.map(tag => `<span class="tag">${this.escapeHtml(tag)}</span>`).join('');
|
||||||
|
|
||||||
|
fileElement.innerHTML = `
|
||||||
|
<div class="file-thumbnail">
|
||||||
|
${thumbnail}
|
||||||
|
<div class="file-overlay">
|
||||||
|
<button class="preview-btn" title="Preview">
|
||||||
|
<i class="fa fa-eye"></i>
|
||||||
|
</button>
|
||||||
|
<button class="download-btn" title="Download">
|
||||||
|
<i class="fa fa-download"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="file-info">
|
||||||
|
<div class="file-name" title="${this.escapeHtml(file.original_filename)}">
|
||||||
|
${this.escapeHtml(file.title || file.original_filename)}
|
||||||
|
</div>
|
||||||
|
<div class="file-meta">
|
||||||
|
<span class="file-size">${file.file_size_formatted}</span>
|
||||||
|
<span class="file-type">.${file.file_type.toUpperCase()}</span>
|
||||||
|
<span class="file-date">${this.formatDate(file.created)}</span>
|
||||||
|
</div>
|
||||||
|
<div class="file-tags">
|
||||||
|
${tags}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
// Bind events
|
||||||
|
fileElement.querySelector('.preview-btn').addEventListener('click', () => {
|
||||||
|
this.previewFile(file);
|
||||||
|
});
|
||||||
|
|
||||||
|
fileElement.querySelector('.download-btn').addEventListener('click', () => {
|
||||||
|
this.downloadFile(file);
|
||||||
|
});
|
||||||
|
|
||||||
|
fileElement.addEventListener('dblclick', () => {
|
||||||
|
this.previewFile(file);
|
||||||
|
});
|
||||||
|
|
||||||
|
return fileElement;
|
||||||
|
}
|
||||||
|
|
||||||
|
getThumbnail(file) {
|
||||||
|
const isImage = ['jpg', 'jpeg', 'png', 'gif', 'webp'].includes(file.file_type.toLowerCase());
|
||||||
|
|
||||||
|
if (isImage && file.thumbnail_path) {
|
||||||
|
return `<img src="${file.thumbnail_path}" alt="${this.escapeHtml(file.title)}" class="thumbnail-img">`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// File type icons
|
||||||
|
const iconMap = {
|
||||||
|
pdf: 'fa-file-pdf',
|
||||||
|
doc: 'fa-file-word',
|
||||||
|
docx: 'fa-file-word',
|
||||||
|
xls: 'fa-file-excel',
|
||||||
|
xlsx: 'fa-file-excel',
|
||||||
|
mp4: 'fa-file-video',
|
||||||
|
mov: 'fa-file-video',
|
||||||
|
avi: 'fa-file-video'
|
||||||
|
};
|
||||||
|
|
||||||
|
const iconClass = iconMap[file.file_type.toLowerCase()] || 'fa-file';
|
||||||
|
|
||||||
|
return `<div class="file-icon"><i class="fa ${iconClass}"></i></div>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
showUploadModal() {
|
||||||
|
const modal = document.getElementById('uploadModal');
|
||||||
|
this.showModal(modal);
|
||||||
|
this.populateUploadFolders(this.folders); // Use stored folders data
|
||||||
|
}
|
||||||
|
|
||||||
|
showFolderModal() {
|
||||||
|
const modal = document.getElementById('folderModal');
|
||||||
|
this.showModal(modal);
|
||||||
|
this.populateParentFolders(this.folders); // Use stored folders data
|
||||||
|
}
|
||||||
|
|
||||||
|
showModal(modal) {
|
||||||
|
modal.style.display = 'flex';
|
||||||
|
modal.classList.add('show');
|
||||||
|
document.body.classList.add('modal-open');
|
||||||
|
}
|
||||||
|
|
||||||
|
closeModal(modal) {
|
||||||
|
modal.classList.remove('show');
|
||||||
|
setTimeout(() => {
|
||||||
|
modal.style.display = 'none';
|
||||||
|
document.body.classList.remove('modal-open');
|
||||||
|
}, 300);
|
||||||
|
}
|
||||||
|
|
||||||
|
handleFileSelect(files) {
|
||||||
|
this.uploadQueue = [];
|
||||||
|
|
||||||
|
Array.from(files).forEach(file => {
|
||||||
|
this.uploadQueue.push({
|
||||||
|
file: file,
|
||||||
|
progress: 0,
|
||||||
|
status: 'pending'
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
this.renderUploadQueue();
|
||||||
|
document.getElementById('startUpload').disabled = this.uploadQueue.length === 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
renderUploadQueue() {
|
||||||
|
const container = document.getElementById('uploadQueue');
|
||||||
|
container.innerHTML = '';
|
||||||
|
|
||||||
|
this.uploadQueue.forEach((item, index) => {
|
||||||
|
const queueItem = document.createElement('div');
|
||||||
|
queueItem.className = 'upload-item';
|
||||||
|
queueItem.innerHTML = `
|
||||||
|
<div class="upload-info">
|
||||||
|
<div class="file-name">${this.escapeHtml(item.file.name)}</div>
|
||||||
|
<div class="file-size">${this.formatFileSize(item.file.size)}</div>
|
||||||
|
</div>
|
||||||
|
<div class="upload-progress">
|
||||||
|
<div class="progress-bar">
|
||||||
|
<div class="progress-fill" style="width: ${item.progress}%"></div>
|
||||||
|
</div>
|
||||||
|
<div class="upload-status">${item.status}</div>
|
||||||
|
</div>
|
||||||
|
<button class="remove-btn" data-index="${index}">
|
||||||
|
<i class="fa fa-times"></i>
|
||||||
|
</button>
|
||||||
|
`;
|
||||||
|
|
||||||
|
queueItem.querySelector('.remove-btn').addEventListener('click', () => {
|
||||||
|
this.removeFromQueue(index);
|
||||||
|
});
|
||||||
|
|
||||||
|
container.appendChild(queueItem);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async startUpload() {
|
||||||
|
const folder = document.getElementById('uploadFolder').value;
|
||||||
|
const tags = document.getElementById('uploadTags').value
|
||||||
|
.split(',')
|
||||||
|
.map(tag => tag.trim())
|
||||||
|
.filter(tag => tag.length > 0);
|
||||||
|
|
||||||
|
for (let i = 0; i < this.uploadQueue.length; i++) {
|
||||||
|
const item = this.uploadQueue[i];
|
||||||
|
await this.uploadFile(item, folder, tags, i);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Switch to the uploaded folder if different from current
|
||||||
|
if (folder && folder !== this.currentFolder) {
|
||||||
|
this.currentFolder = folder;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.loadFiles();
|
||||||
|
this.closeModal(document.getElementById('uploadModal'));
|
||||||
|
this.showToast('Files uploaded successfully!', 'success');
|
||||||
|
}
|
||||||
|
|
||||||
|
async uploadFile(item, folderId, tags, index) {
|
||||||
|
const formData = new FormData();
|
||||||
|
formData.append('file', item.file);
|
||||||
|
formData.append('folder_id', folderId);
|
||||||
|
formData.append('tags', JSON.stringify(tags));
|
||||||
|
formData.append('title', item.file.name.replace(/\.[^/.]+$/, ""));
|
||||||
|
|
||||||
|
item.status = 'uploading';
|
||||||
|
this.updateQueueItem(index, item);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch('index.php?page=marketing&action=marketing_upload', {
|
||||||
|
method: 'POST',
|
||||||
|
body: formData,
|
||||||
|
onUploadProgress: (progressEvent) => {
|
||||||
|
item.progress = Math.round((progressEvent.loaded * 100) / progressEvent.total);
|
||||||
|
this.updateQueueItem(index, item);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`HTTP error! status: ${response.status}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const text = await response.text();
|
||||||
|
if (!text || text.trim() === '') {
|
||||||
|
throw new Error('Empty response from upload server');
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = JSON.parse(text);
|
||||||
|
|
||||||
|
if (result.success) {
|
||||||
|
item.status = 'completed';
|
||||||
|
item.progress = 100;
|
||||||
|
} else {
|
||||||
|
throw new Error(result.error || 'Upload failed');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
item.status = 'error';
|
||||||
|
item.error = error.message;
|
||||||
|
this.showToast(error.message, 'error');
|
||||||
|
}
|
||||||
|
|
||||||
|
this.updateQueueItem(index, item);
|
||||||
|
}
|
||||||
|
|
||||||
|
async createFolder() {
|
||||||
|
const folderName = document.getElementById('folderName').value.trim();
|
||||||
|
const parentId = document.getElementById('parentFolder').value;
|
||||||
|
const description = document.getElementById('folderDescription').value.trim();
|
||||||
|
|
||||||
|
if (!folderName) {
|
||||||
|
this.showToast('Folder name is required', 'error');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const formData = new FormData();
|
||||||
|
formData.append('folder_name', folderName);
|
||||||
|
formData.append('parent_id', parentId || '');
|
||||||
|
formData.append('description', description);
|
||||||
|
|
||||||
|
const response = await fetch('index.php?page=marketing&action=marketing_folders', {
|
||||||
|
method: 'POST',
|
||||||
|
body: formData
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`HTTP error! status: ${response.status}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const text = await response.text();
|
||||||
|
if (!text || text.trim() === '') {
|
||||||
|
throw new Error('Empty response from server');
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = JSON.parse(text);
|
||||||
|
|
||||||
|
if (data && (data.success || data.rowID)) {
|
||||||
|
this.closeModal(document.getElementById('folderModal'));
|
||||||
|
this.loadFolders();
|
||||||
|
this.showToast('Folder created successfully!', 'success');
|
||||||
|
} else if (data.error) {
|
||||||
|
throw new Error(data.error);
|
||||||
|
} else {
|
||||||
|
throw new Error('Unexpected response format');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Create folder error:', error);
|
||||||
|
this.showToast(error.message || 'Error creating folder', 'error');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async deleteFile(file) {
|
||||||
|
if (!confirm(`Are you sure you want to delete "${file.title || file.original_filename}"?`)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const formData = new FormData();
|
||||||
|
formData.append('file_id', file.id);
|
||||||
|
|
||||||
|
const response = await fetch('index.php?page=marketing&action=marketing_delete', {
|
||||||
|
method: 'POST',
|
||||||
|
body: formData
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`HTTP error! status: ${response.status}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const text = await response.text();
|
||||||
|
if (!text || text.trim() === '') {
|
||||||
|
throw new Error('Empty response from delete server');
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = JSON.parse(text);
|
||||||
|
|
||||||
|
if (data && (data.success || !data.error)) {
|
||||||
|
this.closeModal(document.getElementById('previewModal'));
|
||||||
|
this.loadFiles();
|
||||||
|
this.showToast('File deleted successfully!', 'success');
|
||||||
|
} else if (data.error) {
|
||||||
|
throw new Error(data.error);
|
||||||
|
} else {
|
||||||
|
throw new Error('Unexpected response format');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
this.showToast(error.message || 'Error deleting file', 'error');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
previewFile(file) {
|
||||||
|
this.selectedFile = file;
|
||||||
|
const modal = document.getElementById('previewModal');
|
||||||
|
const title = document.getElementById('previewTitle');
|
||||||
|
const content = document.getElementById('previewContent');
|
||||||
|
|
||||||
|
title.textContent = file.title || file.original_filename;
|
||||||
|
|
||||||
|
// Generate preview content based on file type
|
||||||
|
if (['jpg', 'jpeg', 'png', 'gif', 'webp'].includes(file.file_type.toLowerCase())) {
|
||||||
|
content.innerHTML = `<img src="${file.file_path}" alt="${this.escapeHtml(file.title)}" style="max-width: 100%; max-height: 500px;">`;
|
||||||
|
} else if (file.file_type.toLowerCase() === 'mp4') {
|
||||||
|
content.innerHTML = `<video controls style="max-width: 100%; max-height: 500px;"><source src="${file.file_path}" type="video/mp4"></video>`;
|
||||||
|
} else {
|
||||||
|
content.innerHTML = `
|
||||||
|
<div class="file-preview-info">
|
||||||
|
<i class="fa ${this.getFileIcon(file.file_type)} fa-4x"></i>
|
||||||
|
<h4>${this.escapeHtml(file.title || file.original_filename)}</h4>
|
||||||
|
<p>File Type: ${file.file_type.toUpperCase()}</p>
|
||||||
|
<p>Size: ${file.file_size_formatted}</p>
|
||||||
|
<p>Created: ${this.formatDate(file.created)}</p>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.showModal(modal);
|
||||||
|
}
|
||||||
|
|
||||||
|
downloadFile(file) {
|
||||||
|
const link = document.createElement('a');
|
||||||
|
link.href = file.file_path;
|
||||||
|
link.download = file.original_filename;
|
||||||
|
document.body.appendChild(link);
|
||||||
|
link.click();
|
||||||
|
document.body.removeChild(link);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Utility methods
|
||||||
|
async apiCall(endpoint, params = {}, method = 'GET') {
|
||||||
|
const url = new URL(`/api.php${endpoint}`, window.location.origin);
|
||||||
|
|
||||||
|
let options = {
|
||||||
|
method: method,
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if (method === 'GET') {
|
||||||
|
Object.keys(params).forEach(key => url.searchParams.append(key, params[key]));
|
||||||
|
} else {
|
||||||
|
options.body = JSON.stringify(params);
|
||||||
|
}
|
||||||
|
|
||||||
|
const response = await fetch(url, options);
|
||||||
|
return await response.json();
|
||||||
|
}
|
||||||
|
|
||||||
|
escapeHtml(text) {
|
||||||
|
const map = {
|
||||||
|
'&': '&',
|
||||||
|
'<': '<',
|
||||||
|
'>': '>',
|
||||||
|
'"': '"',
|
||||||
|
"'": '''
|
||||||
|
};
|
||||||
|
return text ? text.replace(/[&<>"']/g, m => map[m]) : '';
|
||||||
|
}
|
||||||
|
|
||||||
|
formatDate(dateString) {
|
||||||
|
return new Date(dateString).toLocaleDateString();
|
||||||
|
}
|
||||||
|
|
||||||
|
formatFileSize(bytes) {
|
||||||
|
const sizes = ['B', 'KB', 'MB', 'GB'];
|
||||||
|
const i = Math.floor(Math.log(bytes) / Math.log(1024));
|
||||||
|
return Math.round(bytes / Math.pow(1024, i) * 100) / 100 + ' ' + sizes[i];
|
||||||
|
}
|
||||||
|
|
||||||
|
debounce(func, wait) {
|
||||||
|
let timeout;
|
||||||
|
return function executedFunction(...args) {
|
||||||
|
const later = () => {
|
||||||
|
clearTimeout(timeout);
|
||||||
|
func(...args);
|
||||||
|
};
|
||||||
|
clearTimeout(timeout);
|
||||||
|
timeout = setTimeout(later, wait);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
showToast(message, type = 'info') {
|
||||||
|
// Simple toast implementation
|
||||||
|
const toast = document.createElement('div');
|
||||||
|
toast.className = `toast toast-${type}`;
|
||||||
|
toast.textContent = message;
|
||||||
|
|
||||||
|
document.body.appendChild(toast);
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
toast.classList.add('show');
|
||||||
|
}, 100);
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
toast.classList.remove('show');
|
||||||
|
setTimeout(() => document.body.removeChild(toast), 300);
|
||||||
|
}, 3000);
|
||||||
|
}
|
||||||
|
|
||||||
|
setViewMode(mode) {
|
||||||
|
this.viewMode = mode;
|
||||||
|
const container = document.getElementById('filesContainer');
|
||||||
|
|
||||||
|
// Update view mode classes
|
||||||
|
container.className = `files-container ${mode}-view`;
|
||||||
|
|
||||||
|
// Update button states
|
||||||
|
document.querySelectorAll('.view-btn').forEach(btn => btn.classList.remove('active'));
|
||||||
|
document.getElementById(`${mode}ViewBtn`).classList.add('active');
|
||||||
|
|
||||||
|
// Re-render files with new view mode
|
||||||
|
this.loadFiles();
|
||||||
|
}
|
||||||
|
|
||||||
|
selectFolder(folderId) {
|
||||||
|
// Clear current folder selection and files BEFORE setting new folder
|
||||||
|
const container = document.getElementById('filesContainer');
|
||||||
|
container.innerHTML = '';
|
||||||
|
|
||||||
|
// Set new current folder
|
||||||
|
this.currentFolder = folderId;
|
||||||
|
|
||||||
|
// Update UI
|
||||||
|
this.updateBreadcrumb();
|
||||||
|
|
||||||
|
// Update active folder in tree
|
||||||
|
document.querySelectorAll('.folder-item').forEach(item => {
|
||||||
|
item.classList.remove('active');
|
||||||
|
});
|
||||||
|
const selectedFolder = document.querySelector(`[data-folder="${folderId}"]`);
|
||||||
|
if (selectedFolder) {
|
||||||
|
selectedFolder.classList.add('active');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load files for the new folder
|
||||||
|
this.loadFiles();
|
||||||
|
}
|
||||||
|
|
||||||
|
updateBreadcrumb() {
|
||||||
|
// Implement breadcrumb navigation
|
||||||
|
const nav = document.getElementById('breadcrumbNav');
|
||||||
|
// This would build breadcrumb based on current folder path
|
||||||
|
}
|
||||||
|
|
||||||
|
updateFileTypeFilters() {
|
||||||
|
this.filters.fileTypes = [];
|
||||||
|
|
||||||
|
document.querySelectorAll('.file-type-filters input[type="checkbox"]:checked').forEach(checkbox => {
|
||||||
|
const types = checkbox.value.split(',');
|
||||||
|
this.filters.fileTypes.push(...types);
|
||||||
|
});
|
||||||
|
|
||||||
|
this.loadFiles();
|
||||||
|
}
|
||||||
|
|
||||||
|
populateTagFilter(tags) {
|
||||||
|
const select = document.getElementById('tagFilter');
|
||||||
|
select.innerHTML = '<option value="">All Tags</option>';
|
||||||
|
|
||||||
|
tags.forEach(tag => {
|
||||||
|
const option = document.createElement('option');
|
||||||
|
option.value = tag.tag_name;
|
||||||
|
option.textContent = `${tag.tag_name} (${tag.usage_count})`;
|
||||||
|
select.appendChild(option);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
populateFolderSelects(folders) {
|
||||||
|
this.populateUploadFolders(folders);
|
||||||
|
this.populateParentFolders(folders);
|
||||||
|
}
|
||||||
|
|
||||||
|
addFolderOptions(select, folders, level = 0) {
|
||||||
|
folders.forEach(folder => {
|
||||||
|
const option = document.createElement('option');
|
||||||
|
option.value = folder.id;
|
||||||
|
option.textContent = ' '.repeat(level) + folder.folder_name;
|
||||||
|
select.appendChild(option);
|
||||||
|
|
||||||
|
if (folder.children && folder.children.length > 0) {
|
||||||
|
this.addFolderOptions(select, folder.children, level + 1);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
populateUploadFolders(folders = []) {
|
||||||
|
// Populate upload folder select
|
||||||
|
const select = document.getElementById('uploadFolder');
|
||||||
|
if (select) {
|
||||||
|
select.innerHTML = '<option value="">Root Folder</option>';
|
||||||
|
this.addFolderOptions(select, folders);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
populateParentFolders(folders = []) {
|
||||||
|
// Populate parent folder select
|
||||||
|
const select = document.getElementById('parentFolder');
|
||||||
|
if (select) {
|
||||||
|
select.innerHTML = '<option value="">Root Folder</option>';
|
||||||
|
this.addFolderOptions(select, folders);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
getFileIcon(fileType) {
|
||||||
|
const iconMap = {
|
||||||
|
pdf: 'fa-file-pdf',
|
||||||
|
doc: 'fa-file-word',
|
||||||
|
docx: 'fa-file-word',
|
||||||
|
xls: 'fa-file-excel',
|
||||||
|
xlsx: 'fa-file-excel',
|
||||||
|
mp4: 'fa-file-video',
|
||||||
|
mov: 'fa-file-video',
|
||||||
|
avi: 'fa-file-video'
|
||||||
|
};
|
||||||
|
|
||||||
|
return iconMap[fileType.toLowerCase()] || 'fa-file';
|
||||||
|
}
|
||||||
|
|
||||||
|
updateQueueItem(index, item) {
|
||||||
|
const queueItems = document.querySelectorAll('.upload-item');
|
||||||
|
if (queueItems[index]) {
|
||||||
|
const progressFill = queueItems[index].querySelector('.progress-fill');
|
||||||
|
const status = queueItems[index].querySelector('.upload-status');
|
||||||
|
|
||||||
|
progressFill.style.width = `${item.progress}%`;
|
||||||
|
status.textContent = item.status;
|
||||||
|
|
||||||
|
if (item.status === 'error') {
|
||||||
|
queueItems[index].classList.add('error');
|
||||||
|
} else if (item.status === 'completed') {
|
||||||
|
queueItems[index].classList.add('completed');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
removeFromQueue(index) {
|
||||||
|
this.uploadQueue.splice(index, 1);
|
||||||
|
this.renderUploadQueue();
|
||||||
|
document.getElementById('startUpload').disabled = this.uploadQueue.length === 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialize when DOM is ready
|
||||||
|
document.addEventListener('DOMContentLoaded', () => {
|
||||||
|
window.marketingManager = new MarketingFileManager();
|
||||||
|
});
|
||||||
@@ -860,9 +860,9 @@ function showPaymentModal(option) {
|
|||||||
<label style="display: block; margin-bottom: 5px; color: #333; font-weight: 500;">Payment Method *</label>
|
<label style="display: block; margin-bottom: 5px; color: #333; font-weight: 500;">Payment Method *</label>
|
||||||
<select name="payment_method" required style="width: 100%; padding: 10px; border: 1px solid #ddd; border-radius: 4px; font-size: 14px;">
|
<select name="payment_method" required style="width: 100%; padding: 10px; border: 1px solid #ddd; border-radius: 4px; font-size: 14px;">
|
||||||
<option value="">Select payment method</option>
|
<option value="">Select payment method</option>
|
||||||
<option value="credit_card">Credit Card</option>
|
${typeof MOLLIE_ENABLED !== 'undefined' && MOLLIE_ENABLED ? '<option value="credit_card">Credit Card</option>' : ''}
|
||||||
<option value="paypal">PayPal</option>
|
${typeof PAYPAL_ENABLED !== 'undefined' && PAYPAL_ENABLED ? '<option value="paypal">PayPal</option>' : ''}
|
||||||
<option value="bank_transfer">Bank Transfer</option>
|
${typeof PAY_ON_DELIVERY_ENABLED !== 'undefined' && PAY_ON_DELIVERY_ENABLED ? '<option value="bank_transfer">Bank Transfer</option>' : ''}
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -906,6 +906,16 @@ function showPaymentModal(option) {
|
|||||||
document.getElementById("paymentForm").onsubmit = async (e) => {
|
document.getElementById("paymentForm").onsubmit = async (e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
const formData = new FormData(e.target);
|
const formData = new FormData(e.target);
|
||||||
|
const paymentMethod = formData.get("payment_method");
|
||||||
|
|
||||||
|
// Auto-determine payment provider based on payment method
|
||||||
|
let paymentProvider = 'mollie'; // default
|
||||||
|
if (paymentMethod === 'paypal') {
|
||||||
|
paymentProvider = 'paypal';
|
||||||
|
} else if (paymentMethod === 'credit_card' || paymentMethod === 'bank_transfer') {
|
||||||
|
paymentProvider = 'mollie';
|
||||||
|
}
|
||||||
|
|
||||||
const paymentData = {
|
const paymentData = {
|
||||||
name: formData.get("name"),
|
name: formData.get("name"),
|
||||||
email: formData.get("email"),
|
email: formData.get("email"),
|
||||||
@@ -913,7 +923,8 @@ function showPaymentModal(option) {
|
|||||||
city: formData.get("city"),
|
city: formData.get("city"),
|
||||||
postal: formData.get("postal"),
|
postal: formData.get("postal"),
|
||||||
country: formData.get("country"),
|
country: formData.get("country"),
|
||||||
payment_method: formData.get("payment_method"),
|
payment_method: paymentMethod,
|
||||||
|
payment_provider: paymentProvider,
|
||||||
version_id: option.version_id,
|
version_id: option.version_id,
|
||||||
price: price,
|
price: price,
|
||||||
currency: currency
|
currency: currency
|
||||||
@@ -1010,7 +1021,9 @@ async function processPayment(paymentData, option, modal) {
|
|||||||
const paymentRequest = {
|
const paymentRequest = {
|
||||||
serial_number: deviceSerialNumber,
|
serial_number: deviceSerialNumber,
|
||||||
version_id: option.version_id,
|
version_id: option.version_id,
|
||||||
user_data: paymentData // name, email, address only
|
payment_method: paymentData.payment_method,
|
||||||
|
payment_provider: paymentData.payment_provider,
|
||||||
|
user_data: paymentData // name, email, address, etc.
|
||||||
};
|
};
|
||||||
|
|
||||||
// Debug logging
|
// Debug logging
|
||||||
@@ -1018,13 +1031,15 @@ async function processPayment(paymentData, option, modal) {
|
|||||||
console.log("=== DEBUG: Payment Request ===");
|
console.log("=== DEBUG: Payment Request ===");
|
||||||
console.log("Serial Number:", deviceSerialNumber);
|
console.log("Serial Number:", deviceSerialNumber);
|
||||||
console.log("Version ID:", option.version_id);
|
console.log("Version ID:", option.version_id);
|
||||||
|
console.log("Payment Method:", paymentData.payment_method);
|
||||||
|
console.log("Payment Provider:", paymentData.payment_provider);
|
||||||
console.log("User Data:", paymentData);
|
console.log("User Data:", paymentData);
|
||||||
console.log("Request payload:", paymentRequest);
|
console.log("Request payload:", paymentRequest);
|
||||||
}
|
}
|
||||||
|
|
||||||
await logCommunication(`Payment initiated for version ${option.version_id}`, 'sent');
|
await logCommunication(`Payment initiated for version ${option.version_id} via ${paymentData.payment_provider}`, 'sent');
|
||||||
|
|
||||||
// Call payment API to create Mollie payment
|
// Call payment API (handles both Mollie and PayPal)
|
||||||
const response = await fetch(link + "/v2/payment", {
|
const response = await fetch(link + "/v2/payment", {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
headers: {
|
headers: {
|
||||||
@@ -1052,16 +1067,16 @@ async function processPayment(paymentData, option, modal) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (result.checkout_url) {
|
if (result.checkout_url) {
|
||||||
await logCommunication(`Redirecting to Mollie payment: ${result.payment_id}`, 'sent');
|
await logCommunication(`Redirecting to ${paymentData.payment_provider} payment: ${result.payment_id}`, 'sent');
|
||||||
|
|
||||||
if (typeof DEBUG !== 'undefined' && DEBUG) {
|
if (typeof DEBUG !== 'undefined' && DEBUG) {
|
||||||
console.log("DEBUG: Redirecting to Mollie checkout...");
|
console.log(`DEBUG: Redirecting to ${paymentData.payment_provider} checkout...`);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Close modal before redirect
|
// Close modal before redirect
|
||||||
document.body.removeChild(modal);
|
document.body.removeChild(modal);
|
||||||
|
|
||||||
// Redirect to Mollie checkout page
|
// Redirect to payment checkout page
|
||||||
window.location.href = result.checkout_url;
|
window.location.href = result.checkout_url;
|
||||||
} else {
|
} else {
|
||||||
throw new Error(result.error || "No checkout URL received");
|
throw new Error(result.error || "No checkout URL received");
|
||||||
|
|||||||
0
custom/morvalwatches/style/VeLiTi-Logo2.png
Executable file → Normal file
0
custom/morvalwatches/style/VeLiTi-Logo2.png
Executable file → Normal file
|
Before Width: | Height: | Size: 2.7 KiB After Width: | Height: | Size: 2.7 KiB |
@@ -32,10 +32,10 @@ $view_contracts = isAllowed('contracts' ,$_SESSION['profile'],$_SESSION['permis
|
|||||||
$GET_VALUES = urlGETdetails($_GET) ?? '';
|
$GET_VALUES = urlGETdetails($_GET) ?? '';
|
||||||
|
|
||||||
//CALL TO API FOR General information
|
//CALL TO API FOR General information
|
||||||
$api_url = '/v1/equipments/'.$GET_VALUES;
|
$api_url = '/v2/equipments/'.$GET_VALUES;
|
||||||
$responses = ioServer($api_url,'');
|
$responses = ioServer($api_url,'');
|
||||||
//Decode Payload
|
//Decode Payload
|
||||||
if (!empty($responses)){$responses = decode_payload($responses);}else{$responses = null;}
|
if (!empty($responses)){$responses = json_decode($responses);}else{$responses = null;}
|
||||||
$responses = $responses[0];
|
$responses = $responses[0];
|
||||||
|
|
||||||
//CALL TO API FOR RELATED
|
//CALL TO API FOR RELATED
|
||||||
@@ -246,6 +246,46 @@ $view .= '<div class="content-block">
|
|||||||
<td style="width:25%;">'.$equipment_label6.'</td>
|
<td style="width:25%;">'.$equipment_label6.'</td>
|
||||||
<td>'.$responses->sw_version.'</td>
|
<td>'.$responses->sw_version.'</td>
|
||||||
</tr>';
|
</tr>';
|
||||||
|
//Check if license is attached
|
||||||
|
if (!empty($responses->sw_version_license)) {
|
||||||
|
$view .= '<tr>
|
||||||
|
<td style="width:25%;">'.($equipment_license ?? 'License').'</td>
|
||||||
|
<td>'.$responses->sw_version_license;
|
||||||
|
|
||||||
|
// Check if license is active
|
||||||
|
$current_date = date('Y-m-d H:i:s');
|
||||||
|
$is_active = false;
|
||||||
|
|
||||||
|
if (!empty($responses->license_status) && $responses->license_status == 1) {
|
||||||
|
$starts_at = $responses->starts_at ?? null;
|
||||||
|
$expires_at = $responses->expires_at ?? null;
|
||||||
|
|
||||||
|
if ($starts_at && $expires_at) {
|
||||||
|
if ($current_date >= $starts_at && $current_date <= $expires_at) {
|
||||||
|
$is_active = true;
|
||||||
|
}
|
||||||
|
} elseif ($starts_at && !$expires_at) {
|
||||||
|
if ($current_date >= $starts_at) {
|
||||||
|
$is_active = true;
|
||||||
|
}
|
||||||
|
} elseif (!$starts_at && $expires_at) {
|
||||||
|
if ($current_date <= $expires_at) {
|
||||||
|
$is_active = true;
|
||||||
|
}
|
||||||
|
} elseif (!$starts_at && !$expires_at) {
|
||||||
|
$is_active = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($is_active) {
|
||||||
|
$view .= ' / '.$enabled ?? 'Active';
|
||||||
|
} else {
|
||||||
|
$view .= ' / '.$disabled ?? 'Inactive';
|
||||||
|
}
|
||||||
|
|
||||||
|
$view .= '</td>
|
||||||
|
</tr>';
|
||||||
|
}
|
||||||
//SHOW ONLY SW_UPGRADE WHEN SET
|
//SHOW ONLY SW_UPGRADE WHEN SET
|
||||||
if (isset($products_software) && $products_software !=''){
|
if (isset($products_software) && $products_software !=''){
|
||||||
foreach ($products_software as $products_soft){
|
foreach ($products_software as $products_soft){
|
||||||
|
|||||||
@@ -53,26 +53,26 @@ if ($equipment_ID !=''){
|
|||||||
}
|
}
|
||||||
|
|
||||||
//GET PRODUCTS
|
//GET PRODUCTS
|
||||||
$api_url = '/v1/products/list=';
|
$api_url = '/v2/products/list=';
|
||||||
$responses = ioServer($api_url,'');
|
$responses = ioServer($api_url,'');
|
||||||
//Decode Payload
|
//Decode Payload
|
||||||
if (!empty($responses)){$products = decode_payload($responses);}else{$products = null;}
|
if (!empty($responses)){$products = json_decode($responses);}else{$products = null;}
|
||||||
|
|
||||||
if (isset($_GET['equipmentID'])) {
|
if (isset($_GET['equipmentID'])) {
|
||||||
// ID param exists, edit an existing product
|
// ID param exists, edit an existing product
|
||||||
//CALL TO API
|
//CALL TO API
|
||||||
$api_url = '/v1/equipments/equipmentID='.$equipment_ID;
|
$api_url = '/v2/equipments/equipmentID='.$equipment_ID;
|
||||||
$responses = ioServer($api_url,'');
|
$responses = ioServer($api_url,'');
|
||||||
//Decode Payload
|
//Decode Payload
|
||||||
if (!empty($responses)){$responses = decode_payload($responses);}else{$responses = null;}
|
if (!empty($responses)){$responses = json_decode($responses,true);}else{$responses = null;}
|
||||||
|
|
||||||
$equipment = json_decode(json_encode($responses[0]), true);
|
$equipment = $responses[0];
|
||||||
|
|
||||||
//GET PRODUCTS_SOFTWARE
|
//GET PRODUCTS_SOFTWARE_VERSIONS
|
||||||
$api_url = '/v1/products_software/productrowid='.$equipment['productrowid'].'&status=1';
|
$api_url = '/v2/products_software_versions/hw_version='.$equipment['hw_version'].'&status=1';
|
||||||
$products_software = ioServer($api_url,'');
|
$products_software = ioServer($api_url,'');
|
||||||
//Decode Payload
|
//Decode Payload
|
||||||
if (!empty($products_software)){$products_software = decode_payload($products_software);}else{$products_software = null;}
|
if (!empty($products_software)){$products_software = json_decode($products_software);}else{$products_software = null;}
|
||||||
|
|
||||||
//------------------------------------
|
//------------------------------------
|
||||||
//CHECK IF USER IS ALSO CREATOR OF RECORD THEN OVERRIDE UPDATE_ALLOWED
|
//CHECK IF USER IS ALSO CREATOR OF RECORD THEN OVERRIDE UPDATE_ALLOWED
|
||||||
@@ -174,7 +174,7 @@ if (isset($products_software) && $products_software !=''){
|
|||||||
';
|
';
|
||||||
foreach ($products_software as $products_soft ){
|
foreach ($products_software as $products_soft ){
|
||||||
if ($products_soft->hw_version == $equipment['hw_version']){
|
if ($products_soft->hw_version == $equipment['hw_version']){
|
||||||
$product_software_list .= '<option value="'.$products_soft->rowID.'" '.($equipment['sw_version_upgrade']==$products_soft->rowID?' selected':'').'>'.$products_soft->version.' - '.$products_soft->software.'</option>
|
$product_software_list .= '<option value="'.$products_soft->rowID.'" '.($equipment['sw_version_upgrade']==$products_soft->rowID?' selected':'').'>'.$products_soft->description.' ('.$products_soft->version.')</option>
|
||||||
';
|
';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
417
marketing.php
417
marketing.php
@@ -10,107 +10,362 @@ if (debug && debug_id == $_SESSION['id']){
|
|||||||
include_once './assets/functions.php';
|
include_once './assets/functions.php';
|
||||||
include_once './settings/settings_redirector.php';
|
include_once './settings/settings_redirector.php';
|
||||||
|
|
||||||
|
$page = 'marketing';
|
||||||
//Check if allowed
|
//Check if allowed
|
||||||
if (isAllowed('marketing',$_SESSION['profile'],$_SESSION['permission'],'R') === 0){
|
if (isAllowed($page,$_SESSION['profile'],$_SESSION['permission'],'R') === 0){
|
||||||
header('location: index.php');
|
header('location: index.php');
|
||||||
exit;
|
exit;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//PAGE Security
|
||||||
|
$update_allowed = isAllowed($page,$_SESSION['profile'],$_SESSION['permission'],'U');
|
||||||
|
$delete_allowed = isAllowed($page,$_SESSION['profile'],$_SESSION['permission'],'D');
|
||||||
|
$create_allowed = isAllowed($page,$_SESSION['profile'],$_SESSION['permission'],'C');
|
||||||
|
|
||||||
//GET PARAMETERS:
|
//GET PARAMETERS:
|
||||||
$product_group = $_GET['product_group'] ?? '';
|
$current_folder = $_GET['folder'] ?? '';
|
||||||
$product_content = $_GET['product_content'] ?? '';
|
$view_mode = $_GET['view'] ?? 'grid';
|
||||||
|
$search_term = $_GET['search'] ?? '';
|
||||||
|
$tag_filter = $_GET['tag'] ?? '';
|
||||||
|
|
||||||
|
// Handle AJAX API requests
|
||||||
|
if (isset($_GET['action'])) {
|
||||||
|
$action = $_GET['action'];
|
||||||
|
|
||||||
|
// Suppress errors for API responses to avoid HTML output breaking JSON
|
||||||
|
error_reporting(0);
|
||||||
|
ini_set('display_errors', 0);
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Marketing folders
|
||||||
|
if ($action === 'marketing_folders') {
|
||||||
|
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||||
|
// Create folder - use standard format expected by POST API
|
||||||
|
$payload = [
|
||||||
|
'folder_name' => $_POST['folder_name'] ?? '',
|
||||||
|
'parent_id' => $_POST['parent_id'] ?? '',
|
||||||
|
'description' => $_POST['description'] ?? ''
|
||||||
|
// rowID is empty = insert (standard pattern)
|
||||||
|
];
|
||||||
|
$response = ioServer('/v2/marketing_folders', json_encode($payload));
|
||||||
|
} else {
|
||||||
|
// Get folders
|
||||||
|
$get_values = urlGETdetails($_GET) ?? '';
|
||||||
|
$response = ioServer('/v2/marketing_folders/' . $get_values, '');
|
||||||
|
}
|
||||||
|
header('Content-Type: application/json');
|
||||||
|
echo $response;
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Marketing files
|
||||||
|
if ($action === 'marketing_files') {
|
||||||
|
// Filter out 'page', 'action', and cache busting timestamp from GET parameters
|
||||||
|
$filtered_params = $_GET;
|
||||||
|
unset($filtered_params['page']);
|
||||||
|
unset($filtered_params['action']);
|
||||||
|
unset($filtered_params['_t']);
|
||||||
|
|
||||||
|
$get_values = urlGETdetails($filtered_params) ?? '';
|
||||||
|
// API expects path segments, not query string: /v2/marketing_files/params
|
||||||
|
$api_url = '/v2/marketing_files/' . $get_values;
|
||||||
|
$response = ioServer($api_url, '');
|
||||||
|
header('Content-Type: application/json');
|
||||||
|
echo $response;
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Marketing tags
|
||||||
|
if ($action === 'marketing_tags') {
|
||||||
|
// Filter out 'page' and 'action' from GET parameters
|
||||||
|
$get_values = urlGETdetails($_GET) ?? '';
|
||||||
|
$response = ioServer('/v2/marketing_tags?' . $get_values, '');
|
||||||
|
header('Content-Type: application/json');
|
||||||
|
echo $response;
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Marketing upload
|
||||||
|
if ($action === 'marketing_upload' && $_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||||
|
if (isset($_FILES['file']) && $_FILES['file']['error'] === UPLOAD_ERR_OK && $_FILES['file']['size'] > 0) {
|
||||||
|
// Use the uploaded file's temp path directly
|
||||||
|
$temp_path = $_FILES['file']['tmp_name'];
|
||||||
|
|
||||||
|
// Get actual MIME type from file content (more secure than trusting browser)
|
||||||
|
$actual_mime_type = mime_content_type($temp_path);
|
||||||
|
|
||||||
|
// Sanitize filename - remove path info and dangerous characters
|
||||||
|
$safe_filename = basename($_FILES['file']['name']);
|
||||||
|
$safe_filename = preg_replace('/[^a-zA-Z0-9._-]/', '_', $safe_filename);
|
||||||
|
|
||||||
|
$fileData = [
|
||||||
|
'file' => new CURLFile($temp_path, $actual_mime_type, $safe_filename)
|
||||||
|
];
|
||||||
|
|
||||||
|
$additionalData = $_POST; // Include any additional POST data
|
||||||
|
|
||||||
|
$token = createCommunicationToken($_SESSION['userkey']);
|
||||||
|
|
||||||
|
$response = ioAPIv2_FileUpload('/v2/marketing_upload/', $fileData, $additionalData, $token);
|
||||||
|
|
||||||
|
// No need to unlink since we didn't move the file
|
||||||
|
} else {
|
||||||
|
$response = json_encode(['error' => 'No file uploaded or upload error']);
|
||||||
|
}
|
||||||
|
header('Content-Type: application/json');
|
||||||
|
echo $response;
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Marketing delete
|
||||||
|
if ($action === 'marketing_delete' && $_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||||
|
$payload = ['file_id' => $_POST['file_id'] ?? ''];
|
||||||
|
$response = ioServer('/v2/marketing_delete', json_encode($payload));
|
||||||
|
header('Content-Type: application/json');
|
||||||
|
echo $response;
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
} catch (Exception $e) {
|
||||||
|
header('Content-Type: application/json');
|
||||||
|
http_response_code(500);
|
||||||
|
echo json_encode(['error' => $e->getMessage()]);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
template_header('Marketing', 'marketing');
|
template_header('Marketing', 'marketing');
|
||||||
echo '
|
?>
|
||||||
|
|
||||||
|
<link rel="stylesheet" href="./style/marketing.css">
|
||||||
|
|
||||||
<div class="content-title">
|
<div class="content-title">
|
||||||
<div class="title">
|
<div class="title">
|
||||||
<i class="fa-solid fa-house"></i>
|
<i class="fa-solid fa-rectangle-ad"></i>
|
||||||
<div class="txt">
|
<div class="txt">
|
||||||
<h2>'.$marketing_h2.'</h2>
|
<h2><?php echo $marketing_h2; ?></h2>
|
||||||
<p>'.$marketing_p.'</p>
|
<p><?php echo $marketing_p; ?></p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="products content-wrapper">
|
|
||||||
<div style="display: flex;align-items: center;align-content: center;flex-wrap: nowrap;flex-direction: column;">
|
<!-- Marketing File Management Interface -->
|
||||||
<div class="">';
|
<div class="marketing-container">
|
||||||
foreach ($marketing_structure as $marketing => $folders){
|
|
||||||
$style = '';
|
<!-- Toolbar -->
|
||||||
if (!empty($product_group) && $product_group !== $marketing) {
|
<div class="marketing-toolbar">
|
||||||
$style = ' style="opacity: 0.5; color: #999; background-color: #f5f5f5;"';
|
<div class="toolbar-left">
|
||||||
} elseif (!empty($product_group) && $product_group === $marketing) {
|
<?php if ($create_allowed === 1): ?>
|
||||||
$style = ' style="background-color: #007cba; color: white;"';
|
<button id="uploadBtn" class="btn btn-primary">
|
||||||
}
|
<i class="fa fa-upload"></i>
|
||||||
echo '<a href="index.php?page=marketing&product_group='.$marketing.'" class="btn"'.$style.'>'.$marketing.'</a>';
|
</button>
|
||||||
}
|
<button id="createFolderBtn" class="btn btn-secondary">
|
||||||
echo'
|
<i class="fa fa-folder-plus"></i>
|
||||||
|
</button>
|
||||||
|
<?php endif; ?>
|
||||||
</div>
|
</div>
|
||||||
<div class="">';
|
|
||||||
// Only show folders if a product group is selected
|
|
||||||
if (!empty($product_group) && isset($marketing_structure[$product_group])) {
|
<div class="toolbar-right">
|
||||||
foreach($marketing_structure[$product_group] as $folder){
|
<!-- Search and Filters -->
|
||||||
echo '<a href="index.php?page=marketing&product_group='.$product_group.'&product_content='.$folder.'" class="btn"> <img src="./assets/images/folder3.png" width="15" height="15" alt=""> '.$folder.'</a>';
|
<div class="search-container">
|
||||||
}
|
<input type="text" id="searchInput" class="search-input" placeholder="Search files..." value="<?php echo htmlspecialchars($search_term); ?>">
|
||||||
}
|
<i class="fa fa-search search-icon"></i>
|
||||||
echo '
|
|
||||||
</div>
|
</div>
|
||||||
</div>';
|
|
||||||
|
|
||||||
|
<select id="tagFilter" class="filter-select">
|
||||||
|
<option value="">All Tags</option>
|
||||||
|
</select>
|
||||||
|
|
||||||
if (isset($product_group) && $product_group !='' && isset($product_content) && $product_content !=''){
|
<div class="view-toggle">
|
||||||
|
<button id="gridViewBtn" class="view-btn <?php echo $view_mode === 'grid' ? 'active' : ''; ?>">
|
||||||
echo '
|
<i class="fa fa-th-large"></i>
|
||||||
<div class="content-block">
|
</button>
|
||||||
<div class="products-wrapper">';
|
<button id="listViewBtn" class="view-btn <?php echo $view_mode === 'list' ? 'active' : ''; ?>">
|
||||||
$dir_name = $main_marketing_dir.$product_group.'/'.$product_content;
|
<i class="fa fa-list"></i>
|
||||||
|
</button>
|
||||||
$files = array_diff(scandir($dir_name), array('.', '..'));
|
</div>
|
||||||
echo'';
|
|
||||||
|
|
||||||
foreach ($files as $file) {
|
|
||||||
$filetype = strtolower(pathinfo($file,PATHINFO_EXTENSION));
|
|
||||||
|
|
||||||
if ( $filetype != '' && $filetype != 'ds_store'){
|
|
||||||
echo '
|
|
||||||
<div class="product">
|
|
||||||
<a href="'.$dir_name.'/'.$file.'" class="product">
|
|
||||||
';
|
|
||||||
if ( $filetype == "jpg" || $filetype == "png" || $filetype == "jpeg" || $filetype == "gif" || $filetype == "png"){
|
|
||||||
echo'
|
|
||||||
<img src="'.$dir_name.'/Thumb/'.$file.'" width="200" height="200" alt=""/> </a>
|
|
||||||
';
|
|
||||||
}
|
|
||||||
if ($filetype == "doc" || $filetype == "docx" || $filetype == "xls"|| $filetype == "xlsx"){
|
|
||||||
echo'
|
|
||||||
<img src="./assets/images/brochure.png" width="200" height="200" alt=""> </a>
|
|
||||||
';
|
|
||||||
}
|
|
||||||
if ( $filetype == "pdf"){
|
|
||||||
echo'
|
|
||||||
<img src="./assets/images/download-pdf.png" width="200" height="200" alt="'.ucfirst(substr($file, 0, strpos($file, "."))).'"></a>
|
|
||||||
<span class="name">'.ucfirst(substr(substr($file, 0, strpos($file, ".")),0 ,25)).'</span>
|
|
||||||
';
|
|
||||||
}
|
|
||||||
if ( $filetype == "mp4"){
|
|
||||||
echo'
|
|
||||||
<video width="200" height="200" controls>
|
|
||||||
<source src="'.$dir_name.'/'.$file.'" type="video/mp4">
|
|
||||||
Your browser does not support the video tag.
|
|
||||||
</video> </a>
|
|
||||||
';
|
|
||||||
}
|
|
||||||
|
|
||||||
echo'
|
|
||||||
<button class="btn"><a href="'.$dir_name.'/'.$file.'" style="text-decoration: none;color: #ffff;"download="">Download</a></button>
|
|
||||||
</div>';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
echo '</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
';
|
|
||||||
}
|
|
||||||
|
|
||||||
|
<!-- Content Area -->
|
||||||
|
<div class="marketing-content">
|
||||||
|
|
||||||
|
<!-- Sidebar -->
|
||||||
|
<div class="marketing-sidebar">
|
||||||
|
<div class="sidebar-section">
|
||||||
|
<h3>Folders</h3>
|
||||||
|
<div id="folderTree" class="folder-tree">
|
||||||
|
<!-- Folder tree will be loaded here -->
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="sidebar-section">
|
||||||
|
<h3>File Types</h3>
|
||||||
|
<div class="file-type-filters">
|
||||||
|
<div class="filter-item">
|
||||||
|
<input type="checkbox" id="filterImages" value="jpg,jpeg,png,gif,webp">
|
||||||
|
<label for="filterImages">
|
||||||
|
<i class="fa fa-image"></i> Images
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<div class="filter-item">
|
||||||
|
<input type="checkbox" id="filterDocuments" value="pdf,doc,docx">
|
||||||
|
<label for="filterDocuments">
|
||||||
|
<i class="fa fa-file-text"></i> Documents
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<div class="filter-item">
|
||||||
|
<input type="checkbox" id="filterSpreadsheets" value="xls,xlsx">
|
||||||
|
<label for="filterSpreadsheets">
|
||||||
|
<i class="fa fa-file-excel"></i> Spreadsheets
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<div class="filter-item">
|
||||||
|
<input type="checkbox" id="filterVideos" value="mp4,mov,avi">
|
||||||
|
<label for="filterVideos">
|
||||||
|
<i class="fa fa-file-video"></i> Videos
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Main Content -->
|
||||||
|
<div class="marketing-main">
|
||||||
|
|
||||||
|
<!-- Files Grid -->
|
||||||
|
<div id="filesContainer" class="files-container <?php echo $view_mode; ?>-view">
|
||||||
|
<!-- Files will be loaded here -->
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Loading Indicator -->
|
||||||
|
<div id="loadingIndicator" class="loading-indicator">
|
||||||
|
<i class="fa fa-spinner fa-spin"></i>
|
||||||
|
<span>Loading files...</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Empty State -->
|
||||||
|
<div id="emptyState" class="empty-state" style="display: none;">
|
||||||
|
<i class="fa fa-folder-open"></i>
|
||||||
|
<h3>No files found</h3>
|
||||||
|
<p>Upload your first file to get started</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Upload Modal -->
|
||||||
|
<?php if ($create_allowed === 1): ?>
|
||||||
|
<div id="uploadModal" class="modal">
|
||||||
|
<div class="modal-content">
|
||||||
|
<div class="modal-header">
|
||||||
|
<h3>Upload Files</h3>
|
||||||
|
<button class="modal-close">×</button>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body">
|
||||||
|
<div class="upload-area" id="uploadArea">
|
||||||
|
<div class="upload-icon">
|
||||||
|
<i class="fa fa-cloud-upload"></i>
|
||||||
|
</div>
|
||||||
|
<h4>Drag & Drop Files Here</h4>
|
||||||
|
<p>or <button class="browse-btn" id="browseBtn">Browse Files</button></p>
|
||||||
|
<input type="file" id="fileInput" multiple accept="image/*,.pdf,.doc,.docx,.xls,.xlsx,.mp4,.mov,.avi">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="uploadQueue" class="upload-queue">
|
||||||
|
<!-- Upload queue items will appear here -->
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="upload-options">
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="uploadFolder">Upload to Folder:</label>
|
||||||
|
<select id="uploadFolder" class="form-control">
|
||||||
|
<option value="">Root Folder</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="uploadTags">Tags (comma separated):</label>
|
||||||
|
<input type="text" id="uploadTags" class="form-control" placeholder="marketing, brochure, product">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer">
|
||||||
|
<button id="startUpload" class="btn btn-primary" disabled>
|
||||||
|
<i class="fa fa-upload"></i>
|
||||||
|
</button>
|
||||||
|
<button class="modal-cancel btn btn-secondary">X</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<?php endif; ?>
|
||||||
|
|
||||||
|
<!-- Create Folder Modal -->
|
||||||
|
<?php if ($create_allowed === 1): ?>
|
||||||
|
<div id="folderModal" class="modal">
|
||||||
|
<div class="modal-content">
|
||||||
|
<div class="modal-header">
|
||||||
|
<h3>Create New Folder</h3>
|
||||||
|
<button class="modal-close">×</button>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body">
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="folderName">Folder Name:</label>
|
||||||
|
<input type="text" id="folderName" class="form-control" placeholder="Enter folder name">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="parentFolder">Parent Folder:</label>
|
||||||
|
<select id="parentFolder" class="form-control">
|
||||||
|
<option value="">Root Folder</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="folderDescription">Description:</label>
|
||||||
|
<textarea id="folderDescription" class="form-control" rows="3" placeholder="Optional description"></textarea>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer">
|
||||||
|
<button id="createFolder" class="btn btn-primary">
|
||||||
|
<i class="fa fa-folder-plus"></i>
|
||||||
|
</button>
|
||||||
|
<button class="modal-cancel btn btn-secondary">X</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<?php endif; ?>
|
||||||
|
|
||||||
|
<!-- File Preview Modal -->
|
||||||
|
<div id="previewModal" class="modal preview-modal">
|
||||||
|
<div class="modal-content">
|
||||||
|
<div class="modal-header">
|
||||||
|
<h3 id="previewTitle">File Preview</h3>
|
||||||
|
<button class="modal-close">×</button>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body">
|
||||||
|
<div id="previewContent" class="preview-content">
|
||||||
|
<!-- Preview content will be loaded here -->
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer">
|
||||||
|
<button id="downloadFile" class="btn btn-primary">
|
||||||
|
<i class="fa fa-download"></i>
|
||||||
|
</button>
|
||||||
|
<?php if ($delete_allowed === 1): ?>
|
||||||
|
<button id="deleteFile" class="btn btn-danger">
|
||||||
|
<i class="fa fa-trash"></i>
|
||||||
|
</button>
|
||||||
|
<?php endif; ?>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script src="./assets/marketing.js"></script>
|
||||||
|
|
||||||
|
<?php
|
||||||
template_footer();
|
template_footer();
|
||||||
?>
|
?>
|
||||||
43
order.php
43
order.php
@@ -42,6 +42,23 @@ $order = ioServer($api_url,'');
|
|||||||
//Decode Payload
|
//Decode Payload
|
||||||
if (!empty($order)){$order = json_decode($order,true);}else{$order = null;}
|
if (!empty($order)){$order = json_decode($order,true);}else{$order = null;}
|
||||||
|
|
||||||
|
//HANDLE STATUS CHANGE
|
||||||
|
if ($update_allowed === 1){
|
||||||
|
if (isset($_POST['payment_status'])) {
|
||||||
|
//GET ALL POST DATA
|
||||||
|
$data = json_encode($_POST, JSON_UNESCAPED_UNICODE);
|
||||||
|
|
||||||
|
//API call
|
||||||
|
$responses = ioServer('/v2/transactions', $data);
|
||||||
|
if ($responses === 'NOK'){
|
||||||
|
|
||||||
|
} else {
|
||||||
|
header('Location: index.php?page=order&id='.$_POST['id'].'&success_msg=2');
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Handle success messages
|
// Handle success messages
|
||||||
if (isset($_GET['success_msg'])) {
|
if (isset($_GET['success_msg'])) {
|
||||||
if ($_GET['success_msg'] == 1) {
|
if ($_GET['success_msg'] == 1) {
|
||||||
@@ -112,11 +129,33 @@ $view .='
|
|||||||
<div class="order-detail">
|
<div class="order-detail">
|
||||||
<h3>Payment Method</h3>
|
<h3>Payment Method</h3>
|
||||||
<p>' . (${$payment_method} ?? $order['header']['payment_method'] ). '</p>
|
<p>' . (${$payment_method} ?? $order['header']['payment_method'] ). '</p>
|
||||||
</div>
|
</div>';
|
||||||
|
|
||||||
|
//STATUS CHANGE FORM
|
||||||
|
if ($update_allowed === 1){
|
||||||
|
$view .='
|
||||||
|
<div class="order-detail">
|
||||||
|
<h3>Payment Status</h3>
|
||||||
|
<form action="" method="post" style="margin: 0;">
|
||||||
|
<p><select id="payment_status" name="payment_status" onchange="this.form.submit();" style="border: none; background: transparent; padding: 0; cursor: pointer;">
|
||||||
|
<option value="0" '.($order['header']['payment_status']==0?' selected':'').'>'.$payment_status_0.'</option>
|
||||||
|
<option value="1" '.($order['header']['payment_status']==1?' selected':'').'>'.$payment_status_1.'</option>
|
||||||
|
<option value="101" '.($order['header']['payment_status']==101?' selected':'').'>'.$payment_status_101.'</option>
|
||||||
|
<option value="102" '.($order['header']['payment_status']==102?' selected':'').'>'.$payment_status_102.'</option>
|
||||||
|
<option value="103" '.($order['header']['payment_status']==103?' selected':'').'>'.$payment_status_103.'</option>
|
||||||
|
<option value="999" '.($order['header']['payment_status']==999?' selected':'').'>'.$payment_status_999.'</option>
|
||||||
|
</select></p>
|
||||||
|
<input type="hidden" name="id" value="'.$order['header']['id'].'">
|
||||||
|
</form>
|
||||||
|
</div>';
|
||||||
|
} else {
|
||||||
|
$view .='
|
||||||
<div class="order-detail">
|
<div class="order-detail">
|
||||||
<h3>Payment Status</h3>
|
<h3>Payment Status</h3>
|
||||||
<p>' . (${$payment_status} ?? $order['header']['payment_status'] ). '</p>
|
<p>' . (${$payment_status} ?? $order['header']['payment_status'] ). '</p>
|
||||||
</div>
|
</div>';
|
||||||
|
}
|
||||||
|
$view .='
|
||||||
<div class="order-detail">
|
<div class="order-detail">
|
||||||
<h3>Date</h3>
|
<h3>Date</h3>
|
||||||
<p>'.getRelativeTime($order['header']['created']). '</p>
|
<p>'.getRelativeTime($order['header']['created']). '</p>
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ $path = [
|
|||||||
'from_version_id' => '',
|
'from_version_id' => '',
|
||||||
'to_version_id' => '',
|
'to_version_id' => '',
|
||||||
'price' => '',
|
'price' => '',
|
||||||
'currency' => 'USD',
|
'currency' => 'EUR',
|
||||||
'description' => '',
|
'description' => '',
|
||||||
'is_active' => 1,
|
'is_active' => 1,
|
||||||
'created' => '',
|
'created' => '',
|
||||||
|
|||||||
@@ -336,6 +336,7 @@ $page_rows_invoice = 25; //invoices
|
|||||||
$page_rows_dealers = 25; //dealers
|
$page_rows_dealers = 25; //dealers
|
||||||
$page_rows_software_versions = 50; //software versions
|
$page_rows_software_versions = 50; //software versions
|
||||||
$page_rows_software_assignment = 50; //software assignment
|
$page_rows_software_assignment = 50; //software assignment
|
||||||
|
$page_rows_folders = 25; //marketing folders
|
||||||
|
|
||||||
//------------------------------------------
|
//------------------------------------------
|
||||||
// Languages supported
|
// Languages supported
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ define('superuser_profile','admin,dashboard,profile,application,assets,firmwaret
|
|||||||
/*Admin*/
|
/*Admin*/
|
||||||
define('admin_profile','account,accounts,admin,dashboard,profile,application,assets,buildtool,buildtool,cartest,cartest_manage,cartests,changelog,communication,communication_send,communications,firmwaretool,histories,history,history_manage,marketing,partner,partners,sales,servicereport,servicereports,contract,contract_manage,contracts,equipment,equipment_data,equipment_healthindex,equipment_manage,equipment_manage_edit,equipments,equipments_mass_update,product,product_manage,products,products_software,products_versions,report_build,report_contracts_billing,report_healthindex,reporting,rma,rma_history,rma_history_manage,rma_manage,rmas,user,user_manage,users');
|
define('admin_profile','account,accounts,admin,dashboard,profile,application,assets,buildtool,buildtool,cartest,cartest_manage,cartests,changelog,communication,communication_send,communications,firmwaretool,histories,history,history_manage,marketing,partner,partners,sales,servicereport,servicereports,contract,contract_manage,contracts,equipment,equipment_data,equipment_healthindex,equipment_manage,equipment_manage_edit,equipments,equipments_mass_update,product,product_manage,products,products_software,products_versions,report_build,report_contracts_billing,report_healthindex,reporting,rma,rma_history,rma_history_manage,rma_manage,rmas,user,user_manage,users');
|
||||||
/*AdminPlus*/
|
/*AdminPlus*/
|
||||||
define('adminplus_profile','account,account_manage,accounts,admin,config,dashboard,profile,settings,api,application,appointment,assets,billing,buildtool,buildtool,cartest,cartest_manage,cartests,catalog,categories,category,changelog,checkout,com_log,communication,communication_send,communications,cronjob,debug,dev,discount,discounts,firmwaretool,generate_download_token,histories,history,history_manage,identity,identity_dealers,language,licenses,logfile,mailer,maintenance,marketing,media,media_manage,media_scanner,media_upload,order,orders,partner,partners,payment,placeorder,pricelists,pricelists_items,pricelists_manage,profiles,register,render_service_report,reset,sales,security,servicereport,servicereports,shipping,shipping_manage,shopping_cart,software_available,software_download,software_update,softwaretool,tax,taxes,test,transactions,transactions_items,translation_manage,translations,translations_details,unscribe,upgrades,uploader,vin,contract,contract_manage,contracts,dealer,dealer_manage,dealers,dealers_media,equipment,equipment_data,equipment_healthindex,equipment_manage,equipment_manage_edit,equipments,equipments_mass_update,product,product_manage,products,products_attributes,products_attributes_items,products_attributes_manage,products_categories,products_configurations,products_media,products_software,products_software_assignment,products_software_assignments,products_software_assignments,products_software_licenses,products_software_upgrade_paths,products_software_upgrade_paths_manage,products_software_version,products_software_version_access_rules_manage,products_software_version_manage,products_software_versions,products_versions,report_build,report_contracts_billing,report_healthindex,report_usage,reporting,rma,rma_history,rma_history_manage,rma_manage,rmas,user,user_credentials,user_manage,users');
|
define('adminplus_profile','account,account_manage,accounts,admin,config,dashboard,profile,settings,api,application,appointment,assets,billing,buildtool,buildtool,cartest,cartest_manage,cartests,catalog,categories,category,changelog,checkout,com_log,communication,communication_send,communications,cronjob,debug,dev,discount,discounts,factuur,firmwaretool,functions,generate_download_token,histories,history,history_manage,identity,identity_dealers,initialize,invoice,language,licenses,logfile,mailer,maintenance,marketing,marketing_delete,marketing_files,marketing_folders,marketing_migrate,marketing_tags,marketing_upload,media,media_manage,media_scanner,media_upload,order,orders,partner,partners,payment,placeorder,pricelists,pricelists_items,pricelists_manage,profiles,register,render_service_report,reset,sales,security,service,servicereport,servicereports,shipping,shipping_manage,shopping_cart,software_available,software_download,software_update,softwaretool,tax,taxes,test,transactions,transactions_items,translation_manage,translations,translations_details,unscribe,upgrades,uploader,vin,webhook_mollie,webhook_paypal,contract,contract_manage,contracts,dealer,dealer_manage,dealers,dealers_media,equipment,equipment_data,equipment_healthindex,equipment_history,equipment_manage,equipment_manage_edit,equipments,equipments_mass_update,product,product_manage,products,products_attributes,products_attributes_items,products_attributes_manage,products_categories,products_configurations,products_media,products_software,products_software_assignment,products_software_assignments,products_software_assignments,products_software_licenses,products_software_upgrade_paths,products_software_upgrade_paths_manage,products_software_version,products_software_version_access_rules_manage,products_software_version_manage,products_software_versions,products_versions,report_build,report_contracts_billing,report_healthindex,report_usage,reporting,rma,rma_history,rma_history_manage,rma_manage,rmas,user,user_credentials,user_manage,users');
|
||||||
/*Build*/
|
/*Build*/
|
||||||
define('build','dashboard,profile,application,buildtool,buildtool,firmwaretool,products_software');
|
define('build','dashboard,profile,application,buildtool,buildtool,firmwaretool,products_software');
|
||||||
/*Commerce*/
|
/*Commerce*/
|
||||||
@@ -14,7 +14,7 @@ define('commerce','admin,dashboard,profile,application,catalog,categories,catego
|
|||||||
/*Distribution*/
|
/*Distribution*/
|
||||||
define('distribution','admin,dashboard,profile,application,assets,firmwaretool,histories,history,history_manage,marketing,partner,partners,servicereport,servicereports,equipment,equipment_manage,equipment_manage_edit,equipments,equipments_mass_update,product,product_manage,products,products_software,products_versions,user,user_manage,users');
|
define('distribution','admin,dashboard,profile,application,assets,firmwaretool,histories,history,history_manage,marketing,partner,partners,servicereport,servicereports,equipment,equipment_manage,equipment_manage_edit,equipments,equipments_mass_update,product,product_manage,products,products_software,products_versions,user,user_manage,users');
|
||||||
/*Firmware*/
|
/*Firmware*/
|
||||||
define('firmware','application,firmwaretool,products_software');
|
define('firmware','application,firmwaretool,software_available,software_download,software_update,softwaretool,transactions,transactions_items,products_software');
|
||||||
/*Garage*/
|
/*Garage*/
|
||||||
define('garage','dashboard,profile,application,cartest,cartest_manage,cartests,products_versions');
|
define('garage','dashboard,profile,application,cartest,cartest_manage,cartests,products_versions');
|
||||||
/*Interface*/
|
/*Interface*/
|
||||||
|
|||||||
@@ -44,11 +44,14 @@ $all_views = [
|
|||||||
"equipment",
|
"equipment",
|
||||||
"equipment_data",
|
"equipment_data",
|
||||||
"equipment_healthindex",
|
"equipment_healthindex",
|
||||||
|
"equipment_history",
|
||||||
"equipment_manage",
|
"equipment_manage",
|
||||||
"equipment_manage_edit",
|
"equipment_manage_edit",
|
||||||
"equipments",
|
"equipments",
|
||||||
"equipments_mass_update",
|
"equipments_mass_update",
|
||||||
|
"factuur",
|
||||||
"firmwaretool",
|
"firmwaretool",
|
||||||
|
"functions",
|
||||||
"generate_download_token",
|
"generate_download_token",
|
||||||
"histories",
|
"histories",
|
||||||
"history",
|
"history",
|
||||||
@@ -63,6 +66,12 @@ $all_views = [
|
|||||||
"mailer",
|
"mailer",
|
||||||
"maintenance",
|
"maintenance",
|
||||||
"marketing",
|
"marketing",
|
||||||
|
"marketing_delete",
|
||||||
|
"marketing_files",
|
||||||
|
"marketing_folders",
|
||||||
|
"marketing_migrate",
|
||||||
|
"marketing_tags",
|
||||||
|
"marketing_upload",
|
||||||
"media",
|
"media",
|
||||||
"media_manage",
|
"media_manage",
|
||||||
"media_scanner",
|
"media_scanner",
|
||||||
@@ -114,6 +123,7 @@ $all_views = [
|
|||||||
"rmas",
|
"rmas",
|
||||||
"sales",
|
"sales",
|
||||||
"security",
|
"security",
|
||||||
|
"service",
|
||||||
"servicereport",
|
"servicereport",
|
||||||
"servicereports",
|
"servicereports",
|
||||||
"settings",
|
"settings",
|
||||||
@@ -141,6 +151,7 @@ $all_views = [
|
|||||||
"users",
|
"users",
|
||||||
"vin",
|
"vin",
|
||||||
"webhook_mollie",
|
"webhook_mollie",
|
||||||
|
"webhook_paypal",
|
||||||
];
|
];
|
||||||
|
|
||||||
?>
|
?>
|
||||||
@@ -20,6 +20,72 @@ $bearertoken = createCommunicationToken($_SESSION['userkey']);
|
|||||||
//+++++++++++++++++++++++++++++++++++++++++++++++++++++
|
//+++++++++++++++++++++++++++++++++++++++++++++++++++++
|
||||||
$payment_return = isset($_GET['order_id']) ? $_GET['order_id'] : null;
|
$payment_return = isset($_GET['order_id']) ? $_GET['order_id'] : null;
|
||||||
$payment_return_status = isset($_GET['payment_return']) ? $_GET['payment_return'] : null;
|
$payment_return_status = isset($_GET['payment_return']) ? $_GET['payment_return'] : null;
|
||||||
|
$paypal_token = isset($_GET['token']) ? $_GET['token'] : null; // PayPal returns with ?token=
|
||||||
|
|
||||||
|
// Handle PayPal return - capture the order directly
|
||||||
|
if ($paypal_token && $payment_return) {
|
||||||
|
try {
|
||||||
|
// Get PayPal access token
|
||||||
|
$ch = curl_init(PAYPAL_URL . '/v1/oauth2/token');
|
||||||
|
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
|
||||||
|
curl_setopt($ch, CURLOPT_POST, true);
|
||||||
|
curl_setopt($ch, CURLOPT_POSTFIELDS, 'grant_type=client_credentials');
|
||||||
|
curl_setopt($ch, CURLOPT_USERPWD, PAYPAL_CLIENT_ID . ':' . PAYPAL_CLIENT_SECRET);
|
||||||
|
curl_setopt($ch, CURLOPT_HTTPHEADER, ['Content-Type: application/x-www-form-urlencoded']);
|
||||||
|
$response = curl_exec($ch);
|
||||||
|
curl_close($ch);
|
||||||
|
$token_data = json_decode($response, true);
|
||||||
|
$access_token = $token_data['access_token'] ?? '';
|
||||||
|
|
||||||
|
if ($access_token) {
|
||||||
|
// Capture the PayPal order
|
||||||
|
$capture_url = PAYPAL_URL . "/v2/checkout/orders/{$paypal_token}/capture";
|
||||||
|
$ch = curl_init($capture_url);
|
||||||
|
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
|
||||||
|
curl_setopt($ch, CURLOPT_POST, true);
|
||||||
|
curl_setopt($ch, CURLOPT_HTTPHEADER, [
|
||||||
|
'Content-Type: application/json',
|
||||||
|
'Authorization: Bearer ' . $access_token
|
||||||
|
]);
|
||||||
|
$response = curl_exec($ch);
|
||||||
|
$http_code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
||||||
|
curl_close($ch);
|
||||||
|
|
||||||
|
if (debug) {
|
||||||
|
debuglog("PayPal Capture: HTTP $http_code - $response");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update transaction status based on capture result
|
||||||
|
if ($http_code == 200 || $http_code == 201) {
|
||||||
|
$capture_result = json_decode($response, true);
|
||||||
|
$capture_status = $capture_result['status'] ?? '';
|
||||||
|
|
||||||
|
$payment_status = null;
|
||||||
|
if ($capture_status === 'COMPLETED') {
|
||||||
|
$payment_status = 1; // Paid
|
||||||
|
} elseif ($capture_status === 'PENDING') {
|
||||||
|
$payment_status = 101; // Pending
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($payment_status !== null) {
|
||||||
|
$pdo = dbConnect($dbname);
|
||||||
|
$sql = 'UPDATE transactions SET payment_status = ? WHERE txn_id = ?';
|
||||||
|
$stmt = $pdo->prepare($sql);
|
||||||
|
$stmt->execute([$payment_status, $payment_return]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Redirect to clean URL
|
||||||
|
header("Location: ?page=softwaretool&payment_return=1&order_id={$payment_return}");
|
||||||
|
exit;
|
||||||
|
|
||||||
|
} catch (Exception $e) {
|
||||||
|
if (debug) {
|
||||||
|
debuglog("PayPal Capture Error: " . $e->getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
template_header('Softwaretool', 'softwaretool','view');
|
template_header('Softwaretool', 'softwaretool','view');
|
||||||
|
|
||||||
@@ -205,6 +271,9 @@ echo '
|
|||||||
<script>
|
<script>
|
||||||
var link = "'.$baseurl.'";
|
var link = "'.$baseurl.'";
|
||||||
var DEBUG = '.(debug ? 'true' : 'false').';
|
var DEBUG = '.(debug ? 'true' : 'false').';
|
||||||
|
var MOLLIE_ENABLED = '.(mollie_enabled ? 'true' : 'false').';
|
||||||
|
var PAYPAL_ENABLED = '.(paypal_enabled ? 'true' : 'false').';
|
||||||
|
var PAY_ON_DELIVERY_ENABLED = '.(pay_on_delivery_enabled ? 'true' : 'false').';
|
||||||
var port, textEncoder, writableStreamClosed, writer, historyIndex = -1;
|
var port, textEncoder, writableStreamClosed, writer, historyIndex = -1;
|
||||||
const lineHistory = [];
|
const lineHistory = [];
|
||||||
|
|
||||||
|
|||||||
715
style/marketing.css
Normal file
715
style/marketing.css
Normal file
@@ -0,0 +1,715 @@
|
|||||||
|
/* Marketing File Management System Styles */
|
||||||
|
|
||||||
|
.marketing-container {
|
||||||
|
width: 100%;
|
||||||
|
height: calc(100vh - 200px);
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
background: var(--color-white, #fff);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Toolbar */
|
||||||
|
.marketing-toolbar {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
background: var(--color-light-grey, #f8f9fa);
|
||||||
|
gap: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toolbar-left, .toolbar-right {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toolbar-center {
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Breadcrumb Navigation */
|
||||||
|
.breadcrumb-nav {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.breadcrumb-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.25rem;
|
||||||
|
padding: 0.25rem 0.5rem;
|
||||||
|
text-decoration: none;
|
||||||
|
color: var(--color-text, #333);
|
||||||
|
border-radius: 4px;
|
||||||
|
transition: background-color 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.breadcrumb-item:hover {
|
||||||
|
background-color: var(--color-hover, #e9ecef);
|
||||||
|
}
|
||||||
|
|
||||||
|
.breadcrumb-item::after {
|
||||||
|
content: '>';
|
||||||
|
margin-left: 0.5rem;
|
||||||
|
color: var(--color-text-light, #6c757d);
|
||||||
|
}
|
||||||
|
|
||||||
|
.breadcrumb-item:last-child::after {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Search and Filters */
|
||||||
|
.search-container {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-input {
|
||||||
|
padding: 0.5rem 2rem 0.5rem 0.75rem;
|
||||||
|
border: 1px solid var(--color-border, #dee2e6);
|
||||||
|
border-radius: 4px;
|
||||||
|
width: 250px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-icon {
|
||||||
|
position: absolute;
|
||||||
|
right: 0.75rem;
|
||||||
|
top: 50%;
|
||||||
|
transform: translateY(-50%);
|
||||||
|
color: var(--color-text-light, #6c757d);
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-select {
|
||||||
|
padding: 0.5rem;
|
||||||
|
border: 1px solid var(--color-border, #dee2e6);
|
||||||
|
border-radius: 4px;
|
||||||
|
min-width: 120px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* View Toggle */
|
||||||
|
.view-toggle {
|
||||||
|
display: flex;
|
||||||
|
border: 1px solid var(--color-border, #dee2e6);
|
||||||
|
border-radius: 4px;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.view-btn {
|
||||||
|
padding: 0.5rem 0.75rem;
|
||||||
|
border: none;
|
||||||
|
background: var(--color-white, #fff);
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.view-btn:hover {
|
||||||
|
background: var(--color-hover, #e9ecef);
|
||||||
|
}
|
||||||
|
|
||||||
|
.view-btn.active {
|
||||||
|
background: var(--color-primary, #005655);
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Content Area */
|
||||||
|
.marketing-content {
|
||||||
|
display: flex;
|
||||||
|
flex: 1;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Sidebar */
|
||||||
|
.marketing-sidebar {
|
||||||
|
width: 250px;
|
||||||
|
background: var(--color-white, #fff);
|
||||||
|
border-right: 1px solid var(--color-border, #dee2e6);
|
||||||
|
overflow-y: auto;
|
||||||
|
padding: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar-section {
|
||||||
|
margin-bottom: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar-section h3 {
|
||||||
|
font-size: 0.9rem;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--color-text-dark, #212529);
|
||||||
|
margin-bottom: 0.75rem;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Folder Tree */
|
||||||
|
.folder-tree {
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.folder-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.5rem;
|
||||||
|
padding: 0.5rem;
|
||||||
|
cursor: pointer;
|
||||||
|
border-radius: 4px;
|
||||||
|
transition: all 0.2s;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.folder-item:hover {
|
||||||
|
background-color: var(--color-hover, #e9ecef);
|
||||||
|
}
|
||||||
|
|
||||||
|
.folder-item.active {
|
||||||
|
background-color: var(--color-primary-light, #cce7f0);
|
||||||
|
color: var(--color-primary, #007cba);
|
||||||
|
}
|
||||||
|
|
||||||
|
.folder-item.root {
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.folder-name {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.file-count {
|
||||||
|
font-size: 0.8rem;
|
||||||
|
color: var(--color-text-light, #6c757d);
|
||||||
|
}
|
||||||
|
|
||||||
|
.expand-icon {
|
||||||
|
cursor: pointer;
|
||||||
|
transition: transform 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.folder-item.expanded .expand-icon {
|
||||||
|
transform: rotate(90deg);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* File Type Filters */
|
||||||
|
.file-type-filters {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-item input[type="checkbox"] {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-item label {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.5rem;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Main Content */
|
||||||
|
.marketing-main {
|
||||||
|
flex: 1;
|
||||||
|
padding: 1rem;
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Files Container */
|
||||||
|
.files-container {
|
||||||
|
display: grid;
|
||||||
|
gap: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.files-container.grid-view {
|
||||||
|
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
|
||||||
|
}
|
||||||
|
|
||||||
|
.files-container.list-view {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* File Items */
|
||||||
|
.file-item {
|
||||||
|
background: var(--color-white, #fff);
|
||||||
|
border: 1px solid var(--color-border, #dee2e6);
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 1rem;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.file-item:hover {
|
||||||
|
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
|
||||||
|
transform: translateY(-2px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.grid-item {
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.list-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 1rem;
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
.list-item .file-thumbnail {
|
||||||
|
width: 60px;
|
||||||
|
height: 60px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.list-item .file-info {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* File Thumbnail */
|
||||||
|
.file-thumbnail {
|
||||||
|
position: relative;
|
||||||
|
width: 100%;
|
||||||
|
height: 150px;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
background: var(--color-light-grey, #f8f9fa);
|
||||||
|
border-radius: 4px;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.thumbnail-img {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
object-fit: cover;
|
||||||
|
}
|
||||||
|
|
||||||
|
.file-icon {
|
||||||
|
font-size: 3rem;
|
||||||
|
color: var(--color-text-light, #6c757d);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* File Overlay */
|
||||||
|
.file-overlay {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
background: rgba(0, 0, 0, 0.7);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 1rem;
|
||||||
|
opacity: 0;
|
||||||
|
transition: opacity 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.file-item:hover .file-overlay {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.file-overlay button {
|
||||||
|
background: var(--color-white, #fff);
|
||||||
|
border: none;
|
||||||
|
border-radius: 50%;
|
||||||
|
width: 40px;
|
||||||
|
height: 40px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.file-overlay button:hover {
|
||||||
|
background: var(--color-primary, #007cba);
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* File Info */
|
||||||
|
.file-info {
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.list-item .file-info {
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
.file-name {
|
||||||
|
font-weight: 600;
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.file-meta {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 0.5rem;
|
||||||
|
font-size: 0.8rem;
|
||||||
|
color: var(--color-text-light, #6c757d);
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.list-item .file-meta {
|
||||||
|
justify-content: flex-start;
|
||||||
|
}
|
||||||
|
|
||||||
|
.file-tags {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 0.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.list-item .file-tags {
|
||||||
|
justify-content: flex-start;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tag {
|
||||||
|
background: var(--color-primary-light, #cce7f0);
|
||||||
|
color: var(--color-primary, #007cba);
|
||||||
|
padding: 0.125rem 0.5rem;
|
||||||
|
border-radius: 12px;
|
||||||
|
font-size: 0.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Loading and Empty States */
|
||||||
|
.loading-indicator, .empty-state {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 3rem;
|
||||||
|
text-align: center;
|
||||||
|
color: var(--color-text-light, #6c757d);
|
||||||
|
}
|
||||||
|
|
||||||
|
.loading-indicator i {
|
||||||
|
font-size: 2rem;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.empty-state i {
|
||||||
|
font-size: 4rem;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
opacity: 0.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Modals */
|
||||||
|
.modal {
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
background: rgba(0, 0, 0, 0.5);
|
||||||
|
display: none;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
z-index: 1000;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal.show {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-content {
|
||||||
|
background: var(--color-white, #fff);
|
||||||
|
border-radius: 8px;
|
||||||
|
max-width: 600px;
|
||||||
|
width: 90%;
|
||||||
|
max-height: 90vh;
|
||||||
|
overflow-y: auto;
|
||||||
|
transform: translateY(-20px);
|
||||||
|
transition: transform 0.3s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal.show .modal-content {
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: 1.5rem;
|
||||||
|
border-bottom: 1px solid var(--color-border, #dee2e6);
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-header h3 {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 1.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-close {
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
font-size: 1.5rem;
|
||||||
|
cursor: pointer;
|
||||||
|
color: var(--color-text-light, #6c757d);
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-body {
|
||||||
|
padding: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-footer {
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
gap: 0.75rem;
|
||||||
|
padding: 1.5rem;
|
||||||
|
border-top: 1px solid var(--color-border, #dee2e6);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Preview Modal */
|
||||||
|
.preview-modal .modal-content {
|
||||||
|
max-width: 800px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.preview-content {
|
||||||
|
text-align: center;
|
||||||
|
min-height: 200px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.file-preview-info {
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.file-preview-info i {
|
||||||
|
color: var(--color-text-light, #6c757d);
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.file-preview-info h4 {
|
||||||
|
margin: 1rem 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Upload Area */
|
||||||
|
.upload-area {
|
||||||
|
border: 2px dashed var(--color-border, #dee2e6);
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 2rem;
|
||||||
|
text-align: center;
|
||||||
|
margin-bottom: 1.5rem;
|
||||||
|
transition: all 0.2s;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.upload-area.drag-over,
|
||||||
|
.upload-area:hover {
|
||||||
|
border-color: var(--color-primary, #005655);
|
||||||
|
background: var(--color-primary-light, #cce7f0);
|
||||||
|
}
|
||||||
|
|
||||||
|
.upload-icon {
|
||||||
|
font-size: 3rem;
|
||||||
|
color: var(--color-text-light, #6c757d);
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.upload-area h4 {
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.browse-btn {
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
color: var(--color-primary, #005655);
|
||||||
|
text-decoration: underline;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.upload-area input[type="file"] {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Upload Queue */
|
||||||
|
.upload-queue {
|
||||||
|
max-height: 300px;
|
||||||
|
overflow-y: auto;
|
||||||
|
margin-bottom: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.upload-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 1rem;
|
||||||
|
padding: 0.75rem;
|
||||||
|
border: 1px solid var(--color-border, #dee2e6);
|
||||||
|
border-radius: 4px;
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.upload-item.completed {
|
||||||
|
border-color: var(--color-success, #28a745);
|
||||||
|
background: var(--color-success-light, #d4edda);
|
||||||
|
}
|
||||||
|
|
||||||
|
.upload-item.error {
|
||||||
|
border-color: var(--color-danger, #dc3545);
|
||||||
|
background: var(--color-danger-light, #f8d7da);
|
||||||
|
}
|
||||||
|
|
||||||
|
.upload-info {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.upload-info .file-name {
|
||||||
|
font-weight: 600;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.upload-info .file-size {
|
||||||
|
font-size: 0.8rem;
|
||||||
|
color: var(--color-text-light, #6c757d);
|
||||||
|
}
|
||||||
|
|
||||||
|
.upload-progress {
|
||||||
|
flex: 2;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress-bar {
|
||||||
|
background: var(--color-light-grey, #f8f9fa);
|
||||||
|
border-radius: 4px;
|
||||||
|
height: 8px;
|
||||||
|
overflow: hidden;
|
||||||
|
margin-bottom: 0.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress-fill {
|
||||||
|
background: var(--color-primary, #007cba);
|
||||||
|
height: 100%;
|
||||||
|
transition: width 0.3s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.upload-status {
|
||||||
|
font-size: 0.8rem;
|
||||||
|
color: var(--color-text-light, #6c757d);
|
||||||
|
}
|
||||||
|
|
||||||
|
.remove-btn {
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
color: var(--color-danger, #dc3545);
|
||||||
|
cursor: pointer;
|
||||||
|
padding: 0.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Upload Options */
|
||||||
|
.upload-options {
|
||||||
|
display: grid;
|
||||||
|
gap: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-group {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-group label {
|
||||||
|
font-weight: 600;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-control {
|
||||||
|
padding: 0.5rem;
|
||||||
|
border: 1px solid var(--color-border, #dee2e6);
|
||||||
|
border-radius: 4px;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-control:focus {
|
||||||
|
outline: none;
|
||||||
|
border-color: var(--color-primary, #005655);
|
||||||
|
box-shadow: 0 0 0 2px rgba(0, 124, 186, 0.25);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/* Toast Notifications */
|
||||||
|
.toast {
|
||||||
|
position: fixed;
|
||||||
|
top: 20px;
|
||||||
|
right: 20px;
|
||||||
|
padding: 0.75rem 1rem;
|
||||||
|
border-radius: 4px;
|
||||||
|
color: white;
|
||||||
|
z-index: 1100;
|
||||||
|
transform: translateX(100%);
|
||||||
|
transition: transform 0.3s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toast.show {
|
||||||
|
transform: translateX(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
.toast-success {
|
||||||
|
background: var(--color-success, #28a745);
|
||||||
|
}
|
||||||
|
|
||||||
|
.toast-error {
|
||||||
|
background: var(--color-danger, #dc3545);
|
||||||
|
}
|
||||||
|
|
||||||
|
.toast-info {
|
||||||
|
background: var(--color-info, #17a2b8);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Drag and Drop */
|
||||||
|
.drag-over {
|
||||||
|
border-color: var(--color-primary, #007cba) !important;
|
||||||
|
background: var(--color-primary-light, #cce7f0) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Responsive Design */
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.marketing-toolbar {
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 0.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toolbar-left,
|
||||||
|
.toolbar-center,
|
||||||
|
.toolbar-right {
|
||||||
|
width: 100%;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.marketing-content {
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.marketing-sidebar {
|
||||||
|
width: 100%;
|
||||||
|
height: auto;
|
||||||
|
max-height: 200px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-input {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.files-container.grid-view {
|
||||||
|
grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-content {
|
||||||
|
width: 95%;
|
||||||
|
margin: 1rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -216,10 +216,8 @@ try {
|
|||||||
date('Y-m-d H:i:s')
|
date('Y-m-d H:i:s')
|
||||||
]);
|
]);
|
||||||
$invoice_id = $pdo->lastInsertId();
|
$invoice_id = $pdo->lastInsertId();
|
||||||
debuglog("WEBHOOK: Invoice created with ID: $invoice_id");
|
|
||||||
} else {
|
} else {
|
||||||
$invoice_id = $existing_invoice['id'];
|
$invoice_id = $existing_invoice['id'];
|
||||||
debuglog("WEBHOOK: Invoice already exists with ID: $invoice_id");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fetch full invoice data with customer details for email
|
// Fetch full invoice data with customer details for email
|
||||||
@@ -238,13 +236,9 @@ try {
|
|||||||
$stmt->execute([$invoice_id]);
|
$stmt->execute([$invoice_id]);
|
||||||
$invoice_data = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
$invoice_data = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||||
|
|
||||||
debuglog("WEBHOOK: Invoice data fetched: " . print_r($invoice_data, true));
|
|
||||||
|
|
||||||
if (!empty($invoice_data)) {
|
if (!empty($invoice_data)) {
|
||||||
debuglog("WEBHOOK: Transforming invoice data...");
|
|
||||||
// Transform the data (group items like the API does)
|
// Transform the data (group items like the API does)
|
||||||
$invoice_cust = transformOrderData($invoice_data);
|
$invoice_cust = transformOrderData($invoice_data);
|
||||||
debuglog("WEBHOOK: Transformed invoice data: " . print_r($invoice_cust, true));
|
|
||||||
|
|
||||||
// Determine invoice language
|
// Determine invoice language
|
||||||
if (!empty($invoice_cust['customer']['language'])) {
|
if (!empty($invoice_cust['customer']['language'])) {
|
||||||
@@ -256,35 +250,22 @@ try {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Generate invoice HTML (using custom template for software upgrades)
|
// Generate invoice HTML (using custom template for software upgrades)
|
||||||
debuglog("WEBHOOK: Calling generateSoftwareInvoice with language: $invoice_language");
|
|
||||||
list($data,$customer_email,$order_id) = generateSoftwareInvoice($invoice_cust,$orderId,$invoice_language);
|
list($data,$customer_email,$order_id) = generateSoftwareInvoice($invoice_cust,$orderId,$invoice_language);
|
||||||
debuglog("WEBHOOK: Invoice generated - Customer email: $customer_email, Order ID: $order_id");
|
|
||||||
debuglog("WEBHOOK: Invoice HTML length: " . strlen($data));
|
|
||||||
|
|
||||||
//+++++++++++++++++++++++++++++++++++++++++++++++++++++
|
//+++++++++++++++++++++++++++++++++++++++++++++++++++++
|
||||||
//CREATE PDF using DomPDF
|
//CREATE PDF using DomPDF
|
||||||
//+++++++++++++++++++++++++++++++++++++++++++++++++++++
|
//+++++++++++++++++++++++++++++++++++++++++++++++++++++
|
||||||
debuglog("WEBHOOK: Creating PDF...");
|
|
||||||
$dompdf->loadHtml($data);
|
$dompdf->loadHtml($data);
|
||||||
$dompdf->setPaper('A4', 'portrait');
|
$dompdf->setPaper('A4', 'portrait');
|
||||||
$dompdf->render();
|
$dompdf->render();
|
||||||
$subject = 'Software Upgrade - Invoice: '.$order_id;
|
$subject = 'Software Upgrade - Invoice: '.$order_id;
|
||||||
$attachment = $dompdf->output();
|
$attachment = $dompdf->output();
|
||||||
debuglog("WEBHOOK: PDF created, size: " . strlen($attachment) . " bytes");
|
|
||||||
|
|
||||||
//+++++++++++++++++++++++++++++++++++++++++++++++++++++
|
//+++++++++++++++++++++++++++++++++++++++++++++++++++++
|
||||||
//Send email via PHPMailer
|
//Send email via PHPMailer
|
||||||
//+++++++++++++++++++++++++++++++++++++++++++++++++++++
|
//+++++++++++++++++++++++++++++++++++++++++++++++++++++
|
||||||
debuglog("WEBHOOK: Attempting to send email to: $customer_email");
|
|
||||||
debuglog("WEBHOOK: Email subject: $subject");
|
|
||||||
debuglog("WEBHOOK: Email config - Host: " . (defined('email_host_name') ? email_host_name : 'NOT DEFINED'));
|
|
||||||
debuglog("WEBHOOK: Email config - Port: " . (defined('email_outgoing_port') ? email_outgoing_port : 'NOT DEFINED'));
|
|
||||||
debuglog("WEBHOOK: Email config - Security: " . (defined('email_outgoing_security') ? email_outgoing_security : 'NOT DEFINED'));
|
|
||||||
debuglog("WEBHOOK: Email config - Username: " . (defined('email') ? email : 'NOT DEFINED'));
|
|
||||||
|
|
||||||
// The send_mail function will exit on error and debuglog the error
|
// The send_mail function will exit on error and debuglog the error
|
||||||
$mail_result = send_mail($customer_email, $subject, $data, $attachment, $subject);
|
$mail_result = send_mail($customer_email, $subject, $data, $attachment, $subject);
|
||||||
debuglog("WEBHOOK: Email sent successfully to: $customer_email");
|
|
||||||
|
|
||||||
// Send to bookkeeping if configured
|
// Send to bookkeeping if configured
|
||||||
if(invoice_bookkeeping){
|
if(invoice_bookkeeping){
|
||||||
|
|||||||
427
webhook_paypal.php
Normal file
427
webhook_paypal.php
Normal file
@@ -0,0 +1,427 @@
|
|||||||
|
<?php
|
||||||
|
// PayPal Webhook for software upgrade payments
|
||||||
|
// Based on webhook_mollie.php structure
|
||||||
|
// Handles PayPal IPN/webhook notifications for payment status updates
|
||||||
|
|
||||||
|
require_once 'settings/config_redirector.php';
|
||||||
|
require_once 'assets/functions.php';
|
||||||
|
include dirname(__FILE__).'/settings/settings_redirector.php';
|
||||||
|
|
||||||
|
// DEBUG: Log webhook call
|
||||||
|
debuglog("PAYPAL WEBHOOK CALLED - POST data: " . print_r($_POST, true));
|
||||||
|
debuglog("PAYPAL WEBHOOK CALLED - RAW input: " . file_get_contents('php://input'));
|
||||||
|
|
||||||
|
//+++++++++++++++++++++++++++++++++++++++++++++++++++++
|
||||||
|
//LOGIN TO API (same as webhook_mollie.php)
|
||||||
|
//+++++++++++++++++++++++++++++++++++++++++++++++++++++
|
||||||
|
debuglog("WEBHOOK: Attempting API authorization...");
|
||||||
|
debuglog("WEBHOOK: Interface user: " . interface_user);
|
||||||
|
$data = json_encode(array("clientID" => interface_user, "clientsecret" => interface_pw), JSON_UNESCAPED_UNICODE);
|
||||||
|
$responses = ioAPIv2('/v2/authorization', $data,'');
|
||||||
|
debuglog("WEBHOOK: Authorization response: " . $responses);
|
||||||
|
if (!empty($responses)){$responses = json_decode($responses,true);}else{$responses = '400';}
|
||||||
|
$clientsecret = $responses['token'];
|
||||||
|
debuglog("WEBHOOK: Token obtained: " . ($clientsecret ? 'YES' : 'NO'));
|
||||||
|
|
||||||
|
//+++++++++++++++++++++++++++++++++++++++++++++++++++++
|
||||||
|
// BASEURL is required for invoice template
|
||||||
|
//+++++++++++++++++++++++++++++++++++++++++++++++++++++
|
||||||
|
$base_url = 'https://'.$_SERVER['SERVER_NAME'].'/';
|
||||||
|
define('base_url', $base_url);
|
||||||
|
|
||||||
|
//+++++++++++++++++++++++++++++++++++++++++++++++++++++
|
||||||
|
// Initialize DomPDF for invoice generation
|
||||||
|
//+++++++++++++++++++++++++++++++++++++++++++++++++++++
|
||||||
|
use Dompdf\Dompdf;
|
||||||
|
use Dompdf\Options;
|
||||||
|
$options = new Options();
|
||||||
|
$options->set('isRemoteEnabled', true);
|
||||||
|
$dompdf = new Dompdf($options);
|
||||||
|
|
||||||
|
//+++++++++++++++++++++++++++++++++++++++++++++++++++++
|
||||||
|
// Language mapping for invoices
|
||||||
|
//+++++++++++++++++++++++++++++++++++++++++++++++++++++
|
||||||
|
$available_languages = [
|
||||||
|
'NL' => 'NL',
|
||||||
|
'BE' => 'NL',
|
||||||
|
'US' => 'US',
|
||||||
|
'GB' => 'US',
|
||||||
|
'DE' => 'DE',
|
||||||
|
'FR' => 'FR',
|
||||||
|
'ES' => 'ES'
|
||||||
|
];
|
||||||
|
|
||||||
|
try {
|
||||||
|
//+++++++++++++++++++++++++++++++++++++++++++++++++++++
|
||||||
|
// Get the webhook event data from PayPal
|
||||||
|
//+++++++++++++++++++++++++++++++++++++++++++++++++++++
|
||||||
|
$raw_input = file_get_contents('php://input');
|
||||||
|
$webhook_event = json_decode($raw_input, true);
|
||||||
|
|
||||||
|
debuglog("PAYPAL WEBHOOK: Decoded event: " . print_r($webhook_event, true));
|
||||||
|
|
||||||
|
//+++++++++++++++++++++++++++++++++++++++++++++++++++++
|
||||||
|
// Verify webhook signature (recommended for production)
|
||||||
|
//+++++++++++++++++++++++++++++++++++++++++++++++++++++
|
||||||
|
if (!debug && PAYPAL_WEBHOOK_ID) {
|
||||||
|
$verified = verifyPayPalWebhookSignature($raw_input, $_SERVER);
|
||||||
|
if (!$verified) {
|
||||||
|
debuglog("PAYPAL WEBHOOK ERROR: Signature verification failed");
|
||||||
|
http_response_code(401);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
debuglog("PAYPAL WEBHOOK: Signature verified successfully");
|
||||||
|
}
|
||||||
|
|
||||||
|
//+++++++++++++++++++++++++++++++++++++++++++++++++++++
|
||||||
|
// Extract event type and resource
|
||||||
|
//+++++++++++++++++++++++++++++++++++++++++++++++++++++
|
||||||
|
$event_type = $webhook_event['event_type'] ?? '';
|
||||||
|
$resource = $webhook_event['resource'] ?? [];
|
||||||
|
|
||||||
|
debuglog("PAYPAL WEBHOOK: Event type: {$event_type}");
|
||||||
|
|
||||||
|
// Get order ID from custom_id in the payment resource
|
||||||
|
$orderId = $resource['custom_id'] ?? $resource['purchase_units'][0]['custom_id'] ?? null;
|
||||||
|
|
||||||
|
if (!$orderId) {
|
||||||
|
debuglog("PAYPAL WEBHOOK ERROR: No custom_id (order_id) in webhook event");
|
||||||
|
http_response_code(400);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
debuglog("PAYPAL WEBHOOK: Order ID: {$orderId}");
|
||||||
|
|
||||||
|
//+++++++++++++++++++++++++++++++++++++++++++++++++++++
|
||||||
|
// Map PayPal event types to payment status
|
||||||
|
//+++++++++++++++++++++++++++++++++++++++++++++++++++++
|
||||||
|
$payment_status = null;
|
||||||
|
|
||||||
|
switch ($event_type) {
|
||||||
|
case 'PAYMENT.CAPTURE.COMPLETED':
|
||||||
|
case 'CHECKOUT.ORDER.APPROVED':
|
||||||
|
$payment_status = 1; // Paid
|
||||||
|
debuglog("PAYPAL WEBHOOK: Payment completed/approved");
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'PAYMENT.CAPTURE.PENDING':
|
||||||
|
case 'CHECKOUT.ORDER.PROCESSED':
|
||||||
|
$payment_status = 101; // Pending
|
||||||
|
debuglog("PAYPAL WEBHOOK: Payment pending");
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'PAYMENT.CAPTURE.DECLINED':
|
||||||
|
case 'PAYMENT.CAPTURE.FAILED':
|
||||||
|
$payment_status = 102; // Failed
|
||||||
|
debuglog("PAYPAL WEBHOOK: Payment failed/declined");
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'PAYMENT.CAPTURE.REFUNDED':
|
||||||
|
$payment_status = 104; // Refunded
|
||||||
|
debuglog("PAYPAL WEBHOOK: Payment refunded");
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'CHECKOUT.ORDER.VOIDED':
|
||||||
|
$payment_status = 999; // Canceled
|
||||||
|
debuglog("PAYPAL WEBHOOK: Payment voided/canceled");
|
||||||
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
debuglog("PAYPAL WEBHOOK: Unhandled event type: {$event_type}");
|
||||||
|
http_response_code(200);
|
||||||
|
echo "OK - Event type not handled";
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
//+++++++++++++++++++++++++++++++++++++++++++++++++++++
|
||||||
|
// Update transaction status directly in database
|
||||||
|
//+++++++++++++++++++++++++++++++++++++++++++++++++++++
|
||||||
|
if ($payment_status !== null) {
|
||||||
|
debuglog("PAYPAL WEBHOOK: Order ID: $orderId, Payment Status: $payment_status");
|
||||||
|
|
||||||
|
$pdo = dbConnect($dbname);
|
||||||
|
|
||||||
|
// Update transaction status
|
||||||
|
$sql = 'UPDATE transactions SET payment_status = ? WHERE txn_id = ?';
|
||||||
|
$stmt = $pdo->prepare($sql);
|
||||||
|
$stmt->execute([$payment_status, $orderId]);
|
||||||
|
|
||||||
|
debuglog("PAYPAL WEBHOOK: Transaction status updated in database");
|
||||||
|
|
||||||
|
// Fetch transaction data for license creation
|
||||||
|
$sql = 'SELECT * FROM transactions WHERE txn_id = ?';
|
||||||
|
$stmt = $pdo->prepare($sql);
|
||||||
|
$stmt->execute([$orderId]);
|
||||||
|
$transaction = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||||
|
|
||||||
|
debuglog("PAYPAL WEBHOOK: Transaction data: " . print_r($transaction, true));
|
||||||
|
|
||||||
|
//+++++++++++++++++++++++++++++++++++++++++++++++++++++
|
||||||
|
// Only create license and invoice if payment is PAID
|
||||||
|
//+++++++++++++++++++++++++++++++++++++++++++++++++++++
|
||||||
|
if ($payment_status == 1 && $transaction !== null && !empty($transaction)) {
|
||||||
|
debuglog("PAYPAL WEBHOOK: Payment is PAID, processing license...");
|
||||||
|
debuglog("PAYPAL WEBHOOK: Transaction ID (auto-increment): " . $transaction['id']);
|
||||||
|
debuglog("PAYPAL WEBHOOK: Transaction txn_id: " . $transaction['txn_id']);
|
||||||
|
|
||||||
|
//+++++++++++++++++++++++++++++++++++++++++++++++++++++
|
||||||
|
// CREATE LICENSE for software upgrade
|
||||||
|
//+++++++++++++++++++++++++++++++++++++++++++++++++++++
|
||||||
|
$pdo = dbConnect($dbname);
|
||||||
|
|
||||||
|
// Fetch transaction items to find software upgrade
|
||||||
|
$sql = 'SELECT * FROM transactions_items WHERE txn_id = ?';
|
||||||
|
$stmt = $pdo->prepare($sql);
|
||||||
|
$stmt->execute([$transaction['id']]);
|
||||||
|
$items = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||||
|
|
||||||
|
debuglog("PAYPAL WEBHOOK: Found " . count($items) . " transaction items");
|
||||||
|
debuglog("PAYPAL WEBHOOK: Items data: " . print_r($items, true));
|
||||||
|
|
||||||
|
foreach ($items as $item) {
|
||||||
|
debuglog("PAYPAL WEBHOOK: Processing item: " . print_r($item, true));
|
||||||
|
|
||||||
|
if (!empty($item['item_options'])) {
|
||||||
|
$options = json_decode($item['item_options'], true);
|
||||||
|
debuglog("PAYPAL WEBHOOK: Item options: " . print_r($options, true));
|
||||||
|
|
||||||
|
// Check if this is a software upgrade (has serial_number and equipment_id)
|
||||||
|
if (isset($options['serial_number']) && isset($options['equipment_id'])) {
|
||||||
|
debuglog("PAYPAL WEBHOOK: This is a software upgrade item");
|
||||||
|
|
||||||
|
// Check if license already exists for this transaction
|
||||||
|
$sql = 'SELECT rowID FROM products_software_licenses WHERE transaction_id = ?';
|
||||||
|
$stmt = $pdo->prepare($sql);
|
||||||
|
$stmt->execute([$orderId]);
|
||||||
|
$existing_license = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||||
|
|
||||||
|
if (!$existing_license) {
|
||||||
|
debuglog("PAYPAL WEBHOOK: No existing license, creating new one...");
|
||||||
|
|
||||||
|
// Generate unique license key
|
||||||
|
$license_key = generateUniqueLicenseKey();
|
||||||
|
|
||||||
|
// Create license
|
||||||
|
$sql = 'INSERT INTO products_software_licenses
|
||||||
|
(version_id, license_type, license_key, status, starts_at, expires_at, transaction_id, created, createdby)
|
||||||
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)';
|
||||||
|
$stmt = $pdo->prepare($sql);
|
||||||
|
$stmt->execute([
|
||||||
|
$item['item_id'], // version_id
|
||||||
|
1, // license_type (1 = upgrade)
|
||||||
|
$license_key,
|
||||||
|
1, // status = active
|
||||||
|
date('Y-m-d H:i:s'),
|
||||||
|
'2099-12-31 23:59:59', // effectively permanent
|
||||||
|
$orderId,
|
||||||
|
date('Y-m-d H:i:s'),
|
||||||
|
'webhook_paypal' // created by PayPal webhook
|
||||||
|
]);
|
||||||
|
|
||||||
|
debuglog("PAYPAL WEBHOOK: License created: $license_key");
|
||||||
|
|
||||||
|
// Update equipment.sw_version_license
|
||||||
|
$sql = 'UPDATE equipment SET sw_version_license = ? WHERE rowID = ?';
|
||||||
|
$stmt = $pdo->prepare($sql);
|
||||||
|
$stmt->execute([$license_key, $options['equipment_id']]);
|
||||||
|
|
||||||
|
debuglog("PAYPAL WEBHOOK: Equipment updated with license: {$options['equipment_id']}");
|
||||||
|
} else {
|
||||||
|
debuglog("PAYPAL WEBHOOK: License already exists for order: $orderId");
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
debuglog("PAYPAL WEBHOOK: Not a software upgrade item (no serial_number/equipment_id)");
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
debuglog("PAYPAL WEBHOOK: No item_options found");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//+++++++++++++++++++++++++++++++++++++++++++++++++++++
|
||||||
|
// Generate INVOICE directly in database
|
||||||
|
//+++++++++++++++++++++++++++++++++++++++++++++++++++++
|
||||||
|
|
||||||
|
// Check if invoice already exists for this transaction
|
||||||
|
$sql = 'SELECT id FROM invoice WHERE txn_id = ?';
|
||||||
|
$stmt = $pdo->prepare($sql);
|
||||||
|
$stmt->execute([$transaction['txn_id']]);
|
||||||
|
$existing_invoice = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||||
|
|
||||||
|
if (!$existing_invoice) {
|
||||||
|
// Create invoice
|
||||||
|
$sql = 'INSERT INTO invoice (txn_id, payment_status, payment_amount, shipping_amount, discount_amount, tax_amount, created)
|
||||||
|
VALUES (?, ?, ?, ?, ?, ?, ?)';
|
||||||
|
$stmt = $pdo->prepare($sql);
|
||||||
|
$stmt->execute([
|
||||||
|
$transaction['txn_id'],
|
||||||
|
$transaction['payment_status'],
|
||||||
|
$transaction['payment_amount'],
|
||||||
|
$transaction['shipping_amount'] ?? 0.00,
|
||||||
|
$transaction['discount_amount'] ?? 0.00,
|
||||||
|
$transaction['tax_amount'] ?? 0.00,
|
||||||
|
date('Y-m-d H:i:s')
|
||||||
|
]);
|
||||||
|
$invoice_id = $pdo->lastInsertId();
|
||||||
|
} else {
|
||||||
|
$invoice_id = $existing_invoice['id'];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fetch full invoice data with customer details for email
|
||||||
|
$sql = 'SELECT tx.*, txi.item_id, txi.item_price, txi.item_quantity, txi.item_options,
|
||||||
|
p.productcode, p.productname, inv.id as invoice, inv.created as invoice_created,
|
||||||
|
i.language as user_language
|
||||||
|
FROM invoice inv
|
||||||
|
LEFT JOIN transactions tx ON tx.txn_id = inv.txn_id
|
||||||
|
LEFT JOIN transactions_items txi ON txi.txn_id = tx.id
|
||||||
|
LEFT JOIN products p ON p.rowID = txi.item_id
|
||||||
|
LEFT JOIN identity i ON i.userkey = tx.account_id
|
||||||
|
WHERE inv.id = ?';
|
||||||
|
$stmt = $pdo->prepare($sql);
|
||||||
|
$stmt->execute([$invoice_id]);
|
||||||
|
$invoice_data = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||||
|
|
||||||
|
if (!empty($invoice_data)) {
|
||||||
|
// Transform the data (group items like the API does)
|
||||||
|
$invoice_cust = transformOrderData($invoice_data);
|
||||||
|
|
||||||
|
// Determine invoice language
|
||||||
|
if (!empty($invoice_cust['customer']['language'])) {
|
||||||
|
$invoice_language = strtoupper($invoice_cust['customer']['language']);
|
||||||
|
} elseif (!empty($invoice_cust['customer']['country']) && isset($available_languages[strtoupper($invoice_cust['customer']['country'])])) {
|
||||||
|
$invoice_language = $available_languages[strtoupper($invoice_cust['customer']['country'])];
|
||||||
|
} else {
|
||||||
|
$invoice_language = 'US'; // Default fallback
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generate invoice HTML (using custom template for software upgrades)
|
||||||
|
list($data,$customer_email,$order_id) = generateSoftwareInvoice($invoice_cust,$orderId,$invoice_language);
|
||||||
|
|
||||||
|
//+++++++++++++++++++++++++++++++++++++++++++++++++++++
|
||||||
|
//CREATE PDF using DomPDF
|
||||||
|
//+++++++++++++++++++++++++++++++++++++++++++++++++++++
|
||||||
|
$dompdf->loadHtml($data);
|
||||||
|
$dompdf->setPaper('A4', 'portrait');
|
||||||
|
$dompdf->render();
|
||||||
|
$subject = 'Software Upgrade - Invoice: '.$order_id;
|
||||||
|
$attachment = $dompdf->output();
|
||||||
|
|
||||||
|
//+++++++++++++++++++++++++++++++++++++++++++++++++++++
|
||||||
|
//Send email via PHPMailer
|
||||||
|
//+++++++++++++++++++++++++++++++++++++++++++++++++++++
|
||||||
|
$mail_result = send_mail($customer_email, $subject, $data, $attachment, $subject);
|
||||||
|
|
||||||
|
// Send to bookkeeping if configured
|
||||||
|
if(invoice_bookkeeping){
|
||||||
|
debuglog("PAYPAL WEBHOOK: Sending to bookkeeping: " . email_bookkeeping);
|
||||||
|
send_mail(email_bookkeeping, $subject, $data, $attachment, $subject);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
debuglog("PAYPAL WEBHOOK: No invoice data found for invoice_id: $invoice_id");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Return 200 OK to PayPal
|
||||||
|
http_response_code(200);
|
||||||
|
echo "OK";
|
||||||
|
|
||||||
|
} catch (Exception $e) {
|
||||||
|
debuglog("PAYPAL WEBHOOK ERROR: " . $e->getMessage());
|
||||||
|
debuglog("PAYPAL WEBHOOK ERROR TRACE: " . $e->getTraceAsString());
|
||||||
|
error_log("PayPal webhook error: " . htmlspecialchars($e->getMessage()));
|
||||||
|
http_response_code(500);
|
||||||
|
}
|
||||||
|
|
||||||
|
//+++++++++++++++++++++++++++++++++++++++++++++++++++++
|
||||||
|
// Helper function to verify PayPal webhook signature
|
||||||
|
//+++++++++++++++++++++++++++++++++++++++++++++++++++++
|
||||||
|
function verifyPayPalWebhookSignature($raw_body, $headers) {
|
||||||
|
if (!PAYPAL_WEBHOOK_ID || !PAYPAL_CLIENT_ID || !PAYPAL_CLIENT_SECRET) {
|
||||||
|
debuglog("PAYPAL WEBHOOK: Signature verification skipped - credentials not configured");
|
||||||
|
return true; // Skip verification if not configured
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get required headers
|
||||||
|
$transmission_id = $headers['HTTP_PAYPAL_TRANSMISSION_ID'] ?? '';
|
||||||
|
$transmission_time = $headers['HTTP_PAYPAL_TRANSMISSION_TIME'] ?? '';
|
||||||
|
$cert_url = $headers['HTTP_PAYPAL_CERT_URL'] ?? '';
|
||||||
|
$auth_algo = $headers['HTTP_PAYPAL_AUTH_ALGO'] ?? '';
|
||||||
|
$transmission_sig = $headers['HTTP_PAYPAL_TRANSMISSION_SIG'] ?? '';
|
||||||
|
|
||||||
|
debuglog("PAYPAL WEBHOOK: Headers - transmission_id: $transmission_id, cert_url: $cert_url");
|
||||||
|
|
||||||
|
if (!$transmission_id || !$transmission_sig) {
|
||||||
|
debuglog("PAYPAL WEBHOOK: Missing signature headers");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Get PayPal access token
|
||||||
|
$access_token = getPayPalAccessToken();
|
||||||
|
|
||||||
|
// Prepare verification request
|
||||||
|
$verify_url = PAYPAL_URL . '/v1/notifications/verify-webhook-signature';
|
||||||
|
$verify_data = [
|
||||||
|
'transmission_id' => $transmission_id,
|
||||||
|
'transmission_time' => $transmission_time,
|
||||||
|
'cert_url' => $cert_url,
|
||||||
|
'auth_algo' => $auth_algo,
|
||||||
|
'transmission_sig' => $transmission_sig,
|
||||||
|
'webhook_id' => PAYPAL_WEBHOOK_ID,
|
||||||
|
'webhook_event' => json_decode($raw_body, true)
|
||||||
|
];
|
||||||
|
|
||||||
|
debuglog("PAYPAL WEBHOOK: Verification request - webhook_id: " . PAYPAL_WEBHOOK_ID);
|
||||||
|
debuglog("PAYPAL WEBHOOK: Verification URL: " . $verify_url);
|
||||||
|
|
||||||
|
$ch = curl_init($verify_url);
|
||||||
|
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
|
||||||
|
curl_setopt($ch, CURLOPT_POST, true);
|
||||||
|
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($verify_data));
|
||||||
|
curl_setopt($ch, CURLOPT_HTTPHEADER, [
|
||||||
|
'Content-Type: application/json',
|
||||||
|
'Authorization: Bearer ' . $access_token
|
||||||
|
]);
|
||||||
|
|
||||||
|
$response = curl_exec($ch);
|
||||||
|
$http_code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
||||||
|
curl_close($ch);
|
||||||
|
|
||||||
|
debuglog("PAYPAL WEBHOOK: Verification response (HTTP $http_code): " . $response);
|
||||||
|
|
||||||
|
$result = json_decode($response, true);
|
||||||
|
$verified = ($http_code == 200 && isset($result['verification_status']) && $result['verification_status'] === 'SUCCESS');
|
||||||
|
|
||||||
|
debuglog("PAYPAL WEBHOOK: Signature verification result: " . ($verified ? 'SUCCESS' : 'FAILED'));
|
||||||
|
return $verified;
|
||||||
|
|
||||||
|
} catch (Exception $e) {
|
||||||
|
debuglog("PAYPAL WEBHOOK: Signature verification error: " . $e->getMessage());
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//+++++++++++++++++++++++++++++++++++++++++++++++++++++
|
||||||
|
// Helper function to get PayPal access token
|
||||||
|
//+++++++++++++++++++++++++++++++++++++++++++++++++++++
|
||||||
|
function getPayPalAccessToken() {
|
||||||
|
$ch = curl_init(PAYPAL_URL . '/v1/oauth2/token');
|
||||||
|
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
|
||||||
|
curl_setopt($ch, CURLOPT_POST, true);
|
||||||
|
curl_setopt($ch, CURLOPT_POSTFIELDS, 'grant_type=client_credentials');
|
||||||
|
curl_setopt($ch, CURLOPT_USERPWD, PAYPAL_CLIENT_ID . ':' . PAYPAL_CLIENT_SECRET);
|
||||||
|
curl_setopt($ch, CURLOPT_HTTPHEADER, ['Content-Type: application/x-www-form-urlencoded']);
|
||||||
|
|
||||||
|
$response = curl_exec($ch);
|
||||||
|
$http_code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
||||||
|
curl_close($ch);
|
||||||
|
|
||||||
|
if ($http_code != 200) {
|
||||||
|
throw new Exception("Failed to get PayPal access token: HTTP $http_code");
|
||||||
|
}
|
||||||
|
|
||||||
|
$result = json_decode($response, true);
|
||||||
|
return $result['access_token'] ?? '';
|
||||||
|
}
|
||||||
|
|
||||||
|
?>
|
||||||
Reference in New Issue
Block a user