# NGauge CMS Modernization Plan: Pagegroup Module

## Executive Summary

This document outlines a phased approach to modernizing the `Pagegroupbe` model and related CMS infrastructure within NGauge_Base. The current implementation has accumulated several architectural antipatterns and technical debt that compromise data integrity, maintainability, and scalability. This plan addresses these issues through structured refactoring across three phases over 4-6 weeks.

**Key Risks Addressed:**
- Transaction safety in multi-model saves (data integrity)
- Code duplication in controller (maintainability)
- Inline JavaScript/CSS in views (testability and reusability)
- PJAX abuse patterns (framework misuse)
- Syntax errors in core model (immediate bugs)

---

## Current State Analysis

### Critical Issues

| Priority | Issue | File | Lines | Impact | Effort |
|----------|-------|------|-------|--------|--------|
| **CRITICAL** | Transaction antipattern in actionUpdate | PagegroupController | 127-133 | Data integrity - partial failures leave inconsistent state | 2h |
| **CRITICAL** | Syntax error in afterSave | Pagegroupbe | 163 | Malformed closing brace prevents compile | 0.5h |
| **HIGH** | 3x method duplication | PagegroupController | 55-98 | Code maintenance nightmare, bug propagation risk | 3h |
| **HIGH** | Typo in getContents chain | Pagegroupbe | 178 | andonCondition typo | 0.5h |
| **MEDIUM** | Inline JavaScript hell | _form.php | 21-80 | Untestable, unmaintainable, tightly coupled | 8h |
| **MEDIUM** | Manual PJAX reloads | _content.php | 20-136 | Framework misuse, form state loss | 4h |
| **MEDIUM** | 9-level nested conditionals | Pagegroupbe | 250-347 | Unmaintainable convertBrackets() logic | 6h |
| **MEDIUM** | Inline CSS styling | _form.php | 129-145 | Should be externalized | 2h |
| **LOW** | Maintenance ops in controller | PagegroupController | 200-244 | actionTidy, actionCreateuploadfolders should be commands | 4h |

**Total Estimated Effort: 29.5 hours (~6 working days)**

### Affected Files

```
NGauge_Base/
├── backend/
│   ├── models/
│   │   └── Pagegroupbe.php (539 lines) - 5 issues
│   ├── controllers/
│   │   └── PagegroupController.php (244 lines) - 3 issues
│   ├── views/pagegroup/
│   │   ├── update.php (60 lines) - Asset registration needed
│   │   ├── _form.php (201 lines) - 60 lines inline JS, 48 lines inline CSS
│   │   └── _content.php (136 lines) - PJAX abuse pattern
│   └── services/ (NEW)
│       └── PageContentUpdateService.php (182 lines) - Phase 1
├── console/
│   ├── controllers/
│   │   └── PagegroupController.php (NEW) - Phase 3
│   └── commands/ (NEW)
│       ├── PagegroupTidyCommand.php (Phase 3)
│       └── PagegroupUploadFoldersCommand.php (Phase 3)
└── web/
    ├── assets/ (NEW)
    │   └── PagegroupAsset.php (24 lines)
    ├── js/ (NEW)
    │   ├── pagegroup-form.js (180 lines)
    │   └── pagegroup-content.js (20 lines)
    └── css/ (NEW)
        └── pagegroup-form.css (140 lines)
```

### Code Inventory

**Models (Pagegroup system):**
- `Pagegroupbe` - Core page model
- `Pagecontentsbe` - Content rows (1:Many relationship)
- `Seobe` - SEO metadata (1:1 relationship)
- `Lookuptablebe` - Type/category lookups

**Controllers:**
- `backend/controllers/PagegroupController` - Web UI (244 lines)
- `console/controllers/PagegroupController` (NEW) - CLI commands

**Views:**
- `backend/views/pagegroup/update.php` - Main edit view (tabs)
- `backend/views/pagegroup/_form.php` - Details form (201 lines)
- `backend/views/pagegroup/_content.php` - Content editor (136 lines)
- `backend/views/pagegroup/_media_tab.php` - Gallery manager
- `backend/views/pagegroup/_urls.php` - URL management
- `common/views/pagegroup/_seo_tab.php` - SEO fields

**Dependencies:**
- `yii\db\Transaction` - Core transaction support
- `yii\web\AssetBundle` - Asset management
- jQuery 1.11+, jQuery UI, TinyMCE, PJAX, Bootstrap 4

---

## Implementation Phases

### Phase 1: Foundation & Critical Fixes (Weeks 1-2)

**Objective:** Achieve transaction safety and fix syntax errors without breaking existing functionality.

**Deliverables:**
1. ✅ Syntax fixes in core models
2. ✅ Service layer for transaction-safe updates
3. ✅ Controller integration with service layer
4. ✅ Comprehensive unit tests

#### 1.1 Syntax Corrections (0.5h)

