feat: add frontend menu management system

master
Manop Kongoon 2026-02-27 09:14:18 +07:00
parent 8c83876074
commit 8ef18d6d30
11 changed files with 567 additions and 0 deletions

View File

@ -2,3 +2,4 @@
- Refactored visitor statistics calculation logic for better accuracy and performance.
- Fixed incorrect aggregation in monthly and yearly statistics.
- Adjusted footer contact information font style for better readability.
- Added frontend menu management system in administrator module (navbar and left menu positions).

View File

@ -0,0 +1,127 @@
<?php
namespace backend\modules\administrator\controllers;
use Yii;
use common\models\CmsMenuFrontend;
use backend\modules\administrator\models\CmsMenuFrontendSearch;
use yii\web\Controller;
use yii\web\NotFoundHttpException;
use yii\filters\VerbFilter;
/**
* MenuFrontendController implements the CRUD actions for CmsMenuFrontend model.
*/
class MenuFrontendController extends Controller
{
/**
* {@inheritdoc}
*/
public function behaviors()
{
return [
'verbs' => [
'class' => VerbFilter::class,
'actions' => [
'delete' => ['POST'],
],
],
];
}
/**
* Lists all CmsMenuFrontend models.
* @return mixed
*/
public function actionIndex()
{
$searchModel = new CmsMenuFrontendSearch();
$dataProvider = $searchModel->search(Yii::$app->request->queryParams);
return $this->render('index', [
'searchModel' => $searchModel,
'dataProvider' => $dataProvider,
]);
}
/**
* Displays a single CmsMenuFrontend model.
* @param integer $id
* @return mixed
* @throws NotFoundHttpException if the model cannot be found
*/
public function actionView($id)
{
return $this->render('view', [
'model' => $this->findModel($id),
]);
}
/**
* Creates a new CmsMenuFrontend model.
* If creation is successful, the browser will be redirected to the 'view' page.
* @return mixed
*/
public function actionCreate()
{
$model = new CmsMenuFrontend();
if ($model->load(Yii::$app->request->post()) && $model->save()) {
return $this->redirect(['view', 'id' => $model->id]);
}
return $this->render('create', [
'model' => $model,
]);
}
/**
* Updates an existing CmsMenuFrontend model.
* If update is successful, the browser will be redirected to the 'view' page.
* @param integer $id
* @return mixed
* @throws NotFoundHttpException if the model cannot be found
*/
public function actionUpdate($id)
{
$model = $this->findModel($id);
if ($model->load(Yii::$app->request->post()) && $model->save()) {
return $this->redirect(['view', 'id' => $model->id]);
}
return $this->render('update', [
'model' => $model,
]);
}
/**
* Deletes an existing CmsMenuFrontend model.
* If deletion is successful, the browser will be redirected to the 'index' page.
* @param integer $id
* @return mixed
* @throws NotFoundHttpException if the model cannot be found
*/
public function actionDelete($id)
{
$this->findModel($id)->delete();
return $this->redirect(['index']);
}
/**
* Finds the CmsMenuFrontend model based on its primary key value.
* If the model is not found, a 404 HTTP exception will be thrown.
* @param integer $id
* @return CmsMenuFrontend the loaded model
* @throws NotFoundHttpException if the model cannot be found
*/
protected function findModel($id)
{
if (($model = CmsMenuFrontend::findOne($id)) !== null) {
return $model;
}
throw new NotFoundHttpException('The requested page does not exist.');
}
}

View File

