Adding vue and stats

This commit is contained in:
Benjamin
2024-11-22 01:38:28 +01:00
parent a2f1b32019
commit 198c5ccd5c
17 changed files with 517 additions and 8 deletions

View File

@@ -0,0 +1,108 @@
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using Newtonsoft.Json;
using pointMaster.Data;
using pointMaster.Models;
namespace pointMaster.Controllers
{
[ApiController]
[Route("api")]
public class StatsApiController : ControllerBase
{
readonly DataContext dataContext;
public StatsApiController(DataContext dataContext)
{
this.dataContext = dataContext;
}
[Authorize(Policy = Roles.Editor)]
[HttpGet("GetPointsOverTime")]
public async Task<object> GetList()
{
var pointData = await dataContext.Points.Include(x => x.Patrulje).Include(x => x.Poster).ToListAsync();
var pointGrouped = pointData.OrderBy(x => x.DateCreated).GroupBy(x => x.Patrulje.Name).ToList();
var retval = new List<PointChartModel>();
var lastChange = DateTime.Now.ToString("MM/d/yyyy H:m:s").Replace('.', ':');
foreach (var group in pointGrouped)
{
var data = new List<PointChartDataModel>();
var total = 0;
foreach (var point in group)
{
var date = DateTime.Parse(point.DateCreated.ToString());
total += point.Points + point.Turnout;
data.Add(new PointChartDataModel
{
x = date.ToString("MM/d/yyyy H:m:s").Replace('.', ':'),
y = total
});
}
data.Add(new PointChartDataModel
{
x = lastChange,
y = total
});
retval.Add(new PointChartModel
{
Name = group.Key,
Data = data
});
}
var rval = JsonConvert.SerializeObject(retval, new JsonSerializerSettings()
{
ReferenceLoopHandling = Newtonsoft.Json.ReferenceLoopHandling.Ignore,
});
return Ok(rval);
}
[Route("getstats")]
public async Task<IActionResult> GetStats()
{
var vm = new List<StatModel>();
var pointData = await dataContext.Points.ToListAsync();
vm.Add(new StatModel
{
Title = "Points givet",
Value = pointData.Sum(x => x.Points + x.Turnout).ToString()
});
return Ok(JsonConvert.SerializeObject(vm));
}
public class PointChartModel
{
[JsonProperty("name")]
public string Name { get; set; } = null!;
[JsonProperty("data")]
public List<PointChartDataModel> Data { get; set; } = null!;
}
public class PointChartDataModel
{
public string x { get; set; } = null!;
public int y { get; set; }
}
public class StatModel
{
public string Title { get; set; } = null!;
public string Value { get; set; } = null!;
}
}
}

View File

@@ -0,0 +1,22 @@
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using Newtonsoft.Json;
using pointMaster.Data;
using pointMaster.Models;
namespace pointMaster.Controllers
{
public class StatsController : Controller
{
private readonly DataContext dataContext;
public StatsController(DataContext dataContext)
{
this.dataContext = dataContext;
}
public async Task<IActionResult> Index()
{
return View();
}
}
}

View File

@@ -18,6 +18,13 @@ COPY . .
WORKDIR "/src/pointMaster" WORKDIR "/src/pointMaster"
RUN dotnet build "./pointMaster.csproj" -c $BUILD_CONFIGURATION -o /app/build RUN dotnet build "./pointMaster.csproj" -c $BUILD_CONFIGURATION -o /app/build
FROM node:20 AS node-build
WORKDIR /js-app
COPY ["js/package.json", "js/package-lock.json*", "./"]
RUN npm install
COPY js/ .
RUN npm run build
# This stage is used to publish the service project to be copied to the final stage # This stage is used to publish the service project to be copied to the final stage
FROM build AS publish FROM build AS publish
ARG BUILD_CONFIGURATION=Release ARG BUILD_CONFIGURATION=Release

View File