**Pagegroupbe.php - Fix Line 163:**
```php
// BEFORE
public function afterSave($insert, $changedAttributes)
{
    parent::afterSave($insert, $changedAttributes);
    // ... code ...
} if ($seo)  // ❌ Malformed brace

// AFTER
public function afterSave($insert, $changedAttributes)
{
    parent::afterSave($insert, $changedAttributes);
    // ... code ...
    if ($seo) {
        // save seo
    }
}
```

**Pagegroupbe.php - Fix Line 178 Typo:**
```php
// BEFORE
->andonCondition(['sys_status' => 'active'])  // ❌ typo

// AFTER
->andOnCondition(['sys_status' => 'active'])
```

**Status:** Unit tests should verify these corrections compile and function.

---

#### 1.2 Service Layer Implementation (8h)

**New File: `backend/services/PageContentUpdateService.php`**

**Purpose:** Centralize multi-model transaction management, validation, and error handling.

**Architecture:**
```php
namespace backend\services;

use yii\db\Transaction;
use backend\models\{Pagegroupbe, Pagecontentsbe, Seobe};

class PageContentUpdateService
{
    private $db;
    
    public function __construct($db)
    {
        $this->db = $db;
    }
    
    /**
     * Transaction-safe update of page and related content
     * 
     * @param Pagegroupbe $page
     * @param array $contentData
     * @param array $seoData
     * @return bool
     * @throws Exception on validation failure
     */
    public function updatePageWithContent(
        Pagegroupbe $page,
        array $contentData,
        array $seoData = []
    ): bool {
        $transaction = $this->db->beginTransaction();
        
        try {
            // Validate all models before any saves
            if (!$page->validate()) {
                throw new Exception('Page validation failed: ' . json_encode($page->getErrors()));
            }
            
            // Update page
            if (!$page->save()) {
                throw new Exception('Page save failed: ' . json_encode($page->getErrors()));
            }
            
            // Update content rows
            if (!empty($contentData)) {
                $this->updateContentRows($page->id, $contentData);
            }
            
            // Update SEO metadata
            if (!empty($seoData)) {
                $this->updateSeo($page->id, $seoData);
            }
            
            $transaction->commit();
            return true;
            
        } catch (Exception $e) {
            $transaction->rollBack();
            throw $e;
        }
    }
    
    // Additional methods for content/seo management
    private function updateContentRows($pageId, array $contentData) { }
    private function updateSeo($pageId, array $seoData) { }
}
```

**Benefits:**
- ✅ Atomic transactions for all multi-step operations
- ✅ Pre-validation before any database writes
- ✅ Automatic rollback on failure
- ✅ Centralized error handling
- ✅ Reusable across other controllers

**Testing Strategy:**
- Unit tests for each service method
- Transaction rollback scenarios
- Validation error handling
- Integration test verifying data consistency

**Deprecation Warning:** This replaces inline transaction patterns. Old chained save approach is deprecated.

---

#### 1.3 Controller Integration (2h)

**File: `backend/controllers/PagegroupController.php`**

**Changes:**

1. **Dependency Injection:**
```php
namespace backend\controllers;

use Yii;
use yii\web\Response;
use backend\services\PageContentUpdateService;
use backend\models\Pagegroupbe;

class PagegroupController extends Controller
{
    private $updateService;
    
    public function __construct($id, $module, PageContentUpdateService $service, $config = [])
    {
        parent::__construct($id, $module, $config);
        $this->updateService = $service;
    }
}
```

2. **Refactor actionUpdate (lines 117-139):**
```php
// BEFORE - Chained saves with no transaction
if ($model->save() && $seo->save()) {
    // ... both saved
}

// AFTER - Transaction-safe service layer
try {
    $this->updateService->updatePageWithContent($model, $contentData, $seoData);
    Yii::$app->session->setFlash('success', 'Page updated successfully');
    return $this->redirect(['view', 'id' => $model->id]);
} catch (Exception $e) {
    Yii::$app->session->setFlash('error', 'Update failed: ' . $e->getMessage());
    return $this->refresh();
}
```

3. **Eliminate Method Duplication (lines 55-98):**
```php
// BEFORE - 3 identical methods
public function actionIndex() { }
public function actionPageblocks() { }
public function actionMailtemplates() { }

// AFTER - Single method with parameter
private function listPages($type)
{
    $dataProvider = new ActiveDataProvider([
        'query' => Pagegroupbe::find()
            ->filterWhere(['type' => $type])
            ->orderBy(['sequence' => SORT_ASC]),
        'pagination' => ['pageSize' => 50],
    ]);
    
    return $this->render('index', ['dataProvider' => $dataProvider]);
}

public function actionIndex() { return $this->listPages('page'); }
public function actionPageblocks() { return $this->listPages('block'); }
public function actionMailtemplates() { return $this->listPages('email'); }
```

**Testing:**
- Verify all three listing routes still work identically
- Confirm transaction safety in update scenarios
- Test error handling and rollback behavior

---

#### 1.4 Unit Tests (3h)

**New File: `backend/tests/unit/PageContentUpdateServiceTest.php`**