@ -0,0 +1,76 @@
<?php
namespace backend\modules\administrator\models;
use yii\base\Model;
use yii\data\ActiveDataProvider;
use common\models\CmsMenuFrontend;
/**
* CmsMenuFrontendSearch represents the model behind the search form of `common\models\CmsMenuFrontend`.
*/
class CmsMenuFrontendSearch extends CmsMenuFrontend
{
/**
* {@inheritdoc}
*/
public function rules()
{
return [
[['id', 'parent_id', 'sort_order', 'status', 'created_at', 'updated_at'], 'integer'],
[['name', 'link', 'position'], 'safe'],
];
}
/**
* {@inheritdoc}
*/
public function scenarios()
{
// bypass scenarios() implementation in the parent class
return Model::scenarios();
}
/**
* Creates data provider instance with search query applied
*
* @param array $params
*
* @return ActiveDataProvider
*/
public function search($params)
{
$query = CmsMenuFrontend::find();
// add conditions that should always apply here
$dataProvider = new ActiveDataProvider([
'query' => $query,
'sort' => ['defaultOrder' => ['position' => SORT_ASC, 'sort_order' => SORT_ASC]]
]);
$this->load($params);
if (!$this->validate()) {
// uncomment the following line if you do not want to return any records when validation fails
// $query->where('0=1');
return $dataProvider;
}
// grid filtering conditions
$query->andFilterWhere([
'id' => $this->id,
'parent_id' => $this->parent_id,
'sort_order' => $this->sort_order,
'status' => $this->status,
'created_at' => $this->created_at,
'updated_at' => $this->updated_at,
]);
$query->andFilterWhere(['like', 'name', $this->name])
->andFilterWhere(['like', 'link', $this->link])
->andFilterWhere(['like', 'position', $this->position]);
return $dataProvider;
}
}

View File

@ -0,0 +1,60 @@
<?php
use common\models\CmsMenuFrontend;
use yii\helpers\Html;
use yii\bootstrap4\ActiveForm;
use yii\helpers\ArrayHelper;
/* @var $this yii\web\View */
/* @var $model common\models\CmsMenuFrontend */
/* @var $form yii\widgets\ActiveForm */
?>
<div class="cms-menu-frontend-form">
<?php $form = ActiveForm::begin(); ?>
<div class="row">
<div class="col-md-6">
<?= $form->field($model, 'name')->textInput(['maxlength' => true]) ?>
</div>
<div class="col-md-6">
<?= $form->field($model, 'link')->textInput(['maxlength' => true]) ?>
</div>
</div>
<div class="row">
<div class="col-md-6">
<?= $form->field($model, 'position')->dropDownList(CmsMenuFrontend::getPositions()) ?>
</div>
<div class="col-md-6">
<?= $form->field($model, 'parent_id')->dropDownList(
ArrayHelper::map(
CmsMenuFrontend::find()
->where(['parent_id' => null])
->andFilterWhere(['<>', 'id', $model->id])
->all(),
'id',
'name'
),
['prompt' => '-- เป็นเมนูหลัก --']
) ?>
</div>
</div>
<div class="row">
<div class="col-md-6">
<?= $form->field($model, 'sort_order')->textInput(['type' => 'number']) ?>
</div>
<div class="col-md-6">
<?= $form->field($model, 'status')->dropDownList([1 => 'ใช้งาน', 0 => 'ไม่ใช้งาน']) ?>
</div>
</div>
<div class="form-group">
<?= Html::submitButton('บันทึก', ['class' => 'btn btn-success']) ?>
</div>
<?php ActiveForm::end(); ?>
</div>

View File

@ -0,0 +1,21 @@
<?php
use yii\helpers\Html;
/* @var $this yii\web\View */
/* @var $model common\models\CmsMenuFrontend */
$this->title = 'เพิ่มเมนู';
$this->params['breadcrumbs'][] = ['label' => 'จัดการเมนูหน้าบ้าน', 'url' => ['index']];
$this->params['breadcrumbs'][] = $this->title;
?>
<div class="card">
<div class="card-header">
<h4 class="card-title"><?= $this->title ?></h4>
</div>
<div class="card-body">
<?= $this->render('_form', [
'model' => $model,
]) ?>
</div>
</div>

View File