@@ -1,4 +1,5 @@
// <auto-generated /> // <auto-generated />
using System;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Migrations; using Microsoft.EntityFrameworkCore.Migrations;
@@ -11,8 +12,8 @@ using pointMaster.Data;
namespace pointMaster.Migrations namespace pointMaster.Migrations
{ {
[DbContext(typeof(DataContext))] [DbContext(typeof(DataContext))]
[Migration("20241116003844_initial")] [Migration("20241121135108_DateTime")]
partial class initial partial class DateTime
{ {
/// <inheritdoc /> /// <inheritdoc />
protected override void BuildTargetModel(ModelBuilder modelBuilder) protected override void BuildTargetModel(ModelBuilder modelBuilder)
@@ -32,6 +33,9 @@ namespace pointMaster.Migrations
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id")); NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
b.Property<DateTime?>("DateCreated")
.HasColumnType("timestamp with time zone");
b.Property<string>("Name") b.Property<string>("Name")
.IsRequired() .IsRequired()
.HasColumnType("text"); .HasColumnType("text");
@@ -52,6 +56,9 @@ namespace pointMaster.Migrations
b.Property<int>("Age") b.Property<int>("Age")
.HasColumnType("integer"); .HasColumnType("integer");
b.Property<DateTime?>("DateCreated")
.HasColumnType("timestamp with time zone");
b.Property<string>("Name") b.Property<string>("Name")
.IsRequired() .IsRequired()
.HasColumnType("text"); .HasColumnType("text");
@@ -74,6 +81,9 @@ namespace pointMaster.Migrations
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id")); NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
b.Property<DateTime?>("DateCreated")
.HasColumnType("timestamp with time zone");
b.Property<int>("PatruljeId") b.Property<int>("PatruljeId")
.HasColumnType("integer"); .HasColumnType("integer");
@@ -103,6 +113,9 @@ namespace pointMaster.Migrations
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id")); NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
b.Property<DateTime?>("DateCreated")
.HasColumnType("timestamp with time zone");
b.Property<string>("Description") b.Property<string>("Description")
.IsRequired() .IsRequired()
.HasColumnType("text"); .HasColumnType("text");

View File

@@ -1,4 +1,5 @@
using Microsoft.EntityFrameworkCore.Migrations; using System;
using Microsoft.EntityFrameworkCore.Migrations;
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
#nullable disable #nullable disable
@@ -6,7 +7,7 @@ using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
namespace pointMaster.Migrations namespace pointMaster.Migrations
{ {
/// <inheritdoc /> /// <inheritdoc />
public partial class initial : Migration public partial class DateTime : Migration
{ {
/// <inheritdoc /> /// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder) protected override void Up(MigrationBuilder migrationBuilder)
@@ -17,7 +18,8 @@ namespace pointMaster.Migrations
{ {
Id = table.Column<int>(type: "integer", nullable: false) Id = table.Column<int>(type: "integer", nullable: false)
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
Name = table.Column<string>(type: "text", nullable: false) Name = table.Column<string>(type: "text", nullable: false),
DateCreated = table.Column<DateTime>(type: "timestamp with time zone", nullable: true)
}, },
constraints: table => constraints: table =>
{ {
@@ -32,7 +34,8 @@ namespace pointMaster.Migrations
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
Name = table.Column<string>(type: "text", nullable: false), Name = table.Column<string>(type: "text", nullable: false),
Location = table.Column<string>(type: "text", nullable: false), Location = table.Column<string>(type: "text", nullable: false),
Description = table.Column<string>(type: "text", nullable: false) Description = table.Column<string>(type: "text", nullable: false),
DateCreated = table.Column<DateTime>(type: "timestamp with time zone", nullable: true)
}, },
constraints: table => constraints: table =>
{ {
@@ -47,7 +50,8 @@ namespace pointMaster.Migrations
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
Name = table.Column<string>(type: "text", nullable: false), Name = table.Column<string>(type: "text", nullable: false),
Age = table.Column<int>(type: "integer", nullable: false), Age = table.Column<int>(type: "integer", nullable: false),
PatruljeId = table.Column<int>(type: "integer", nullable: false) PatruljeId = table.Column<int>(type: "integer", nullable: false),
DateCreated = table.Column<DateTime>(type: "timestamp with time zone", nullable: true)
}, },
constraints: table => constraints: table =>
{ {
@@ -69,7 +73,8 @@ namespace pointMaster.Migrations
Points = table.Column<int>(type: "integer", nullable: false), Points = table.Column<int>(type: "integer", nullable: false),
Turnout = table.Column<int>(type: "integer", nullable: false), Turnout = table.Column<int>(type: "integer", nullable: false),
PatruljeId = table.Column<int>(type: "integer", nullable: false), PatruljeId = table.Column<int>(type: "integer", nullable: false),
PosterId = table.Column<int>(type: "integer", nullable: false) PosterId = table.Column<int>(type: "integer", nullable: false),
DateCreated = table.Column<DateTime>(type: "timestamp with time zone", nullable: true)
}, },
constraints: table => constraints: table =>
{ {

View File

@@ -1,4 +1,5 @@
// <auto-generated /> // <auto-generated />
using System;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion; using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
@@ -29,6 +30,9 @@ namespace pointMaster.Migrations
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id")); NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
b.Property<DateTime?>("DateCreated")
.HasColumnType("timestamp with time zone");
b.Property<string>("Name") b.Property<string>("Name")
.IsRequired() .IsRequired()
.HasColumnType("text"); .HasColumnType("text");
@@ -49,6 +53,9 @@ namespace pointMaster.Migrations
b.Property<int>("Age") b.Property<int>("Age")
.HasColumnType("integer"); .HasColumnType("integer");
b.Property<DateTime?>("DateCreated")
.HasColumnType("timestamp with time zone");
b.Property<string>("Name") b.Property<string>("Name")
.IsRequired() .IsRequired()
.HasColumnType("text"); .HasColumnType("text");
@@ -71,6 +78,9 @@ namespace pointMaster.Migrations
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id")); NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
b.Property<DateTime?>("DateCreated")
.HasColumnType("timestamp with time zone");
b.Property<int>("PatruljeId") b.Property<int>("PatruljeId")
.HasColumnType("integer"); .HasColumnType("integer");
@@ -100,6 +110,9 @@ namespace pointMaster.Migrations
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id")); NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
b.Property<DateTime?>("DateCreated")
.HasColumnType("timestamp with time zone");
b.Property<string>("Description") b.Property<string>("Description")
.IsRequired() .IsRequired()
.HasColumnType("text"); .HasColumnType("text");

View File

@@ -10,6 +10,7 @@
<link rel="stylesheet" href="~/pointMaster.styles.css" asp-append-version="true" /> <link rel="stylesheet" href="~/pointMaster.styles.css" asp-append-version="true" />
</head> </head>
<body> <body>
<div id="app">
<header> <header>
<nav class="navbar navbar-expand-sm navbar-toggleable-sm navbar-light bg-white border-bottom box-shadow mb-3"> <nav class="navbar navbar-expand-sm navbar-toggleable-sm navbar-light bg-white border-bottom box-shadow mb-3">
<div class="container-fluid"> <div class="container-fluid">
@@ -37,9 +38,11 @@
&copy; 2024 - pointMaster - <a asp-area="" asp-controller="Home" asp-action="Privacy">Privacy</a> &copy; 2024 - pointMaster - <a asp-area="" asp-controller="Home" asp-action="Privacy">Privacy</a>
</div> </div>
</footer> *@ </footer> *@
</div>
<script src="~/lib/jquery/dist/jquery.min.js"></script> <script src="~/lib/jquery/dist/jquery.min.js"></script>
<script src="~/lib/bootstrap/dist/js/bootstrap.bundle.min.js"></script> <script src="~/lib/bootstrap/dist/js/bootstrap.bundle.min.js"></script>
<script src="~/js/site.js" asp-append-version="true"></script> <script src="~/js/site.js" asp-append-version="true"></script>
<script src="~/js/dist/app.js" asp-append-version="true"></script>
@await RenderSectionAsync("Scripts", required: false) @await RenderSectionAsync("Scripts", required: false)
</body> </body>
</html> </html>

View File

@@ -0,0 +1,7 @@
@*
For more information on enabling MVC for empty projects, visit https://go.microsoft.com/fwlink/?LinkID=397860
*@
@{
ViewData["title"] = "Stats";
}
<hello-world></hello-world>

View File

@@ -0,0 +1,24 @@
{
"name": "website",
"version": "0.1.0",
"description": "",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"watch": "vue-cli-service build --mode development --watch",
"build": "vue-cli-service build"
},
"author": "",
"license": "ISC",
"dependencies": {
"@vue/cli-plugin-typescript": "^5.0.8",
"@vue/cli-service": "^5.0.8",
"apexcharts": "^4.0.0",
"axios": "^1.7.7",
"vue": "^3.5.13",
"vue-class-component": "^7.2.6"
},
"devDependencies": {
"vue-cli-service": "^5.0.10"
}
}

View File

@@ -0,0 +1,104 @@
<template>
<div class="stat-dashboard">
<div>
<h1>Stats</h1>
<div class="stats" v-for="data in StatData">
<div>
</div>
</div>
</div>
<div>
<p class="stat-title">Point over tid</p>
<div id="chart"></div>
</div>
</div>
</template>
<script lang="ts">
import ApexCharts, { ApexOptions } from 'apexcharts';
import axios from 'axios';
import { defineComponent, onMounted } from 'vue';
export default defineComponent({
name: 'HelloWorld',
setup() {
var StatData: Stat[] = [];
const GetStats = async () => {
var data = (await axios.get("/api/getstats")).data as Stat[];
StatData = data;
}
onMounted(async () => {
await GetStats();
await Chart();
});
}
});
const Chart = async () => {
var options: ApexOptions = {
chart: {
type: "line",
toolbar: {
show: false,
},
},
xaxis: {
type: "datetime"
},
series: []
};
const chart = new ApexCharts(document.querySelector("#chart"), options);
chart.render();
const data = (await axios.get("/api/GetPointsOverTime")).data;
console.log(data);
chart.updateOptions({
series: data
} as ApexOptions)
}
</script>
<style lang="css">
.stat-dashboard {
display: flex;
height: 60vh;
justify-content: space-between;
}
#chart {
height: 300px;
width: 500px;
}
.stats {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 2rem;
max-width: 50vw;
}
.stat {
background-color: rgba(0, 0, 0, 0.1);
padding: 10px;
height: 7rem;
width: 10rem;
border-radius: 5px;
text-align: center;
}
.stat-title {
font-weight: 700;
font-size: 1.25rem;
}
.stat .stat-value {
font-size: 2rem;
}
</style>

View File

@@ -0,0 +1,110 @@
<template>
<div class="stat-dashboard">
<div>
<h1>Stats</h1>
<div class="stats">
<div class="stat" v-for="(data, index) in StatData" :key="index">
<p class="stat-title">{{ data.Title }}</p>
<p class="stat-value">{{ data.Value }}</p>
</div>
</div>
</div>
<div>
<p class="stat-title">Point over tid</p>
<div id="chart"></div>
</div>
</div>
</template>
<script lang="ts">
import ApexCharts, { ApexOptions } from 'apexcharts';
import axios from 'axios';
import { defineComponent, onMounted, ref } from 'vue';
export default defineComponent({
name: "Stats",
setup() {
const StatData = ref<Stat[]>([]);
const GetStats = async () => {
const data = (await axios.get("/api/getstats")).data as Stat[]
StatData.value = data;
};
const initializeChart = async () => {
const options: ApexOptions = {
chart: {
type: "line",
toolbar: {
show: false,
},
},
xaxis: {
type: "datetime"
},
series: []
};
const chart = new ApexCharts(document.querySelector("#chart"), options);
chart.render();
const data = (await axios.get("/api/GetPointsOverTime")).data;
chart.updateOptions({
series: data
} as ApexOptions);
};
onMounted(async () => {
await GetStats();
await initializeChart();
});
return {
StatData
}
}
})
</script>
<style lang="css">
.stat-dashboard {
display: flex;
height: 60vh;
justify-content: space-between;
}
#chart {
height: 300px;
width: 500px;
}
.stats {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 2rem;
max-width: 50vw;
}
.stat {
background-color: rgba(0, 0, 0, 0.1);
padding: 10px;
height: 7rem;
width: 10rem;
border-radius: 5px;
text-align: center;
box-shadow: 0px 0px 5px rgba(0,0,0,0.1);
}
.stat-title {
font-weight: 700;
font-size: 1.25rem;
}
.stat .stat-value {
font-size: 2rem;
}
</style>

View File

@@ -0,0 +1,11 @@
import { createApp } from "vue";
import HelloWorld from "./components/HelloWorld.vue";
import Stats from "./components/Stats.vue";
const app = createApp({});
app.component("hello-world", Stats);
(window as any).app = app;
app.mount("#app");

6
pointMaster/js/src/shims-vue.d.ts vendored Normal file
View File

@@ -0,0 +1,6 @@
/* eslint-disable */
declare module '*.vue' {
import type { DefineComponent } from 'vue'
const component: DefineComponent<{}, {}, any>
export default component
}

13
pointMaster/js/src/typings.d.ts vendored Normal file
View File

@@ -0,0 +1,13 @@
type Point = {
Id: number;
Points: number;
Turnout: number;
postName: string;
patruljeName: string;
DateCreated: Date;
}
type Stat = {
Title: string;
Value: string;
}

View File

@@ -0,0 +1,42 @@
{
"compilerOptions": {
"target": "esnext",
"module": "esnext",
"strict": true,
"jsx": "preserve",
"importHelpers": true,
"moduleResolution": "node",
"resolveJsonModule": true,
"experimentalDecorators": true,
"esModuleInterop": true,
"allowSyntheticDefaultImports": true,
"strictNullChecks": false,
"sourceMap": true,
"baseUrl": ".",
"types": [
"webpack-env"
],
"paths": {
"@/*": [
"src/*"
]
},
"lib": [
"esnext",
"dom",
"dom.iterable",
"scripthost"
],
"skipLibCheck": true,
},
"include": [
"src/**/*.ts",
"src/**/*.tsx",
"src/**/*.vue",
"tests/**/*.ts",
"tests/**/*.tsx",
],
"exclude": [
"node_modules"
]
}

View File

@@ -0,0 +1,19 @@
module.exports = {
outputDir: "../wwwroot/js/dist", // Output everything into the `dist` folder
assetsDir: "", // Prevent nested `assets` folder
publicPath: "./", // Use relative paths for assets
filenameHashing: false, // Disable hash in filenames
runtimeCompiler: true, // Enable runtime compilation
configureWebpack: {
resolve: {
extensions: ['.vue', '.js', '.json'],
},
output: {
filename: '[name].js', // Keep the main app filename dynamic
chunkFilename: '[name].js', // Ensure chunk filenames are unique
},
},
chainWebpack: config => {
config.optimization.splitChunks(false); // Disable chunk splitting
},
};

View File

@@ -18,11 +18,13 @@
</PackageReference> </PackageReference>
<PackageReference Include="Microsoft.VisualStudio.Azure.Containers.Tools.Targets" Version="1.21.0" /> <PackageReference Include="Microsoft.VisualStudio.Azure.Containers.Tools.Targets" Version="1.21.0" />
<PackageReference Include="Microsoft.VisualStudio.Web.CodeGeneration.Design" Version="8.0.7" /> <PackageReference Include="Microsoft.VisualStudio.Web.CodeGeneration.Design" Version="8.0.7" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="8.0.10" /> <PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="8.0.10" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<Folder Include="Migrations\" /> <Folder Include="Migrations\" />
<Folder Include="js\" />
</ItemGroup> </ItemGroup>
</Project> </Project>