```php
namespace backend\tests\unit;

use Yii;
use PHPUnit\Framework\TestCase;
use backend\services\PageContentUpdateService;
use backend\models\{Pagegroupbe, Pagecontentsbe};

class PageContentUpdateServiceTest extends TestCase
{
    private $service;
    private $db;
    
    protected function setUp(): void
    {
        $this->db = Yii::$app->db;
        $this->service = new PageContentUpdateService($this->db);
    }
    
    public function testUpdatePageWithContentSuccess()
    {
        $page = new Pagegroupbe(['name' => 'Test Page', 'type' => 'page']);
        $contentData = [
            ['order' => 1, 'type' => 'text', 'content' => 'Hello'],
        ];
        
        $result = $this->service->updatePageWithContent($page, $contentData);
        
        $this->assertTrue($result);
        $this->assertNotEmpty($page->id);
    }
    
    public function testUpdatePageWithContentSeoFails_Rollback()
    {
        // Set invalid SEO to trigger failure
        $page = new Pagegroupbe(['name' => 'Test']);
        $seoData = ['title' => str_repeat('x', 500)]); // Too long
        
        $this->expectException(Exception::class);
        $this->service->updatePageWithContent($page, [], $seoData);
        
        // Verify page not saved if SEO fails
        $this->assertEmpty($page->id);
    }
}
```

**Tests to cover:**
- ✅ Successful multi-step update
- ✅ Page save failure → rollback all
- ✅ Content save failure → rollback all
- ✅ SEO save failure → rollback page + content
- ✅ Validation errors caught before DB write
- ✅ Concurrent update conflicts

---

### Phase 2: Controller & Data Quality (Weeks 2-3)

**Objective:** Further refactor controller, extract maintenance operations, ensure data consistency.

**Deliverables:**
1. ✅ Method deduplication complete
2. ✅ Maintenance operations as console commands
3. ✅ Integration tests with service layer
4. ✅ Database health check command

#### 2.1 Console Commands (4h)

**New File: `console/controllers/PagegroupController.php`**

**Purpose:** Move maintenance operations from web controller to CLI for safer execution.

```php
namespace console\controllers;

use yii\console\Controller;
use backend\models\Pagegroupbe;

class PagegroupController extends Controller
{
    /**
     * Clean up orphaned content rows, invalid status values
     */
    public function actionTidy()
    {
        // Move from line 201-210 of web PagegroupController
        $this->stdout("Tidying pagegroup data...\n");
        
        // Remove orphaned content rows
        $removed = Pagecontentsbe::deleteAll('page_id NOT IN (SELECT id FROM pagegroup)');
        $this->stdout("Removed $removed orphaned content rows\n");
        
        // Fix invalid statuses
        $updated = Pagegroupbe::updateAll(['status' => 'active'], ['status' => '']);
        $this->stdout("Fixed $updated invalid status values\n");
    }
    
    /**
     * Create upload folders for new page groups
     */
    public function actionCreateUploadFolders()
    {
        // Move from line 211-230 of web PagegroupController
        $this->stdout("Creating upload folders...\n");
        
        // Implementation with file system operations
    }
}
```

**Benefits:**
- ✅ Safer execution (CLI only, no HTTP timeout)
- ✅ Cleaner separation of concerns
- ✅ Can be scheduled via cron
- ✅ Better logging and error handling
- ✅ No security exposure to web interface

**Testing:**
- Verify folder creation works
- Verify cleanup doesn't delete valid data
- Test with large datasets

---

#### 2.2 Data Consistency Check (2h)

**New File: `backend/commands/PagegroupHealthCheck.php`**

```php
public function actionHealthCheck()
{
    $issues = [];
    
    // Check for orphaned content rows (data integrity)
    $orphaned = Pagecontentsbe::find()
        ->leftJoin('pagegroup', 'pagecontents.page_id = pagegroup.id')
        ->where(['pagegroup.id' => null])
        ->count();
    
    if ($orphaned > 0) {
        $issues[] = "$orphaned orphaned content rows found";
    }
    
    // Check for duplicate URL slugs at same language
    $duplicates = Pagegroupbe::find()
        ->select(['url', 'language', 'COUNT(*) as cnt'])
        ->groupBy(['url', 'language'])
        ->having('cnt > 1')
        ->all();
    
    if (!empty($duplicates)) {
        $issues[] = count($duplicates) . " pages with duplicate URLs";
    }
    
    // Check for invalid status values
    $invalidStatus = Pagegroupbe::find()
        ->where(['NOT IN', 'status', ['active', 'inactive', 'draft']])
        ->count();
    
    if ($invalidStatus > 0) {
        $issues[] = "$invalidStatus pages with invalid status values";
    }
    
    if (empty($issues)) {
        $this->stdout("✓ Pagegroup data integrity check passed\n");
    } else {
        foreach ($issues as $issue) {
            $this->stdout("✗ $issue\n", Console::FG_RED);
        }
    }
}
```

---

#### 2.3 Integration Tests (3h)

