Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 569
0.00% covered (danger)
0.00%
0 / 23
CRAP
0.00% covered (danger)
0.00%
0 / 1
SupportAdminController
0.00% covered (danger)
0.00%
0 / 569
0.00% covered (danger)
0.00%
0 / 23
14520
0.00% covered (danger)
0.00%
0 / 1
 __construct
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
 checkAdminPermission
0.00% covered (danger)
0.00%
0 / 7
0.00% covered (danger)
0.00%
0 / 1
6
 getArticles
0.00% covered (danger)
0.00%
0 / 35
0.00% covered (danger)
0.00%
0 / 1
56
 createArticle
0.00% covered (danger)
0.00%
0 / 41
0.00% covered (danger)
0.00%
0 / 1
56
 updateArticle
0.00% covered (danger)
0.00%
0 / 49
0.00% covered (danger)
0.00%
0 / 1
272
 deleteArticle
0.00% covered (danger)
0.00%
0 / 24
0.00% covered (danger)
0.00%
0 / 1
30
 getCategories
0.00% covered (danger)
0.00%
0 / 14
0.00% covered (danger)
0.00%
0 / 1
12
 createCategory
0.00% covered (danger)
0.00%
0 / 29
0.00% covered (danger)
0.00%
0 / 1
30
 updateCategory
0.00% covered (danger)
0.00%
0 / 35
0.00% covered (danger)
0.00%
0 / 1
110
 deleteCategory
0.00% covered (danger)
0.00%
0 / 31
0.00% covered (danger)
0.00%
0 / 1
42
 getPendingSuggestions
0.00% covered (danger)
0.00%
0 / 35
0.00% covered (danger)
0.00%
0 / 1
30
 reviewSuggestion
0.00% covered (danger)
0.00%
0 / 29
0.00% covered (danger)
0.00%
0 / 1
56
 getAnalytics
0.00% covered (danger)
0.00%
0 / 33
0.00% covered (danger)
0.00%
0 / 1
72
 getPopularArticles
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
2
 getCategoryStats
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
2
 generateSlug
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
2
 uploadImage
0.00% covered (danger)
0.00%
0 / 50
0.00% covered (danger)
0.00%
0 / 1
90
 getExtensionFromMime
0.00% covered (danger)
0.00%
0 / 7
0.00% covered (danger)
0.00%
0 / 1
2
 deleteComment
0.00% covered (danger)
0.00%
0 / 29
0.00% covered (danger)
0.00%
0 / 1
30
 getComments
0.00% covered (danger)
0.00%
0 / 43
0.00% covered (danger)
0.00%
0 / 1
72
 getStats
0.00% covered (danger)
0.00%
0 / 24
0.00% covered (danger)
0.00%
0 / 1
12
 getActivity
0.00% covered (danger)
0.00%
0 / 32
0.00% covered (danger)
0.00%
0 / 1
30
 getUserName
