How can I implement a voting system in Ionic6 + Yii2

At the moment my project is based on an application about place recommendations. I have implemented a relational database with MariaDB with data and their respective relationships. To make the requests with an API Rest I am using yii2 in my backend, it is already configured and ready to start handling my requests from the API Rest.

I’m figuring out how to implement a voting system, I already have the database to store those votes, however, I can’t figure out how to make this code work:

This is the .ts code of the implementation that I am trying to make: tab1.page.ts

import { Component, OnInit, ViewChild } from '@angular/core';

import {
  InfiniteScrollCustomEvent,
  IonInfiniteScroll,
  LoadingController,
} from '@ionic/angular';
import { PublicacionService } from '../services/publicacion.service';

@Component({
  selector: 'app-tab1',
  templateUrl: 'tab1.page.html',
  styleUrls: ['tab1.page.scss'],
})
export class Tab1Page implements OnInit {
  @ViewChild(IonInfiniteScroll) infiniteScroll: IonInfiniteScroll;

  publicacion: any;

  publicaciones = [];

  constructor(
    private publicacionService: PublicacionService,
    private loadingCtrl: LoadingController
  ) {}

  ngOnInit(): void {
    this.loadPublicaciones();
  }

  async loadPublicaciones(event?: InfiniteScrollCustomEvent) {
    const loading = await this.loadingCtrl.create({
      spinner: 'crescent',
    });

    await loading.present();

    this.publicacionService.getDatos().subscribe(
      (res) => {
        loading.dismiss();
        this.publicaciones = res;
        event?.target.complete();
      },
      (err) => {
        console.log(err);
        loading.dismiss();
      }
    );
  }

  guardarVoto() {
    try {
      const publicacion = this.publicacion.value;
      this.publicacionService.postVote(publicacion).subscribe(
        (res) => {
          console.log('voto guardado');
        },
        (err) => {
          if (err === 422) {
            console.log('ya has votado por este post');
          }
        }
      );
    } catch (e) {
      console.log(e);
    }
  }
}

This is the code for the HTML implementation: tab1.page.html

<ion-header>
  <ion-toolbar color="primary">
    <ion-title color="light"> NextStop </ion-title>
  </ion-toolbar>
</ion-header>

<ion-content>
  <ion-list *ngFor="let publicacion of publicaciones">
    <ion-list-header>
      <ion-label>{{publicacion.pub_titulo}}</ion-label>
    </ion-list-header>
    <ion-row
      class="ion-padding publication-row"
      tappable
      [routerLink]="['/tab1', publicacion.pub_id]"
    >
      <ion-col size="6">
        <div class="publication-tags">
          <ion-label *ngFor="let etiqueta of publicacion.etiqueta"
            >{{etiqueta.eti_nombre}}
          </ion-label>
        </div>
        <div class="publication-user">
          <ion-icon name="person-circle-sharp"></ion-icon>
          <ion-label> {{publicacion.usuario}}</ion-label>
        </div>
        <div class="publication-date">
          <ion-label color="medium"
            >{{publicacion.pub_fecha | date: 'short'}}</ion-label
          >
        </div>
      </ion-col>
      <ion-col size="6">
        <div
          class="publication-image"
          [style.background-image]="'url(http://nextstop.test/img/'+publicacion.imagen+')'"
        ></div>
      </ion-col>
    </ion-row>
    <ion-row>
      <ion-col size="12" class="publication-actions border-bottom">
        <ion-button
          (click)="guardarVoto()"
          fill="clear"
          class="ion-no-margin"
          color="dark"
        >
          <ion-icon
            name="arrow-up-outline"
            color="success"
            slot="icon-only"
            size="small"
          ></ion-icon>
        </ion-button>
        <span class="publication-vote-count">{{publicacion.votos}}</span>
        <ion-button fill="clear" class="ion-no-margin" color="dark">
          <ion-icon
            name="arrow-down-outline"
            color="danger"
            slot="icon-only"
            size="small"
          ></ion-icon>
        </ion-button>
        <ion-button fill="clear" class="ion-no-margin" color="dark">
          <ion-icon
            name="chatbubble-outline"
            color="dark"
            slot="icon-only"
            size="small"
          ></ion-icon>
          <span class="publication-icon-count"
            >{{publicacion.comentarios}}</span
          >
        </ion-button>
        <ion-button fill="clear" class="ion-no-margin" color="dark">
          <ion-icon
            name="eye-outline"
            color="dark"
            slot="icon-only"
            size="small"
          ></ion-icon>
          <span class="publication-icon-count">{{publicacion.visitas}}</span>
        </ion-button>
      </ion-col>
    </ion-row>
  </ion-list>