**New File: `backend/tests/integration/PagegroupUpdateFlowTest.php`**

Tests complete update flows end-to-end:
- Create page → add content → set SEO → view result
- Edit page → modify content → update SEO → save
- Delete operations with cascade cleanup
- Concurrent update handling

---

### Phase 3: View Modernization & JavaScript Extraction (Weeks 3-4)

**Objective:** Extract inline JavaScript/CSS, create testable asset bundles, improve maintainability.

**Deliverables:**
1. ✅ Asset bundle infrastructure
2. ✅ JavaScript extraction with namespace
3. ✅ Stylesheet externalization
4. ✅ Frontend tests
5. ✅ Performance optimization

#### 3.1 Asset Bundle Creation (1h)

**New File: `backend/assets/PagegroupAsset.php`**

```php
namespace backend\assets;

use yii\web\AssetBundle;

class PagegroupAsset extends AssetBundle
{
    public $basePath = '@webroot';
    public $baseUrl = '@web';
    
    public $css = [
        'css/pagegroup-form.css',
    ];
    
    public $js = [
        'js/pagegroup-form.js',
        'js/pagegroup-content.js',
    ];
    
    public $depends = [
        'yii\web\YiiAsset',
        'yii\bootstrap4\BootstrapAsset',
        'yii\jui\JuiAsset',
    ];
}
```

**Benefits:**
- ✅ Automatic dependency resolution
- ✅ Publication to web root
- ✅ Minification ready
- ✅ Version hash for cache busting
- ✅ Clean registration in views

---

#### 3.2 JavaScript Extraction (8h)

**New File: `web/js/pagegroup-form.js`**

Extracts all form handlers from `_form.php` (lines 21-80) into controlled namespace:

```javascript
/**
 * Pagegroup Form Handler
 * Manages page details form including field saving, row management, and TinyMCE integration
 */
(function() {
    'use strict';
    
    const PagegroupForm = {
        config: {
            baseUrl: '/NGauge',
            contentContainer: '#rows',
            messageId: '#flashmessage'
        },
        
        /**
         * Initialize form handlers
         */
        init: function() {
            this.bindFieldHandlers();
            this.bindSortable();
            this.setupValidation();
        },
        
        /**
         * Bind save handlers to form fields
         */
        bindFieldHandlers: function() {
            const self = this;
            
            $('[data-field-save]').on('blur', function() {
                const $field = $(this);
                const id = $field.data('id');
                const field = $field.data('field-save');
                const value = $field.val();
                
                self.saveField(id, field, value);
            });
        },
        
        /**
         * Save single field via AJAX
         */
        saveField: function(id, field, value) {
            const url = this.config.baseUrl + '/pagecontents/savefield';
            const self = this;
            
            $.ajax({
                type: 'POST',
                url: url,
                data: { id: id, field: field, value: value },
                success: function(response) {
                    self.showMessage(response, 'success');
                },
                error: function(xhr) {
                    self.showMessage('Error saving field', 'error');
                    console.error('Save failed:', xhr.responseText);
                }
            });
        },
        
        /**
         * Add new content row
         */
        addRow: function() {
            const url = this.config.baseUrl + '/pagecontents/addrow';
            const pageId = $('[data-page-id]').data('page-id');
            const self = this;
            
            $.ajax({
                type: 'POST',
                url: url,
                data: { id: pageId },
                success: function(response) {
                    if (response === 'Row Added') {
                        self.reloadPjax();
                    }
                },
                error: function(xhr) {
                    self.showMessage('Failed to add row', 'error');
                }
            });
        },
        
        /**
         * Remove content row
         */
        removeRow: function(id) {
            if (!confirm('Remove this content row?')) {
                return;
            }
            
            const url = this.config.baseUrl + '/pagecontents/removerow';
            const self = this;
            
            $.ajax({
                type: 'POST',
                url: url,
                data: { id: id },
                success: function(response) {
                    if (response === 'removed') {
                        self.reloadPjax();
                    }
                },
                error: function(xhr) {
                    self.showMessage('Failed to remove row', 'error');
                }
            });
        },
        
        /**
         * Update single row
         */
        updateRow: function(data) {
            const url = this.config.baseUrl + '/pagecontents/updaterow';
            const pageId = $('[data-page-id]').data('page-id');
            const self = this;
            
            $.ajax({
                type: 'POST',
                url: url,
                data: { data: data, id: pageId },
                success: function(response) {
                    if (response === 'updated') {
                        self.reloadPjax();
                    }
                },
                error: function(xhr) {
                    self.showMessage('Failed to update row', 'error');
                }
            });
        },
        
        /**
         * Save entire row with TinyMCE content
         */
        saveTab: function(id, contents) {
            const url = this.config.baseUrl + '/pagecontents/savetab';
            const rowId = $('#rowid' + id).val();
            const pageId = $('#page' + id).val();
            const self = this;
            
            $.ajax({
                type: 'POST',
                url: url,
                data: { id: rowId, contents: contents, page: pageId },
                success: function(response) {
                    if (response === 'saved') {
                        self.reloadPjax();
                    }
                },
                error: function(xhr) {
                    self.showMessage('Failed to save row', 'error');
                }
            });
        },
        
        /**
         * Reload PJAX container and reinitialize TinyMCE
         */
        reloadPjax: function() {
            tinymce.remove();
            $.pjax.reload({ container: this.config.contentContainer });
            
            // Reinitialize after reload
            setTimeout(() => {
                this.bindFieldHandlers();
                this.bindSortable();
            }, 500);
        },
        
        /**
         * Setup jQuery UI sortable for drag-drop reordering
         */
        bindSortable: function() {
            const self = this;
            
            $('.sortable-rows').sortable({
                handle: '.drag-handle',
                update: function(event, ui) {
                    const order = $(this).sortable('toArray', { attribute: 'data-row-id' });
                    self.updateRowOrder(order);
                }
            });
        },
        
        /**
         * Update row order after drag-drop
         */
        updateRowOrder: function(order) {
            const url = this.config.baseUrl + '/pagecontents/reorder';
            
            $.ajax({
                type: 'POST',
                url: url,
                data: { order: order },
                error: function(xhr) {
                    console.error('Reorder failed:', xhr.responseText);
                }
            });
        },
        
        /**
         * Show temporary message to user
         */
        showMessage: function(message, type = 'info') {
            type = type === 'error' ? 'danger' : type;
            const $message = $('<div>')
                .addClass('alert alert-' + type)
                .html(message)
                .appendTo(this.config.messageId);
            
            setTimeout(() => {
                $message.fadeOut(() => $message.remove());
            }, 3000);
        },
        
        /**
         * Setup client-side validation
         */
        setupValidation: function() {
            $('form.pagegroup-form').on('submit', function() {
                const name = $('[name="Pagegroupbe[name]"]').val();
                if (!name || name.trim() === '') {
                    alert('Page name is required');
                    return false;
                }
            });
        }
    };
    
    // Backward compatibility - expose global functions
    window.addRow = function() { PagegroupForm.addRow(); };
    window.removeRow = function(id) { PagegroupForm.removeRow(id); };
    window.updateRow = function(data) { PagegroupForm.updateRow(data); };
    window.saveField = function(id, field, value) { PagegroupForm.saveField(id, field, value); };
    window.saveRow = function(id, contents) { PagegroupForm.saveTab(id, contents); };
    
    // Initialize on document ready
    $(document).ready(function() {
        PagegroupForm.init();
    });
    
    // Expose for testing
    window.PagegroupForm = PagegroupForm;
})();
```