0.00% covered (danger)
0.00%
0 / 8
0.00% covered (danger)
0.00%
0 / 1
20
1<?php
2
3namespace BuyerKiosk\Support\Controllers;
4
5use BuyerKiosk\Support\ArticleManager;
6use BuyerKiosk\Support\Article;
7use BuyerKiosk\Support\Category;
8use BuyerKiosk\Support\CategoryManager;
9use BuyerKiosk\Support\SearchManager;
10use Exception;
11
12/**
13 * Support Admin Controller
14 *
15 * Handles admin operations for the Support Portal including
16 * article management, category management, edit suggestions moderation,
17 * and analytics.
18 *
19 * All methods require uri_support_admin permission.
20 *
21 * @package BuyerKiosk\Support
22 */
23class SupportAdminController
24{
25    /**
26     * @var \Slim\Slim Slim application instance
27     */
28    private $app;
29
30    /**
31     * @var \PDO Database connection (central kiosk_users DB)
32     */
33    private $db;
34
35    /**
36     * Constructor
37     *
38     * @param \Slim\Slim $app Slim application instance
39     */
40    public function __construct($app)
41    {
42        $this->app = $app;
43        // Use global database for support portal articles/categories (shared across all stores)
44        $this->db = \dbConnectByName('kiosk_buykiosk');
45    }
46
47    /**
48     * Check if user has admin permission
49     */
50    private function checkAdminPermission()
51    {
52        if (!$this->app->user->checkAccess('uri_support_admin')) {
53            $this->app->response->headers->set('Content-Type', 'application/json');
54            $this->app->halt(403, json_encode([
55                'error' => 'Access denied. Requires admin permission'
56            ]));
57            return false;
58        }
59        return true;
60    }
61
62    /**
63     * GET /api/support/admin/articles/
64     * Get all articles with optional filters
65     *
66     * Query params:
67     *   - status: string (draft, published, archived)
68     *   - categoryId: int
69     *   - limit: int (default: 20)
70     *   - offset: int (default: 0)
71     */
72    public function getArticles()
73    {
74        if (!$this->checkAdminPermission()) return;
75
76        try {
77            $status = $this->app->request->get('status');
78            $categoryId = $this->app->request->get('categoryId');
79
80            // Support both page/pageSize and limit/offset params
81            $page = $this->app->request->get('page');
82            $pageSize = $this->app->request->get('pageSize');
83            $limit = $this->app->request->get('limit');
84            $offset = $this->app->request->get('offset');
85
86            // If page/pageSize provided, convert to limit/offset
87            if ($page !== null && $pageSize !== null) {
88                $page = max(1, (int) $page);
89                $pageSize = max(1, min((int) $pageSize, 100));
90                $limit = $pageSize;
91                $offset = ($page - 1) * $pageSize;
92            } else {
93                $limit = $limit ? (int) $limit : 20;
94                $limit = max(1, min($limit, 100));
95                $offset = $offset ? (int) $offset : 0;
96                $offset = max(0, $offset);
97            }
98
99            $manager = new ArticleManager($this->db);
100            $result = $manager->getAll($status, $categoryId, $limit, $offset);
101
102            // Convert Article objects to arrays
103            $articlesData = array_map(function($article) {
104                return $article->toArray();
105            }, $result['articles']);
106
107            $this->app->response->headers->set('Content-Type', 'application/json');
108            $this->app->response->setBody(json_encode([
109                'success' => true,
110                'data' => $articlesData,
111                'total' => $result['total'],
112                'offset' => $offset,
113                'limit' => $limit
114            ]));
115        } catch (Exception $e) {
116            error_log("SupportAdminController::getArticles error: " . $e->getMessage());
117            $this->app->response->headers->set('Content-Type', 'application/json');
118            $this->app->halt(500, json_encode([
119                'error' => 'Failed to retrieve articles'
120            ]));
121        }
122    }
123
124    /**
125     * POST /api/support/admin/articles/
126     * Create a new article
127     *
128     * Request body: {
129     *   "title": string,
130     *   "slug": string,
131     *   "content": string,
132     *   "excerpt": string,
133     *   "categoryId": int,
134     *   "status": string (draft, published, archived),
135     *   "isFeatured": bool,
136     *   "tags": array,
137     *   "metaDescription": string
138     * }
139     */
140    public function createArticle()
141    {
142        if (!$this->checkAdminPermission()) return;
143
144        try {
145            $data = json_decode($this->app->request->getBody(), true);
146
147            // Validate required fields
148            if (empty($data['title']) || empty($data['content'])) {
149                $this->app->response->headers->set('Content-Type', 'application/json');
150                $this->app->halt(400, json_encode([
151                    'error' => 'title and content are required'
152                ]));
153                return;
154            }
155
156            // Auto-generate slug if not provided
157            if (empty($data['slug'])) {
158                $data['slug'] = $this->generateSlug($data['title']);
159            }
160
161            // Verify unique slug
162            $manager = new ArticleManager($this->db);
163            if ($manager->slugExists($data['slug'])) {
164                $this->app->response->headers->set('Content-Type', 'application/json');
165                $this->app->halt(400, json_encode([
166                    'error' => 'Slug already exists'
167                ]));
168                return;
169            }
170
171            $article = new Article();
172            $article->title = $data['title'];
173            $article->slug = $data['slug'];
174            $article->content = $data['content'];
175            $article->excerpt = $data['excerpt'] ?? '';
176            $article->categoryId = $data['categoryId'] ?? null;
177            $article->status = $data['status'] ?? 'draft';
178            $article->isFeatured = $data['isFeatured'] ?? false;
179            $article->tags = $data['tags'] ?? [];
180            $article->metaDescription = $data['metaDescription'] ?? '';
181            $article->authorId = $this->app->user->user_id;
182
183            $articleId = $article->save($this->db);
184
185            $this->app->response->setStatus(201);
186            $this->app->response->headers->set('Content-Type', 'application/json');
187            $this->app->response->setBody(json_encode([
188                'success' => true,
189                'articleId' => $articleId
190            ]));
191        } catch (Exception $e) {
192            error_log("SupportAdminController::createArticle error: " . $e->getMessage());
193            $this->app->response->headers->set('Content-Type', 'application/json');
194            $this->app->halt(500, json_encode([
195                'error' => 'Failed to create article'
196            ]));
197        }
198    }
199
200    /**
201     * PUT /api/support/admin/articles/:id/
202     * Update an existing article
203     *
204     * @param int $id Article ID
205     */
206    public function updateArticle($id)
207    {
208        if (!$this->checkAdminPermission()) return;
209
210        try {
211            $data = json_decode($this->app->request->getBody(), true);
212
213            $manager = new ArticleManager($this->db);
214            $article = $manager->getById($id);
215
216            if (!$article) {
217                $this->app->response->headers->set('Content-Type', 'application/json');
218                $this->app->halt(404, json_encode([
219                    'error' => 'Article not found'
220                ]));
221                return;
222            }
223
224            // Update fields if provided
225            if (isset($data['title'])) {
226                $article->title = $data['title'];
227            }
228            if (isset($data['slug'])) {
229                // Verify unique slug (excluding current article)
230                if ($data['slug'] !== $article->slug && $manager->slugExists($data['slug'])) {
231                    $this->app->response->headers->set('Content-Type', 'application/json');
232                    $this->app->halt(400, json_encode([
233                        'error' => 'Slug already exists'
234                    ]));
235                    return;
236                }
237                $article->slug = $data['slug'];
238            }
239            if (isset($data['content'])) {
240                $article->content = $data['content'];
241            }
242            if (isset($data['excerpt'])) {
243                $article->excerpt = $data['excerpt'];
244            }
245            if (isset($data['categoryId'])) {
246                $article->categoryId = $data['categoryId'];
247            }
248            if (isset($data['status'])) {
249                $article->status = $data['status'];
250            }
251            if (isset($data['isFeatured'])) {
252                $article->isFeatured = $data['isFeatured'];
253            }
254            if (isset($data['tags'])) {
255                $article->tags = $data['tags'];
256            }
257            if (isset($data['metaDescription'])) {
258                $article->metaDescription = $data['metaDescription'];
259            }
260
261            if ($article->update($this->db)) {
262                $this->app->response->headers->set('Content-Type', 'application/json');
263                $this->app->response->setBody(json_encode([
264                    'success' => true
265                ]));
266            } else {
267                $this->app->response->headers->set('Content-Type', 'application/json');
268                $this->app->halt(500, json_encode([
269                    'error' => 'Failed to update article'
270                ]));
271            }
272        } catch (Exception $e) {
273            error_log("SupportAdminController::updateArticle error: " . $e->getMessage());
274            $this->app->response->headers->set('Content-Type', 'application/json');
275            $this->app->halt(500, json_encode([
276                'error' => 'Failed to update article'
277            ]));
278        }
279    }
280
281    /**
282     * DELETE /api/support/admin/articles/:id/
283     * Delete an article
284     *
285     * @param int $id Article ID
286     */
287    public function deleteArticle($id)
288    {
289        if (!$this->checkAdminPermission()) return;
290
291        try {
292            $manager = new ArticleManager($this->db);
293            $article = $manager->getById($id);
294
295            if (!$article) {
296                $this->app->response->headers->set('Content-Type', 'application/json');
297                $this->app->halt(404, json_encode([
298                    'error' => 'Article not found'
299                ]));
300                return;
301            }
302
303            if ($article->delete($this->db)) {
304                $this->app->response->headers->set('Content-Type', 'application/json');
305                $this->app->response->setBody(json_encode([
306                    'success' => true
307                ]));
308            } else {
309                $this->app->response->headers->set('Content-Type', 'application/json');
310                $this->app->halt(500, json_encode([
311                    'error' => 'Failed to delete article'
312                ]));
313            }
314        } catch (Exception $e) {
315            error_log("SupportAdminController::deleteArticle error: " . $e->getMessage());
316            $this->app->response->headers->set('Content-Type', 'application/json');
317            $this->app->halt(500, json_encode([
318                'error' => 'Failed to delete article'
319            ]));
320        }
321    }
322
323    /**
324     * GET /api/support/admin/categories/
325     * Get all categories
326     */
327    public function getCategories()
328    {
329        if (!$this->checkAdminPermission()) return;
330
331        try {
332            $manager = new CategoryManager($this->db);
333            $categories = $manager->getAll();
334
335            $this->app->response->headers->set('Content-Type', 'application/json');
336            $this->app->response->setBody(json_encode([
337                'success' => true,
338                'data' => $categories
339            ]));
340        } catch (Exception $e) {
341            error_log("SupportAdminController::getCategories error: " . $e->getMessage());
342            $this->app->response->headers->set('Content-Type', 'application/json');
343            $this->app->halt(500, json_encode([
344                'error' => 'Failed to retrieve categories'
345            ]));
346        }
347    }
348
349    /**
350     * POST /api/support/admin/categories/
351     * Create a new category
352     *
353     * Request body: {
354     *   "name": string,
355     *   "slug": string,
356     *   "description": string,
357     *   "icon": string,
358     *   "sortOrder": int
359     * }
360     */
361    public function createCategory()
362    {
363        if (!$this->checkAdminPermission()) return;
364
365        try {
366            $data = json_decode($this->app->request->getBody(), true);
367
368            if (empty($data['name'])) {
369                $this->app->response->headers->set('Content-Type', 'application/json');
370                $this->app->halt(400, json_encode([
371                    'error' => 'name is required'
372                ]));
373                return;
374            }
375
376            // Auto-generate slug if not provided
377            if (empty($data['slug'])) {
378                $data['slug'] = $this->generateSlug($data['name']);
379            }
380
381            $category = new Category();
382            $category->name = $data['name'];
383            $category->slug = $data['slug'];
384            $category->description = $data['description'] ?? '';
385            $category->icon = $data['icon'] ?? null;
386            $category->sortOrder = $data['sortOrder'] ?? 0;
387
388            $categoryId = $category->save($this->db);
389
390            $this->app->response->setStatus(201);
391            $this->app->response->headers->set('Content-Type', 'application/json');
392            $this->app->response->setBody(json_encode([
393                'success' => true,
394                'categoryId' => $categoryId
395            ]));
396        } catch (Exception $e) {
397            error_log("SupportAdminController::createCategory error: " . $e->getMessage());
398            $this->app->response->headers->set('Content-Type', 'application/json');
399            $this->app->halt(500, json_encode([
400                'error' => 'Failed to create category'
401            ]));
402        }
403    }
404
405    /**
406     * PUT /api/support/admin/categories/:id/
407     * Update a category
408     *
409     * @param int $id Category ID
410     */
411    public function updateCategory($id)
412    {
413        if (!$this->checkAdminPermission()) return;
414
415        try {
416            $data = json_decode($this->app->request->getBody(), true);
417
418            $manager = new CategoryManager($this->db);
419            $category = $manager->getById($id);
420
421            if (!$category) {
422                $this->app->response->headers->set('Content-Type', 'application/json');
423                $this->app->halt(404, json_encode([
424                    'error' => 'Category not found'
425                ]));
426                return;
427            }
428
429            // Update fields if provided
430            if (isset($data['name'])) {
431                $category->name = $data['name'];
432            }
433            if (isset($data['slug'])) {
434                $category->slug = $data['slug'];
435            }
436            if (isset($data['description'])) {
437                $category->description = $data['description'];
438            }
439            if (isset($data['icon'])) {
440                $category->icon = $data['icon'];
441            }
442            if (isset($data['sortOrder'])) {
443                $category->sortOrder = $data['sortOrder'];
444            }
445
446            if ($category->update($this->db)) {
447                $this->app->response->headers->set('Content-Type', 'application/json');
448                $this->app->response->setBody(json_encode([
449                    'success' => true
450                ]));
451            } else {
452                $this->app->response->headers->set('Content-Type', 'application/json');
453                $this->app->halt(500, json_encode([
454                    'error' => 'Failed to update category'
455                ]));
456            }
457        } catch (Exception $e) {
458            error_log("SupportAdminController::updateCategory error: " . $e->getMessage());
459            $this->app->response->headers->set('Content-Type', 'application/json');
460            $this->app->halt(500, json_encode([
461                'error' => 'Failed to update category'
462            ]));
463        }
464    }
465
466    /**
467     * DELETE /api/support/admin/categories/:id/
468     * Delete a category
469     *
470     * @param int $id Category ID
471     */
472    public function deleteCategory($id)
473    {
474        if (!$this->checkAdminPermission()) return;
475
476        try {
477            $manager = new CategoryManager($this->db);
478            $category = $manager->getById($id);
479
480            if (!$category) {
481                $this->app->response->headers->set('Content-Type', 'application/json');
482                $this->app->halt(404, json_encode([
483                    'error' => 'Category not found'
484                ]));
485                return;
486            }
487
488            // Check if category has articles
489            $articleCount = $manager->getArticleCount($id);
490            if ($articleCount > 0) {
491                $this->app->response->headers->set('Content-Type', 'application/json');
492                $this->app->halt(400, json_encode([
493                    'error' => "Cannot delete category with {$articleCount} articles"
494                ]));
495                return;
496            }
497
498            if ($category->delete($this->db)) {
499                $this->app->response->headers->set('Content-Type', 'application/json');
500                $this->app->response->setBody(json_encode([
501                    'success' => true
502                ]));
503            } else {
504                $this->app->response->headers->set('Content-Type', 'application/json');
505                $this->app->halt(500, json_encode([
506                    'error' => 'Failed to delete category'
507                ]));
508            }
509        } catch (Exception $e) {
510            error_log("SupportAdminController::deleteCategory error: " . $e->getMessage());
511            $this->app->response->headers->set('Content-Type', 'application/json');
512            $this->app->halt(500, json_encode([
513                'error' => 'Failed to delete category'
514            ]));
515        }
516    }
517
518    /**
519     * GET /api/support/admin/suggestions/
520     * Get pending edit suggestions
521     *
522     * Query params:
523     *   - status: string (pending, approved, rejected)
524     *   - limit: int (default: 20)
525     *   - offset: int (default: 0)
526     */
527    public function getPendingSuggestions()
528    {
529        if (!$this->checkAdminPermission()) return;
530
531        try {
532            $status = $this->app->request->get('status') ?? 'pending';
533            $limit = $this->app->request->get('limit');
534            $limit = $limit ? (int) $limit : 20;
535            $limit = max(1, min($limit, 100));
536
537            $offset = $this->app->request->get('offset');
538            $offset = $offset ? (int) $offset : 0;
539            $offset = max(0, $offset);
540
541            $query = "
542                SELECT
543                    s.id,
544                    s.articleId,
545                    s.userId,
546                    s.suggestion,
547                    s.section,
548                    s.status,
549                    s.createdAt,
550                    s.reviewedAt,
551                    s.reviewedBy,
552                    a.title as articleTitle,
553                    u.display_name as userName
554                FROM support_edit_suggestions s
555                LEFT JOIN support_articles a ON s.articleId = a.id
556                LEFT JOIN uf_users u ON s.userId = u.id
557                WHERE s.status = :status
558                ORDER BY s.createdAt DESC
559                LIMIT :limit OFFSET :offset
560            ";
561
562            $stmt = $this->db->prepare($query);
563            $stmt->bindValue(':status', $status, \PDO::PARAM_STR);
564            $stmt->bindValue(':limit', $limit, \PDO::PARAM_INT);
565            $stmt->bindValue(':offset', $offset, \PDO::PARAM_INT);
566            $stmt->execute();
567
568            $suggestions = $stmt->fetchAll(\PDO::FETCH_ASSOC);
569
570            // Get total count
571            $countStmt = $this->db->prepare("
572                SELECT COUNT(*) as total
573                FROM support_edit_suggestions
574                WHERE status = :status
575            ");
576            $countStmt->bindValue(':status', $status, \PDO::PARAM_STR);
577            $countStmt->execute();
578            $total = $countStmt->fetch(\PDO::FETCH_ASSOC)['total'];
579
580            $this->app->response->headers->set('Content-Type', 'application/json');
581            $this->app->response->setBody(json_encode([
582                'success' => true,
583                'data' => $suggestions,
584                'total' => $total,
585                'offset' => $offset,
586                'limit' => $limit
587            ]));
588        } catch (Exception $e) {
589            error_log("SupportAdminController::getPendingSuggestions error: " . $e->getMessage());
590            $this->app->response->headers->set('Content-Type', 'application/json');
591            $this->app->halt(500, json_encode([
592                'error' => 'Failed to retrieve suggestions'
593            ]));
594        }
595    }
596
597    /**
598     * POST /api/support/admin/suggestions/:id/review/
599     * Approve or reject an edit suggestion
600     *
601     * Request body: {
602     *   "action": string (approve, reject),
603     *   "note": string (optional)
604     * }
605     *
606     * @param int $id Suggestion ID
607     */
608    public function reviewSuggestion($id)
609    {
610        if (!$this->checkAdminPermission()) return;
611
612        try {
613            $data = json_decode($this->app->request->getBody(), true);
614
615            if (empty($data['action']) || !in_array($data['action'], ['approve', 'reject'])) {
616                $this->app->response->headers->set('Content-Type', 'application/json');
617                $this->app->halt(400, json_encode([
618                    'error' => 'action must be "approve" or "reject"'
619                ]));
620                return;
621            }
622
623            $status = $data['action'] === 'approve' ? 'approved' : 'rejected';
624
625            $stmt = $this->db->prepare("
626                UPDATE support_edit_suggestions
627                SET status = :status,
628                    reviewedAt = NOW(),
629                    reviewedBy = :reviewedBy
630                WHERE id = :id
631            ");
632
633            $stmt->bindValue(':status', $status, \PDO::PARAM_STR);
634            $stmt->bindValue(':reviewedBy', $this->app->user->user_id, \PDO::PARAM_INT);
635            $stmt->bindValue(':id', $id, \PDO::PARAM_INT);
636
637            if ($stmt->execute()) {
638                $this->app->response->headers->set('Content-Type', 'application/json');
639                $this->app->response->setBody(json_encode([
640                    'success' => true
641                ]));
642            } else {
643                $this->app->response->headers->set('Content-Type', 'application/json');
644                $this->app->halt(500, json_encode([
645                    'error' => 'Failed to review suggestion'
646                ]));
647            }
648        } catch (Exception $e) {
649            error_log("SupportAdminController::reviewSuggestion error: " . $e->getMessage());
650            $this->app->response->headers->set('Content-Type', 'application/json');
651            $this->app->halt(500, json_encode([
652                'error' => 'Failed to review suggestion'
653            ]));
654        }
655    }
656
657    /**
658     * GET /api/support/admin/analytics/
659     * Get analytics data including popular searches, articles, etc.
660     *
661     * Query params:
662     *   - type: string (searches, popular_articles, category_stats)
663     *   - days: int (default: 30)
664     */
665    public function getAnalytics()
666    {
667        if (!$this->checkAdminPermission()) return;
668
669        try {
670            $type = $this->app->request->get('type') ?? 'searches';
671            $days = $this->app->request->get('days');
672            $days = $days ? (int) $days : 30;
673            $days = max(1, min($days, 365));
674
675            $data = [];
676
677            switch ($type) {
678                case 'searches':
679                    $data = SearchLog::getTopSearches($this->db, $days);
680                    break;
681
682                case 'popular_articles':
683                    $data = $this->getPopularArticles($days);
684                    break;
685
686                case 'category_stats':
687                    $data = $this->getCategoryStats();
688                    break;
689
690                default:
691                    $this->app->response->headers->set('Content-Type', 'application/json');
692                    $this->app->halt(400, json_encode([
693                        'error' => 'Invalid analytics type'
694                    ]));
695                    return;
696            }
697
698            $this->app->response->headers->set('Content-Type', 'application/json');
699            $this->app->response->setBody(json_encode([
700                'success' => true,
701                'type' => $type,
702                'days' => $days,
703                'data' => $data
704            ]));
705        } catch (Exception $e) {
706            error_log("SupportAdminController::getAnalytics error: " . $e->getMessage());
707            $this->app->response->headers->set('Content-Type', 'application/json');
708            $this->app->halt(500, json_encode([
709                'error' => 'Failed to retrieve analytics'
710            ]));
711        }
712    }
713
714    /**
715     * Get popular articles by views
716     *
717     * @param int $days Number of days to look back
718     * @return array
719     */
720    private function getPopularArticles($days)
721    {
722        $stmt = $this->db->prepare("
723            SELECT
724                id,
725                title,
726                slug,
727                views,
728                helpfulCount,
729                notHelpfulCount,
730                commentsCount
731            FROM support_articles
732            WHERE status = 'published'
733            AND createdAt >= DATE_SUB(NOW(), INTERVAL :days DAY)
734            ORDER BY views DESC
735            LIMIT 20
736        ");
737
738        $stmt->bindValue(':days', $days, \PDO::PARAM_INT);
739        $stmt->execute();
740
741        return $stmt->fetchAll(\PDO::FETCH_ASSOC);
742    }
743
744    /**
745     * Get category statistics
746     *
747     * @return array
748     */
749    private function getCategoryStats()
750    {
751        $stmt = $this->db->query("
752            SELECT
753                c.id,
754                c.name,
755                c.slug,
756                COUNT(a.id) as articleCount,
757                SUM(a.views) as totalViews,
758                SUM(a.helpfulCount) as totalHelpful
759            FROM support_categories c
760            LEFT JOIN support_articles a ON c.id = a.categoryId
761            GROUP BY c.id
762            ORDER BY articleCount DESC
763        ");
764
765        return $stmt->fetchAll(\PDO::FETCH_ASSOC);
766    }
767
768    /**
769     * Generate URL-friendly slug from string
770     *
771     * @param string $str
772     * @return string
773     */
774    private function generateSlug($str)
775    {
776        $str = strtolower(trim($str));
777        $str = preg_replace('/[^a-z0-9-]/', '-', $str);
778        $str = preg_replace('/-+/', '-', $str);
779        return trim($str, '-');
780    }
781
782    /**
783     * POST /api/support/admin/upload/
784     * Upload an image for use in articles
785     *
786     * Accepts multipart/form-data with 'image' file field
787     * Returns the URL of the uploaded image
788     */
789    public function uploadImage()
790    {
791        if (!$this->checkAdminPermission()) return;
792
793        try {
794            // Check if file was uploaded
795            if (!isset($_FILES['image']) || $_FILES['image']['error'] !== UPLOAD_ERR_OK) {
796                $this->app->response->headers->set('Content-Type', 'application/json');
797                $this->app->halt(400, json_encode([
798                    'error' => 'No image file uploaded or upload error'
799                ]));
800                return;
801            }
802
803            $file = $_FILES['image'];
804
805            // Validate file type
806            $allowedTypes = ['image/jpeg', 'image/png', 'image/gif', 'image/webp'];
807            $finfo = new \finfo(FILEINFO_MIME_TYPE);
808            $mimeType = $finfo->file($file['tmp_name']);
809
810            if (!in_array($mimeType, $allowedTypes)) {
811                $this->app->response->headers->set('Content-Type', 'application/json');
812                $this->app->halt(400, json_encode([
813                    'error' => 'Invalid file type. Allowed: JPEG, PNG, GIF, WebP'
814                ]));
815                return;
816            }
817
818            // Validate file size (max 5MB)
819            $maxSize = 5 * 1024 * 1024;
820            if ($file['size'] > $maxSize) {
821                $this->app->response->headers->set('Content-Type', 'application/json');
822                $this->app->halt(400, json_encode([
823                    'error' => 'File too large. Maximum size is 5MB'
824                ]));
825                return;
826            }
827
828            // Generate unique filename
829            $extension = $this->getExtensionFromMime($mimeType);
830            $filename = 'support_' . uniqid() . '_' . time() . '.' . $extension;
831
832            // Create upload directory if it doesn't exist
833            $uploadDir = $_SERVER['DOCUMENT_ROOT'] . '/images/support/articles/';
834            if (!is_dir($uploadDir)) {
835                mkdir($uploadDir, 0755, true);
836            }
837
838            // Move uploaded file
839            $destination = $uploadDir . $filename;
840            if (!move_uploaded_file($file['tmp_name'], $destination)) {
841                $this->app->response->headers->set('Content-Type', 'application/json');
842                $this->app->halt(500, json_encode([
843                    'error' => 'Failed to save uploaded file'
844                ]));
845                return;
846            }
847
848            // Return the URL
849            $url = '/images/support/articles/' . $filename;
850
851            $this->app->response->setStatus(201);
852            $this->app->response->headers->set('Content-Type', 'application/json');
853            $this->app->response->setBody(json_encode([
854                'success' => true,
855                'url' => $url,
856                'filename' => $filename
857            ]));
858        } catch (Exception $e) {
859            error_log("SupportAdminController::uploadImage error: " . $e->getMessage());
860            $this->app->response->headers->set('Content-Type', 'application/json');
861            $this->app->halt(500, json_encode([
862                'error' => 'Failed to upload image'
863            ]));
864        }
865    }
866
867    /**
868     * Get file extension from MIME type
869     *
870     * @param string $mimeType
871     * @return string
872     */
873    private function getExtensionFromMime($mimeType)
874    {
875        $map = [
876            'image/jpeg' => 'jpg',
877            'image/png' => 'png',
878            'image/gif' => 'gif',
879            'image/webp' => 'webp'
880        ];
881        return $map[$mimeType] ?? 'jpg';
882    }
883
884    /**
885     * DELETE /api/support/admin/comments/:id/
886     * Delete a comment (moderation)
887     *
888     * @param int $id Comment ID
889     */
890    public function deleteComment($id)
891    {
892        if (!$this->checkAdminPermission()) return;
893
894        try {
895            // Comments are stored in the global DB
896            $stmt = $this->db->prepare("
897                SELECT id FROM support_comments WHERE id = :id
898            ");
899            $stmt->execute([':id' => $id]);
900
901            if (!$stmt->fetch()) {
902                $this->app->response->headers->set('Content-Type', 'application/json');
903                $this->app->halt(404, json_encode([
904                    'error' => 'Comment not found'
905                ]));
906                return;
907            }
908
909            // Delete the comment
910            $deleteStmt = $this->db->prepare("
911                DELETE FROM support_comments WHERE id = :id
912            ");
913
914            if ($deleteStmt->execute([':id' => $id])) {
915                // Update comment count on article
916                $this->db->exec("
917                    UPDATE support_articles a
918                    SET commentsCount = (
919                        SELECT COUNT(*) FROM support_comments c WHERE c.articleId = a.id
920                    )
921                ");
922
923                $this->app->response->headers->set('Content-Type', 'application/json');
924                $this->app->response->setBody(json_encode([
925                    'success' => true
926                ]));
927            } else {
928                $this->app->response->headers->set('Content-Type', 'application/json');
929                $this->app->halt(500, json_encode([
930                    'error' => 'Failed to delete comment'
931                ]));
932            }
933        } catch (Exception $e) {
934            error_log("SupportAdminController::deleteComment error: " . $e->getMessage());
935            $this->app->response->headers->set('Content-Type', 'application/json');
936            $this->app->halt(500, json_encode([
937                'error' => 'Failed to delete comment'
938            ]));
939        }
940    }
941
942    /**
943     * GET /api/support/admin/comments/
944     * Get all comments with optional filters for moderation
945     *
946     * Query params:
947     *   - articleId: int (optional)
948     *   - limit: int (default: 50)
949     *   - offset: int (default: 0)
950     */
951    public function getComments()
952    {
953        if (!$this->checkAdminPermission()) return;
954
955        try {
956            $articleId = $this->app->request->get('articleId');
957            $limit = $this->app->request->get('limit');
958            $limit = $limit ? (int) $limit : 50;
959            $limit = max(1, min($limit, 100));
960
961            $offset = $this->app->request->get('offset');
962            $offset = $offset ? (int) $offset : 0;
963            $offset = max(0, $offset);
964
965            $whereClause = '';
966            $params = [];
967
968            if ($articleId) {
969                $whereClause = 'WHERE c.articleId = :articleId';
970                $params[':articleId'] = $articleId;
971            }
972
973            $query = "
974                SELECT
975                    c.id,
976                    c.articleId,
977                    c.userId,
978                    c.content,
979                    c.createdAt,
980                    a.title as articleTitle,
981                    u.display_name as userName
982                FROM support_comments c
983                LEFT JOIN support_articles a ON c.articleId = a.id
984                LEFT JOIN uf_users u ON c.userId = u.id
985                {$whereClause}
986                ORDER BY c.createdAt DESC
987                LIMIT :limit OFFSET :offset
988            ";
989
990            $stmt = $this->db->prepare($query);
991            foreach ($params as $key => $value) {
992                $stmt->bindValue($key, $value);
993            }
994            $stmt->bindValue(':limit', $limit, \PDO::PARAM_INT);
995            $stmt->bindValue(':offset', $offset, \PDO::PARAM_INT);
996            $stmt->execute();
997
998            $comments = $stmt->fetchAll(\PDO::FETCH_ASSOC);
999
1000            // Get total count
1001            $countQuery = "SELECT COUNT(*) as total FROM support_comments c {$whereClause}";
1002            $countStmt = $this->db->prepare($countQuery);
1003            foreach ($params as $key => $value) {
1004                $countStmt->bindValue($key, $value);
1005            }
1006            $countStmt->execute();
1007            $total = $countStmt->fetch(\PDO::FETCH_ASSOC)['total'];
1008
1009            $this->app->response->headers->set('Content-Type', 'application/json');
1010            $this->app->response->setBody(json_encode([
1011                'success' => true,
1012                'data' => $comments,
1013                'total' => $total,
1014                'offset' => $offset,
1015                'limit' => $limit
1016            ]));
1017        } catch (Exception $e) {
1018            error_log("SupportAdminController::getComments error: " . $e->getMessage());
1019            $this->app->response->headers->set('Content-Type', 'application/json');
1020            $this->app->halt(500, json_encode([
1021                'error' => 'Failed to retrieve comments'
1022            ]));
1023        }
1024    }
1025
1026    /**
1027     * GET /api/support/admin/stats/
1028     * Get dashboard statistics
1029     */
1030    public function getStats()
1031    {
1032        if (!$this->checkAdminPermission()) return;
1033
1034        try {
1035            // Get total articles
1036            $articlesStmt = $this->db->query("SELECT COUNT(*) as total FROM support_articles");
1037            $totalArticles = $articlesStmt->fetch(\PDO::FETCH_ASSOC)['total'];
1038
1039            // Get total categories
1040            $categoriesStmt = $this->db->query("SELECT COUNT(*) as total FROM support_categories");
1041            $totalCategories = $categoriesStmt->fetch(\PDO::FETCH_ASSOC)['total'];
1042
1043            // Get pending suggestions (need to check across all store DBs)
1044            // For now, we'll return 0 - this should be enhanced to aggregate from store DBs
1045            $pendingSuggestions = 0;
1046
1047            // Get total searches (last 30 days)
1048            $searchesStmt = $this->db->prepare("
1049                SELECT COUNT(*) as total FROM support_search_log
1050                WHERE created_at >= DATE_SUB(NOW(), INTERVAL 30 DAY)
1051            ");
1052            $searchesStmt->execute();
1053            $totalSearches = $searchesStmt->fetch(\PDO::FETCH_ASSOC)['total'] ?? 0;
1054
1055            $this->app->response->headers->set('Content-Type', 'application/json');
1056            $this->app->response->setBody(json_encode([
1057                'success' => true,
1058                'totalArticles' => (int) $totalArticles,
1059                'totalCategories' => (int) $totalCategories,
1060                'pendingSuggestions' => (int) $pendingSuggestions,
1061                'totalSearches' => (int) $totalSearches
1062            ]));
1063        } catch (Exception $e) {
1064            error_log("SupportAdminController::getStats error: " . $e->getMessage());
1065            $this->app->response->headers->set('Content-Type', 'application/json');
1066            $this->app->halt(500, json_encode([
1067                'error' => 'Failed to retrieve stats'
1068            ]));
1069        }
1070    }
1071
1072    /**
1073     * GET /api/support/admin/activity/
1074     * Get recent activity for dashboard
1075     *
1076     * Query params:
1077     *   - limit: int (default: 10)
1078     */
1079    public function getActivity()
1080    {
1081        if (!$this->checkAdminPermission()) return;
1082
1083        try {
1084            $limit = $this->app->request->get('limit');
1085            $limit = $limit ? (int) $limit : 10;
1086            $limit = max(1, min($limit, 50));
1087
1088            $activities = [];
1089
1090            // Get recently created/updated articles
1091            $articlesStmt = $this->db->prepare("
1092                SELECT
1093                    id,
1094                    title,
1095                    'article_update' as type,
1096                    updatedAt as createdAt,
1097                    authorId as userId
1098                FROM support_articles
1099                ORDER BY updatedAt DESC
1100                LIMIT :limit
1101            ");
1102            $articlesStmt->bindValue(':limit', $limit, \PDO::PARAM_INT);
1103            $articlesStmt->execute();
1104
1105            $articleActivities = $articlesStmt->fetchAll(\PDO::FETCH_ASSOC);
1106
1107            foreach ($articleActivities as $activity) {
1108                $activities[] = [
1109                    'type' => $activity['type'],
1110                    'title' => 'Updated article: ' . $activity['title'],
1111                    'createdAt' => $activity['createdAt'],
1112                    'userName' => $this->getUserName($activity['userId'])
1113                ];
1114            }
1115
1116            // Sort by date descending
1117            usort($activities, function($a, $b) {
1118                return strtotime($b['createdAt']) - strtotime($a['createdAt']);
1119            });
1120
1121            // Limit to requested amount
1122            $activities = array_slice($activities, 0, $limit);
1123
1124            $this->app->response->headers->set('Content-Type', 'application/json');
1125            $this->app->response->setBody(json_encode([
1126                'success' => true,
1127                'activities' => $activities
1128            ]));
1129        } catch (Exception $e) {
1130            error_log("SupportAdminController::getActivity error: " . $e->getMessage());
1131            $this->app->response->headers->set('Content-Type', 'application/json');
1132            $this->app->halt(500, json_encode([
1133                'error' => 'Failed to retrieve activity'
1134            ]));
1135        }
1136    }
1137
1138    /**
1139     * Get user display name by ID
1140     *
1141     * @param int $userId
1142     * @return string
1143     */
1144    private function getUserName($userId)
1145    {
1146        if (!$userId) return 'Unknown';
1147
1148        try {
1149            $usersDb = \dbConnectByName('kiosk_users');
1150            $stmt = $usersDb->prepare("SELECT display_name FROM uf_users WHERE id = :id");
1151            $stmt->execute([':id' => $userId]);
1152            $result = $stmt->fetch(\PDO::FETCH_ASSOC);
1153            return $result ? $result['display_name'] : 'Unknown';
1154        } catch (Exception $e) {
1155            return 'Unknown';
1156        }
1157    }
1158}