Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 500
0.00% covered (danger)
0.00%
0 / 16
CRAP
0.00% covered (danger)
0.00%
0 / 1
SupportApiController
0.00% covered (danger)
0.00%
0 / 500
0.00% covered (danger)
0.00%
0 / 16
5402
0.00% covered (danger)
0.00%
0 / 1
 __construct
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
2
 getCategories
0.00% covered (danger)
0.00%
0 / 15
0.00% covered (danger)
0.00%
0 / 1
6
 getCategory
0.00% covered (danger)
0.00%
0 / 31
0.00% covered (danger)
0.00%
0 / 1
12
 getFeaturedArticles
0.00% covered (danger)
0.00%
0 / 15
0.00% covered (danger)
0.00%
0 / 1
6
 getRecentArticles
0.00% covered (danger)
0.00%
0 / 15
0.00% covered (danger)
0.00%
0 / 1
6
 getArticleBySlug
0.00% covered (danger)
0.00%
0 / 25
0.00% covered (danger)
0.00%
0 / 1
12
 search
0.00% covered (danger)
0.00%
0 / 31
0.00% covered (danger)
0.00%
0 / 1
30
 getArticle
0.00% covered (danger)
0.00%
0 / 36
0.00% covered (danger)
0.00%
0 / 1
20
 trackView
0.00% covered (danger)
0.00%
0 / 13
0.00% covered (danger)
0.00%
0 / 1
6
 addReaction
0.00% covered (danger)
0.00%
0 / 62
0.00% covered (danger)
0.00%
0 / 1
110
 removeReaction
0.00% covered (danger)
0.00%
0 / 32
0.00% covered (danger)
0.00%
0 / 1
20
 getComments
0.00% covered (danger)
0.00%
0 / 30
0.00% covered (danger)
0.00%
0 / 1
30
 addComment
0.00% covered (danger)
0.00%
0 / 48
0.00% covered (danger)
0.00%
0 / 1
56
 deleteComment
0.00% covered (danger)
0.00%
0 / 34
0.00% covered (danger)
0.00%
0 / 1
42
 submitEditSuggestion
0.00% covered (danger)
0.00%
0 / 60
0.00% covered (danger)
0.00%
0 / 1
90
 uploadImage