**Benefits:**
- ✅ IIFE scope prevents global namespace pollution
- ✅ Centralized configuration (baseUrl, selectors)
- ✅ Error handling and logging
- ✅ Testable methods with clear contracts
- ✅ Backward compatibility with existing HTML
- ✅ Auto-initialization on ready

---

#### 3.3 Content Editor JavaScript (2h)

**New File: `web/js/pagegroup-content.js`**

Enhanced content editor with previews and validation:

```javascript
/**
 * Pagegroup Content Editor
 * Handles rich content editing with real-time preview
 */
(function() {
    'use strict';
    
    const PagegroupContent = {
        initialized: false,
        
        init: function() {
            if (this.initialized) return;
            
            this.setupTinyMce();
            this.bindPreviewHandlers();
            this.initialized = true;
        },
        
        setupTinyMce: function() {
            if (typeof tinymce === 'undefined') return;
            
            tinymce.init({
                selector: 'textarea.rich-content',
                plugins: 'link image lists',
                toolbar: 'undo redo | styleselect | bold italic | alignleft aligncenter alignright | bullist numlist | link image',
                height: 300,
                menubar: false
            });
        },
        
        bindPreviewHandlers: function() {
            const self = this;
            
            $('[data-content-preview]').on('click', function() {
                const contentId = $(this).data('content-id');
                self.showPreview(contentId);
            });
        },
        
        showPreview: function(contentId) {
            // Implementation for content preview modal
        }
    };
    
    $(document).ready(function() {
        PagegroupContent.init();
    });
    
    window.PagegroupContent = PagegroupContent;
})();
```

---

#### 3.4 Stylesheet Extraction (2h)

**New File: `web/css/pagegroup-form.css`**

Extract 48 lines of inline CSS from `_form.php` (lines 129-145):