</ion-content>

Finally the service that I am using right now: publicacion.service.ts

/* eslint-disable @typescript-eslint/naming-convention */
import { Injectable } from '@angular/core';

import { HttpClient, HttpHeaders } from '@angular/common/http';
import { map } from 'rxjs/operators';

import { throwError } from 'rxjs';
import { catchError } from 'rxjs/operators';

export class VotoPublicacion {
  vop_fkusuario: number;
  vop_fkpublicacion: number;
  vop_fecha: Date;
}

@Injectable({
  providedIn: 'root',
})
export class PublicacionService {
  baseURL = 'http://nextstop.test/publicacion-rest';
  token = '100-token';

  httpHeader = {
    headers: new HttpHeaders({
      'Content-Type': 'application/json',
      Authorization: 'Bearer ' + this.token,
    }),
  };

  constructor(private http: HttpClient) {}

  getDatos() {
    return this.http
      .get(
        this.baseURL +
          '?expand=usuario,lugar,imagen,etiqueta,visitas,votos,comentarios'
      )
      .pipe(map((res: any) => res));
  }

  getPublicacionDetails(id: string) {
    return this.http
      .get(
        this.baseURL +
          's/' +
          id +
          '?expand=usuario,imagenes,etiquetas,visitas,votos,comentarios'
      )
      .pipe(map((res: any) => res));
  }

  postVote(voto_publicacion: VotoPublicacion) {
    return this.http
      .post(this.baseURL + 's', voto_publicacion, this.httpHeader)
      .pipe(
        map((res: any) => res),
        catchError((error) => throwError(error.status || 'Server error.'))
      );
  }
}

I attach screenshots of the table that I will point to store the votes of a user when clicking the upvote button and at the same time synchronize the vote count.

image

This is the code in the model of Publicacion where I configure the transactions and queries to the database with Yii2:

<?php

namespace app\models;

use Yii;

/**
 * This is the model class for table "publicacion".
 *
 * @property int $pub_id
 * @property string $pub_titulo
 * @property string $pub_descripcion
 * @property string|null $pub_fecha
 * @property int $pub_fkusuario
 * @property int|null $pub_fklugar
 *
 * @property Comentario[] $comentarios
 * @property EtiquetaPublicacion[] $etiquetaPublicacions
 * @property ImagenPublicacion[] $imagenPublicacions
 * @property Lugar $pubFklugar
 * @property Usuario $pubFkusuario
 * @property VisitaPublicacion[] $visitaPublicacions
 * @property VotoPublicacion[] $votoPublicacions
 */
class Publicacion extends \yii\db\ActiveRecord
{
    /**
     * {@inheritdoc}
     */
    public static function tableName()
    {
        return 'publicacion';
    }

    /**
     * {@inheritdoc}
     */
    public function rules()
    {
        return [
            [['pub_titulo', 'pub_descripcion', 'pub_fkusuario'], 'required'],
            [['pub_descripcion'], 'string'],
            [['pub_fecha'], 'safe'],
            [['pub_fkusuario', 'pub_fklugar'], 'integer'],
            [['pub_titulo'], 'string', 'max' => 100],
            [['pub_fklugar'], 'exist', 'skipOnError' => true, 'targetClass' => Lugar::className(), 'targetAttribute' => ['pub_fklugar' => 'lug_id']],
            [['pub_fkusuario'], 'exist', 'skipOnError' => true, 'targetClass' => Usuario::className(), 'targetAttribute' => ['pub_fkusuario' => 'usu_id']],
        ];
    }

    /**
     * {@inheritdoc}
     */
    public function attributeLabels()
    {
        return [
            'pub_id' => 'Pub ID',
            'pub_titulo' => 'Pub Titulo',
            'pub_descripcion' => 'Pub Descripcion',
            'pub_fecha' => 'Pub Fecha',
            'pub_fkusuario' => 'Pub Fkusuario',
            'pub_fklugar' => 'Pub Fklugar',
        ];
    }

    /**
     * Gets query for [[Comentarios]].
     *
     * @return \yii\db\ActiveQuery
     */
    public function getComentarios()
    {
        return $this->hasMany(Comentario::className(), ['com_fkpublicacion' => 'pub_id']);
    }