@ -0,0 +1,55 @@
<?php
use common\models\CmsMenuFrontend;
use yii\helpers\Html;
use yii\grid\GridView;
/* @var $this yii\web\View */
/* @var $searchModel backend\modules\administrator\models\CmsMenuFrontendSearch */
/* @var $dataProvider yii\data\ActiveDataProvider */
$this->title = 'จัดการเมนูหน้าบ้าน';
$this->params['breadcrumbs'][] = $this->title;
?>
<div class="card">
<div class="card-header">
<h4 class="card-title"><?= $this->title ?></h4>
<div class="heading-elements">
<ul class="list-inline mb-0">
<li><?= Html::a('<i class="fa fa-plus"></i> เพิ่มเมนู', ['create'], ['class' => 'btn btn-sm btn-success']) ?></li>
</ul>
</div>
</div>
<div class="card-body">
<?= GridView::widget([
'dataProvider' => $dataProvider,
'filterModel' => $searchModel,
'columns' => [
['class' => 'yii\grid\SerialColumn'],
[
'attribute' => 'name',
'value' => function ($model) {
return ($model->parent_id ? '— ' : '') . $model->name;
}
],
'link',
[
'attribute' => 'position',
'filter' => CmsMenuFrontend::getPositions(),
'value' => function ($model) {
return CmsMenuFrontend::getPositions()[$model->position] ?? $model->position;
}
],
'sort_order',
[
'attribute' => 'status',
'filter' => [0 => 'ไม่ใช้งาน', 1 => 'ใช้งาน'],
'value' => function ($model) {
return $model->status ? 'ใช้งาน' : 'ไม่ใช้งาน';
}
],
['class' => 'yii\grid\ActionColumn'],
],
]); ?>
</div>
</div>

View File

@ -0,0 +1,22 @@
<?php
use yii\helpers\Html;
/* @var $this yii\web\View */
/* @var $model common\models\CmsMenuFrontend */
$this->title = 'แก้ไขเมนู: ' . $model->name;
$this->params['breadcrumbs'][] = ['label' => 'จัดการเมนูหน้าบ้าน', 'url' => ['index']];
$this->params['breadcrumbs'][] = ['label' => $model->name, 'url' => ['view', 'id' => $model->id]];
$this->params['breadcrumbs'][] = 'แก้ไข';
?>
<div class="card">
<div class="card-header">
<h4 class="card-title"><?= $this->title ?></h4>
</div>
<div class="card-body">
<?= $this->render('_form', [
'model' => $model,
]) ?>
</div>
</div>

View File

@ -0,0 +1,56 @@
<?php
use common\models\CmsMenuFrontend;
use yii\helpers\Html;
use yii\widgets\DetailView;
/* @var $this yii\web\View */
/* @var $model common\models\CmsMenuFrontend */
$this->title = $model->name;
$this->params['breadcrumbs'][] = ['label' => 'จัดการเมนูหน้าบ้าน', 'url' => ['index']];
$this->params['breadcrumbs'][] = $this->title;
\yii\web\YiiAsset::register($this);
?>
<div class="card">
<div class="card-header">
<h4 class="card-title"><?= $this->title ?></h4>
<div class="heading-elements">
<ul class="list-inline mb-0">
<li><?= Html::a('แก้ไข', ['update', 'id' => $model->id], ['class' => 'btn btn-sm btn-primary']) ?></li>
<li><?= Html::a('ลบ', ['delete', 'id' => $model->id], [
'class' => 'btn btn-sm btn-danger',
'data' => [
'confirm' => 'คุณแน่ใจหรือไม่ว่าต้องการลบรายการนี้?',
'method' => 'post',
],
]) ?></li>
</ul>
</div>
</div>
<div class="card-body">
<?= DetailView::widget([
'model' => $model,
'attributes' => [
'id',
[
'attribute' => 'parent_id',
'value' => $model->parent ? $model->parent->name : '-- เป็นเมนูหลัก --',
],
'name',
'link',
[
'attribute' => 'position',
'value' => CmsMenuFrontend::getPositions()[$model->position] ?? $model->position,
],
'sort_order',
[
'attribute' => 'status',
'value' => $model->status ? 'ใช้งาน' : 'ไม่ใช้งาน',
],
'created_at:datetime',
'updated_at:datetime',
],
]) ?>
</div>
</div>

View File