```css
/**
 * Pagegroup CMS Form Styles
 * Extracted from _form.php for maintainability and cache optimization
 */

/* Form Containers */
.pagegroupbe-form .card {
    margin-bottom: 2rem;
    box-shadow: 0 2px 4px rgba(0,0,0,0.1);
}

.pagegroupbe-form .card-header {
    background-color: #f8f9fa;
    border-bottom: 2px solid #e9ecef;
}

.pagegroupbe-form .card-body {
    padding: 1.5rem;
}

/* Drag-Drop Interface */
.sortable-rows {
    list-style: none;
    padding: 0;
}

.sortable-rows li {
    padding: 1rem;
    margin-bottom: 0.5rem;
    background: #fff;
    border: 1px solid #dee2e6;
    border-radius: 0.25rem;
    cursor: move;
}

.drag-handle {
    cursor: grab;
    padding: 0.5rem;
    color: #999;
}

.drag-handle:hover {
    color: #666;
}

/* Validation Feedback */
.has-error .form-control {
    border-color: #dc3545;
    background-color: #fff5f5;
}

.invalid-feedback {
    display: block;
    color: #dc3545;
    font-size: 0.875rem;
    margin-top: 0.25rem;
}

/* Message Alerts */
#message,
#flashmessage {
    margin: 1rem 0;
}

.alert {
    padding: 1rem;
    border-radius: 0.25rem;
    animation: slideDown 0.3s ease-in;
}

@keyframes slideDown {
    from {
        opacity: 0;
        transform: translateY(-10px);
    }
    to {
        opacity: 1;
        transform: translateY(0);
    }
}

/* TinyMCE Editor */
.mce-container {
    margin: 1rem 0;
}

.mce-top-part {
    border-radius: 4px 4px 0 0;
}

.mce-edit-area {
    min-height: 300px;
}

/* Sidebar Navigation */
.page-sidebar {
    position: sticky;
    top: 20px;
}

.nav-pills .nav-link.active {
    background-color: #007bff;
}

/* Button Groups */
.btn-group-sm {
    margin-top: 0.5rem;
}

.btn-group-sm .btn {
    padding: 0.25rem 0.5rem;
    font-size: 0.75rem;
}

/* Content Preview */
.preview-pane {
    background: #f8f9fa;
    border: 1px solid #dee2e6;
    border-radius: 0.25rem;
    padding: 1rem;
    margin-top: 1rem;
}

/* Responsive Adjustments */
@media (max-width: 768px) {
    .pagegroupbe-form .row {
        flex-direction: column;
    }
    
    .col-md-3, .col-md-4, .col-md-12 {
        flex: 0 0 100%;
        max-width: 100%;
    }
    
    .page-sidebar {
        position: static;
        margin-top: 1rem;
    }
}
```

---

#### 3.5 View Integration (2h)

**File: `backend/views/pagegroup/update.php`**

Register asset bundle and remove inline JavaScript:

```php
<?php

use yii\helpers\Html;
use yii\bootstrap4\Tabs;
use backend\assets\PagegroupAsset;

// Register asset bundle (replaces inline JS and CSS)
PagegroupAsset::register($this);

/* @var $this yii\web\View */
/* @var $model backend\models\Pagegroupbe */

$this->title = 'Edit Page: ' . $model->name;
$this->params['breadcrumbs'][] = ['label' => 'Pages', 'url' => ['index']];
$this->params['breadcrumbs'][] = $this->title;

?>

<div class="page-wrapper">
    <?= Tabs::widget([
        'items' => [
            [
                'label' => 'Details',
                'content' => $this->render('_form', ['model' => $model, 'form' => $form]),
                'active' => true,
            ],
            [
                'label' => 'Content',
                'content' => $this->render('_content', ['model' => $model]),
            ],
            // ... other tabs
        ],
    ]) ?>
</div>
```

---

#### 3.6 Frontend Tests (3h)

**New File: `frontend/tests/pagegroup-form.test.js`**

```javascript
describe('PagegroupForm', function() {
    let pageForm;
    
    beforeEach(function() {
        pageForm = getPageFormInstance();
    });
    
    describe('addRow', function() {
        it('should send POST to /NGauge/pagecontents/addrow', function(done) {
            spyOn($, 'ajax').and.callFake(function(options) {
                expect(options.type).toBe('POST');
                expect(options.url).toContain('/pagecontents/addrow');
                done();
            });
            
            pageForm.addRow();
        });
        
        it('should reload PJAX on success', function(done) {
            spyOn(PagegroupForm, 'reloadPjax');
            
            // Mock jQuery ajax
            $.ajax = function(options) {
                options.success('Row Added');
            };
            
            pageForm.addRow();
            
            expect(PagegroupForm.reloadPjax).toHaveBeenCalled();
            done();
        });
    });
    
    describe('saveField', function() {
        it('should preserve field value on error', function(done) {
            const originalValue = 'test value';
            
            $.ajax = function(options) {
                expect(options.data.value).toBe(originalValue);
                options.error({});
            };
            
            pageForm.saveField(1, 'name', originalValue);
            done();
        });
    });
    
    describe('removeRow', function() {
        it('should ask for confirmation', function() {
            spyOn(window, 'confirm').and.returnValue(false);
            pageForm.removeRow(1);
            
            expect(window.confirm).toHaveBeenCalled();
        });
    });
});
```

**Test Coverage Goals:**
- ✅ AJAX request URL validation
- ✅ Success/error handling
- ✅ PJAX reload behavior
- ✅ TinyMCE cleanup on reload
- ✅ Forward/backward compatibility

---

### Phase 4: Cleanup & Documentation (Post-Release)