    /**
     * Gets query for [[EtiquetaPublicacions]].
     *
     * @return \yii\db\ActiveQuery
     */
    public function getEtiquetaPublicacions()
    {
        return $this->hasMany(EtiquetaPublicacion::className(), ['etp_fkpublicacion' => 'pub_id']);
    }

    /**
     * Gets query for [[ImagenPublicacions]].
     *
     * @return \yii\db\ActiveQuery
     */
    public function getImagenPublicacions()
    {
        return $this->hasMany(ImagenPublicacion::className(), ['imp_fkpublicacion' => 'pub_id']);
    }

    /**
     * Gets query for [[PubFklugar]].
     *
     * @return \yii\db\ActiveQuery
     */
    public function getPubFklugar()
    {
        return $this->hasOne(Lugar::className(), ['lug_id' => 'pub_fklugar']);
    }

    /**
     * Gets query for [[PubFkusuario]].
     *
     * @return \yii\db\ActiveQuery
     */
    public function getPubFkusuario()
    {
        return $this->hasOne(Usuario::className(), ['usu_id' => 'pub_fkusuario']);
    }

    /**
     * Gets query for [[VisitaPublicacions]].
     *
     * @return \yii\db\ActiveQuery
     */
    public function getVisitaPublicacions()
    {
        return $this->hasMany(VisitaPublicacion::className(), ['vip_fkpublicacion' => 'pub_id']);
    }

    /**
     * Gets query for [[VotoPublicacions]].
     *
     * @return \yii\db\ActiveQuery
     */
    public function getVotoPublicacions()
    {
        return $this->hasMany(VotoPublicacion::className(), ['vop_fkpublicacion' => 'pub_id']);
    }

    public function extraFields()
    {
        return [
            'usuario' => function($item) {
                return $item->pubFkusuario->usu_nombre;
            },
            'lugar' => function($item) {
                return $item->pubFklugar->lug_nombre;
            },
            'imagen' => function($item) {
                return $item->imagen;
            },
            'imagenes' => function($item){
                return $item->allImagenes;
            },
            'etiqueta' => function($item) {
                return $item->etiqueta;
            },
            'etiquetas' => function($item) {
                return $item->allEtiquetas;
            },
            'visitas' => function($item) {
                return $item->allVisitas;
            },
            'votos' => function($item) {
                return $item->allVotos;
            },
            'comentarios' => function($item) {
                return $item->allComentarios;
            }
        ];
    }

    public function getImagen()
    {
        $imagen = ImagenPublicacion::find()->where(['imp_fkpublicacion' => $this->pub_id])->one();

        return isset($imagen) ? $imagen->imp_url : 'https://ionicframework.com/docs/img/demos/card-media.png';
    }

    public function getAllImagenes()
    {
        $imagenes_pub = ImagenPublicacion::find()->where(['imp_fkpublicacion' => $this->pub_id])->all();

        $imagenes = [];

        foreach ($imagenes_pub as $imagen) {
            $imagenes[] = ['imp_url' => $imagen->imp_url];
        }

        return $imagenes;
    }

    public function getEtiqueta()
    {
        $etiquetaspub = EtiquetaPublicacion::find()->where(['etp_fkpublicacion' => $this->pub_id])->limit(2)->all();
        $etiquetas = [];
        $sin_etiqueta = [['eti_nombre' => '']];

        foreach ($etiquetaspub as $etiqueta) {
            $etiquetas[] = ['eti_nombre' => $etiqueta->etpFketiqueta->eti_nombre];
        }

        return count($etiquetas) > 0 ? $etiquetas : $sin_etiqueta;
    }

    public function getAllEtiquetas()
    {
        $etiquetas_pub = EtiquetaPublicacion::find()->where(['etp_fkpublicacion' => $this->pub_id])->all();

        $etiquetas = [];

        foreach ($etiquetas_pub as $etiqueta) {
            $etiquetas[] = ['eti_nombre' => $etiqueta->etpFketiqueta->eti_nombre];
        }
        
        return $etiquetas;
    }

    public function getAllVisitas()
    {        
        return Publicacion::find()->joinWith(['visitaPublicacions'])->where(['vip_fkpublicacion' => $this->pub_id])->count();
    }

    public function getAllVotos()
    {        
        return Publicacion::find()->joinWith(['votoPublicacions'])->where(['vop_fkpublicacion' => $this->pub_id])->count();
    }

    public function getAllComentarios()
    {
        return Publicacion::find()->joinWith(['comentarios'])->where(['com_fkpublicacion' => $this->pub_id])->count();
    }
}