From 3604e844a024a59d58a09afff68b9d04668abf5a Mon Sep 17 00:00:00 2001
From: Fabio Manganiello <fabio@manganiello.tech>
Date: Sat, 5 Apr 2025 22:20:31 +0200
Subject: [PATCH] Fixed type errors

---
 frontend/src/components/Map.vue               |  5 +-
 frontend/src/components/stats/MetricsForm.vue |  8 +--
 frontend/src/mixins/Dates.vue                 |  7 +-
 frontend/src/models/LocationQuery.ts          |  6 ++
 frontend/src/styles/layout.scss               |  2 +-
 frontend/src/views/Stats.vue                  | 71 +++++++++++++------
 6 files changed, 70 insertions(+), 29 deletions(-)

diff --git a/frontend/src/components/Map.vue b/frontend/src/components/Map.vue
index 3e2e12e..166bd27 100644
--- a/frontend/src/components/Map.vue
+++ b/frontend/src/components/Map.vue
@@ -159,7 +159,7 @@ export default {
 
   data() {
     return {
-      countries: [] as string[],
+      countries: [] as Country[],
       devices: [] as UserDevice[],
       loading: false,
       map: null as Optional<Map>,
@@ -307,10 +307,11 @@ export default {
       await this.processQueryChange(this.locationQuery, oldQuery)
     },
 
-    async getCountries() {
+    async getCountries(): Promise<Country[]> {
       return (
         await this.getStats(
           new StatsRequest({
+            // @ts-ignore
             userId: this.$root.user.id,
             groupBy: ['country'],
             order: 'desc',
diff --git a/frontend/src/components/stats/MetricsForm.vue b/frontend/src/components/stats/MetricsForm.vue
index dc11a2e..4615521 100644
--- a/frontend/src/components/stats/MetricsForm.vue
+++ b/frontend/src/components/stats/MetricsForm.vue
@@ -2,13 +2,13 @@
   <form class="metrics-form" @submit.prevent="$emit('submit', newMetrics)">
     <div class="metrics">
       <div v-for="enabled, metric in newMetrics" :key="metric" class="metric">
-        <label :for="metric">
+        <label :for="metric.toString()">
           <input
             type="checkbox"
-            :id="metric"
-            v-model="newMetrics[metric]"
+            :id="metric.toString()"
+            v-model="newMetrics[metric.toString()]"
           />&nbsp;
-          {{ displayName(metric) }}
+          {{ displayName(metric.toString()) }}
         </label>
       </div>
     </div>
diff --git a/frontend/src/mixins/Dates.vue b/frontend/src/mixins/Dates.vue
index abe609c..d8866c2 100644
--- a/frontend/src/mixins/Dates.vue
+++ b/frontend/src/mixins/Dates.vue
@@ -12,7 +12,12 @@ export default {
         return '-'
       }
 
-      let dateStr = this.normalizeDate(date).toString().replace(/GMT.*/, '').trim() as string
+      date = this.normalizeDate(date)
+      if (!date) {
+        return '-'
+      }
+
+      let dateStr = date.toString().replace(/GMT.*/, '').trim() as string
       if (!opts.dayOfWeek) {
         dateStr = dateStr.slice(4)
       }
diff --git a/frontend/src/models/LocationQuery.ts b/frontend/src/models/LocationQuery.ts
index f7211e1..6026947 100644
--- a/frontend/src/models/LocationQuery.ts
+++ b/frontend/src/models/LocationQuery.ts
@@ -15,6 +15,8 @@ class LocationQuery {
   public country: Optional<string> = null;
   public locality: Optional<string> = null;
   public postalCode: Optional<string> = null;
+  public address: Optional<string> = null;
+  public description: Optional<string> = null;
   public order: string = 'desc';
 
   constructor(data: {
@@ -32,6 +34,8 @@ class LocationQuery {
     country?: Optional<string>;
     locality?: Optional<string>;
     postalCode?: Optional<string>;
+    address?: Optional<string>;
+    description?: Optional<string>;
     order?: Optional<string>;
   }) {
     this.limit = data.limit || this.limit;
@@ -48,6 +52,8 @@ class LocationQuery {
     this.country = data.country || this.country;
     this.locality = data.locality || this.locality;
     this.postalCode = data.postalCode || this.postalCode;
+    this.address = data.address || this.address;
+    this.description = data.description || this.description;
     this.order = data.order || this.order;
   }
 }
diff --git a/frontend/src/styles/layout.scss b/frontend/src/styles/layout.scss
index 6c024fd..5923f4f 100644
--- a/frontend/src/styles/layout.scss
+++ b/frontend/src/styles/layout.scss
@@ -27,7 +27,7 @@ $screen-size-to-breakpoint: (
   @each $screen-size, $breakpoint in $screen-size-to-breakpoint {
     .col-#{$screen-size}-#{$i} {
       @media (min-width: map.get($breakpoints, $breakpoint)) {
-        width: (100% / 12) * $i;
+        width: calc((100% / 12) * $i);
       }
     }
   }
diff --git a/frontend/src/views/Stats.vue b/frontend/src/views/Stats.vue
index ee7112b..86e251b 100644
--- a/frontend/src/views/Stats.vue
+++ b/frontend/src/views/Stats.vue
@@ -1,10 +1,16 @@
 <template>
   <div class="stats view">
     <div class="wrapper">
-      <h1>
-        <font-awesome-icon icon="chart-line" />&nbsp;
-        Statistics
-      </h1>
+      <div class="header">
+        <h1>
+          <font-awesome-icon icon="chart-line" />&nbsp;
+          Statistics
+        </h1>
+
+        <small v-if="stats.length">
+          Showing <b>{{ stats.length }}</b> records
+        </small>
+      </div>
 
       <div class="loading-container" v-if="loading">
         <Loading />
@@ -23,7 +29,7 @@
             </tr>
           </thead>
           <tbody>
-            <tr v-for="stat in stats" :key="stat.key">
+            <tr v-for="stat, i in stats" :key="i">
               <td class="key" v-for="value, attr in stat.key" :key="attr">
                 {{ displayValue(attr, value) }}
               </td>
@@ -61,10 +67,8 @@
 </template>
 
 <script lang="ts">
-import { getCountryData, getEmojiFlag } from 'countries-list';
-import type { TCountryCode } from 'countries-list';
-
 import Api from '../mixins/Api.vue';
+import Country from '../models/Country';
 import Dates from '../mixins/Dates.vue';
 import FloatingButton from '../elements/FloatingButton.vue';
 import Loading from '../elements/Loading.vue';
@@ -106,6 +110,7 @@ export default {
   computed: {
     query() {
       return new StatsRequest({
+        // @ts-ignore
         userId: this.$root.user.id,
         groupBy: Object.entries(this.metrics)
           .filter(([_, enabled]) => enabled)
@@ -158,7 +163,13 @@ export default {
         }
       }
 
-      this.metrics = metrics;
+      this.metrics = metrics as {
+        country: boolean,
+        locality: boolean,
+        address: boolean,
+        postalCode: boolean,
+        description: boolean,
+      };
     },
 
     closeForm() {
@@ -166,7 +177,14 @@ export default {
     },
 
     onMetricsSubmit(newMetrics: Record<string, boolean>) {
-      this.metrics = newMetrics;
+      this.metrics = newMetrics as {
+        country: boolean,
+        locality: boolean,
+        address: boolean,
+        postalCode: boolean,
+        description: boolean,
+      };
+
       this.closeForm();
     },
 
@@ -187,13 +205,12 @@ export default {
         return "<missing>";
       }
 
-      const cc = countryCode.toUpperCase() as TCountryCode;
-      const country = getCountryData(cc);
+      const country = Country.fromCode(countryCode);
       if (!country) {
         return countryCode;
       }
 
-      return `${getEmojiFlag(cc)} ${country.name}`;
+      return `${country.flag} ${country.name}`;
     },
 
     displayDate(date: Date | string | number | null | undefined) {
@@ -209,14 +226,6 @@ export default {
       },
       deep: true,
     },
-
-    showSelectForm(newValue: boolean) {
-      if (newValue) {
-        this.$nextTick(() => {
-          this.$refs.metricsForm?.focus();
-        });
-      }
-    },
   },
 
   async created() {
@@ -253,6 +262,26 @@ export default {
     }
   }
 
+  .header {
+    display: flex;
+    flex-direction: column;
+    align-items: center;
+    justify-content: center;
+
+    small {
+      margin: -1em 0 1em 0;
+      opacity: 0.6;
+    }
+  }
+
+  .list {
+    .no-data {
+      font-size: 1.2rem;
+      margin: 1rem;
+      text-align: center;
+    }
+  }
+
   table {
     tbody {
       tr {