**Objective:** Monitor production, gather metrics, document learnings.

**Deliverables:**
1. ✅ Remove deprecated inline patterns
2. ✅ Optimize asset bundles
3. ✅ Update developer documentation
4. ✅ Performance profiling

---

## Risk Assessment & Mitigation

| Risk | Impact | Probability | Mitigation |
|------|--------|-------------|-----------|
| **Data loss during transaction refactoring** | CRITICAL | MEDIUM | Comprehensive backup before Phase 1, rollback test, dry-run on staging |
| **PJAX reload breaks form state** | HIGH | MEDIUM | Manual test matrix for all browsers, integration tests, A/B test |
| **Backward compatibility break** | HIGH | LOW | Global function compatibility layer, feature flag for old vs new JS |
| **TinyMCE initialization fails after reload** | MEDIUM | MEDIUM | Test reload scenarios with multiple editors, tinymce.remove() verification |
| **Syntax error prevents compile** | MEDIUM | HIGH | PHP linting in CI/CD, syntax validation test |
| **Service layer doesn't rollback on second save** | MEDIUM | MEDIUM | Unit tests for partial failure scenarios |
| **New asset bundle not loaded** | MEDIUM | LOW | Verify asset registration in all views, test cache busting |
| **Double-initialization of JavaScript handlers** | LOW | MEDIUM | Initialization guard flag, verify single PagegroupForm namespace |

---

## Testing Strategy

### Unit Tests (Phase 1)
```
backend/tests/unit/
├── PagegroupbeTest.php
├── PageContentUpdateServiceTest.php
└── PagegroupControllerTest.php
```

**Coverage Goals:**
- Service layer: 90%+ coverage
- Model methods: 85%+ coverage
- Controller actions: 75%+ coverage

**Test Frameworks:**
- PHPUnit for backend
- Jasmine/Jest for frontend

### Integration Tests (Phase 2)
```
backend/tests/integration/
├── PagegroupUpdateFlowTest.php
├── TransactionRollbackTest.php
└── ConsoleCommandTest.php
```

### End-to-End Tests (Phase 3)
```
Scenarios:
1. Create page → add content → save SEO → view
2. Edit page → modify 3 content rows → reorder → save
3. Delete page → verify cascade cleanup
4. Concurrent edits → verify transaction isolation
5. Browser: Chrome, Firefox, Safari, Edge (frontend only)
```

### Performance Tests
- Query count: should not increase
- Asset bundle size: compress before vs after
- Page load time: < 2s target
- AJAX response time: < 500ms target

---

## Deployment Strategy

### Staging Deployment (Week-end)
1. Deploy Phase 1 code to staging
2. Run full test suite (unit + integration + e2e)
3. Performance baseline measurement
4. UAT by QA team (24 hours)
5. Sign-off by product lead

### Production Deployment (Monday morning)
**Phase 1 (Critical fixes):**
```bash
# Week 1 Monday - Syntax fixes + Service layer
1. Backup production database
2. Merge to develop branch
3. CI/CD picks up changes
4. Deploy to production
5. Health check: verify no syntax errors
6. Monitor: slow query log, error logs
7. Rollback plan: git revert if needed
```

**Phase 2 (Controller refactoring):**
```bash
# Week 2 - Method deduplication + Console commands
1. Deploy console commands first
2. Test with: yii pagegroup/tidy --dry-run
3. Deploy controller changes
4. Monitor: listing page performance
5. Verify all three listing routes work
```

**Phase 3 (View modernization):**
```bash
# Week 3-4 - JavaScript/CSS extraction
1. Deploy assets first (no view changes yet)
2. Verify CSS loads correctly
3. Deploy updated views (gradual, not all at once)
4. Monitor: console errors, failed AJAX requests
5. Browser compatibility testing
```

### Rollback Procedures

**If data corruption occurs:**
```bash
# Full rollback to pre-refactoring state
git revert <commit-hash>
git push
# Restore database from backup
mysql < backup.sql
```

**If syntax error introduced:**
```bash
# Immediate revert
git revert <commit-hash>
git push
# No database recovery needed
```

**If AJAX calls fail:**
```bash
# Deactivate PagegroupAsset in views
# Restore inline JavaScript temporarily
# Fix issues in feature branch
# Redeploy when ready
```

---

## Timeline & Resource Allocation

```
Week 1 (29 hours total)
├── Mon-Wed: Phase 1.1-1.2 (Syntax fixes + Service layer)
│   └── 8 dev hours + 2 review hours
├── Wed-Thu: Phase 1.3 (Controller integration)
│   └── 2 dev hours + 1 review hour
└── Fri: Phase 1.4 (Unit tests + staging deployment)
    └── 3 dev hours + 2 QA hours

Week 2
├── Mon-Tue: Phase 2.1 (Console commands)
│   └── 4 dev hours + 1 review hour
├── Wed-Thu: Phase 2.2 (Health check)
│   └── 2 dev hours
└── Fri: Phase 2.3 (Integration tests + prod deployment)
    └── 3 dev hours + 2 QA hours

Week 3-4
├── Mon-Wed: Phase 3.1-3.4 (Asset extraction)
│   └── 13 dev hours + 3 review hours
├── Thu-Fri: Phase 3.5 (Frontend tests)
    └── 3 dev hours + 2 QA hours

Post-Release
└── Monitor + Documentation (2 weeks)
```