0.00% covered (danger)
0.00%
0 / 50
0.00% covered (danger)
0.00%
0 / 1
72
1<?php
2
3namespace BuyerKiosk\Support\Controllers;
4
5use BuyerKiosk\Support\ArticleManager;
6use BuyerKiosk\Support\Article;
7use BuyerKiosk\Support\Reaction;
8use BuyerKiosk\Support\Comment;
9use BuyerKiosk\Support\SearchManager;
10use Exception;
11use Throwable;
12
13/**
14 * Support System API Controller
15 *
16 * Handles REST API endpoints for the Support Portal including
17 * search, article retrieval, reactions, comments, and edit suggestions.
18 *
19 * @package BuyerKiosk\Support
20 */
21class SupportApiController
22{
23    /**
24     * @var \Slim\Slim Slim application instance
25     */
26    private $app;
27
28    /**
29     * @var \Store Store object
30     */
31    private $store;
32
33    /**
34     * @var \PDO Database connection
35     */
36    private $db;
37
38    /**
39     * Constructor
40     *
41     * @param \Slim\Slim $app Slim application instance
42     * @param \Store $store Store object
43     */
44    public function __construct($app, \Store $store)
45    {
46        $this->app = $app;
47        $this->store = $store;
48        $this->db = \dbConnectByName($store->getDbName());
49    }
50
51    /**
52     * GET /api/:typeNum/support/categories/
53     * Get all active categories with article counts
54     *
55     * @param string $typeNum Store identifier
56     */
57    public function getCategories($typeNum)
58    {
59        try {
60            $globalDb = \dbConnectByName('kiosk_buykiosk');
61
62            $stmt = $globalDb->query("
63                SELECT c.*,
64                       COUNT(a.id) as articleCount
65                FROM support_categories c
66                LEFT JOIN support_articles a ON a.category_id = c.id AND a.status = 'published'
67                WHERE c.is_active = 1
68                GROUP BY c.id
69                ORDER BY c.sort_order ASC, c.name ASC
70            ");
71
72            $categories = $stmt->fetchAll(\PDO::FETCH_ASSOC);
73
74            $this->app->response->headers->set('Content-Type', 'application/json');
75            $this->app->response->setBody(json_encode([
76                'success' => true,
77                'data' => $categories
78            ]));
79        } catch (Exception $e) {
80            error_log("SupportApiController::getCategories error: " . $e->getMessage());
81            $this->app->response->headers->set('Content-Type', 'application/json');
82            $this->app->halt(500, json_encode([
83                'error' => 'Failed to retrieve categories'
84            ]));
85        }
86    }
87
88    /**
89     * GET /api/:typeNum/support/category/:slug/
90     * Get a category with its articles
91     *
92     * @param string $typeNum Store identifier
93     * @param string $slug Category slug
94     */
95    public function getCategory($typeNum, $slug)
96    {
97        try {
98            $globalDb = \dbConnectByName('kiosk_buykiosk');
99
100            // Get category
101            $stmt = $globalDb->prepare("
102                SELECT * FROM support_categories
103                WHERE slug = :slug AND is_active = 1
104            ");
105            $stmt->bindValue(':slug', $slug, \PDO::PARAM_STR);
106            $stmt->execute();
107            $category = $stmt->fetch(\PDO::FETCH_ASSOC);
108
109            if (!$category) {
110                $this->app->response->headers->set('Content-Type', 'application/json');
111                $this->app->halt(404, json_encode([
112                    'error' => 'Category not found'
113                ]));
114                return;
115            }
116
117            // Get articles in category
118            $stmt = $globalDb->prepare("
119                SELECT a.*, c.name as categoryName
120                FROM support_articles a
121                LEFT JOIN support_categories c ON c.id = a.category_id
122                WHERE a.category_id = :categoryId AND a.status = 'published'
123                ORDER BY a.is_featured DESC, a.updated_at DESC
124            ");
125            $stmt->bindValue(':categoryId', $category['id'], \PDO::PARAM_INT);
126            $stmt->execute();
127            $articles = $stmt->fetchAll(\PDO::FETCH_ASSOC);
128
129            $this->app->response->headers->set('Content-Type', 'application/json');
130            $this->app->response->setBody(json_encode([
131                'success' => true,
132                'data' => [
133                    'category' => $category,
134                    'articles' => $articles
135                ]
136            ]));
137        } catch (Exception $e) {
138            error_log("SupportApiController::getCategory error: " . $e->getMessage());
139            $this->app->response->headers->set('Content-Type', 'application/json');
140            $this->app->halt(500, json_encode([
141                'error' => 'Failed to retrieve category'
142            ]));
143        }
144    }
145
146    /**
147     * GET /api/:typeNum/support/articles/featured/
148     * Get featured articles
149     *
150     * @param string $typeNum Store identifier
151     */
152    public function getFeaturedArticles($typeNum)
153    {
154        try {
155            $globalDb = \dbConnectByName('kiosk_buykiosk');
156
157            $stmt = $globalDb->query("
158                SELECT a.*, c.name as categoryName
159                FROM support_articles a
160                LEFT JOIN support_categories c ON c.id = a.category_id
161                WHERE a.status = 'published' AND a.is_featured = 1
162                ORDER BY a.updated_at DESC
163                LIMIT 5
164            ");
165
166            $articles = $stmt->fetchAll(\PDO::FETCH_ASSOC);
167
168            $this->app->response->headers->set('Content-Type', 'application/json');
169            $this->app->response->setBody(json_encode([
170                'success' => true,
171                'data' => $articles
172            ]));
173        } catch (Exception $e) {
174            error_log("SupportApiController::getFeaturedArticles error: " . $e->getMessage());
175            $this->app->response->headers->set('Content-Type', 'application/json');
176            $this->app->halt(500, json_encode([
177                'error' => 'Failed to retrieve featured articles'
178            ]));
179        }
180    }
181
182    /**
183     * GET /api/:typeNum/support/articles/recent/
184     * Get recently updated articles
185     *
186     * @param string $typeNum Store identifier
187     */
188    public function getRecentArticles($typeNum)
189    {
190        try {
191            $globalDb = \dbConnectByName('kiosk_buykiosk');
192
193            $stmt = $globalDb->query("
194                SELECT a.*, c.name as categoryName
195                FROM support_articles a
196                LEFT JOIN support_categories c ON c.id = a.category_id
197                WHERE a.status = 'published'
198                ORDER BY a.updated_at DESC
199                LIMIT 10
200            ");
201
202            $articles = $stmt->fetchAll(\PDO::FETCH_ASSOC);
203
204            $this->app->response->headers->set('Content-Type', 'application/json');
205            $this->app->response->setBody(json_encode([
206                'success' => true,
207                'data' => $articles
208            ]));
209        } catch (Exception $e) {
210            error_log("SupportApiController::getRecentArticles error: " . $e->getMessage());
211            $this->app->response->headers->set('Content-Type', 'application/json');
212            $this->app->halt(500, json_encode([
213                'error' => 'Failed to retrieve recent articles'
214            ]));
215        }
216    }
217
218    /**
219     * GET /api/:typeNum/support/article/:slug/
220     * Get a single article by slug
221     *
222     * @param string $typeNum Store identifier
223     * @param string $slug Article slug
224     */
225    public function getArticleBySlug($typeNum, $slug)
226    {
227        try {
228            $globalDb = \dbConnectByName('kiosk_buykiosk');
229
230            $stmt = $globalDb->prepare("
231                SELECT a.*, c.name as categoryName
232                FROM support_articles a
233                LEFT JOIN support_categories c ON c.id = a.category_id
234                WHERE a.slug = :slug AND a.status = 'published'
235            ");
236            $stmt->bindValue(':slug', $slug, \PDO::PARAM_STR);
237            $stmt->execute();
238            $article = $stmt->fetch(\PDO::FETCH_ASSOC);
239
240            if (!$article) {
241                $this->app->response->headers->set('Content-Type', 'application/json');
242                $this->app->halt(404, json_encode([
243                    'error' => 'Article not found'
244                ]));
245                return;
246            }
247
248            // Increment view count
249            $globalDb->prepare("UPDATE support_articles SET view_count = view_count + 1 WHERE id = :id")
250                ->execute([':id' => $article['id']]);
251
252            $this->app->response->headers->set('Content-Type', 'application/json');
253            $this->app->response->setBody(json_encode([
254                'success' => true,
255                'data' => $article
256            ]));
257        } catch (Exception $e) {
258            error_log("SupportApiController::getArticleBySlug error: " . $e->getMessage());
259            $this->app->response->headers->set('Content-Type', 'application/json');
260            $this->app->halt(500, json_encode([
261                'error' => 'Failed to retrieve article'
262            ]));
263        }
264    }
265
266    /**
267     * GET /api/:typeNum/support/search/
268     * Search articles and log the query
269     *
270     * Query params:
271     *   - q: string - search query
272     *   - limit: int (default: 10)
273     *   - offset: int (default: 0)
274     */
275    public function search($typeNum)
276    {
277        try {
278            $query = $this->app->request->get('q');
279
280            if (empty($query)) {
281                $this->app->response->headers->set('Content-Type', 'application/json');
282                $this->app->halt(400, json_encode([
283                    'error' => 'Search query is required'
284                ]));
285                return;
286            }
287
288            $limit = $this->app->request->get('limit');
289            $limit = $limit ? (int) $limit : 10;
290            $limit = max(1, min($limit, 50)); // Clamp between 1 and 50
291
292            // Use global DB for articles and search
293            $globalDb = \dbConnectByName('kiosk_buykiosk');
294
295            // Use SearchManager for search with logging
296            $searchManager = new SearchManager($globalDb);
297            $storeId = $this->store->getId();
298            $userId = $this->app->user->user_id ?? 0;
299
300            $result = $searchManager->search($query, $storeId, $userId, $limit);
301
302            // Convert Article objects to arrays for JSON response
303            $articlesData = [];
304            foreach ($result['results'] as $article) {
305                $articlesData[] = $article->toArray();
306            }
307
308            $this->app->response->headers->set('Content-Type', 'application/json');
309            $this->app->response->setBody(json_encode([
310                'success' => true,
311                'data' => $articlesData,
312                'total' => $result['count'],
313                'limit' => $limit
314            ]));
315        } catch (Exception $e) {
316            error_log("SupportApiController::search error: " . $e->getMessage());
317            $this->app->response->headers->set('Content-Type', 'application/json');
318            $this->app->halt(500, json_encode([
319                'error' => 'Search failed'
320            ]));
321        }
322    }
323
324    /**
325     * GET /api/:typeNum/support/articles/:id/
326     * Get a single article with reactions and comments
327     *
328     * @param string $typeNum Store identifier
329     * @param int $id Article ID
330     */
331    public function getArticle($typeNum, $id)
332    {
333        try {
334            // Articles are in the global DB
335            $globalDb = \dbConnectByName('kiosk_buykiosk');
336            $manager = new ArticleManager($globalDb);
337            $article = $manager->getArticleById($id);
338
339            if (!$article) {
340                $this->app->response->headers->set('Content-Type', 'application/json');
341                $this->app->halt(404, json_encode([
342                    'error' => 'Article not found'
343                ]));
344                return;
345            }
346
347            // Increment view count
348            $globalDb->prepare("UPDATE support_articles SET view_count = view_count + 1 WHERE id = :id")
349                ->execute([':id' => $id]);
350
351            // Get reactions from store DB
352            $reactionCounts = Reaction::getCountsForArticle($this->db, $id);
353
354            // Get user's reaction if logged in (use employeeId)
355            $userReaction = null;
356            $employeeId = $this->app->user->employee_id ?? 0;
357            if ($employeeId) {
358                $userReaction = Reaction::getUserReaction(
359                    $this->db,
360                    $id,
361                    $employeeId
362                );
363            }
364
365            // Get comments count from store DB
366            $commentsCount = Comment::getCountForArticle($this->db, $id);
367
368            $articleData = $article->toArray();
369            $articleData['reactionCounts'] = $reactionCounts;
370            $articleData['userReaction'] = $userReaction;
371            $articleData['commentsCount'] = $commentsCount;
372
373            $this->app->response->headers->set('Content-Type', 'application/json');
374            $this->app->response->setBody(json_encode([
375                'success' => true,
376                'data' => $articleData
377            ]));
378        } catch (Exception $e) {
379            error_log("SupportApiController::getArticle error: " . $e->getMessage());
380            $this->app->response->headers->set('Content-Type', 'application/json');
381            $this->app->halt(500, json_encode([
382                'error' => 'Failed to retrieve article'
383            ]));
384        }
385    }
386
387    /**
388     * POST /api/:typeNum/support/articles/:articleId/view/
389     * Track article view (increment view count)
390     *
391     * @param string $typeNum Store identifier
392     * @param int $articleId Article ID
393     */
394    public function trackView($typeNum, $articleId)
395    {
396        try {
397            // Articles are in the global DB
398            $globalDb = \dbConnectByName('kiosk_buykiosk');
399
400            // Increment view count
401            $stmt = $globalDb->prepare("UPDATE support_articles SET view_count = view_count + 1 WHERE id = :id");
402            $stmt->execute([':id' => $articleId]);
403
404            $this->app->response->headers->set('Content-Type', 'application/json');
405            $this->app->response->setBody(json_encode([
406                'success' => true
407            ]));
408        } catch (Exception $e) {
409            error_log("SupportApiController::trackView error: " . $e->getMessage());
410            $this->app->response->headers->set('Content-Type', 'application/json');
411            $this->app->halt(500, json_encode([
412                'error' => 'Failed to track view'
413            ]));
414        }
415    }
416
417    /**
418     * POST /api/:typeNum/support/articles/:articleId/reaction/
419     * Add or toggle a reaction to an article
420     *
421     * Request body: {
422     *   "reactionType": string ("helpful", "not_helpful")
423     * }
424     *
425     * @param string $typeNum Store identifier
426     * @param int $articleId Article ID
427     */
428    public function addReaction($typeNum, $articleId)
429    {
430        try {
431            // Support both JSON and form-encoded data
432            $data = json_decode($this->app->request->getBody(), true);
433            if (empty($data)) {
434                // Fallback to POST params for form-encoded data
435                $data = $this->app->request->post();
436            }
437
438            // Accept 'type' or 'reactionType'
439            $reactionType = $data['reactionType'] ?? $data['type'] ?? null;
440
441            if (empty($reactionType)) {
442                $this->app->response->headers->set('Content-Type', 'application/json');
443                $this->app->halt(400, json_encode([
444                    'error' => 'reactionType or type is required'
445                ]));
446                return;
447            }
448
449            // Valid types per migration: 'like', 'helpful', 'heart'
450            if (!in_array($reactionType, ['like', 'helpful', 'heart'])) {
451                $this->app->response->headers->set('Content-Type', 'application/json');
452                $this->app->halt(400, json_encode([
453                    'error' => 'Invalid reactionType. Must be "like", "helpful", or "heart"'
454                ]));
455                return;
456            }
457
458            // Verify article exists using global DB
459            $globalDb = \dbConnectByName('kiosk_buykiosk');
460            $stmt = $globalDb->prepare("SELECT id FROM support_articles WHERE id = :id");
461            $stmt->execute([':id' => $articleId]);
462            if (!$stmt->fetch()) {
463                $this->app->response->headers->set('Content-Type', 'application/json');
464                $this->app->halt(404, json_encode([
465                    'error' => 'Article not found'
466                ]));
467                return;
468            }
469
470            // Get employeeId from user - try user_employee_links first, fall back to user id
471            $userId = $this->app->user->id ?? 0;
472            if (!$userId) {
473                $this->app->response->headers->set('Content-Type', 'application/json');
474                $this->app->halt(400, json_encode([
475                    'error' => 'User context required for reactions'
476                ]));
477                return;
478            }
479
480            // Try to get linked employee ID for this store
481            $employeeId = 0;
482            $centralDb = \dbConnectByName('kiosk_users');
483            $linkStmt = $centralDb->prepare(
484                "SELECT employeeId FROM user_employee_links WHERE userId = :userId AND typeNum = :typeNum"
485            );
486            $linkStmt->execute([':userId' => $userId, ':typeNum' => $typeNum]);
487            $link = $linkStmt->fetch(\PDO::FETCH_ASSOC);
488            if ($link) {
489                $employeeId = (int) $link['employeeId'];
490            }
491
492            // If no employee link, use user id as a fallback (prefixed to avoid collision)
493            if (!$employeeId) {
494                $employeeId = $userId;
495            }
496
497            $result = Reaction::toggle($this->db, $articleId, $employeeId, $reactionType);
498            $reactionCounts = Reaction::getCountsForArticle($this->db, $articleId);
499
500            $this->app->response->headers->set('Content-Type', 'application/json');
501            $this->app->response->setBody(json_encode([
502                'success' => true,
503                'action' => $result['action'],
504                'reactionCounts' => $reactionCounts
505            ]));
506        } catch (\Slim\Exception\Stop $e) {
507            // Re-throw Slim's Stop exception (from halt())
508            throw $e;
509        } catch (Throwable $e) {
510            error_log("SupportApiController::addReaction error: " . $e->getMessage() . " - " . $e->getTraceAsString());
511            $this->app->response->headers->set('Content-Type', 'application/json');
512            $this->app->response->setStatus(500);
513            $this->app->response->setBody(json_encode([
514                'error' => 'Failed to toggle reaction',
515                'debug' => $e->getMessage(),
516                'trace' => $e->getFile() . ':' . $e->getLine()
517            ]));
518        }
519    }
520
521    /**
522     * DELETE /api/:typeNum/support/articles/:articleId/reaction/
523     * Remove a reaction from an article
524     *
525     * @param string $typeNum Store identifier
526     * @param int $articleId Article ID
527     */
528    public function removeReaction($typeNum, $articleId)
529    {
530        try {
531            // Verify article exists using global DB
532            $globalDb = \dbConnectByName('kiosk_buykiosk');
533            $stmt = $globalDb->prepare("SELECT id FROM support_articles WHERE id = :id");
534            $stmt->execute([':id' => $articleId]);
535            if (!$stmt->fetch()) {
536                $this->app->response->headers->set('Content-Type', 'application/json');
537                $this->app->halt(404, json_encode([
538                    'error' => 'Article not found'
539                ]));
540                return;
541            }
542
543            // Get employeeId and reactionType from request
544            $data = json_decode($this->app->request->getBody(), true);
545            $reactionType = $data['type'] ?? 'helpful';
546            $employeeId = $this->app->user->employee_id ?? 0;
547
548            if (!$employeeId) {
549                $this->app->response->headers->set('Content-Type', 'application/json');
550                $this->app->halt(400, json_encode([
551                    'error' => 'Employee context required'
552                ]));
553                return;
554            }
555
556            // Use toggle to remove the reaction (if it exists, it will be removed)
557            $result = Reaction::toggle($this->db, $articleId, $employeeId, $reactionType);
558            $reactionCounts = Reaction::getCountsForArticle($this->db, $articleId);
559
560            $this->app->response->headers->set('Content-Type', 'application/json');
561            $this->app->response->setBody(json_encode([
562                'success' => true,
563                'action' => $result['action'],
564                'reactionCounts' => $reactionCounts
565            ]));
566        } catch (Exception $e) {
567            error_log("SupportApiController::removeReaction error: " . $e->getMessage());
568            $this->app->response->headers->set('Content-Type', 'application/json');
569            $this->app->halt(500, json_encode([
570                'error' => 'Failed to remove reaction'
571            ]));
572        }
573    }
574
575    /**
576     * GET /api/:typeNum/support/articles/:articleId/comments/
577     * Get all comments for an article
578     *
579     * @param string $typeNum Store identifier
580     * @param int $articleId Article ID
581     */
582    public function getComments($typeNum, $articleId)
583    {
584        try {
585            // Verify article exists using global DB
586            $globalDb = \dbConnectByName('kiosk_buykiosk');
587            $stmt = $globalDb->prepare("SELECT id FROM support_articles WHERE id = :id");
588            $stmt->execute([':id' => $articleId]);
589            if (!$stmt->fetch()) {
590                $this->app->response->headers->set('Content-Type', 'application/json');
591                $this->app->halt(404, json_encode([
592                    'error' => 'Article not found'
593                ]));
594                return;
595            }
596
597            $comments = Comment::getForArticle($this->db, $articleId);
598
599            // Convert Comment objects to arrays for JSON response
600            $commentsData = [];
601            foreach ($comments as $comment) {
602                $commentsData[] = [
603                    'id' => $comment->id,
604                    'articleId' => $comment->articleId,
605                    'authorName' => $comment->employeeName ?: 'Anonymous',
606                    'content' => $comment->comment,
607                    'createdAt' => $comment->createdAt
608                ];
609            }
610
611            $this->app->response->headers->set('Content-Type', 'application/json');
612            $this->app->response->setBody(json_encode([
613                'success' => true,
614                'data' => $commentsData
615            ]));
616        } catch (Exception $e) {
617            error_log("SupportApiController::getComments error: " . $e->getMessage());
618            $this->app->response->headers->set('Content-Type', 'application/json');
619            $this->app->halt(500, json_encode([
620                'error' => 'Failed to retrieve comments'
621            ]));
622        }
623    }
624
625    /**
626     * POST /api/:typeNum/support/articles/:articleId/comments/
627     * Add a comment to an article
628     *
629     * Request body: {
630     *   "comment": string
631     * }
632     *
633     * @param string $typeNum Store identifier
634     * @param int $articleId Article ID
635     */
636    public function addComment($typeNum, $articleId)
637    {
638        try {
639            // Support both JSON and form-encoded data
640            $data = json_decode($this->app->request->getBody(), true);
641            if (empty($data)) {
642                // Fallback to POST params for form-encoded data
643                $data = $this->app->request->post();
644            }
645
646            if (empty($data['comment'])) {
647                $this->app->response->headers->set('Content-Type', 'application/json');
648                $this->app->halt(400, json_encode([
649                    'error' => 'comment is required'
650                ]));
651                return;
652            }
653
654            if (strlen($data['comment']) > 2000) {
655                $this->app->response->headers->set('Content-Type', 'application/json');
656                $this->app->halt(400, json_encode([
657                    'error' => 'Comment exceeds maximum length of 2000 characters'
658                ]));
659                return;
660            }
661
662            // Verify article exists using global DB
663            $globalDb = \dbConnectByName('kiosk_buykiosk');
664            $stmt = $globalDb->prepare("SELECT id FROM support_articles WHERE id = :id");
665            $stmt->execute([':id' => $articleId]);
666            if (!$stmt->fetch()) {
667                $this->app->response->headers->set('Content-Type', 'application/json');
668                $this->app->halt(404, json_encode([
669                    'error' => 'Article not found'
670                ]));
671                return;
672            }
673
674            // Get employeeId from user - comments are tied to employees
675            $employeeId = $this->app->user->employee_id ?? 0;
676            $employeeName = $this->app->user->display_name ?? 'Anonymous';
677
678            $comment = new Comment($this->db);
679            $comment->articleId = $articleId;
680            $comment->employeeId = $employeeId;
681            $comment->employeeName = $employeeName;
682            $comment->comment = trim($data['comment']);
683
684            if ($comment->save()) {
685                $this->app->response->setStatus(201);
686                $this->app->response->headers->set('Content-Type', 'application/json');
687                $this->app->response->setBody(json_encode([
688                    'success' => true,
689                    'commentId' => $comment->id
690                ]));
691            } else {
692                $this->app->response->headers->set('Content-Type', 'application/json');
693                $this->app->halt(500, json_encode([
694                    'error' => 'Failed to save comment'
695                ]));
696            }
697        } catch (Exception $e) {
698            error_log("SupportApiController::addComment error: " . $e->getMessage());
699            $this->app->response->headers->set('Content-Type', 'application/json');
700            $this->app->halt(500, json_encode([
701                'error' => 'Failed to add comment'
702            ]));
703        }
704    }
705
706    /**
707     * DELETE /api/:typeNum/support/articles/:articleId/comments/:commentId/
708     * Delete own comment
709     *
710     * @param string $typeNum Store identifier
711     * @param int $articleId Article ID
712     * @param int $commentId Comment ID
713     */
714    public function deleteComment($typeNum, $articleId, $commentId)
715    {
716        try {
717            // Get comment directly from database
718            $stmt = $this->db->prepare("SELECT * FROM support_article_comments WHERE id = :id");
719            $stmt->execute([':id' => $commentId]);
720            $commentRow = $stmt->fetch(\PDO::FETCH_ASSOC);
721
722            if (!$commentRow) {
723                $this->app->response->headers->set('Content-Type', 'application/json');
724                $this->app->halt(404, json_encode([
725                    'error' => 'Comment not found'
726                ]));
727                return;
728            }
729
730            // Verify ownership or admin permission
731            $employeeId = $this->app->user->employee_id ?? 0;
732            $isAdmin = $this->app->user->checkAccess('uri_support_admin');
733
734            if ($commentRow['employee_id'] != $employeeId && !$isAdmin) {
735                $this->app->response->headers->set('Content-Type', 'application/json');
736                $this->app->halt(403, json_encode([
737                    'error' => 'Access denied. You can only delete your own comments'
738                ]));
739                return;
740            }
741
742            // Delete using Comment model
743            $comment = new Comment($this->db);
744            $comment->id = $commentId;
745            if ($comment->delete()) {
746                $this->app->response->headers->set('Content-Type', 'application/json');
747                $this->app->response->setBody(json_encode([
748                    'success' => true
749                ]));
750            } else {
751                $this->app->response->headers->set('Content-Type', 'application/json');
752                $this->app->halt(500, json_encode([
753                    'error' => 'Failed to delete comment'
754                ]));
755            }
756        } catch (Exception $e) {
757            error_log("SupportApiController::deleteComment error: " . $e->getMessage());
758            $this->app->response->headers->set('Content-Type', 'application/json');
759            $this->app->halt(500, json_encode([
760                'error' => 'Failed to delete comment'
761            ]));
762        }
763    }
764
765    /**
766     * POST /api/:typeNum/support/articles/:articleId/suggest-edit/
767     * Submit an edit suggestion for an article
768     *
769     * Request body: {
770     *   "suggestion": string,
771     *   "notes": string (optional)
772     * }
773     *
774     * @param string $typeNum Store identifier
775     * @param int $articleId Article ID
776     */
777    public function submitEditSuggestion($typeNum, $articleId)
778    {
779        try {
780            $data = json_decode($this->app->request->getBody(), true);
781
782            if (empty($data['suggestion'])) {
783                $this->app->response->headers->set('Content-Type', 'application/json');
784                $this->app->halt(400, json_encode([
785                    'error' => 'suggestion is required'
786                ]));
787                return;
788            }
789
790            // Verify article exists using global DB
791            $globalDb = \dbConnectByName('kiosk_buykiosk');
792            $stmt = $globalDb->prepare("SELECT id FROM support_articles WHERE id = :id");
793            $stmt->execute([':id' => $articleId]);
794            if (!$stmt->fetch()) {
795                $this->app->response->headers->set('Content-Type', 'application/json');
796                $this->app->halt(404, json_encode([
797                    'error' => 'Article not found'
798                ]));
799                return;
800            }
801
802            // Get employeeId from user - use user_employee_links or fall back to user id
803            $userId = $this->app->user->id ?? 0;
804            if (!$userId) {
805                $this->app->response->headers->set('Content-Type', 'application/json');
806                $this->app->halt(400, json_encode([
807                    'error' => 'User context required for suggestions'
808                ]));
809                return;
810            }
811
812            // Try to get linked employee ID for this store
813            $employeeId = 0;
814            $centralDb = \dbConnectByName('kiosk_users');
815            $linkStmt = $centralDb->prepare(
816                "SELECT employeeId FROM user_employee_links WHERE userId = :userId AND typeNum = :typeNum"
817            );
818            $linkStmt->execute([':userId' => $userId, ':typeNum' => $typeNum]);
819            $link = $linkStmt->fetch(\PDO::FETCH_ASSOC);
820            if ($link) {
821                $employeeId = (int) $link['employeeId'];
822            }
823
824            // If no employee link, use user id as a fallback
825            if (!$employeeId) {
826                $employeeId = $userId;
827            }
828
829            // Insert suggestion into store database (edit suggestions are per-store)
830            $stmt = $this->db->prepare("
831                INSERT INTO support_edit_suggestions
832                (article_id, employee_id, suggested_content_md, suggestion_note, status)
833                VALUES (:article_id, :employee_id, :suggested_content_md, :suggestion_note, 'pending')
834            ");
835
836            $stmt->bindValue(':article_id', $articleId, \PDO::PARAM_INT);
837            $stmt->bindValue(':employee_id', $employeeId, \PDO::PARAM_INT);
838            $stmt->bindValue(':suggested_content_md', trim($data['suggestion']), \PDO::PARAM_STR);
839            $stmt->bindValue(':suggestion_note', $data['notes'] ?? null, \PDO::PARAM_STR);
840
841            if ($stmt->execute()) {
842                $suggestionId = $this->db->lastInsertId();
843
844                $this->app->response->setStatus(201);
845                $this->app->response->headers->set('Content-Type', 'application/json');
846                $this->app->response->setBody(json_encode([
847                    'success' => true,
848                    'suggestionId' => $suggestionId
849                ]));
850            } else {
851                $this->app->response->headers->set('Content-Type', 'application/json');
852                $this->app->halt(500, json_encode([
853                    'error' => 'Failed to submit suggestion'
854                ]));
855            }
856        } catch (\Slim\Exception\Stop $e) {
857            // Re-throw Slim's Stop exception (from halt())
858            throw $e;
859        } catch (Exception $e) {
860            error_log("SupportApiController::submitEditSuggestion error: " . $e->getMessage());
861            $this->app->response->headers->set('Content-Type', 'application/json');
862            $this->app->halt(500, json_encode([
863                'error' => 'Failed to submit suggestion'
864            ]));
865        }
866    }
867
868    /**
869     * POST /api/:typeNum/support/upload-image/
870     * Upload an image for use in articles
871     *
872     * Requires uri_support_admin permission
873     *
874     * @param string $typeNum Store identifier
875     */
876    public function uploadImage($typeNum)
877    {
878        // Check permission
879        if (!$this->app->user->checkAccess('uri_support_admin')) {
880            $this->app->response->headers->set('Content-Type', 'application/json');
881            $this->app->halt(403, json_encode([
882                'error' => 'Access denied. Requires admin permission'
883            ]));
884            return;
885        }
886
887        try {
888            if (empty($_FILES['image'])) {
889                $this->app->response->headers->set('Content-Type', 'application/json');
890                $this->app->halt(400, json_encode([
891                    'error' => 'No image file provided'
892                ]));
893                return;
894            }
895
896            $file = $_FILES['image'];
897
898            // Validate file type
899            $allowedTypes = ['image/jpeg', 'image/png', 'image/gif', 'image/webp'];
900            if (!in_array($file['type'], $allowedTypes)) {
901                $this->app->response->headers->set('Content-Type', 'application/json');
902                $this->app->halt(400, json_encode([
903                    'error' => 'Invalid file type. Only JPEG, PNG, GIF, and WebP images are allowed'
904                ]));
905                return;
906            }
907
908            // Validate file size (max 5MB)
909            if ($file['size'] > 5 * 1024 * 1024) {
910                $this->app->response->headers->set('Content-Type', 'application/json');
911                $this->app->halt(400, json_encode([
912                    'error' => 'File size exceeds maximum of 5MB'
913                ]));
914                return;
915            }
916
917            // Generate unique filename
918            $extension = pathinfo($file['name'], PATHINFO_EXTENSION);
919            $filename = uniqid('support_img_') . '.' . $extension;
920
921            // Upload directory (adjust path as needed)
922            $uploadDir = $_SERVER['DOCUMENT_ROOT'] . '/uploads/support/';
923            if (!is_dir($uploadDir)) {
924                mkdir($uploadDir, 0755, true);
925            }
926
927            $uploadPath = $uploadDir . $filename;
928
929            if (move_uploaded_file($file['tmp_name'], $uploadPath)) {
930                $imageUrl = '/uploads/support/' . $filename;
931
932                $this->app->response->setStatus(201);
933                $this->app->response->headers->set('Content-Type', 'application/json');
934                $this->app->response->setBody(json_encode([
935                    'success' => true,
936                    'url' => $imageUrl
937                ]));
938            } else {
939                $this->app->response->headers->set('Content-Type', 'application/json');
940                $this->app->halt(500, json_encode([
941                    'error' => 'Failed to upload image'
942                ]));
943            }
944        } catch (Exception $e) {
945            error_log("SupportApiController::uploadImage error: " . $e->getMessage());
946            $this->app->response->headers->set('Content-Type', 'application/json');
947            $this->app->halt(500, json_encode([
948                'error' => 'Failed to upload image'
949            ]));
950        }
951    }
952}