@ -92,6 +92,7 @@ if (Yii::$app->user->can('administrator')) {
['label' => '<span>เพิ่มผู้ใช้งาน</span>', 'url' => ['/administrator/user/create']],
//['label' => '<span>เมนู</span>', 'url' => ['/administrator/menu/index']],
]];
$menu[] = ['label' => '<i class="fa fa-list"></i> <span>เมนูหน้าบ้าน</span>', 'url' => ['/administrator/menu-frontend/index']];
$menu[] = ['label' => '<i class="fa fa-lock"></i> <span>ควบคุมสิทธิ์การเข้าถึง</span>', 'url' => ['/admin/assignment/index'], 'items' => [
['label' => '<span>การกำหนด</span>', 'url' => ['/admin/assignment/index']],
['label' => '<span>บทบาท</span>', 'url' => ['/admin/role/index']],

View File

@ -0,0 +1,103 @@
<?php
namespace common\models;
use Yii;
use yii\behaviors\TimestampBehavior;
/**
* This is the model class for table "cms_menu_frontend".
*
* @property int $id
* @property int|null $parent_id
* @property string $name
* @property string|null $link
* @property string $position navbar, left_menu
* @property int|null $sort_order
* @property int|null $status
* @property int|null $created_at
* @property int|null $updated_at
*
* @property CmsMenuFrontend $parent
* @property CmsMenuFrontend[] $cmsMenuFrontends
*/
class CmsMenuFrontend extends \yii\db\ActiveRecord
{
/**
* {@inheritdoc}
*/
public static function tableName()
{
return 'cms_menu_frontend';
}
/**
* {@inheritdoc}
*/
public function behaviors()
{
return [
TimestampBehavior::class,
];
}
/**
* {@inheritdoc}
*/
public function rules()
{
return [
[['parent_id', 'sort_order', 'status', 'created_at', 'updated_at'], 'integer'],
[['name', 'position'], 'required'],
[['name', 'link'], 'string', 'max' => 255],
[['position'], 'string', 'max' => 50],
[['parent_id'], 'exist', 'skipOnError' => true, 'targetClass' => CmsMenuFrontend::class, 'targetAttribute' => ['parent_id' => 'id']],
];
}
/**
* {@inheritdoc}
*/
public function attributeLabels()
{
return [
'id' => 'ID',
'parent_id' => 'เมนูหลัก',
'name' => 'ชื่อเมนู',
'link' => 'ลิงก์',
'position' => 'ตำแหน่ง',
'sort_order' => 'ลำดับ',
'status' => 'สถานะ',
'created_at' => 'สร้างเมื่อ',
'updated_at' => 'แก้ไขเมื่อ',
];
}
/**
* Gets query for [[Parent]].
*
* @return \yii\db\ActiveQuery
*/
public function getParent()
{
return $this->hasOne(CmsMenuFrontend::class, ['id' => 'parent_id']);
}
/**
* Gets query for [[CmsMenuFrontends]].
*
* @return \yii\db\ActiveQuery
*/
public function getCmsMenuFrontends()
{
return $this->hasMany(CmsMenuFrontend::class, ['parent_id' => 'id']);
}
public static function getPositions()
{
return [
'navbar' => 'Navbar (แถบเมนูบน)',
'left_menu' => 'Left Menu (แถบเมนูข้าง)',
];
}
}

View File

@ -0,0 +1,45 @@
<?php
use yii\db\Migration;
/**
* Handles the creation of table `{{%cms_menu_frontend}}`.
*/
class m260227_020845_create_cms_menu_frontend_table extends Migration
{
/**
* {@inheritdoc}
*/
public function safeUp()
{
$this->createTable('{{%cms_menu_frontend}}', [
'id' => $this->primaryKey(),
'parent_id' => $this->integer()->defaultValue(null),
'name' => $this->string(255)->notNull(),
'link' => $this->string(255)->defaultValue(null),
'position' => $this->string(50)->notNull()->comment('navbar, left_menu'),
'sort_order' => $this->integer()->defaultValue(0),
'status' => $this->smallInteger(1)->defaultValue(1),
'created_at' => $this->integer(),
'updated_at' => $this->integer(),
]);
$this->addForeignKey(
'fk-cms_menu_frontend-parent_id',
'{{%cms_menu_frontend}}',
'parent_id',
'{{%cms_menu_frontend}}',
'id',
'CASCADE'
);
}
/**
* {@inheritdoc}
*/
public function safeDown()
{
$this->dropForeignKey('fk-cms_menu_frontend-parent_id', '{{%cms_menu_frontend}}');
$this->dropTable('{{%cms_menu_frontend}}');
}
}