**Resource Requirements:**
- **Backend Developer:** 1.0 FTE (Weeks 1-2)
- **Frontend Developer:** 0.5 FTE (Weeks 3-4)
- **QA/Testing:** 0.3 FTE (continuous)
- **DevOps/infra:** 0.2 FTE (deployment support)

---

## Success Criteria

| Criterion | Target | Measurement |
|-----------|--------|-------------|
| **Zero data loss** | 100% | Database integrity check post-deployment |
| **All tests pass** | 100% | CI/CD pipeline status |
| **Reduced code duplication** | 75% reduction | Duplication metric in PHPStan |
| **No performance regression** | <5% increase | Page load time, query count monitoring |
| **Browser compatibility** | All major | Selenium test grid results |
| **Code review approval** | 100% | GitHub PRs with 2+ approvals |
| **Documentation complete** | 100% | Developer guide, API docs updated |

---

## Monitoring & Observability

### Production Metrics (Post-deployment)
```
- Error rate: Should not increase
- Slow query: Should decrease (transaction wrapper)
- HTTP 404s: Should not increase (URL changes)
- AJAX timeout: Should not change
- Page load time: Should improve (asset bundling)
```

### Logging
```
backend/logs/error.log:
- Transaction rollback events
- Service layer validation errors
- Unexpected AJAX failures

webroot/js/console:
- JavaScript errors (browser console)
- PJAX reload failures
- TinyMCE initialization issues
```

### Alerts
```
- SQL error rate > 0.1%: Page severity
- AJAX error rate > 5%: Critical
- Asset bundle 404: Critical (users can't edit)
- Service layer exception: High
```

---

## Documentation Updates

### For Developers

**File: `NGauge_Base/docs/PAGEGROUP_MODERNIZATION.md`**
- Architecture overview of service layer
- Usage examples for PageContentUpdateService
- JavaScript module pattern explanation
- Asset bundle registration guide

**File: `NGauge_Base/docs/API_REFERENCE.md`**
- PageContentUpdateService API
- Console commands reference
- AJAX endpoint documentation

### For DevOps

**File: `.github/workflows/pagegroup-deploy.yml`**
- Automated testing on PR
- Staging deployment trigger
- Prod deployment approval gate

---

## Future Considerations

### Post-Refactoring Improvements

1. **GraphQL API Layer** (Phase 4)
   - Replace AJAX with GraphQL queries
   - Standardized error handling
   - Better caching strategy

2. **Frontend Framework Migration** (Phase 5)
   - Migrate from jQuery to Vue.js/React
   - Component-based architecture
   - Real-time collaboration features

3. **Content Versioning** (Phase 4)
   - Track content history
   - Revert to previous versions
   - Audit trail for compliance

4. **Performance Optimization** (Phase 3+)
   - Implement content caching strategy
   - Database query optimization
   - Asset minification & CDN delivery

---

## Sign-Off

| Role | Name | Date | Signature |
|------|------|------|-----------|
| Project Lead | TBD | | |
| Tech Lead | TBD | | |
| QA Lead | TBD | | |
| Product Owner | TBD | | |

---

## Appendix: Reference Implementations

### A. Service Layer Usage Example
```php
// In controller
$service = new PageContentUpdateService(Yii::$app->db);

try {
    $service->updatePageWithContent(
        $page,
        ['row1' => [...], 'row2' => [...]],
        ['title' => 'SEO Title', 'meta' => 'Description']
    );
} catch (Exception $e) {
    Yii::$app->session->setFlash('error', $e->getMessage());
}
```

### B. Asset Bundle Registration
```php
// In view
use backend\assets\PagegroupAsset;

PagegroupAsset::register($this);

// Automatically publishes:
// - /web/css/pagegroup-form.css
// - /web/js/pagegroup-form.js
// - /web/js/pagegroup-content.js
// - Registers all dependencies
```

### C. JavaScript API Usage
```javascript
// Old inline approach (deprecated after Phase 3)
addRow(); // Global function

// New namespaced approach (Phase 3+)
PagegroupForm.addRow();
PagegroupForm.saveField(1, 'name', 'value');
PagegroupForm.removeRow(2);
```

---

## Glossary

- **PJAX:** Push State + AJAX for partial page updates
- **TinyMCE:** Rich text editor for content
- **Asset Bundle:** Yii mechanism for managing CSS/JS dependencies
- **Transaction:** Database ACID operation that ensures all-or-nothing execution
- **Rollback:** Revert all changes in a failed transaction
- **Scope pollution:** Global namespace contamination by JavaScript code
- **IIFE:** Immediately-Invoked Function Expression for scope isolation

