class-studiou-wc-fpp-admin.php 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470
  1. <?php
  2. if (!defined('WPINC')) {
  3. die;
  4. }
  5. class Studiou_WC_FPP_Admin {
  6. /** @var Studiou_WC_FPP_DB */
  7. private $db;
  8. public function __construct($db) {
  9. $this->db = $db;
  10. // AJAX handlers
  11. add_action('wp_ajax_studiou_wcfpp_update_status', array($this, 'ajax_update_status'));
  12. add_action('wp_ajax_studiou_wcfpp_delete_files', array($this, 'ajax_delete_files'));
  13. add_action('wp_ajax_studiou_wcfpp_download_zip', array($this, 'ajax_download_zip'));
  14. add_action('wp_ajax_studiou_wcfpp_download_csv', array($this, 'ajax_download_csv'));
  15. add_action('wp_ajax_studiou_wcfpp_bulk_status', array($this, 'ajax_bulk_status'));
  16. // Handle direct ZIP / CSV download and file download (marks as downloaded)
  17. add_action('admin_init', array($this, 'handle_zip_download'));
  18. add_action('admin_init', array($this, 'handle_csv_download'));
  19. add_action('admin_init', array($this, 'handle_file_download'));
  20. }
  21. public function render_summary_page() {
  22. // Process filters
  23. $current_status = isset($_GET['status']) ? sanitize_text_field($_GET['status']) : '';
  24. $current_product = isset($_GET['product_id']) ? absint($_GET['product_id']) : 0;
  25. $current_order = isset($_GET['order_id']) ? absint($_GET['order_id']) : 0;
  26. $current_search = isset($_GET['s']) ? sanitize_text_field($_GET['s']) : '';
  27. $current_page = isset($_GET['paged']) ? max(1, absint($_GET['paged'])) : 1;
  28. $per_page = 30;
  29. $allowed_orderby = array('order_id', 'status', 'created_at');
  30. $orderby_in = isset($_GET['orderby']) ? sanitize_text_field($_GET['orderby']) : 'created_at';
  31. $orderby = in_array($orderby_in, $allowed_orderby, true) ? $orderby_in : 'created_at';
  32. $order_in = isset($_GET['order']) ? strtoupper(sanitize_text_field($_GET['order'])) : 'DESC';
  33. $order = ($order_in === 'ASC') ? 'ASC' : 'DESC';
  34. $args = array(
  35. 'status' => $current_status,
  36. 'product_id' => $current_product,
  37. 'order_id' => $current_order,
  38. 'search' => $current_search,
  39. 'limit' => $per_page,
  40. 'offset' => ($current_page - 1) * $per_page,
  41. 'orderby' => $orderby,
  42. 'order' => $order,
  43. );
  44. $records = $this->db->get_file_records($args);
  45. $total = $this->db->count_file_records($args);
  46. $total_pages = ceil($total / $per_page);
  47. // Get products that have FPP enabled for filter dropdown
  48. $fpp_products = $this->get_fpp_products();
  49. // Expose active sort to the view
  50. $current_orderby = $orderby;
  51. $current_order_dir = $order;
  52. include STUDIOU_WCFPP_PLUGIN_DIR . 'views/admin-summary.php';
  53. }
  54. private function get_fpp_products() {
  55. $args = array(
  56. 'post_type' => 'product',
  57. 'post_status' => 'publish',
  58. 'posts_per_page' => -1,
  59. 'meta_query' => array(
  60. array(
  61. 'key' => '_studiou_fpp_enabled',
  62. 'value' => 'yes',
  63. ),
  64. ),
  65. 'fields' => 'ids',
  66. );
  67. $product_ids = get_posts($args);
  68. $products = array();
  69. foreach ($product_ids as $pid) {
  70. $products[$pid] = get_the_title($pid);
  71. }
  72. return $products;
  73. }
  74. public function ajax_update_status() {
  75. check_ajax_referer('studiou-wcfpp-nonce', 'nonce');
  76. if (!current_user_can('manage_woocommerce')) {
  77. wp_send_json_error(array('message' => __('Insufficient permissions.', 'studiou-wc-free-photo-product')));
  78. return;
  79. }
  80. $file_id = isset($_POST['file_id']) ? absint($_POST['file_id']) : 0;
  81. $status = isset($_POST['status']) ? sanitize_text_field($_POST['status']) : '';
  82. if (!$file_id || !$status) {
  83. wp_send_json_error(array('message' => __('Invalid data.', 'studiou-wc-free-photo-product')));
  84. return;
  85. }
  86. $result = $this->db->update_file_status($file_id, $status);
  87. if ($result === false) {
  88. wp_send_json_error(array('message' => __('Failed to update status.', 'studiou-wc-free-photo-product')));
  89. return;
  90. }
  91. wp_send_json_success(array('message' => __('Status updated.', 'studiou-wc-free-photo-product')));
  92. }
  93. public function ajax_bulk_status() {
  94. check_ajax_referer('studiou-wcfpp-nonce', 'nonce');
  95. if (!current_user_can('manage_woocommerce')) {
  96. wp_send_json_error(array('message' => __('Insufficient permissions.', 'studiou-wc-free-photo-product')));
  97. return;
  98. }
  99. $file_ids = isset($_POST['file_ids']) ? array_map('absint', (array) $_POST['file_ids']) : array();
  100. $status = isset($_POST['status']) ? sanitize_text_field($_POST['status']) : '';
  101. if (empty($file_ids) || !$status) {
  102. wp_send_json_error(array('message' => __('Invalid data.', 'studiou-wc-free-photo-product')));
  103. return;
  104. }
  105. $updated = 0;
  106. foreach ($file_ids as $fid) {
  107. if ($this->db->update_file_status($fid, $status)) {
  108. $updated++;
  109. }
  110. }
  111. wp_send_json_success(array(
  112. 'message' => sprintf(__('%d file(s) updated.', 'studiou-wc-free-photo-product'), $updated),
  113. ));
  114. }
  115. public function ajax_delete_files() {
  116. check_ajax_referer('studiou-wcfpp-nonce', 'nonce');
  117. if (!current_user_can('manage_woocommerce')) {
  118. wp_send_json_error(array('message' => __('Insufficient permissions.', 'studiou-wc-free-photo-product')));
  119. return;
  120. }
  121. $file_ids = isset($_POST['file_ids']) ? array_map('absint', (array) $_POST['file_ids']) : array();
  122. if (empty($file_ids)) {
  123. wp_send_json_error(array('message' => __('No files selected.', 'studiou-wc-free-photo-product')));
  124. return;
  125. }
  126. $deleted = 0;
  127. foreach ($file_ids as $fid) {
  128. if ($this->db->delete_file_record($fid)) {
  129. $deleted++;
  130. }
  131. }
  132. wp_send_json_success(array(
  133. 'message' => sprintf(__('%d file(s) deleted.', 'studiou-wc-free-photo-product'), $deleted),
  134. ));
  135. }
  136. public function ajax_download_zip() {
  137. check_ajax_referer('studiou-wcfpp-nonce', 'nonce');
  138. if (!current_user_can('manage_woocommerce')) {
  139. wp_send_json_error(array('message' => __('Insufficient permissions.', 'studiou-wc-free-photo-product')));
  140. return;
  141. }
  142. $file_ids = isset($_POST['file_ids']) ? array_map('absint', (array) $_POST['file_ids']) : array();
  143. if (empty($file_ids)) {
  144. wp_send_json_error(array('message' => __('No files selected.', 'studiou-wc-free-photo-product')));
  145. return;
  146. }
  147. $records = $this->db->get_file_records_by_ids($file_ids);
  148. if (empty($records)) {
  149. wp_send_json_error(array('message' => __('No files found.', 'studiou-wc-free-photo-product')));
  150. return;
  151. }
  152. // Generate ZIP
  153. $upload_dir = wp_upload_dir();
  154. $zip_name = 'fpp-files-' . date('Y-m-d-His') . '.zip';
  155. $zip_path = $upload_dir['basedir'] . '/studiou-fpp-chunks/' . $zip_name;
  156. if (!class_exists('ZipArchive')) {
  157. wp_send_json_error(array('message' => __('ZIP extension not available on server.', 'studiou-wc-free-photo-product')));
  158. return;
  159. }
  160. $zip = new ZipArchive();
  161. if ($zip->open($zip_path, ZipArchive::CREATE) !== true) {
  162. wp_send_json_error(array('message' => __('Could not create ZIP file.', 'studiou-wc-free-photo-product')));
  163. return;
  164. }
  165. $added = 0;
  166. foreach ($records as $record) {
  167. $file_path = wp_get_original_image_path($record->attachment_id);
  168. if (!$file_path) {
  169. $file_path = get_attached_file($record->attachment_id);
  170. }
  171. if ($file_path && file_exists($file_path)) {
  172. // Use order number prefix if available
  173. $prefix = $record->order_id ? 'order-' . $record->order_id . '_' : '';
  174. $zip->addFile($file_path, $prefix . $record->file_name);
  175. $added++;
  176. }
  177. }
  178. $zip->close();
  179. if ($added === 0) {
  180. if (file_exists($zip_path)) {
  181. unlink($zip_path);
  182. }
  183. wp_send_json_error(array('message' => __('No files could be added to archive.', 'studiou-wc-free-photo-product')));
  184. return;
  185. }
  186. // Generate a nonce-protected download URL
  187. $download_url = add_query_arg(array(
  188. 'studiou_fpp_download' => 1,
  189. 'file' => $zip_name,
  190. '_wpnonce' => wp_create_nonce('studiou-fpp-zip-download'),
  191. ), admin_url('admin.php'));
  192. wp_send_json_success(array(
  193. 'download_url' => $download_url,
  194. 'file_count' => $added,
  195. ));
  196. }
  197. public function handle_zip_download() {
  198. if (!isset($_GET['studiou_fpp_download']) || !isset($_GET['file'])) {
  199. return;
  200. }
  201. if (!wp_verify_nonce($_GET['_wpnonce'], 'studiou-fpp-zip-download')) {
  202. wp_die(__('Security check failed.', 'studiou-wc-free-photo-product'));
  203. }
  204. if (!current_user_can('manage_woocommerce')) {
  205. wp_die(__('Insufficient permissions.', 'studiou-wc-free-photo-product'));
  206. }
  207. $file_name = sanitize_file_name($_GET['file']);
  208. $upload_dir = wp_upload_dir();
  209. $file_path = $upload_dir['basedir'] . '/studiou-fpp-chunks/' . $file_name;
  210. if (!file_exists($file_path) || pathinfo($file_name, PATHINFO_EXTENSION) !== 'zip') {
  211. wp_die(__('File not found.', 'studiou-wc-free-photo-product'));
  212. }
  213. header('Content-Type: application/zip');
  214. header('Content-Disposition: attachment; filename="' . $file_name . '"');
  215. header('Content-Length: ' . filesize($file_path));
  216. header('Pragma: no-cache');
  217. readfile($file_path);
  218. // Clean up the ZIP file after download
  219. unlink($file_path);
  220. exit;
  221. }
  222. public function ajax_download_csv() {
  223. check_ajax_referer('studiou-wcfpp-nonce', 'nonce');
  224. if (!current_user_can('manage_woocommerce')) {
  225. wp_send_json_error(array('message' => __('Insufficient permissions.', 'studiou-wc-free-photo-product')));
  226. return;
  227. }
  228. $file_ids = isset($_POST['file_ids']) ? array_map('absint', (array) $_POST['file_ids']) : array();
  229. if (empty($file_ids)) {
  230. wp_send_json_error(array('message' => __('No files selected.', 'studiou-wc-free-photo-product')));
  231. return;
  232. }
  233. $records = $this->db->get_file_records_by_ids($file_ids);
  234. if (empty($records)) {
  235. wp_send_json_error(array('message' => __('No files found.', 'studiou-wc-free-photo-product')));
  236. return;
  237. }
  238. $upload_dir = wp_upload_dir();
  239. $csv_name = 'fpp-records-' . date('Y-m-d-His') . '.csv';
  240. $csv_dir = $upload_dir['basedir'] . '/studiou-fpp-chunks';
  241. if (!file_exists($csv_dir)) {
  242. wp_mkdir_p($csv_dir);
  243. }
  244. $csv_path = $csv_dir . '/' . $csv_name;
  245. $fh = fopen($csv_path, 'wb');
  246. if (!$fh) {
  247. wp_send_json_error(array('message' => __('Could not create CSV file.', 'studiou-wc-free-photo-product')));
  248. return;
  249. }
  250. // UTF-8 BOM so Excel opens the file with the correct encoding
  251. fwrite($fh, "\xEF\xBB\xBF");
  252. // Header row — column labels translated
  253. fputcsv($fh, array(
  254. __('ID', 'studiou-wc-free-photo-product'),
  255. __('File Name', 'studiou-wc-free-photo-product'),
  256. __('Product', 'studiou-wc-free-photo-product'),
  257. __('Variation ID', 'studiou-wc-free-photo-product'),
  258. __('Order', 'studiou-wc-free-photo-product'),
  259. __('Customer', 'studiou-wc-free-photo-product'),
  260. __('Status', 'studiou-wc-free-photo-product'),
  261. __('Date', 'studiou-wc-free-photo-product'),
  262. __('File URL', 'studiou-wc-free-photo-product'),
  263. ));
  264. foreach ($records as $record) {
  265. $product_title = get_the_title($record->product_id);
  266. if ((int) $record->customer_id > 0) {
  267. $user = get_userdata((int) $record->customer_id);
  268. $customer_name = $user ? $user->display_name : '#' . (int) $record->customer_id;
  269. } else {
  270. $customer_name = __('Guest', 'studiou-wc-free-photo-product');
  271. }
  272. $file_url = wp_get_attachment_url((int) $record->attachment_id);
  273. fputcsv($fh, array(
  274. (int) $record->id,
  275. (string) $record->file_name,
  276. (string) $product_title,
  277. (int) $record->variation_id,
  278. (int) $record->order_id ? '#' . (int) $record->order_id : '',
  279. (string) $customer_name,
  280. (string) self::get_status_label($record->status),
  281. (string) $record->created_at,
  282. (string) ($file_url ?: ''),
  283. ));
  284. }
  285. fclose($fh);
  286. $download_url = add_query_arg(array(
  287. 'studiou_fpp_download_csv' => 1,
  288. 'file' => $csv_name,
  289. '_wpnonce' => wp_create_nonce('studiou-fpp-csv-download'),
  290. ), admin_url('admin.php'));
  291. wp_send_json_success(array(
  292. 'download_url' => $download_url,
  293. 'row_count' => count($records),
  294. ));
  295. }
  296. public function handle_csv_download() {
  297. if (!isset($_GET['studiou_fpp_download_csv']) || !isset($_GET['file'])) {
  298. return;
  299. }
  300. if (!wp_verify_nonce($_GET['_wpnonce'] ?? '', 'studiou-fpp-csv-download')) {
  301. wp_die(__('Security check failed.', 'studiou-wc-free-photo-product'));
  302. }
  303. if (!current_user_can('manage_woocommerce')) {
  304. wp_die(__('Insufficient permissions.', 'studiou-wc-free-photo-product'));
  305. }
  306. $file_name = sanitize_file_name($_GET['file']);
  307. $upload_dir = wp_upload_dir();
  308. $file_path = $upload_dir['basedir'] . '/studiou-fpp-chunks/' . $file_name;
  309. if (!file_exists($file_path) || pathinfo($file_name, PATHINFO_EXTENSION) !== 'csv') {
  310. wp_die(__('File not found.', 'studiou-wc-free-photo-product'));
  311. }
  312. header('Content-Type: text/csv; charset=UTF-8');
  313. header('Content-Disposition: attachment; filename="' . $file_name . '"');
  314. header('Content-Length: ' . filesize($file_path));
  315. header('Pragma: no-cache');
  316. readfile($file_path);
  317. unlink($file_path);
  318. exit;
  319. }
  320. /**
  321. * Handle single file download — serves the file and marks status as "downloaded".
  322. */
  323. public function handle_file_download() {
  324. if (!isset($_GET['studiou_fpp_download_file'])) {
  325. return;
  326. }
  327. if (!wp_verify_nonce($_GET['_wpnonce'] ?? '', 'studiou-fpp-download-file')) {
  328. wp_die(__('Security check failed.', 'studiou-wc-free-photo-product'));
  329. }
  330. if (!current_user_can('manage_woocommerce')) {
  331. wp_die(__('Insufficient permissions.', 'studiou-wc-free-photo-product'));
  332. }
  333. $file_record_id = isset($_GET['file_record_id']) ? absint($_GET['file_record_id']) : 0;
  334. $attachment_id = isset($_GET['attachment_id']) ? absint($_GET['attachment_id']) : 0;
  335. if (!$attachment_id) {
  336. wp_die(__('File not found.', 'studiou-wc-free-photo-product'));
  337. }
  338. // Mark as downloaded
  339. if ($file_record_id) {
  340. $record = $this->db->get_file_record($file_record_id);
  341. if ($record && $record->status === 'ready') {
  342. $this->db->update_file_status($file_record_id, 'downloaded');
  343. }
  344. }
  345. // Serve the original (unscaled) file — WP auto-scales images above the big-image threshold.
  346. $file_path = wp_get_original_image_path($attachment_id);
  347. if (!$file_path) {
  348. $file_path = get_attached_file($attachment_id);
  349. }
  350. if (!$file_path || !file_exists($file_path)) {
  351. wp_die(__('File not found.', 'studiou-wc-free-photo-product'));
  352. }
  353. $file_name = basename($file_path);
  354. $mime_type = mime_content_type($file_path) ?: 'application/octet-stream';
  355. header('Content-Type: ' . $mime_type);
  356. header('Content-Disposition: attachment; filename="' . $file_name . '"');
  357. header('Content-Length: ' . filesize($file_path));
  358. header('Pragma: no-cache');
  359. readfile($file_path);
  360. exit;
  361. }
  362. public static function get_status_label($status) {
  363. $labels = array(
  364. 'ready' => __('Ready', 'studiou-wc-free-photo-product'),
  365. 'downloaded' => __('Downloaded', 'studiou-wc-free-photo-product'),
  366. 'solved' => __('Solved', 'studiou-wc-free-photo-product'),
  367. );
  368. return isset($labels[$status]) ? $labels[$status] : $status;
  369. }
  370. public static function get_status_class($status) {
  371. $classes = array(
  372. 'ready' => 'studiou-fpp-status-ready',
  373. 'downloaded' => 'studiou-fpp-status-downloaded',
  374. 'solved' => 'studiou-fpp-status-solved',
  375. );
  376. return isset($classes[$status]) ? $classes[$status] : '';
  377. }
  378. public static function get_order_edit_url($order_id) {
  379. if (class_exists('Automattic\WooCommerce\Utilities\OrderUtil')
  380. && \Automattic\WooCommerce\Utilities\OrderUtil::custom_orders_table_usage_is_enabled()) {
  381. return admin_url('admin.php?page=wc-orders&action=edit&id=' . $order_id);
  382. }
  383. return admin_url('post.php?post=' . $order_id . '&action=edit');
  384. }
  385. }