File manager - Edit - /home/webtrixia/.trash/api.php.1
Back
<?php header('Content-Type: application/json; charset=utf-8'); header('Access-Control-Allow-Origin: *'); header('Access-Control-Allow-Methods: GET, POST, DELETE, OPTIONS'); header('Access-Control-Allow-Headers: Content-Type'); if($_SERVER['REQUEST_METHOD']==='OPTIONS'){http_response_code(200);exit;} // ── CONFIG (created by w_hub_install.php on first run — never hardcoded here) ── if(!file_exists(__DIR__.'/config.php')){ http_response_code(200); echo json_encode(['ok'=>false,'error'=>'no_config', 'message'=>'Базата данни не е конфигурирана. Отвори w_hub_install.php, за да инсталираш хъба на този сървър.'],JSON_UNESCAPED_UNICODE); exit; } require __DIR__.'/config.php'; if(!defined('TG_TOKEN')) define('TG_TOKEN',''); if(!defined('TG_CHAT_ID')) define('TG_CHAT_ID',''); // ── SAFETY NET: never let a PHP error/exception leak raw HTML to the frontend ── set_exception_handler(function($e){ if(ob_get_length()) ob_clean(); http_response_code(500); header('Content-Type: application/json; charset=utf-8'); echo json_encode(['ok'=>false,'error'=>'Сървърна грешка: '.$e->getMessage()],JSON_UNESCAPED_UNICODE); exit; }); set_error_handler(function($severity,$message,$file,$line){ if(!(error_reporting() & $severity)) return false; throw new ErrorException($message,0,$severity,$file,$line); }); function tg($msg, $chatId=null){ $chat = $chatId ?: TG_CHAT_ID; if(empty($chat)) return; try { $url='https://api.telegram.org/bot'.TG_TOKEN.'/sendMessage'; $payload=json_encode(['chat_id'=>$chat,'text'=>$msg,'parse_mode'=>'HTML']); if(function_exists('curl_init')){ $ch=curl_init($url); curl_setopt_array($ch,[ CURLOPT_POST=>true, CURLOPT_POSTFIELDS=>$payload, CURLOPT_HTTPHEADER=>['Content-Type: application/json'], CURLOPT_RETURNTRANSFER=>true, CURLOPT_TIMEOUT=>5, CURLOPT_SSL_VERIFYPEER=>false, ]); curl_exec($ch); curl_close($ch); } else { $opts=['http'=>['method'=>'POST','header'=>'Content-Type: application/json', 'content'=>$payload,'timeout'=>3,'ignore_errors'=>true]]; @file_get_contents($url,false,stream_context_create($opts)); } } catch(Exception $e){ /* silent fail */ } } // ── DB ── function db(){ static $p; if(!$p){ try{ $p=new PDO('mysql:host='.DB_HOST.';dbname='.DB_NAME.';charset=utf8mb4', DB_USER,DB_PASS,[PDO::ATTR_ERRMODE=>PDO::ERRMODE_EXCEPTION, PDO::ATTR_DEFAULT_FETCH_MODE=>PDO::FETCH_ASSOC]); }catch(PDOException $e){ http_response_code(500); die(json_encode(['ok'=>false,'error'=>'DB: '.$e->getMessage()])); } } return $p; } function ok($d){echo json_encode(['ok'=>true,'data'=>$d],JSON_UNESCAPED_UNICODE);exit;} function err($m){http_response_code(400);echo json_encode(['ok'=>false,'error'=>$m],JSON_UNESCAPED_UNICODE);exit;} function body(){return json_decode(file_get_contents('php://input'),true)??[];} function hub_session_is_admin(): bool { return !empty($_SESSION['hub_is_admin']); } function requireHubAdminOrSecret($pdo, $secret): void { if (hub_session_is_admin()) return; checkLicense($pdo, $secret); } function clientIp(): string { foreach(['HTTP_CF_CONNECTING_IP','HTTP_X_REAL_IP','HTTP_X_FORWARDED_FOR','REMOTE_ADDR'] as $k){ $v=$_SERVER[$k]??''; if(!$v) continue; $ip=trim(explode(',', $v)[0]); if(filter_var($ip, FILTER_VALIDATE_IP)) return $ip; } return ''; } // ── DOCUMENT NUMBERING ── function nextDocNumber($pdo,$type){ $year=date('Y'); $pdo->prepare("INSERT INTO doc_counters (type,year,counter) VALUES (?,?,1) ON DUPLICATE KEY UPDATE counter=counter+1")->execute([$type,$year]); $c=$pdo->prepare("SELECT counter FROM doc_counters WHERE type=? AND year=?"); $c->execute([$type,$year]); return str_pad((string)$c->fetchColumn(),10,'0',STR_PAD_LEFT); } // ── RAW SMTP SENDER (no external libraries needed) ── function smtpSend($host,$port,$secure,$user,$pass,$fromEmail,$fromName,$to,$subject,$htmlBody){ $errno=0;$errstr=''; $fp=@fsockopen(($secure==='ssl'?'ssl://':'').$host,(int)$port,$errno,$errstr,12); if(!$fp) throw new Exception('Connect failed: '.$errstr); stream_set_timeout($fp,12); $read=function() use ($fp){ $d=''; while($str=fgets($fp,515)){ $d.=$str; if(isset($str[3])&&$str[3]===' ') break; } return $d; }; $write=function($cmd) use ($fp){ fwrite($fp,$cmd."\r\n"); }; $read(); $write('EHLO webtrixia.com'); $read(); if($secure==='tls'){ $write('STARTTLS'); $read(); stream_socket_enable_crypto($fp,true,STREAM_CRYPTO_METHOD_TLS_CLIENT); $write('EHLO webtrixia.com'); $read(); } $write('AUTH LOGIN'); $read(); $write(base64_encode($user)); $read(); $write(base64_encode($pass)); $r=$read(); if(strpos($r,'235')===false) throw new Exception('SMTP auth failed: '.$r); $write('MAIL FROM: <'.$fromEmail.'>'); $read(); $write('RCPT TO: <'.$to.'>'); $read(); $write('DATA'); $read(); $headers ="From: =?UTF-8?B?".base64_encode($fromName)."?= <{$fromEmail}>\r\n"; $headers.="To: <{$to}>\r\n"; $headers.="Subject: =?UTF-8?B?".base64_encode($subject)."?=\r\n"; $headers.="MIME-Version: 1.0\r\nContent-Type: text/html; charset=UTF-8\r\n"; $write($headers."\r\n".$htmlBody."\r\n."); $r=$read(); $write('QUIT'); fclose($fp); if(strpos($r,'250')===false) throw new Exception('Send failed: '.$r); return true; } $action=$_GET['action']??''; $method=$_SERVER['REQUEST_METHOD']; if(session_status()===PHP_SESSION_NONE) session_start(); function hub_license_status(){ static $status=null; if($status!==null) return $status; $guard=__DIR__.'/licenses/client/guard.php'; if(!file_exists($guard)){ return $status=['state'=>'active','modules'=>[],'message'=>'Локален режим.','offline'=>true]; } try{ require_once $guard; return $status=wt_guard_status(); }catch(Throwable $e){ return $status=['state'=>'active','modules'=>[],'message'=>'Лицензният контрол е временно недостъпен.','offline'=>true]; } } if($action==='license_status'){ ok(hub_license_status()); } $licensePublicActions=['install','install_platform','install_invoicing','branding_settings','license_status','test','tg_updates','tg_set_chat']; if(!in_array($action,$licensePublicActions,true)){ $ls=hub_license_status(); if(in_array($ls['state']??'active',['locked','invalid'],true)){ http_response_code(402); echo json_encode(['ok'=>false,'error'=>'license_locked','license'=>$ls, 'message'=>$ls['message']??'Лицензът не е активен.'],JSON_UNESCAPED_UNICODE); exit; } } // ── INSTALL ── if($action==='install'){ $pdo=db(); $tables=["CREATE TABLE IF NOT EXISTS users ( id INT AUTO_INCREMENT PRIMARY KEY, username VARCHAR(100) UNIQUE NOT NULL, password VARCHAR(255) NOT NULL, name VARCHAR(100), role VARCHAR(100), initials VARCHAR(5), color VARCHAR(20) DEFAULT 'av-p', is_admin TINYINT(1) DEFAULT 0, online TINYINT(1) DEFAULT 1, telegram_chat_id VARCHAR(50), created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP )","CREATE TABLE IF NOT EXISTS projects ( id VARCHAR(36) PRIMARY KEY, name VARCHAR(255) NOT NULL, client VARCHAR(255),phone VARCHAR(100), status VARCHAR(50) DEFAULT 'new', total DECIMAL(10,2) DEFAULT 0,received DECIMAL(10,2) DEFAULT 0, start_date DATE NULL,deadline DATE NULL,progress INT DEFAULT 0, assignee VARCHAR(100),tags TEXT,recurring VARCHAR(20),notes TEXT, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP )","CREATE TABLE IF NOT EXISTS tasks ( id VARCHAR(36) PRIMARY KEY,text TEXT NOT NULL, assignee VARCHAR(100),urgent TINYINT(1) DEFAULT 0,done TINYINT(1) DEFAULT 0, due_date DATETIME NULL,note TEXT, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP )","CREATE TABLE IF NOT EXISTS notes ( id VARCHAR(36) PRIMARY KEY,title VARCHAR(255) NOT NULL, body TEXT,author VARCHAR(100),urgent TINYINT(1) DEFAULT 0, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP )","CREATE TABLE IF NOT EXISTS clients ( id VARCHAR(36) PRIMARY KEY,name VARCHAR(255) NOT NULL, company VARCHAR(255),email VARCHAR(255),phone VARCHAR(100),eik VARCHAR(50),notes TEXT, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP )","CREATE TABLE IF NOT EXISTS hosting ( id VARCHAR(36) PRIMARY KEY,domain VARCHAR(255) NOT NULL, client VARCHAR(255),server VARCHAR(255),type VARCHAR(50),disk VARCHAR(50),plan VARCHAR(50), ssl_expires DATE NULL,created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP )","CREATE TABLE IF NOT EXISTS passwords ( id VARCHAR(36) PRIMARY KEY,site VARCHAR(255) NOT NULL, username VARCHAR(255),pass VARCHAR(500),category VARCHAR(50),client VARCHAR(255), created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP )","CREATE TABLE IF NOT EXISTS offers ( id VARCHAR(36) PRIMARY KEY,num VARCHAR(50),client VARCHAR(255), service VARCHAR(255),total DECIMAL(10,2) DEFAULT 0,status VARCHAR(50) DEFAULT 'Чакаща', offer_type VARCHAR(30) DEFAULT 'one_time', details LONGTEXT, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP )","CREATE TABLE IF NOT EXISTS contracts ( id VARCHAR(36) PRIMARY KEY,name VARCHAR(255),client VARCHAR(255), sign_date DATE NULL,created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP )","CREATE TABLE IF NOT EXISTS presence ( id INT AUTO_INCREMENT PRIMARY KEY,user_id INT NOT NULL, status VARCHAR(30) NOT NULL, ip_address VARCHAR(60), user_agent VARCHAR(255), latitude DECIMAL(10,7) NULL, longitude DECIMAL(10,7) NULL, accuracy DECIMAL(10,2) NULL, location_note VARCHAR(255), created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP )","CREATE TABLE IF NOT EXISTS activity ( id INT AUTO_INCREMENT PRIMARY KEY,who VARCHAR(100),what TEXT, color VARCHAR(20),initials VARCHAR(5), created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP )"]; foreach($tables as $sql) $pdo->exec($sql); // Targets table (separate because it was added later) $pdo->exec("CREATE TABLE IF NOT EXISTS targets ( id INT AUTO_INCREMENT PRIMARY KEY, user_id INT NOT NULL, created_by INT NOT NULL, title VARCHAR(255) NOT NULL, description TEXT, target_value INT NOT NULL, current_value INT DEFAULT 0, unit VARCHAR(50) DEFAULT 'бр.', date DATE NOT NULL, status VARCHAR(20) DEFAULT 'active', created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP )"); // Safe column additions $alters=[ "ALTER TABLE tasks ADD COLUMN due_date DATETIME NULL", "ALTER TABLE tasks ADD COLUMN note TEXT", "ALTER TABLE users ADD COLUMN telegram_chat_id VARCHAR(50)", "ALTER TABLE passwords ADD COLUMN client VARCHAR(255)", "ALTER TABLE offers ADD COLUMN offer_type VARCHAR(30) DEFAULT 'one_time'", "ALTER TABLE offers ADD COLUMN details LONGTEXT", "ALTER TABLE offers ADD COLUMN updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP", "ALTER TABLE presence ADD COLUMN ip_address VARCHAR(60)", "ALTER TABLE presence ADD COLUMN user_agent VARCHAR(255)", "ALTER TABLE presence ADD COLUMN latitude DECIMAL(10,7) NULL", "ALTER TABLE presence ADD COLUMN longitude DECIMAL(10,7) NULL", "ALTER TABLE presence ADD COLUMN accuracy DECIMAL(10,2) NULL", "ALTER TABLE presence ADD COLUMN location_note VARCHAR(255)", ]; foreach($alters as $sql){try{$pdo->exec($sql);}catch(Exception $e){}} // Default admin $cnt=$pdo->query("SELECT COUNT(*) FROM users")->fetchColumn(); if($cnt==0){ $pdo->prepare("INSERT INTO users (username,password,name,role,initials,color,is_admin) VALUES (?,?,?,?,?,?,1)") ->execute(['admin',password_hash('webtrixia2025',PASSWORD_DEFAULT),'Администратор','Admin','АД','av-p']); } ok('Tables ready'); } // ── GET TELEGRAM CHAT ID (helper) ── if($action==='tg_updates'){ // Call this to find your chat_id after writing /start to the bot $url='https://api.telegram.org/bot'.TG_TOKEN.'/getUpdates'; $resp=@file_get_contents($url); $data=json_decode($resp,true); $chats=[]; foreach($data['result']??[] as $upd){ $chat=$upd['message']['chat']??$upd['channel_post']['chat']??null; if($chat) $chats[]=['id'=>$chat['id'],'type'=>$chat['type'],'title'=>$chat['title']??$chat['first_name']??'']; } ok(['updates'=>count($data['result']??[]),'chats'=>array_values(array_unique($chats,SORT_REGULAR))]); } // ── SET TELEGRAM CHAT ID ── if($action==='tg_set_chat'){ // GET: api.php?action=tg_set_chat&chat_id=-100xxxxxxx $chat_id=$_GET['chat_id']??''; if(!$chat_id) err('Missing chat_id'); $file=__DIR__.'/config.php'; $content=file_get_contents($file); if(strpos($content,'TG_CHAT_ID')!==false){ $content=preg_replace( "/define\\('TG_CHAT_ID',\\s*'[^']*'\\s*\\);/", "define('TG_CHAT_ID','{$chat_id}');", $content ); } else { $content=rtrim($content)."\ndefine('TG_CHAT_ID','{$chat_id}');\n"; } file_put_contents($file,$content); tg("✅ <b>Webtrixia Hub</b>\n\nТелеграм известията са активирани!\n\nВсяко присъствие на екипа ще бъде докладвано тук."); ok('Chat ID saved: '.$chat_id); } // ── LOGIN ── if($action==='login'){ $b=body(); $stmt=db()->prepare("SELECT * FROM users WHERE username=?"); $stmt->execute([$b['username']??'']); $u=$stmt->fetch(); if($u&&password_verify($b['password']??'',$u['password'])){ $_SESSION['hub_user_id']=(int)$u['id']; $_SESSION['hub_is_admin']=((int)($u['is_admin']??0)===1); unset($u['password']); ok($u); } else err('Грешно потребителско име или парола'); } // ── USERS ── if($action==='users'){ $pdo=db(); if($method==='GET'){ $rows=$pdo->query("SELECT id,username,name,role,initials,color,is_admin,online,telegram_chat_id FROM users ORDER BY id")->fetchAll(); ok($rows); } if($method==='POST'){ $b=body(); if(isset($b['id'])&&$b['id']){ if(!empty($b['password'])){ $stmt=$pdo->prepare("UPDATE users SET name=?,role=?,initials=?,color=?,is_admin=?,online=?,username=?,password=?,telegram_chat_id=? WHERE id=?"); $stmt->execute([$b['name'],$b['role'],$b['initials'],$b['color'],$b['is_admin']??0,$b['online']??1, $b['username'],password_hash($b['password'],PASSWORD_DEFAULT),$b['telegram_chat_id']??null,$b['id']]); } else { $stmt=$pdo->prepare("UPDATE users SET name=?,role=?,initials=?,color=?,is_admin=?,online=?,username=?,telegram_chat_id=? WHERE id=?"); $stmt->execute([$b['name'],$b['role'],$b['initials'],$b['color'],$b['is_admin']??0,$b['online']??1, $b['username'],$b['telegram_chat_id']??null,$b['id']]); } ok(['id'=>$b['id']]); } else { $pdo->prepare("INSERT INTO users (username,password,name,role,initials,color,is_admin,telegram_chat_id) VALUES (?,?,?,?,?,?,?,?)") ->execute([$b['username'],password_hash($b['password']??'hub2025',PASSWORD_DEFAULT), $b['name'],$b['role'],$b['initials'],$b['color']??'av-p',$b['is_admin']??0,$b['telegram_chat_id']??null]); ok(['id'=>$pdo->lastInsertId()]); } } if($method==='DELETE'){ $id=$_GET['id']??''; db()->prepare("DELETE FROM users WHERE id=? AND is_admin=0")->execute([$id]); ok(true); } } // ── PRESENCE ── if($action==='presence'){ $pdo=db(); foreach([ "ALTER TABLE presence ADD COLUMN ip_address VARCHAR(60)", "ALTER TABLE presence ADD COLUMN user_agent VARCHAR(255)", "ALTER TABLE presence ADD COLUMN latitude DECIMAL(10,7) NULL", "ALTER TABLE presence ADD COLUMN longitude DECIMAL(10,7) NULL", "ALTER TABLE presence ADD COLUMN accuracy DECIMAL(10,2) NULL", "ALTER TABLE presence ADD COLUMN location_note VARCHAR(255)", ] as $sql){try{$pdo->exec($sql);}catch(Exception $e){}} if($method==='GET'){ $log=$pdo->query("SELECT p.*,u.name,u.initials,u.color FROM presence p JOIN users u ON p.user_id=u.id WHERE DATE(p.created_at)=CURDATE() ORDER BY p.created_at DESC")->fetchAll(); $current=$pdo->query("SELECT p.*,u.name,u.initials,u.color FROM presence p JOIN users u ON p.user_id=u.id WHERE p.id IN (SELECT MAX(id) FROM presence WHERE DATE(created_at)=CURDATE() GROUP BY user_id)")->fetchAll(); ok(['log'=>$log,'current'=>$current]); } if($method==='POST'){ $b=body(); $user_id=(int)($b['user_id']??0); $status=$b['status']??''; if(!$user_id||!$status) err('Missing data: user_id='.$user_id.' status='.$status); try { $lat=is_numeric($b['latitude']??null)?$b['latitude']:null; $lng=is_numeric($b['longitude']??null)?$b['longitude']:null; $acc=is_numeric($b['accuracy']??null)?$b['accuracy']:null; $note=substr((string)($b['location_note']??''),0,255); $ip=clientIp(); $ua=substr((string)($_SERVER['HTTP_USER_AGENT']??''),0,255); // Insert presence record $pdo->prepare("INSERT INTO presence (user_id,status,ip_address,user_agent,latitude,longitude,accuracy,location_note) VALUES (?,?,?,?,?,?,?,?)")->execute([$user_id,$status,$ip,$ua,$lat,$lng,$acc,$note]); // Get user info $user=$pdo->prepare("SELECT name,role,telegram_chat_id FROM users WHERE id=?"); $user->execute([$user_id]); $u=$user->fetch(); // Status emojis and messages $icons=['arrived'=>'🟢','break'=>'☕','back'=>'🔵','left'=>'🔴','toilet'=>'🚽','smoke'=>'🚬','smoke_back'=>'💨']; $labels=['arrived'=>'Пристигна на работа','break'=>'Излезе на почивка','back'=>'Върна се от почивка','left'=>'Тръгна от офиса','toilet'=>'Излезе до тоалетна','smoke'=>'Излезе на цигара','smoke_back'=>'Върна се от цигара']; $icon=$icons[$status]??'⚪'; $label=$labels[$status]??$status; $time=date('H:i'); $name=$u['name']??'Служител'; $role=$u['role']??''; // Send Telegram to GROUP/CHANNEL $msg="{$icon} <b>{$name}</b>" . ($role?" ({$role})":"") . "\n{$label}\n<i>🕐 {$time}</i>"; tg($msg); // Also send personal TG if user has their own chat_id $personalChatId=$u['telegram_chat_id']??''; if(!empty($personalChatId)&&$personalChatId!==TG_CHAT_ID){ $personalMsgs=[ 'arrived'=>"🟢 <b>Добре дошъл на работа!</b>\nОтбелязан си: {$time}", 'break'=>"☕ <b>Приятна почивка!</b>\nИзлезна в: {$time}", 'back'=>"🔵 <b>Добре дошъл обратно!</b>\nВърна се: {$time}", 'left'=>"🔴 <b>Лека вечер!</b>\nТръгна в: {$time}" ]; tg($personalMsgs[$status]??$msg, $personalChatId); } } catch(Exception $e) { err('Presence error: '.$e->getMessage()); } ok(true); } } // ── PROJECTS ── if($action==='projects'){ $pdo=db(); if($method==='GET') ok($pdo->query("SELECT * FROM projects ORDER BY created_at DESC")->fetchAll()); if($method==='POST'){ $b=body();$id=$b['id']??uniqid('p',true); $tags=isset($b['tags'])?json_encode($b['tags'],JSON_UNESCAPED_UNICODE):'[]'; $pdo->prepare("INSERT INTO projects (id,name,client,phone,status,total,received,start_date,deadline,progress,assignee,tags,recurring,notes) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?) ON DUPLICATE KEY UPDATE name=VALUES(name),client=VALUES(client),phone=VALUES(phone), status=VALUES(status),total=VALUES(total),received=VALUES(received), start_date=VALUES(start_date),deadline=VALUES(deadline),progress=VALUES(progress), assignee=VALUES(assignee),tags=VALUES(tags),recurring=VALUES(recurring),notes=VALUES(notes)") ->execute([$id,$b['name']??'',$b['client']??'',$b['phone']??'',$b['status']??'new', $b['total']??0,$b['received']??0,$b['start']??null,$b['deadline']??null, $b['progress']??0,$b['assignee']??'',$tags,$b['recurring']??'',$b['notes']??'']); ok(['id'=>$id]); } if($method==='DELETE'){$pdo->prepare("DELETE FROM projects WHERE id=?")->execute([$_GET['id']??'']);ok(true);} } // ── TASKS ── if($action==='tasks'){ $pdo=db(); if($method==='GET') ok($pdo->query("SELECT * FROM tasks ORDER BY due_date IS NULL, due_date ASC, created_at DESC")->fetchAll()); if($method==='POST'){ $b=body();$id=$b['id']??uniqid('t',true); $pdo->prepare("INSERT INTO tasks (id,text,assignee,urgent,done,due_date,note) VALUES (?,?,?,?,?,?,?) ON DUPLICATE KEY UPDATE text=VALUES(text),assignee=VALUES(assignee), urgent=VALUES(urgent),done=VALUES(done),due_date=VALUES(due_date),note=VALUES(note)") ->execute([$id,$b['text']??'',$b['assignee']??'',$b['urgent']??0,$b['done']??0, $b['due_date']??null,$b['note']??'']); ok(['id'=>$id]); } if($method==='DELETE'){$pdo->prepare("DELETE FROM tasks WHERE id=?")->execute([$_GET['id']??'']);ok(true);} } // ── NOTES ── if($action==='notes'){ $pdo=db(); if($method==='GET') ok($pdo->query("SELECT * FROM notes ORDER BY created_at DESC")->fetchAll()); if($method==='POST'){ $b=body();$id=$b['id']??uniqid('n',true); $pdo->prepare("INSERT INTO notes (id,title,body,author,urgent) VALUES (?,?,?,?,?) ON DUPLICATE KEY UPDATE title=VALUES(title),body=VALUES(body),author=VALUES(author),urgent=VALUES(urgent)") ->execute([$id,$b['title']??'',$b['body']??'',$b['author']??'',$b['urgent']??0]); ok(['id'=>$id]); } if($method==='DELETE'){$pdo->prepare("DELETE FROM notes WHERE id=?")->execute([$_GET['id']??'']);ok(true);} } // ── CLIENTS ── if($action==='clients'){ $pdo=db(); if($method==='GET') ok($pdo->query("SELECT * FROM clients ORDER BY created_at DESC")->fetchAll()); if($method==='POST'){ $b=body();$id=$b['id']??uniqid('c',true); $pdo->prepare("INSERT INTO clients (id,name,company,email,phone,eik,notes,vat_number,address,city,country,mol,iban,contact_person) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?) ON DUPLICATE KEY UPDATE name=VALUES(name),company=VALUES(company),email=VALUES(email), phone=VALUES(phone),eik=VALUES(eik),notes=VALUES(notes),vat_number=VALUES(vat_number), address=VALUES(address),city=VALUES(city),country=VALUES(country),mol=VALUES(mol), iban=VALUES(iban),contact_person=VALUES(contact_person)") ->execute([$id,$b['name']??'',$b['company']??'',$b['email']??'',$b['phone']??'',$b['eik']??'',$b['notes']??'', $b['vat_number']??'',$b['address']??'',$b['city']??'',$b['country']??'България',$b['mol']??'', $b['iban']??'',$b['contact_person']??'']); ok(['id'=>$id]); } if($method==='DELETE'){$pdo->prepare("DELETE FROM clients WHERE id=?")->execute([$_GET['id']??'']);ok(true);} } // ── HOSTING ── if($action==='hosting'){ $pdo=db(); if($method==='GET') ok($pdo->query("SELECT * FROM hosting ORDER BY created_at DESC")->fetchAll()); if($method==='POST'){ $b=body();$id=$b['id']??uniqid('h',true); $pdo->prepare("INSERT INTO hosting (id,domain,client,server,type,disk,plan,ssl_expires) VALUES (?,?,?,?,?,?,?,?) ON DUPLICATE KEY UPDATE domain=VALUES(domain),client=VALUES(client),server=VALUES(server), type=VALUES(type),disk=VALUES(disk),plan=VALUES(plan),ssl_expires=VALUES(ssl_expires)") ->execute([$id,$b['domain']??'',$b['client']??'',$b['server']??'',$b['type']??'cPanel', $b['disk']??'',$b['plan']??'Starter',$b['ssl']??null]); ok(['id'=>$id]); } if($method==='DELETE'){$pdo->prepare("DELETE FROM hosting WHERE id=?")->execute([$_GET['id']??'']);ok(true);} } // ── PASSWORDS ── if($action==='passwords'){ $pdo=db(); try{$pdo->exec("ALTER TABLE passwords ADD COLUMN client VARCHAR(255)");}catch(Exception $e){} if($method==='GET') ok($pdo->query("SELECT * FROM passwords ORDER BY client, created_at DESC")->fetchAll()); if($method==='POST'){ $b=body();$id=$b['id']??uniqid('pw',true); $pdo->prepare("INSERT INTO passwords (id,site,username,pass,category,client) VALUES (?,?,?,?,?,?) ON DUPLICATE KEY UPDATE site=VALUES(site),username=VALUES(username),pass=VALUES(pass), category=VALUES(category),client=VALUES(client)") ->execute([$id,$b['site']??'',$b['user']??'',$b['pass']??'',$b['cat']??'other',$b['client']??'']); ok(['id'=>$id]); } if($method==='DELETE'){$pdo->prepare("DELETE FROM passwords WHERE id=?")->execute([$_GET['id']??'']);ok(true);} } // ── OFFERS ── if($action==='offers'){ $pdo=db(); foreach([ "ALTER TABLE offers ADD COLUMN offer_type VARCHAR(30) DEFAULT 'one_time'", "ALTER TABLE offers ADD COLUMN details LONGTEXT", "ALTER TABLE offers ADD COLUMN updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP", ] as $sql){try{$pdo->exec($sql);}catch(Exception $e){}} if($method==='GET'){ if(!empty($_GET['id'])){ $st=$pdo->prepare("SELECT * FROM offers WHERE id=?"); $st->execute([$_GET['id']]); $row=$st->fetch(); if(!$row) err('Офертата не е намерена'); ok($row); } ok($pdo->query("SELECT * FROM offers ORDER BY created_at DESC")->fetchAll()); } if($method==='POST'){ $b=body();$id=$b['id']??uniqid('o',true); $details=$b['details']??null; if(is_array($details)) $details=json_encode($details,JSON_UNESCAPED_UNICODE); $pdo->prepare("INSERT INTO offers (id,num,client,service,total,status,offer_type,details) VALUES (?,?,?,?,?,?,?,?) ON DUPLICATE KEY UPDATE num=VALUES(num),client=VALUES(client),service=VALUES(service), total=VALUES(total),status=VALUES(status),offer_type=VALUES(offer_type),details=VALUES(details)") ->execute([$id,$b['num']??'',$b['client']??'',$b['service']??'',$b['total']??0, $b['status']??'Чакаща',$b['offer_type']??'one_time',$details]); ok(['id'=>$id]); } if($method==='DELETE'){$pdo->prepare("DELETE FROM offers WHERE id=?")->execute([$_GET['id']??'']);ok(true);} } // ── CONTRACTS ── if($action==='contracts'){ $pdo=db(); if($method==='GET') ok($pdo->query("SELECT * FROM contracts ORDER BY created_at DESC")->fetchAll()); if($method==='POST'){ $b=body();$id=$b['id']??uniqid('con',true); $pdo->prepare("INSERT INTO contracts (id,name,client,sign_date) VALUES (?,?,?,?) ON DUPLICATE KEY UPDATE name=VALUES(name),client=VALUES(client),sign_date=VALUES(sign_date)") ->execute([$id,$b['name']??'',$b['client']??'',$b['sign_date']??null]); ok(['id'=>$id]); } if($method==='DELETE'){$pdo->prepare("DELETE FROM contracts WHERE id=?")->execute([$_GET['id']??'']);ok(true);} } // ── ACTIVITY ── if($action==='activity'){ $pdo=db(); if($method==='GET') ok($pdo->query("SELECT * FROM activity ORDER BY created_at DESC LIMIT 50")->fetchAll()); if($method==='POST'){ $b=body(); $pdo->prepare("INSERT INTO activity (who,what,color,initials) VALUES (?,?,?,?)") ->execute([$b['who']??'Система',$b['what']??'',$b['color']??'av-p',$b['initials']??'??']); ok(true); } } // ── STATS ── if($action==='stats'){ $pdo=db(); $total=$pdo->query("SELECT COALESCE(SUM(total),0) FROM projects WHERE status!='done'")->fetchColumn(); $recv=$pdo->query("SELECT COALESCE(SUM(received),0) FROM projects WHERE status!='done'")->fetchColumn(); $active=$pdo->query("SELECT COUNT(*) FROM projects WHERE status!='done'")->fetchColumn(); $tasks=$pdo->query("SELECT COUNT(*) FROM tasks WHERE done=0")->fetchColumn(); $ssl=$pdo->query("SELECT COUNT(*) FROM hosting WHERE ssl_expires IS NOT NULL AND ssl_expires<DATE_ADD(NOW(),INTERVAL 30 DAY) AND ssl_expires>NOW()")->fetchColumn(); ok(['active_projects'=>(int)$active,'total_offered'=>(float)$total,'total_received'=>(float)$recv, 'total_pending'=>(float)($total-$recv),'tasks_pending'=>(int)$tasks,'ssl_warnings'=>(int)$ssl]); } // ── TARGETS ── if($action==='targets'){ $pdo=db(); if($method==='GET'){ $date=$_GET['date']??date('Y-m-d'); $user_id=$_GET['user_id']??null; if($user_id){ $rows=$pdo->prepare("SELECT t.*, u.name as user_name, u.initials, u.color, cb.name as created_by_name FROM targets t JOIN users u ON t.user_id=u.id JOIN users cb ON t.created_by=cb.id WHERE t.date=? AND t.user_id=? ORDER BY t.created_at DESC"); $rows->execute([$date,$user_id]); } else { $rows=$pdo->prepare("SELECT t.*, u.name as user_name, u.initials, u.color, cb.name as created_by_name FROM targets t JOIN users u ON t.user_id=u.id JOIN users cb ON t.created_by=cb.id WHERE t.date=? ORDER BY t.user_id, t.created_at DESC"); $rows->execute([$date]); } ok($rows->fetchAll()); } if($method==='POST'){ $b=body(); $id=$b['id']??null; if($id){ // Update existing (progress update) $pdo->prepare("UPDATE targets SET current_value=?, status=? WHERE id=?") ->execute([$b['current_value']??0, $b['status']??'active', $id]); ok(['id'=>$id]); } else { // Create new target $pdo->prepare("INSERT INTO targets (user_id,created_by,title,description,target_value,current_value,unit,date,status) VALUES (?,?,?,?,?,?,?,?,?)") ->execute([$b['user_id'],$b['created_by'],$b['title'],$b['description']??'',$b['target_value'],$b['current_value']??0,$b['unit']??'бр.',$b['date']??date('Y-m-d'),'active']); ok(['id'=>$pdo->lastInsertId()]); } } if($method==='DELETE'){ $pdo->prepare("DELETE FROM targets WHERE id=?")->execute([$_GET['id']??0]); ok(true); } } // ── TEST ── if($action==='test'){ $result=['php'=>phpversion(),'db'=>'error','tg'=>'not sent']; try{ db(); $result['db']='OK'; }catch(Exception $e){ $result['db']=$e->getMessage(); } try{ tg("🧪 Test от Webtrixia Hub - ".date('H:i')); $result['tg']='Sent!'; }catch(Exception $e){ $result['tg_error']=$e->getMessage(); } ok($result); } // ── PRESENCE REPORT ── if($action==='presence_report'){ $pdo=db(); $from=$_GET['from']??date('Y-m-01'); // default: first of month $to=$_GET['to']??date('Y-m-d'); // Get all presence records in range $rows=$pdo->prepare(" SELECT p.user_id, p.status, p.created_at, u.name, u.initials, u.color FROM presence p JOIN users u ON p.user_id=u.id WHERE DATE(p.created_at) BETWEEN ? AND ? ORDER BY p.user_id, p.created_at ASC "); $rows->execute([$from,$to]); $records=$rows->fetchAll(); // Group by user and date $byUser=[]; foreach($records as $r){ $uid=$r['user_id']; $date=substr($r['created_at'],0,10); if(!isset($byUser[$uid])){ $byUser[$uid]=['id'=>$uid,'name'=>$r['name'],'initials'=>$r['initials'],'color'=>$r['color'],'days'=>[],'total_minutes'=>0,'days_worked'=>0]; } if(!isset($byUser[$uid]['days'][$date])){ $byUser[$uid]['days'][$date]=['events'=>[],'work_minutes'=>0,'break_minutes'=>0,'smoke_minutes'=>0,'toilet_count'=>0]; } $byUser[$uid]['days'][$date]['events'][]=['status'=>$r['status'],'time'=>$r['created_at']]; } // Calculate hours per user per day foreach($byUser as $uid=>&$user){ foreach($user['days'] as $date=>&$day){ $events=$day['events']; $workStart=null;$breakStart=null;$smokeStart=null; $workMin=0;$breakMin=0;$smokeMin=0;$toiletCount=0; foreach($events as $ev){ $t=strtotime($ev['time']); $s=$ev['status']; if($s==='arrived'||$s==='back'||$s==='smoke_back'){ $workStart=$t; if($s==='smoke_back'&&$smokeStart){ $smokeMin+=round(($t-$smokeStart)/60);$smokeStart=null; } } elseif($s==='break'||$s==='left'){ if($workStart){$workMin+=round(($t-$workStart)/60);$workStart=null;} if($s==='break')$breakStart=$t; } elseif($s==='smoke'){ if($workStart){$workMin+=round(($t-$workStart)/60);$workStart=null;} $smokeStart=$t; } elseif($s==='toilet'){ $toiletCount++; } } // If still working (no left event today) if($workStart){ $now=strtotime('now'); $workMin+=round(($now-$workStart)/60); } $day['work_minutes']=$workMin; $day['break_minutes']=$breakMin; $day['smoke_minutes']=$smokeMin; $day['toilet_count']=$toiletCount; $day['work_hours']=round($workMin/60,1); $day['timeline']=array_map(function($e){return ['status'=>$e['status'],'time'=>substr($e['time'],11,5)];}, $events); if($workMin>0){ $user['total_minutes']+=$workMin; $user['days_worked']++; } } $user['total_hours']=round($user['total_minutes']/60,1); $user['avg_hours_per_day']=$user['days_worked']>0?round($user['total_minutes']/$user['days_worked']/60,1):0; unset($user); } // Summary stats $totalUsers=count($byUser); $allHours=array_sum(array_column($byUser,'total_hours')); ok([ 'from'=>$from, 'to'=>$to, 'users'=>array_values($byUser), 'total_hours'=>round($allHours,1), 'total_users'=>$totalUsers ]); } // ══════════════════════════════════════════════════════ // ── INVOICING + HR MODULE ── // ══════════════════════════════════════════════════════ // ── ONE-TIME INSTALL FOR NEW TABLES ── if($action==='install_invoicing'){ $pdo=db(); $alters=[ "ALTER TABLE clients ADD COLUMN vat_number VARCHAR(50)", "ALTER TABLE clients ADD COLUMN address VARCHAR(500)", "ALTER TABLE clients ADD COLUMN city VARCHAR(150)", "ALTER TABLE clients ADD COLUMN country VARCHAR(100) DEFAULT 'България'", "ALTER TABLE clients ADD COLUMN mol VARCHAR(255)", "ALTER TABLE clients ADD COLUMN iban VARCHAR(50)", "ALTER TABLE clients ADD COLUMN contact_person VARCHAR(255)", ]; foreach($alters as $sql){try{$pdo->exec($sql);}catch(Exception $e){}} $pdo->exec("CREATE TABLE IF NOT EXISTS company_settings ( id INT PRIMARY KEY, name VARCHAR(255), eik VARCHAR(50), vat_number VARCHAR(50), address VARCHAR(500), city VARCHAR(150), country VARCHAR(100) DEFAULT 'България', mol VARCHAR(255), iban VARCHAR(50), bic VARCHAR(20), bank_name VARCHAR(255), logo_url VARCHAR(500), default_vat_percent DECIMAL(5,2) DEFAULT 20, currency VARCHAR(10) DEFAULT 'EUR', prefix_proforma VARCHAR(20) DEFAULT '', prefix_invoice VARCHAR(20) DEFAULT '', prefix_credit_note VARCHAR(20) DEFAULT 'КИ', prefix_debit_note VARCHAR(20) DEFAULT 'ДИ', prefix_offer VARCHAR(20) DEFAULT 'ОФ', prefix_quote VARCHAR(20) DEFAULT 'ОФ', smtp_host VARCHAR(255), smtp_port INT DEFAULT 587, smtp_user VARCHAR(255), smtp_pass VARCHAR(255), smtp_secure VARCHAR(10) DEFAULT 'tls', smtp_from_name VARCHAR(255), smtp_from_email VARCHAR(255) )"); $cnt=$pdo->query("SELECT COUNT(*) FROM company_settings")->fetchColumn(); if($cnt==0){ $pdo->exec("INSERT INTO company_settings (id,name,eik,vat_number,address,city,mol,iban,bic,bank_name) VALUES (1,'УЕБТРИКСИА ЕООД','207419451','BG207419451','ул. Мусала 11','6750 гр. Ардино', 'ХАКАН АЛКАН ОСМАН','BG76DEMI92401000318480','DEMIBGSF','Търговска Банка Д АД')"); } $pdo->exec("CREATE TABLE IF NOT EXISTS doc_counters ( type VARCHAR(20) NOT NULL, year INT NOT NULL, counter INT DEFAULT 0, PRIMARY KEY(type,year) )"); $pdo->exec("CREATE TABLE IF NOT EXISTS documents ( id VARCHAR(36) PRIMARY KEY, type VARCHAR(20) NOT NULL, number VARCHAR(50), full_number VARCHAR(50), client_id VARCHAR(36), client_snapshot TEXT, issue_date DATE, tax_date DATE, due_date DATE NULL, items TEXT, subtotal DECIMAL(12,2) DEFAULT 0, vat_percent DECIMAL(5,2) DEFAULT 20, vat_amount DECIMAL(12,2) DEFAULT 0, total DECIMAL(12,2) DEFAULT 0, currency VARCHAR(10) DEFAULT 'EUR', status VARCHAR(30) DEFAULT 'draft', payment_method VARCHAR(50) DEFAULT 'bank', notes TEXT, related_doc_id VARCHAR(36) NULL, recurring_id VARCHAR(36) NULL, created_by VARCHAR(100), created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP )"); $pdo->exec("CREATE TABLE IF NOT EXISTS recurring_invoices ( id VARCHAR(36) PRIMARY KEY, client_id VARCHAR(36), doc_type VARCHAR(20) DEFAULT 'invoice', items TEXT, vat_percent DECIMAL(5,2) DEFAULT 20, notes TEXT, interval_type VARCHAR(20) DEFAULT 'monthly', next_run_date DATE, active TINYINT(1) DEFAULT 1, last_generated_at TIMESTAMP NULL, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP )"); $pdo->exec("CREATE TABLE IF NOT EXISTS hr_requests ( id VARCHAR(36) PRIMARY KEY, user_id INT, employee_name VARCHAR(255), position VARCHAR(255), egn VARCHAR(20), type VARCHAR(30) NOT NULL, start_date DATE NULL, end_date DATE NULL, days INT DEFAULT 0, notice_days INT DEFAULT 0, last_work_date DATE NULL, reason TEXT, status VARCHAR(20) DEFAULT 'pending', approved_by VARCHAR(100), created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP )"); $pdo->exec("CREATE TABLE IF NOT EXISTS email_log ( id INT AUTO_INCREMENT PRIMARY KEY, document_id VARCHAR(36), to_email VARCHAR(255), subject VARCHAR(500), status VARCHAR(20), error TEXT, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP )"); ok('Invoicing + HR tables ready'); } // ── COMPANY SETTINGS (own company profile + numbering prefixes + SMTP) ── if($action==='company_settings'){ $pdo=db(); if($method==='GET'){ $r=$pdo->query("SELECT * FROM company_settings WHERE id=1")->fetch(); ok($r?:[]); } if($method==='POST'){ $b=body(); $fields=['name','eik','vat_number','address','city','country','mol','iban','bic','bank_name','logo_url', 'default_vat_percent','currency','prefix_proforma','prefix_invoice','prefix_credit_note','prefix_debit_note', 'prefix_offer','prefix_quote','smtp_host','smtp_port','smtp_user','smtp_pass','smtp_secure','smtp_from_name','smtp_from_email']; $set=[];$vals=[]; foreach($fields as $f){ if(array_key_exists($f,$b)){ $set[]="$f=?"; $vals[]=$b[$f]; } } if($set) $pdo->prepare("UPDATE company_settings SET ".implode(',',$set)." WHERE id=1")->execute($vals); ok(true); } } // ── EIK / VAT LOOKUP (via official free EU VIES service) ── if($action==='eik_lookup'){ $eik=preg_replace('/\D/','',$_GET['eik']??''); $country=strtoupper($_GET['country']??'BG'); if(!$eik) err('Missing eik'); $vat=substr($eik,0,9); try{ $ch=curl_init('https://ec.europa.eu/taxation_customs/vies/rest-api/check-vat-number'); curl_setopt_array($ch,[ CURLOPT_POST=>true, CURLOPT_POSTFIELDS=>json_encode(['countryCode'=>$country,'vatNumber'=>$vat]), CURLOPT_HTTPHEADER=>['Content-Type: application/json'], CURLOPT_RETURNTRANSFER=>true,CURLOPT_TIMEOUT=>8,CURLOPT_SSL_VERIFYPEER=>false, ]); $resp=curl_exec($ch);curl_close($ch); $data=json_decode($resp,true); if($data && !empty($data['valid'])){ ok(['found'=>true,'name'=>$data['name']??'','address'=>$data['address']??'', 'vat_number'=>$country.$vat,'eik'=>$eik]); } else { ok(['found'=>false,'message'=>'Няма активна ДДС регистрация в VIES за този номер — въведете данните ръчно.']); } }catch(Exception $e){ ok(['found'=>false,'message'=>'VIES услугата на ЕС временно недостъпна — въведете данните ръчно.']); } } // ── DOCUMENTS (proforma / invoice / credit_note / debit_note / offer / quote) ── if($action==='documents'){ $pdo=db(); if($method==='GET'){ if(!empty($_GET['id'])){ $s=$pdo->prepare("SELECT * FROM documents WHERE id=?"); $s->execute([$_GET['id']]); $d=$s->fetch(); if($d){$d['items']=json_decode($d['items'],true);$d['client_snapshot']=json_decode($d['client_snapshot'],true);} ok($d); } $where=[];$params=[]; foreach(['type','client_id','status'] as $f){ if(!empty($_GET[$f])){$where[]="$f=?";$params[]=$_GET[$f];} } $sql="SELECT * FROM documents".($where?' WHERE '.implode(' AND ',$where):'')." ORDER BY created_at DESC"; $st=$pdo->prepare($sql);$st->execute($params); $rows=$st->fetchAll(); foreach($rows as &$r){$r['items']=json_decode($r['items'],true);$r['client_snapshot']=json_decode($r['client_snapshot'],true);} ok($rows); } if($method==='POST'){ $b=body(); $type=$b['type']??'invoice'; $isNew=empty($b['id']); $id=$b['id']??uniqid('d',true); $items=$b['items']??[]; $subtotal=0;foreach($items as $it){$subtotal+=(float)($it['qty']??1)*(float)($it['price']??0);} $vatPercent=isset($b['vat_percent'])?(float)$b['vat_percent']:20; $vatAmount=round($subtotal*$vatPercent/100,2); $total=round($subtotal+$vatAmount,2); $comp=$pdo->query("SELECT * FROM company_settings WHERE id=1")->fetch()?:[]; $snapshot=null; if(!empty($b['client_id'])){ $cs=$pdo->prepare("SELECT * FROM clients WHERE id=?");$cs->execute([$b['client_id']]); $cRow=$cs->fetch();if($cRow)$snapshot=$cRow; } if(!empty($b['client_snapshot']))$snapshot=$b['client_snapshot']; if($isNew){ $prefixMap=['proforma'=>$comp['prefix_proforma']??'','invoice'=>$comp['prefix_invoice']??'', 'credit_note'=>$comp['prefix_credit_note']??'КИ','debit_note'=>$comp['prefix_debit_note']??'ДИ', 'offer'=>$comp['prefix_offer']??'ОФ','quote'=>$comp['prefix_quote']??'ОФ']; $num=nextDocNumber($pdo,$type); $full=($prefixMap[$type]??'').$num; } else { $ex=$pdo->prepare("SELECT number,full_number FROM documents WHERE id=?");$ex->execute([$id]); $exr=$ex->fetch(); $num=$exr['number']??nextDocNumber($pdo,$type); $full=$exr['full_number']??$num; } $pdo->prepare("INSERT INTO documents (id,type,number,full_number,client_id,client_snapshot,issue_date,tax_date,due_date, items,subtotal,vat_percent,vat_amount,total,currency,status,payment_method,notes,related_doc_id,recurring_id,created_by) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?) ON DUPLICATE KEY UPDATE client_id=VALUES(client_id),client_snapshot=VALUES(client_snapshot), issue_date=VALUES(issue_date),tax_date=VALUES(tax_date),due_date=VALUES(due_date),items=VALUES(items), subtotal=VALUES(subtotal),vat_percent=VALUES(vat_percent),vat_amount=VALUES(vat_amount),total=VALUES(total), currency=VALUES(currency),status=VALUES(status),payment_method=VALUES(payment_method),notes=VALUES(notes)") ->execute([$id,$type,$num,$full,$b['client_id']??null,json_encode($snapshot,JSON_UNESCAPED_UNICODE), $b['issue_date']??date('Y-m-d'),$b['tax_date']??($b['issue_date']??date('Y-m-d')),$b['due_date']??null, json_encode($items,JSON_UNESCAPED_UNICODE),$subtotal,$vatPercent,$vatAmount,$total, $b['currency']??($comp['currency']??'EUR'),$b['status']??'draft',$b['payment_method']??'bank', $b['notes']??'',$b['related_doc_id']??null,$b['recurring_id']??null,$b['created_by']??'']); ok(['id'=>$id,'number'=>$num,'full_number'=>$full,'total'=>$total]); } if($method==='DELETE'){$pdo->prepare("DELETE FROM documents WHERE id=?")->execute([$_GET['id']??'']);ok(true);} } // ── DOCUMENT PRINT DATA (own company + client snapshot + items, for invoice-doc.html) ── if($action==='document_full'){ $pdo=db(); $id=$_GET['id']??''; $s=$pdo->prepare("SELECT * FROM documents WHERE id=?");$s->execute([$id]); $doc=$s->fetch(); if(!$doc) err('Документът не е намерен'); $doc['items']=json_decode($doc['items'],true); $doc['client_snapshot']=json_decode($doc['client_snapshot'],true); $comp=$pdo->query("SELECT * FROM company_settings WHERE id=1")->fetch(); ok(['document'=>$doc,'company'=>$comp]); } // ── INVOICE REPORTS / СПРАВКИ ── if($action==='invoice_stats'){ $pdo=db(); $byMonth=$pdo->query("SELECT DATE_FORMAT(issue_date,'%Y-%m') ym, SUM(total) total FROM documents WHERE type='invoice' GROUP BY ym ORDER BY ym DESC LIMIT 12")->fetchAll(); $byType=$pdo->query("SELECT type, COUNT(*) cnt, SUM(total) total FROM documents GROUP BY type")->fetchAll(); $byClientRaw=$pdo->query("SELECT client_id, client_snapshot, SUM(total) total, COUNT(*) cnt FROM documents WHERE type='invoice' GROUP BY client_id ORDER BY total DESC LIMIT 10")->fetchAll(); foreach($byClientRaw as &$r){$sn=json_decode($r['client_snapshot'],true);$r['name']=$sn['name']??'—';unset($r['client_snapshot']);} $unpaid=$pdo->query("SELECT COUNT(*) cnt, COALESCE(SUM(total),0) total FROM documents WHERE type='invoice' AND status IN ('sent','overdue')")->fetch(); ok(['by_month'=>array_reverse($byMonth),'by_type'=>$byType,'by_client'=>$byClientRaw,'unpaid'=>$unpaid]); } // ── EXPORT TO CSV (compatible with import into accounting software) ── if($action==='documents_export'){ $pdo=db(); $type=$_GET['type']??''; $sql="SELECT * FROM documents".($type?" WHERE type=".$pdo->quote($type):"")." ORDER BY issue_date"; $rows=$pdo->query($sql)->fetchAll(); header('Content-Type: text/csv; charset=utf-8'); header('Content-Disposition: attachment; filename="webtrixia_documents.csv"'); echo "\xEF\xBB\xBF"; $out=fopen('php://output','w'); fputcsv($out,['Номер','Тип','Дата на издаване','Данъчна дата','Клиент','ЕИК','ДДС номер','Данъчна основа','ДДС %','ДДС','Общо','Валута','Статус']); foreach($rows as $r){ $snap=json_decode($r['client_snapshot'],true); fputcsv($out,[$r['full_number'],$r['type'],$r['issue_date'],$r['tax_date'],$snap['name']??'', $snap['eik']??'',$snap['vat_number']??'',$r['subtotal'],$r['vat_percent'],$r['vat_amount'], $r['total'],$r['currency'],$r['status']]); } fclose($out); exit; } // ── IMPORT CLIENTS FROM CSV ── if($action==='clients_import'){ $pdo=db(); $b=body(); $csv=$b['csv']??''; if(!$csv) err('Missing csv'); $lines=preg_split('/\r\n|\r|\n/',trim($csv)); $header=array_map('trim',str_getcsv(array_shift($lines))); $map=array_flip($header); $count=0; foreach($lines as $line){ if(!trim($line))continue; $row=str_getcsv($line); $get=function($k) use($row,$map){return isset($map[$k])&&isset($row[$map[$k]])?trim($row[$map[$k]]):'';}; $id=uniqid('c',true); $pdo->prepare("INSERT INTO clients (id,name,company,email,phone,eik,vat_number,address,city,mol,notes) VALUES (?,?,?,?,?,?,?,?,?,?,?)") ->execute([$id,$get('name'),$get('company'),$get('email'),$get('phone'),$get('eik'), $get('vat_number'),$get('address'),$get('city'),$get('mol'),'']); $count++; } ok(['imported'=>$count]); } // ── RECURRING / ПЕРИОДИЧНИ ФАКТУРИ ── if($action==='recurring'){ $pdo=db(); if($method==='GET') ok($pdo->query("SELECT * FROM recurring_invoices ORDER BY created_at DESC")->fetchAll()); if($method==='POST'){ $b=body();$id=$b['id']??uniqid('r',true); $pdo->prepare("INSERT INTO recurring_invoices (id,client_id,doc_type,items,vat_percent,notes,interval_type,next_run_date,active) VALUES (?,?,?,?,?,?,?,?,?) ON DUPLICATE KEY UPDATE client_id=VALUES(client_id),doc_type=VALUES(doc_type),items=VALUES(items), vat_percent=VALUES(vat_percent),notes=VALUES(notes),interval_type=VALUES(interval_type), next_run_date=VALUES(next_run_date),active=VALUES(active)") ->execute([$id,$b['client_id']??null,$b['doc_type']??'invoice',json_encode($b['items']??[],JSON_UNESCAPED_UNICODE), $b['vat_percent']??20,$b['notes']??'',$b['interval_type']??'monthly',$b['next_run_date']??date('Y-m-d'),$b['active']??1]); ok(['id'=>$id]); } if($method==='DELETE'){$pdo->prepare("DELETE FROM recurring_invoices WHERE id=?")->execute([$_GET['id']??'']);ok(true);} } // ── CRON ENDPOINT: generates due recurring invoices. Call from cPanel Cron Jobs. ── if($action==='run_recurring'){ $key=$_GET['key']??''; if($key!=='webtrixia_cron_2026') err('Forbidden'); $pdo=db(); $due=$pdo->query("SELECT * FROM recurring_invoices WHERE active=1 AND next_run_date<=CURDATE()")->fetchAll(); $comp=$pdo->query("SELECT * FROM company_settings WHERE id=1")->fetch()?:[]; $created=[]; foreach($due as $r){ $items=json_decode($r['items'],true)?:[]; $subtotal=0;foreach($items as $it)$subtotal+=(float)($it['qty']??1)*(float)($it['price']??0); $vatAmount=round($subtotal*$r['vat_percent']/100,2);$total=round($subtotal+$vatAmount,2); $cs=$pdo->prepare("SELECT * FROM clients WHERE id=?");$cs->execute([$r['client_id']]);$snap=$cs->fetch(); $id=uniqid('d',true); $num=nextDocNumber($pdo,$r['doc_type']); $prefixMap=['proforma'=>$comp['prefix_proforma']??'','invoice'=>$comp['prefix_invoice']??'']; $full=($prefixMap[$r['doc_type']]??'').$num; $pdo->prepare("INSERT INTO documents (id,type,number,full_number,client_id,client_snapshot,issue_date,tax_date, items,subtotal,vat_percent,vat_amount,total,currency,status,notes,recurring_id,created_by) VALUES (?,?,?,?,?,?,CURDATE(),CURDATE(),?,?,?,?,?,?,?,?,?,?)") ->execute([$id,$r['doc_type'],$num,$full,$r['client_id'],json_encode($snap,JSON_UNESCAPED_UNICODE), json_encode($items,JSON_UNESCAPED_UNICODE),$subtotal,$r['vat_percent'],$vatAmount,$total, $comp['currency']??'EUR','draft',$r['notes'],$r['id'],'cron']); $interval=$r['interval_type']==='monthly'?'+1 month':($r['interval_type']==='yearly'?'+1 year':($r['interval_type']==='quarterly'?'+3 month':'+1 month')); $newNext=date('Y-m-d',strtotime($r['next_run_date'].' '.$interval)); $pdo->prepare("UPDATE recurring_invoices SET next_run_date=?,last_generated_at=NOW() WHERE id=?")->execute([$newNext,$r['id']]); $created[]=$full; } ok(['created'=>$created]); } // ── SEND DOCUMENT BY EMAIL ── if($action==='send_document_email'){ $pdo=db();$b=body(); $docId=$b['document_id']??'';$to=$b['to']??''; if(!$docId||!$to) err('Missing document_id or to'); $s=$pdo->prepare("SELECT * FROM documents WHERE id=?");$s->execute([$docId]);$doc=$s->fetch(); if(!$doc) err('Document not found'); $comp=$pdo->query("SELECT * FROM company_settings WHERE id=1")->fetch(); if(empty($comp['smtp_host'])) err('SMTP не е конфигуриран (Настройки → Имейл)'); $snap=json_decode($doc['client_snapshot'],true); $typeLabels=['proforma'=>'Проформа фактура','invoice'=>'Фактура','credit_note'=>'Кредитно известие', 'debit_note'=>'Дебитно известие','offer'=>'Оферта','quote'=>'Оферта']; $label=$typeLabels[$doc['type']]??'Документ'; $link=$b['link']??''; $subject=$b['subject']??($label.' № '.$doc['full_number']); $body2=$b['body']??("<p>Здравейте".(!empty($snap['name'])?', '.$snap['name']:'').",</p>". "<p>Изпращаме Ви {$label} № {$doc['full_number']} на стойност {$doc['total']} {$doc['currency']}.</p>". ($link?"<p><a href=\"{$link}\">Преглед / изтегляне</a></p>":""). "<p>Поздрави,<br>".($comp['name']??'Webtrixia')."</p>"); try{ smtpSend($comp['smtp_host'],$comp['smtp_port']??587,$comp['smtp_secure']??'tls',$comp['smtp_user'],$comp['smtp_pass'], $comp['smtp_from_email']?:$comp['smtp_user'],$comp['smtp_from_name']?:$comp['name'],$to,$subject,$body2); $pdo->prepare("INSERT INTO email_log (document_id,to_email,subject,status) VALUES (?,?,?,?)")->execute([$docId,$to,$subject,'sent']); if($doc['status']==='draft') $pdo->prepare("UPDATE documents SET status='sent' WHERE id=?")->execute([$docId]); ok(true); }catch(Exception $e){ $pdo->prepare("INSERT INTO email_log (document_id,to_email,subject,status,error) VALUES (?,?,?,?,?)") ->execute([$docId,$to,$subject,'error',$e->getMessage()]); err('Грешка при изпращане: '.$e->getMessage()); } } // ── HR REQUESTS: Молба за отпуск (платен/неплатен) + Предизвестие за напускане ── if($action==='hr_requests'){ $pdo=db(); if($method==='GET') ok($pdo->query("SELECT * FROM hr_requests ORDER BY created_at DESC")->fetchAll()); if($method==='POST'){ $b=body();$id=$b['id']??uniqid('hr',true); $pdo->prepare("INSERT INTO hr_requests (id,user_id,employee_name,position,egn,type,start_date,end_date,days,notice_days,last_work_date,reason,status,approved_by) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?) ON DUPLICATE KEY UPDATE employee_name=VALUES(employee_name),position=VALUES(position),egn=VALUES(egn), type=VALUES(type),start_date=VALUES(start_date),end_date=VALUES(end_date),days=VALUES(days), notice_days=VALUES(notice_days),last_work_date=VALUES(last_work_date),reason=VALUES(reason), status=VALUES(status),approved_by=VALUES(approved_by)") ->execute([$id,$b['user_id']??null,$b['employee_name']??'',$b['position']??'',$b['egn']??'', $b['type']??'leave_paid',$b['start_date']??null,$b['end_date']??null,$b['days']??0, $b['notice_days']??0,$b['last_work_date']??null,$b['reason']??'',$b['status']??'pending',$b['approved_by']??'']); ok(['id'=>$id]); } if($method==='DELETE'){$pdo->prepare("DELETE FROM hr_requests WHERE id=?")->execute([$_GET['id']??'']);ok(true);} } // ── HR REQUEST STATUS ONLY (approve/reject without touching other fields) ── if($action==='hr_request_status'){ $pdo=db();$b=body(); $id=$b['id']??'';if(!$id)err('Missing id'); $pdo->prepare("UPDATE hr_requests SET status=?,approved_by=? WHERE id=?") ->execute([$b['status']??'pending',$b['approved_by']??'',$id]); ok(true); } // ══════════════════════════════════════════════════════ // ── WHITE-LABEL / LICENSING / MULTI-HUB PLATFORM ── // ══════════════════════════════════════════════════════ if($action==='install_platform'){ $pdo=db(); $pdo->exec("CREATE TABLE IF NOT EXISTS branding_settings ( id INT PRIMARY KEY, product_name VARCHAR(255) DEFAULT 'Webtrixia Hub', logo_url VARCHAR(500), favicon_url VARCHAR(500), primary_color VARCHAR(20) DEFAULT '#6c63ff', secondary_color VARCHAR(20) DEFAULT '#a78bfa', footer_text VARCHAR(255) DEFAULT '', powered_by_visible TINYINT(1) DEFAULT 1, hub_type VARCHAR(30) DEFAULT 'agency', license_key VARCHAR(64), admin_locked TINYINT(1) DEFAULT 0, modules_enabled TEXT, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP )"); $cnt=$pdo->query("SELECT COUNT(*) FROM branding_settings")->fetchColumn(); if($cnt==0){ $key=bin2hex(random_bytes(16)); $defaultModules=json_encode(['projects'=>1,'clients'=>1,'finance'=>1,'tasks'=>1,'offers'=>1,'contracts'=>1, 'calendar'=>1,'hosting'=>1,'passwords'=>1,'notes'=>1,'reports'=>1,'presence'=>1,'targets'=>1, 'workreport'=>1,'users'=>1,'invoicing'=>1,'hr_requests'=>1,'waybills'=>1]); $pdo->prepare("INSERT INTO branding_settings (id,product_name,license_key,modules_enabled) VALUES (1,?,?,?)") ->execute(['Webtrixia Hub',$key,$defaultModules]); } $pdo->exec("CREATE TABLE IF NOT EXISTS client_hubs ( id VARCHAR(36) PRIMARY KEY, name VARCHAR(255), url VARCHAR(500), license_key VARCHAR(64), hub_type VARCHAR(30) DEFAULT 'agency', notes TEXT, last_sync TIMESTAMP NULL, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP )"); $pdo->exec("CREATE TABLE IF NOT EXISTS hub_subscriptions ( id VARCHAR(36) PRIMARY KEY, hub_id VARCHAR(36) NOT NULL, module VARCHAR(50) NOT NULL, price_monthly DECIMAL(10,2) DEFAULT 0, currency VARCHAR(10) DEFAULT 'EUR', active TINYINT(1) DEFAULT 1, started_at DATE NULL, next_billing_date DATE NULL, notes TEXT, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP )"); $pdo->exec("CREATE TABLE IF NOT EXISTS waybills ( id VARCHAR(36) PRIMARY KEY, number VARCHAR(50), date DATE, driver_name VARCHAR(255), vehicle_plate VARCHAR(50), vehicle_type VARCHAR(100), route_from VARCHAR(255), route_to VARCHAR(255), km_start DECIMAL(10,1) DEFAULT 0, km_end DECIMAL(10,1) DEFAULT 0, fuel_liters DECIMAL(10,2) DEFAULT 0, fuel_cost DECIMAL(10,2) DEFAULT 0, cargo VARCHAR(500), client VARCHAR(255), status VARCHAR(20) DEFAULT 'active', notes TEXT, created_by VARCHAR(100), created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP )"); ok(['message'=>'Platform tables ready','license_key'=>$pdo->query("SELECT license_key FROM branding_settings WHERE id=1")->fetchColumn()]); } function checkLicense($pdo,$secret){ $r=$pdo->query("SELECT license_key FROM branding_settings WHERE id=1")->fetch(); if(!$r || $r['license_key']!==$secret) err('Невалиден административен ключ'); } // ── BRANDING (white-label identity) ── if($action==='branding_settings'){ $pdo=db(); if($method==='GET'){ $r=$pdo->query("SELECT id,product_name,logo_url,favicon_url,primary_color,secondary_color,footer_text, powered_by_visible,hub_type,admin_locked,modules_enabled FROM branding_settings WHERE id=1")->fetch(); if($r){ $r['modules_enabled']=json_decode($r['modules_enabled'],true)?:[]; $ls=hub_license_status(); if(!empty($ls['modules'])) $r['modules_enabled']=$ls['modules']; $r['license_state']=$ls['state']??'active'; $r['license_message']=$ls['message']??''; $r['license_valid_until']=$ls['valid_until']??null; $r['license_grace_until']=$ls['grace_until']??null; if(in_array($r['license_state'],['locked','invalid'],true)){ foreach($r['modules_enabled'] as $mk=>$mv) $r['modules_enabled'][$mk]=0; } } ok($r?:[]); } if($method==='POST'){ $b=body(); requireHubAdminOrSecret($pdo,$b['admin_secret']??''); $fields=['product_name','logo_url','favicon_url','primary_color','secondary_color','footer_text', 'powered_by_visible','hub_type']; $set=[];$vals=[]; foreach($fields as $f){ if(array_key_exists($f,$b)){ $set[]="$f=?"; $vals[]=$b[$f]; } } if($set) $pdo->prepare("UPDATE branding_settings SET ".implode(',',$set)." WHERE id=1")->execute($vals); ok(true); } } // ── MODULE TOGGLE (called locally from own w_admin.php, or remotely from a master hub) ── if($action==='module_toggle'){ $pdo=db();$b=body(); requireHubAdminOrSecret($pdo,$b['admin_secret']??''); $module=$b['module']??'';$enabled=(int)($b['enabled']??0); if(!$module) err('Missing module'); $r=$pdo->query("SELECT modules_enabled FROM branding_settings WHERE id=1")->fetch(); $mods=json_decode($r['modules_enabled']??'{}',true)?:[]; $mods[$module]=$enabled; $pdo->prepare("UPDATE branding_settings SET modules_enabled=? WHERE id=1")->execute([json_encode($mods)]); ok($mods); } // ── LOCK / UNLOCK ADMIN ACCESS (before handing the hub to a client) ── if($action==='admin_lock'){ $pdo=db();$b=body(); requireHubAdminOrSecret($pdo,$b['admin_secret']??''); $pdo->prepare("UPDATE branding_settings SET admin_locked=? WHERE id=1")->execute([(int)($b['locked']??1)]); ok(true); } // ── FULL DATA EXPORT (for migrating this hub to another hosting) ── if($action==='data_export_all'){ $pdo=db(); checkLicense($pdo,$_GET['admin_secret']??''); $tables=['users','projects','tasks','notes','clients','hosting','passwords','offers','contracts','targets', 'company_settings','doc_counters','documents','recurring_invoices','hr_requests','email_log', 'branding_settings','client_hubs','waybills']; $dump=[]; foreach($tables as $t){ try{ $dump[$t]=$pdo->query("SELECT * FROM `$t`")->fetchAll(); }catch(Exception $e){ $dump[$t]=[]; } } header('Content-Type: application/json; charset=utf-8'); header('Content-Disposition: attachment; filename="webtrixia_hub_backup_'.date('Y-m-d').'.json"'); echo json_encode(['exported_at'=>date('c'),'tables'=>$dump],JSON_UNESCAPED_UNICODE); exit; } // ── FULL DATA IMPORT (restore a backup on a fresh install) ── if($action==='data_import_all'){ $pdo=db();$b=body(); checkLicense($pdo,$b['admin_secret']??''); $dump=$b['tables']??[]; if(!$dump) err('Missing tables payload'); foreach($dump as $table=>$rows){ if(!preg_match('/^[a-z_]+$/',$table)) continue; if(!is_array($rows)||!count($rows)) continue; foreach($rows as $row){ $cols=array_keys($row); $placeholders=implode(',',array_fill(0,count($cols),'?')); $colList=implode(',',array_map(function($c){return "`$c`";},$cols)); $updates=implode(',',array_map(function($c){return "`$c`=VALUES(`$c`)";},$cols)); try{ $pdo->prepare("INSERT INTO `$table` ($colList) VALUES ($placeholders) ON DUPLICATE KEY UPDATE $updates") ->execute(array_values($row)); }catch(Exception $e){ /* skip row-level errors, continue import */ } } } ok('Import complete'); } // ── FACTORY RESET (wipe business data, keep schema — for reselling to the next client) ── if($action==='factory_reset'){ $pdo=db();$b=body(); checkLicense($pdo,$b['admin_secret']??''); if(($b['confirm']??'')!=='WIPE') err('Missing confirm=WIPE'); $tables=['projects','tasks','notes','clients','hosting','passwords','offers','contracts','targets','activity', 'presence','documents','doc_counters','recurring_invoices','hr_requests','email_log','waybills']; foreach($tables as $t){ try{ $pdo->exec("TRUNCATE TABLE `$t`"); }catch(Exception $e){} } // keep exactly one admin user, reset password $pdo->exec("DELETE FROM users WHERE is_admin=0"); $pdo->prepare("UPDATE users SET password=? WHERE is_admin=1")->execute([password_hash('webtrixia2025',PASSWORD_DEFAULT)]); ok('Factory reset complete'); } // ── MASTER: CLIENT HUB REGISTRY (used by hub.webtrixia.com to manage installs it has sold) ── if($action==='client_hubs'){ $pdo=db(); if($method==='GET') ok($pdo->query("SELECT * FROM client_hubs ORDER BY created_at DESC")->fetchAll()); if($method==='POST'){ $b=body();$id=$b['id']??uniqid('ch',true); $pdo->prepare("INSERT INTO client_hubs (id,name,url,license_key,hub_type,notes) VALUES (?,?,?,?,?,?) ON DUPLICATE KEY UPDATE name=VALUES(name),url=VALUES(url),license_key=VALUES(license_key), hub_type=VALUES(hub_type),notes=VALUES(notes)") ->execute([$id,$b['name']??'',$b['url']??'',$b['license_key']??'',$b['hub_type']??'agency',$b['notes']??'']); ok(['id'=>$id]); } if($method==='DELETE'){$pdo->prepare("DELETE FROM client_hubs WHERE id=?")->execute([$_GET['id']??'']);ok(true);} } if($action==='client_hub_ping'){ $pdo=db();$id=$_GET['id']??''; $pdo->prepare("UPDATE client_hubs SET last_sync=NOW() WHERE id=?")->execute([$id]); ok(true); } // ── HUB SUBSCRIPTIONS (per-module monthly billing of client hubs) ── if($action==='hub_subscriptions'){ $pdo=db(); if($method==='GET'){ $where='';$params=[]; if(!empty($_GET['hub_id'])){ $where=' WHERE hub_id=?'; $params[]=$_GET['hub_id']; } $st=$pdo->prepare("SELECT * FROM hub_subscriptions".$where." ORDER BY created_at DESC"); $st->execute($params); ok($st->fetchAll()); } if($method==='POST'){ $b=body();$id=$b['id']??uniqid('hs',true); $pdo->prepare("INSERT INTO hub_subscriptions (id,hub_id,module,price_monthly,currency,active,started_at,next_billing_date,notes) VALUES (?,?,?,?,?,?,?,?,?) ON DUPLICATE KEY UPDATE module=VALUES(module),price_monthly=VALUES(price_monthly),currency=VALUES(currency), active=VALUES(active),started_at=VALUES(started_at),next_billing_date=VALUES(next_billing_date),notes=VALUES(notes)") ->execute([$id,$b['hub_id']??'',$b['module']??'',$b['price_monthly']??0,$b['currency']??'EUR', (int)($b['active']??1),$b['started_at']??date('Y-m-d'),$b['next_billing_date']??date('Y-m-d',strtotime('+1 month')),$b['notes']??'']); ok(['id'=>$id]); } if($method==='DELETE'){$pdo->prepare("DELETE FROM hub_subscriptions WHERE id=?")->execute([$_GET['id']??'']);ok(true);} } // ── Mark a subscription as paid: advances next_billing_date one month ── if($action==='hub_subscription_paid'){ $pdo=db();$b=body(); $id=$b['id']??'';if(!$id)err('Missing id'); $s=$pdo->prepare("SELECT next_billing_date FROM hub_subscriptions WHERE id=?");$s->execute([$id]); $row=$s->fetch(); if(!$row) err('Not found'); $base=$row['next_billing_date']?:date('Y-m-d'); $next=date('Y-m-d',strtotime($base.' +1 month')); $pdo->prepare("UPDATE hub_subscriptions SET next_billing_date=?,active=1 WHERE id=?")->execute([$next,$id]); ok(['next_billing_date'=>$next]); } // ── Billing dashboard stats: MRR, overdue, counts ── if($action==='hub_billing_stats'){ $pdo=db(); $mrr=$pdo->query("SELECT COALESCE(SUM(price_monthly),0) FROM hub_subscriptions WHERE active=1")->fetchColumn(); $activeCnt=$pdo->query("SELECT COUNT(*) FROM hub_subscriptions WHERE active=1")->fetchColumn(); $overdue=$pdo->query("SELECT hs.*, ch.name AS hub_name FROM hub_subscriptions hs LEFT JOIN client_hubs ch ON ch.id=hs.hub_id WHERE hs.active=1 AND hs.next_billing_date < CURDATE() ORDER BY hs.next_billing_date")->fetchAll(); $hubCnt=$pdo->query("SELECT COUNT(*) FROM client_hubs")->fetchColumn(); ok(['mrr'=>(float)$mrr,'active_subscriptions'=>(int)$activeCnt,'hubs'=>(int)$hubCnt,'overdue'=>$overdue]); } // ── WAYBILLS / ПЪТНИ ЛИСТОВЕ ── if($action==='waybills'){ $pdo=db(); if($method==='GET') ok($pdo->query("SELECT * FROM waybills ORDER BY date DESC, created_at DESC")->fetchAll()); if($method==='POST'){ $b=body();$id=$b['id']??uniqid('wb',true); $pdo->prepare("INSERT INTO waybills (id,number,date,driver_name,vehicle_plate,vehicle_type,route_from,route_to, km_start,km_end,fuel_liters,fuel_cost,cargo,client,status,notes,created_by) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?) ON DUPLICATE KEY UPDATE number=VALUES(number),date=VALUES(date),driver_name=VALUES(driver_name), vehicle_plate=VALUES(vehicle_plate),vehicle_type=VALUES(vehicle_type),route_from=VALUES(route_from), route_to=VALUES(route_to),km_start=VALUES(km_start),km_end=VALUES(km_end),fuel_liters=VALUES(fuel_liters), fuel_cost=VALUES(fuel_cost),cargo=VALUES(cargo),client=VALUES(client),status=VALUES(status),notes=VALUES(notes)") ->execute([$id,$b['number']??('ПЛ-'.date('Ymd').'-'.rand(100,999)),$b['date']??date('Y-m-d'), $b['driver_name']??'',$b['vehicle_plate']??'',$b['vehicle_type']??'',$b['route_from']??'',$b['route_to']??'', $b['km_start']??0,$b['km_end']??0,$b['fuel_liters']??0,$b['fuel_cost']??0,$b['cargo']??'', $b['client']??'',$b['status']??'active',$b['notes']??'',$b['created_by']??'']); ok(['id'=>$id]); } if($method==='DELETE'){$pdo->prepare("DELETE FROM waybills WHERE id=?")->execute([$_GET['id']??'']);ok(true);} } if($action==='waybill_status'){ $pdo=db();$b=body(); $id=$b['id']??'';if(!$id)err('Missing id'); $pdo->prepare("UPDATE waybills SET status=? WHERE id=?")->execute([$b['status']??'active',$id]); ok(true); } if($action==='verify_secret'){ $pdo=db(); checkLicense($pdo,$_GET['admin_secret']??($_POST['admin_secret']??'')); ok(true); } http_response_code(404); echo json_encode(['ok'=>false,'error'=>'Unknown action: '.$action]);
| ver. 1.4 |
Github
|
.
| PHP 8.2.31 | Generation time: 0.01 |
proxy
|
phpinfo
|
Settings