Add counts to Dashboard (#2)
Also refactored database parameters with a few extension methods; ready for profile view page
This commit is contained in:
parent
fe3510b818
commit
97b3de1cea
|
@ -6,6 +6,7 @@
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
<PackageReference Include="Blazored.Toast" Version="3.1.2" />
|
||||||
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly" Version="5.0.1" />
|
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly" Version="5.0.1" />
|
||||||
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.DevServer" Version="5.0.1" PrivateAssets="all" />
|
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.DevServer" Version="5.0.1" PrivateAssets="all" />
|
||||||
<PackageReference Include="Microsoft.AspNetCore.WebUtilities" Version="2.2.0" />
|
<PackageReference Include="Microsoft.AspNetCore.WebUtilities" Version="2.2.0" />
|
||||||
|
|
|
@ -1,47 +1,41 @@
|
||||||
@page "/citizen/dashboard"
|
@page "/citizen/dashboard"
|
||||||
@inject HttpClient http
|
|
||||||
@inject AppState state
|
|
||||||
|
|
||||||
<h3>Welcome, @state.User!.Name!</h3>
|
<h3>Welcome, @State.User!.Name!</h3>
|
||||||
|
|
||||||
@if (retrievingProfile)
|
@if (RetrievingData)
|
||||||
{
|
{
|
||||||
<p>Retrieving your employment profile...</p>
|
<p>Retrieving your employment profile...</p>
|
||||||
}
|
}
|
||||||
else if (profile != null)
|
else
|
||||||
{
|
{
|
||||||
<p>Your employment profile was last updated @profile.LastUpdatedOn</p>
|
if (Profile != null)
|
||||||
|
{
|
||||||
|
<p>
|
||||||
|
Your employment profile was last updated <FullDateTime TheDate="@Profile.LastUpdatedOn" />. Your profile currently
|
||||||
|
lists @SkillCount skill@(SkillCount != 1 ? "s" : "").
|
||||||
|
</p>
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
<p>You do not have an employment profile established; click “Profile”* in the menu to get started!</p>
|
<p>You do not have an employment profile established; click “Profile”* in the menu to get started!</p>
|
||||||
<p><em>* Once it's there...</em></p>
|
}
|
||||||
|
<p>
|
||||||
|
There @(ProfileCount == 1 ? "is" : "are") @(ProfileCount == 0 ? "no" : ProfileCount) employment
|
||||||
|
profile@(ProfileCount != 1 ? "s" : "") from citizens of Gitmo Nation.
|
||||||
|
@if (ProfileCount > 0)
|
||||||
|
{
|
||||||
|
<text>Take a look around and see if you can help them find work!</text>
|
||||||
|
}
|
||||||
|
</p>
|
||||||
}
|
}
|
||||||
|
|
||||||
@if (errorMessage != null)
|
@if (ErrorMessages.Count > 0)
|
||||||
{
|
{
|
||||||
<p>@errorMessage</p>
|
<p><strong>The following error(s) occurred:</strong></p>
|
||||||
}
|
<p>
|
||||||
@code {
|
@foreach (var msg in ErrorMessages)
|
||||||
|
|
||||||
bool retrievingProfile = true;
|
|
||||||
Profile? profile = null;
|
|
||||||
string? errorMessage = null;
|
|
||||||
|
|
||||||
protected override async Task OnInitializedAsync()
|
|
||||||
{
|
{
|
||||||
if (state.User != null)
|
@msg<br>
|
||||||
{
|
|
||||||
var profileResult = await ServerApi.RetrieveProfile(http, state);
|
|
||||||
if (profileResult.IsOk)
|
|
||||||
{
|
|
||||||
profile = profileResult.Ok;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
errorMessage = profileResult.Error;
|
|
||||||
}
|
|
||||||
retrievingProfile = false;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
</p>
|
||||||
}
|
}
|
||||||
|
|
96
src/JobsJobsJobs/Client/Pages/Citizen/Dashboard.razor.cs
Normal file
96
src/JobsJobsJobs/Client/Pages/Citizen/Dashboard.razor.cs
Normal file
|
@ -0,0 +1,96 @@
|
||||||
|
using JobsJobsJobs.Shared;
|
||||||
|
using JobsJobsJobs.Shared.Api;
|
||||||
|
using Microsoft.AspNetCore.Components;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Net.Http;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace JobsJobsJobs.Client.Pages.Citizen
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// The first page a user sees after signing in
|
||||||
|
/// </summary>
|
||||||
|
public partial class Dashboard : ComponentBase
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Whether the data is being retrieved
|
||||||
|
/// </summary>
|
||||||
|
private bool RetrievingData { get; set; } = true;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The user's profile
|
||||||
|
/// </summary>
|
||||||
|
private Profile? Profile { get; set; } = null;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The number of skills in the user's profile
|
||||||
|
/// </summary>
|
||||||
|
private long SkillCount { get; set; } = 0L;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The number of profiles
|
||||||
|
/// </summary>
|
||||||
|
private long ProfileCount { get; set; } = 0L;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Error messages from data access
|
||||||
|
/// </summary>
|
||||||
|
private IList<string> ErrorMessages { get; } = new List<string>();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The HTTP client to use for API access
|
||||||
|
/// </summary>
|
||||||
|
[Inject]
|
||||||
|
public HttpClient Http { get; set; } = default!;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The current application state
|
||||||
|
/// </summary>
|
||||||
|
[Inject]
|
||||||
|
public AppState State { get; set; } = default!;
|
||||||
|
|
||||||
|
|
||||||
|
protected override async Task OnInitializedAsync()
|
||||||
|
{
|
||||||
|
if (State.User != null)
|
||||||
|
{
|
||||||
|
ServerApi.SetJwt(Http, State);
|
||||||
|
var profileTask = ServerApi.RetrieveProfile(Http, State);
|
||||||
|
var profileCountTask = ServerApi.RetrieveOne<Count>(Http, "profile/count");
|
||||||
|
var skillCountTask = ServerApi.RetrieveOne<Count>(Http, "profile/skill-count");
|
||||||
|
|
||||||
|
await Task.WhenAll(profileTask, profileCountTask, skillCountTask);
|
||||||
|
|
||||||
|
if (profileTask.Result.IsOk)
|
||||||
|
{
|
||||||
|
Profile = profileTask.Result.Ok;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
ErrorMessages.Add(profileTask.Result.Error);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (profileCountTask.Result.IsOk)
|
||||||
|
{
|
||||||
|
ProfileCount = profileCountTask.Result.Ok?.Value ?? 0;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
ErrorMessages.Add(profileCountTask.Result.Error);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (skillCountTask.Result.IsOk)
|
||||||
|
{
|
||||||
|
SkillCount = skillCountTask.Result.Ok?.Value ?? 0;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
ErrorMessages.Add(skillCountTask.Result.Error);
|
||||||
|
}
|
||||||
|
|
||||||
|
RetrievingData = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,4 +1,5 @@
|
||||||
using JobsJobsJobs.Shared;
|
using Blazored.Toast.Services;
|
||||||
|
using JobsJobsJobs.Shared;
|
||||||
using JobsJobsJobs.Shared.Api;
|
using JobsJobsJobs.Shared.Api;
|
||||||
using Microsoft.AspNetCore.Components;
|
using Microsoft.AspNetCore.Components;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
@ -51,6 +52,12 @@ namespace JobsJobsJobs.Client.Pages.Citizen
|
||||||
[Inject]
|
[Inject]
|
||||||
private AppState State { get; set; } = default!;
|
private AppState State { get; set; } = default!;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Toast service
|
||||||
|
/// </summary>
|
||||||
|
[Inject]
|
||||||
|
private IToastService Toasts { get; set; } = default!;
|
||||||
|
|
||||||
protected override async Task OnInitializedAsync()
|
protected override async Task OnInitializedAsync()
|
||||||
{
|
{
|
||||||
ServerApi.SetJwt(Http, State);
|
ServerApi.SetJwt(Http, State);
|
||||||
|
@ -128,12 +135,13 @@ namespace JobsJobsJobs.Client.Pages.Citizen
|
||||||
var res = await Http.PostAsJsonAsync("/api/profile/save", ProfileForm);
|
var res = await Http.PostAsJsonAsync("/api/profile/save", ProfileForm);
|
||||||
if (res.IsSuccessStatusCode)
|
if (res.IsSuccessStatusCode)
|
||||||
{
|
{
|
||||||
// TODO: success notification
|
Toasts.ShowSuccess("Profile Saved Successfully");
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
// TODO: probably not the best way to handle this...
|
var error = await res.Content.ReadAsStringAsync();
|
||||||
ErrorMessages.Add(await res.Content.ReadAsStringAsync());
|
if (!string.IsNullOrEmpty(error)) error = $"- {error}";
|
||||||
|
Toasts.ShowError($"{(int)res.StatusCode} {error}");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,13 +1,10 @@
|
||||||
|
using Blazored.Toast;
|
||||||
using Microsoft.AspNetCore.Components.WebAssembly.Hosting;
|
using Microsoft.AspNetCore.Components.WebAssembly.Hosting;
|
||||||
using Microsoft.Extensions.Configuration;
|
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
using Microsoft.Extensions.Logging;
|
|
||||||
using NodaTime;
|
using NodaTime;
|
||||||
using NodaTime.Serialization.SystemTextJson;
|
using NodaTime.Serialization.SystemTextJson;
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Net.Http;
|
using System.Net.Http;
|
||||||
using System.Text;
|
|
||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
@ -23,6 +20,7 @@ namespace JobsJobsJobs.Client
|
||||||
builder.Services.AddScoped(sp => new HttpClient { BaseAddress = new Uri(builder.HostEnvironment.BaseAddress) });
|
builder.Services.AddScoped(sp => new HttpClient { BaseAddress = new Uri(builder.HostEnvironment.BaseAddress) });
|
||||||
builder.Services.AddSingleton(new AppState());
|
builder.Services.AddSingleton(new AppState());
|
||||||
builder.Services.AddSingleton(new JsonSerializerOptions().ConfigureForNodaTime(DateTimeZoneProviders.Tzdb));
|
builder.Services.AddSingleton(new JsonSerializerOptions().ConfigureForNodaTime(DateTimeZoneProviders.Tzdb));
|
||||||
|
builder.Services.AddBlazoredToast();
|
||||||
await builder.Build().RunAsync();
|
await builder.Build().RunAsync();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -131,7 +131,7 @@ namespace JobsJobsJobs.Client
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <typeparam name="T">The type of item expected</typeparam>
|
/// <typeparam name="T">The type of item expected</typeparam>
|
||||||
/// <param name="http">The HTTP client to use for server communication</param>
|
/// <param name="http">The HTTP client to use for server communication</param>
|
||||||
/// <param name="url">The API URL to use call</param>
|
/// <param name="url">The API URL to call</param>
|
||||||
/// <returns>A result with the items, or an error if one occurs</returns>
|
/// <returns>A result with the items, or an error if one occurs</returns>
|
||||||
/// <remarks>The caller is responsible for setting the JWT on the HTTP client</remarks>
|
/// <remarks>The caller is responsible for setting the JWT on the HTTP client</remarks>
|
||||||
public static async Task<Result<IEnumerable<T>>> RetrieveMany<T>(HttpClient http, string url)
|
public static async Task<Result<IEnumerable<T>>> RetrieveMany<T>(HttpClient http, string url)
|
||||||
|
@ -150,5 +150,29 @@ namespace JobsJobsJobs.Client
|
||||||
return Result<IEnumerable<T>>.AsError($"Unable to parse result: {ex.Message}");
|
return Result<IEnumerable<T>>.AsError($"Unable to parse result: {ex.Message}");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Retrieve one item from the given URL
|
||||||
|
/// </summary>
|
||||||
|
/// <typeparam name="T">The type of item expected</typeparam>
|
||||||
|
/// <param name="http">The HTTP client to use for server communication</param>
|
||||||
|
/// <param name="url">The API URL to call</param>
|
||||||
|
/// <returns>A result with the item (possibly null), or an error if one occurs</returns>
|
||||||
|
/// <remarks>The caller is responsible for setting the JWT on the HTTP client</remarks>
|
||||||
|
public static async Task<Result<T?>> RetrieveOne<T>(HttpClient http, string url)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
return Result<T?>.AsOk(await http.GetFromJsonAsync<T>($"/api/{url}", _serializerOptions));
|
||||||
|
}
|
||||||
|
catch (HttpRequestException ex)
|
||||||
|
{
|
||||||
|
return Result<T?>.AsError(ex.Message);
|
||||||
|
}
|
||||||
|
catch (JsonException ex)
|
||||||
|
{
|
||||||
|
return Result<T?>.AsError($"Unable to parse result: {ex.Message}");
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
23
src/JobsJobsJobs/Client/Shared/FullDateTime.razor
Normal file
23
src/JobsJobsJobs/Client/Shared/FullDateTime.razor
Normal file
|
@ -0,0 +1,23 @@
|
||||||
|
@using NodaTime
|
||||||
|
@using NodaTime.Text
|
||||||
|
@using System.Globalization
|
||||||
|
|
||||||
|
@Translated
|
||||||
|
|
||||||
|
@code {
|
||||||
|
/// <summary>
|
||||||
|
/// The pattern with which dates will be formatted
|
||||||
|
/// </summary>
|
||||||
|
private static InstantPattern pattern = InstantPattern.Create("ld<D> ' at ' lt<t>", CultureInfo.CurrentCulture);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The date to be formatted
|
||||||
|
/// </summary>
|
||||||
|
[Parameter]
|
||||||
|
public Instant TheDate { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The formatted date
|
||||||
|
/// </summary>
|
||||||
|
private string Translated => pattern.Format(TheDate);
|
||||||
|
}
|
|
@ -1,5 +1,6 @@
|
||||||
@inherits LayoutComponentBase
|
@inherits LayoutComponentBase
|
||||||
@inject IJSRuntime js
|
@inject IJSRuntime js
|
||||||
|
@using Blazored.Toast.Configuration
|
||||||
|
|
||||||
<div class="page">
|
<div class="page">
|
||||||
<div class="sidebar">
|
<div class="sidebar">
|
||||||
|
@ -20,6 +21,8 @@
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<BlazoredToasts Position="ToastPosition.BottomRight"
|
||||||
|
ShowProgressBar="true" />
|
||||||
|
|
||||||
@code {
|
@code {
|
||||||
async void PlayJobs() => await js.InvokeVoidAsync("Audio.play", "pelosijobs");
|
async void PlayJobs() => await js.InvokeVoidAsync("Audio.play", "pelosijobs");
|
||||||
|
|
|
@ -1,4 +1,6 @@
|
||||||
@using System.Net.Http
|
@using Blazored.Toast
|
||||||
|
@using Blazored.Toast.Services
|
||||||
|
@using System.Net.Http
|
||||||
@using System.Net.Http.Json
|
@using System.Net.Http.Json
|
||||||
@using Microsoft.AspNetCore.Components.Forms
|
@using Microsoft.AspNetCore.Components.Forms
|
||||||
@using Microsoft.AspNetCore.Components.Routing
|
@using Microsoft.AspNetCore.Components.Routing
|
||||||
|
|
|
@ -2,13 +2,14 @@
|
||||||
<html>
|
<html>
|
||||||
|
|
||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8" />
|
<meta charset="utf-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
|
||||||
<title>JobsJobsJobs</title>
|
<title>JobsJobsJobs</title>
|
||||||
<base href="/" />
|
<base href="/">
|
||||||
<link href="css/bootstrap/bootstrap.min.css" rel="stylesheet" />
|
<link href="css/bootstrap/bootstrap.min.css" rel="stylesheet">
|
||||||
<link href="css/app.css" rel="stylesheet" />
|
<link href="css/app.css" rel="stylesheet">
|
||||||
<link href="JobsJobsJobs.Client.styles.css" rel="stylesheet" />
|
<link href="JobsJobsJobs.Client.styles.css" rel="stylesheet">
|
||||||
|
<link href="_content/Blazored.Toast/blazored-toast.min.css" rel="stylesheet">
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body>
|
<body>
|
||||||
|
|
|
@ -103,5 +103,19 @@ namespace JobsJobsJobs.Server.Areas.Api.Controllers
|
||||||
await _db.OpenAsync();
|
await _db.OpenAsync();
|
||||||
return Ok(await _db.FindSkillsByCitizen(CurrentCitizenId));
|
return Ok(await _db.FindSkillsByCitizen(CurrentCitizenId));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[HttpGet("count")]
|
||||||
|
public async Task<IActionResult> GetProfileCount()
|
||||||
|
{
|
||||||
|
await _db.OpenAsync();
|
||||||
|
return Ok(new Count(await _db.CountProfiles()));
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpGet("skill-count")]
|
||||||
|
public async Task<IActionResult> GetSkillCount()
|
||||||
|
{
|
||||||
|
await _db.OpenAsync();
|
||||||
|
return Ok(new Count(await _db.CountSkills(CurrentCitizenId)));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,9 +1,5 @@
|
||||||
using JobsJobsJobs.Shared;
|
using JobsJobsJobs.Shared;
|
||||||
using Npgsql;
|
using Npgsql;
|
||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Linq;
|
|
||||||
using System.Runtime.CompilerServices;
|
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
namespace JobsJobsJobs.Server.Data
|
namespace JobsJobsJobs.Server.Data
|
||||||
|
@ -31,13 +27,11 @@ namespace JobsJobsJobs.Server.Data
|
||||||
{
|
{
|
||||||
using var cmd = conn.CreateCommand();
|
using var cmd = conn.CreateCommand();
|
||||||
cmd.CommandText = "SELECT * FROM citizen WHERE na_user = @na_user";
|
cmd.CommandText = "SELECT * FROM citizen WHERE na_user = @na_user";
|
||||||
cmd.Parameters.Add(new NpgsqlParameter("@na_user", naUser));
|
cmd.AddString("na_user", naUser);
|
||||||
|
|
||||||
using NpgsqlDataReader rdr = await cmd.ExecuteReaderAsync().ConfigureAwait(false);
|
using NpgsqlDataReader rdr = await cmd.ExecuteReaderAsync().ConfigureAwait(false);
|
||||||
if (await rdr.ReadAsync().ConfigureAwait(false))
|
if (await rdr.ReadAsync().ConfigureAwait(false)) return ToCitizen(rdr);
|
||||||
{
|
|
||||||
return ToCitizen(rdr);
|
|
||||||
}
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -54,12 +48,12 @@ namespace JobsJobsJobs.Server.Data
|
||||||
) VALUES(
|
) VALUES(
|
||||||
@na_user, @display_name, @profile_url, @joined_on, @last_seen_on, @id
|
@na_user, @display_name, @profile_url, @joined_on, @last_seen_on, @id
|
||||||
)";
|
)";
|
||||||
cmd.Parameters.Add(new NpgsqlParameter("@id", citizen.Id.ToString()));
|
cmd.AddString("id", citizen.Id);
|
||||||
cmd.Parameters.Add(new NpgsqlParameter("@na_user", citizen.NaUser));
|
cmd.AddString("na_user", citizen.NaUser);
|
||||||
cmd.Parameters.Add(new NpgsqlParameter("@display_name", citizen.DisplayName));
|
cmd.AddString("display_name", citizen.DisplayName);
|
||||||
cmd.Parameters.Add(new NpgsqlParameter("@profile_url", citizen.ProfileUrl));
|
cmd.AddString("profile_url", citizen.ProfileUrl);
|
||||||
cmd.Parameters.Add(new NpgsqlParameter("@joined_on", citizen.JoinedOn));
|
cmd.AddInstant("joined_on", citizen.JoinedOn);
|
||||||
cmd.Parameters.Add(new NpgsqlParameter("@last_seen_on", citizen.LastSeenOn));
|
cmd.AddInstant("last_seen_on", citizen.LastSeenOn);
|
||||||
|
|
||||||
await cmd.ExecuteNonQueryAsync().ConfigureAwait(false);
|
await cmd.ExecuteNonQueryAsync().ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
|
@ -76,9 +70,9 @@ namespace JobsJobsJobs.Server.Data
|
||||||
SET display_name = @display_name,
|
SET display_name = @display_name,
|
||||||
last_seen_on = @last_seen_on
|
last_seen_on = @last_seen_on
|
||||||
WHERE id = @id";
|
WHERE id = @id";
|
||||||
cmd.Parameters.Add(new NpgsqlParameter("@id", citizen.Id.ToString()));
|
cmd.AddString("id", citizen.Id);
|
||||||
cmd.Parameters.Add(new NpgsqlParameter("@display_name", citizen.DisplayName));
|
cmd.AddString("display_name", citizen.DisplayName);
|
||||||
cmd.Parameters.Add(new NpgsqlParameter("@last_seen_on", citizen.LastSeenOn));
|
cmd.AddInstant("last_seen_on", citizen.LastSeenOn);
|
||||||
|
|
||||||
await cmd.ExecuteNonQueryAsync().ConfigureAwait(false);
|
await cmd.ExecuteNonQueryAsync().ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
|
|
|
@ -11,8 +11,10 @@ namespace JobsJobsJobs.Server.Data
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Extensions to the Npgsql data reader
|
/// Extensions to the Npgsql data reader
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public static class NpgsqlDataReaderExtensions
|
public static class NpgsqlExtensions
|
||||||
{
|
{
|
||||||
|
#region Data Reader
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Get a boolean by its name
|
/// Get a boolean by its name
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
@ -48,5 +50,44 @@ namespace JobsJobsJobs.Server.Data
|
||||||
/// <param name="name">The name of the column to check</param>
|
/// <param name="name">The name of the column to check</param>
|
||||||
/// <returns>True if the column is null, false if not</returns>
|
/// <returns>True if the column is null, false if not</returns>
|
||||||
public static bool IsDBNull(this NpgsqlDataReader rdr, string name) => rdr.IsDBNull(rdr.GetOrdinal(name));
|
public static bool IsDBNull(this NpgsqlDataReader rdr, string name) => rdr.IsDBNull(rdr.GetOrdinal(name));
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region Command
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Add a string parameter
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="name">The name of the parameter</param>
|
||||||
|
/// <param name="value">The value of the parameter</param>
|
||||||
|
public static void AddString(this NpgsqlCommand cmd, string name, object value) =>
|
||||||
|
cmd.Parameters.Add(
|
||||||
|
new NpgsqlParameter<string>($"@{name}", value is string @val ? @val : value.ToString()!));
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Add a boolean parameter
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="name">The name of the parameter</param>
|
||||||
|
/// <param name="value">The value of the parameter</param>
|
||||||
|
public static void AddBool(this NpgsqlCommand cmd, string name, bool value) =>
|
||||||
|
cmd.Parameters.Add(new NpgsqlParameter<bool>($"@{name}", value));
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Add an Instant parameter
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="name">The name of the parameter</param>
|
||||||
|
/// <param name="value">The value of the parameter</param>
|
||||||
|
public static void AddInstant(this NpgsqlCommand cmd, string name, Instant value) =>
|
||||||
|
cmd.Parameters.Add(new NpgsqlParameter<Instant>($"@{name}", value));
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Add a parameter that may be null
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="name">The name of the parameter</param>
|
||||||
|
/// <param name="value">The value of the parameter</param>
|
||||||
|
public static void AddMaybeNull(this NpgsqlCommand cmd, string name, object? value) =>
|
||||||
|
cmd.Parameters.Add(new NpgsqlParameter($"@{name}", value == null ? DBNull.Value : value));
|
||||||
|
|
||||||
|
#endregion
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -1,6 +1,5 @@
|
||||||
using JobsJobsJobs.Shared;
|
using JobsJobsJobs.Shared;
|
||||||
using Npgsql;
|
using Npgsql;
|
||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
|
@ -9,7 +8,7 @@ using System.Threading.Tasks;
|
||||||
namespace JobsJobsJobs.Server.Data
|
namespace JobsJobsJobs.Server.Data
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Extensions to the NpgsqlConnection type to support manipulation of profiles
|
/// Extensions to the Connection type to support manipulation of profiles
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public static class ProfileExtensions
|
public static class ProfileExtensions
|
||||||
{
|
{
|
||||||
|
@ -53,7 +52,7 @@ namespace JobsJobsJobs.Server.Data
|
||||||
FROM profile p
|
FROM profile p
|
||||||
INNER JOIN continent c ON p.continent_id = c.id
|
INNER JOIN continent c ON p.continent_id = c.id
|
||||||
WHERE citizen_id = @id";
|
WHERE citizen_id = @id";
|
||||||
cmd.Parameters.Add(new NpgsqlParameter("@id", citizen.Id.ToString()));
|
cmd.AddString("id", citizen.Id);
|
||||||
|
|
||||||
using var rdr = await cmd.ExecuteReaderAsync().ConfigureAwait(false);
|
using var rdr = await cmd.ExecuteReaderAsync().ConfigureAwait(false);
|
||||||
return await rdr.ReadAsync().ConfigureAwait(false) ? ToProfile(rdr) : null;
|
return await rdr.ReadAsync().ConfigureAwait(false) ? ToProfile(rdr) : null;
|
||||||
|
@ -84,17 +83,16 @@ namespace JobsJobsJobs.Server.Data
|
||||||
last_updated_on = @last_updated_on,
|
last_updated_on = @last_updated_on,
|
||||||
experience = @experience
|
experience = @experience
|
||||||
WHERE profile.citizen_id = excluded.citizen_id";
|
WHERE profile.citizen_id = excluded.citizen_id";
|
||||||
cmd.Parameters.Add(new NpgsqlParameter("@citizen_id", profile.Id.ToString()));
|
cmd.AddString("citizen_id", profile.Id);
|
||||||
cmd.Parameters.Add(new NpgsqlParameter("@seeking_employment", profile.SeekingEmployment));
|
cmd.AddBool("seeking_employment", profile.SeekingEmployment);
|
||||||
cmd.Parameters.Add(new NpgsqlParameter("@is_public", profile.IsPublic));
|
cmd.AddBool("is_public", profile.IsPublic);
|
||||||
cmd.Parameters.Add(new NpgsqlParameter("@continent_id", profile.ContinentId.ToString()));
|
cmd.AddString("continent_id", profile.ContinentId);
|
||||||
cmd.Parameters.Add(new NpgsqlParameter("@region", profile.Region));
|
cmd.AddString("region", profile.Region);
|
||||||
cmd.Parameters.Add(new NpgsqlParameter("@remote_work", profile.RemoteWork));
|
cmd.AddBool("remote_work", profile.RemoteWork);
|
||||||
cmd.Parameters.Add(new NpgsqlParameter("@full_time", profile.FullTime));
|
cmd.AddBool("full_time", profile.FullTime);
|
||||||
cmd.Parameters.Add(new NpgsqlParameter("@biography", profile.Biography.Text));
|
cmd.AddString("biography", profile.Biography.Text);
|
||||||
cmd.Parameters.Add(new NpgsqlParameter("@last_updated_on", profile.LastUpdatedOn));
|
cmd.AddInstant("last_updated_on", profile.LastUpdatedOn);
|
||||||
cmd.Parameters.Add(new NpgsqlParameter("@experience",
|
cmd.AddMaybeNull("experience", profile.Experience);
|
||||||
profile.Experience == null ? DBNull.Value : profile.Experience.Text));
|
|
||||||
|
|
||||||
await cmd.ExecuteNonQueryAsync().ConfigureAwait(false);
|
await cmd.ExecuteNonQueryAsync().ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
|
@ -109,7 +107,7 @@ namespace JobsJobsJobs.Server.Data
|
||||||
{
|
{
|
||||||
using var cmd = conn.CreateCommand();
|
using var cmd = conn.CreateCommand();
|
||||||
cmd.CommandText = "SELECT * FROM skill WHERE citizen_id = @citizen_id";
|
cmd.CommandText = "SELECT * FROM skill WHERE citizen_id = @citizen_id";
|
||||||
cmd.Parameters.Add(new NpgsqlParameter("citizen_id", citizenId.ToString()));
|
cmd.AddString("citizen_id", citizenId);
|
||||||
|
|
||||||
var result = new List<Skill>();
|
var result = new List<Skill>();
|
||||||
using var rdr = await cmd.ExecuteReaderAsync().ConfigureAwait(false);
|
using var rdr = await cmd.ExecuteReaderAsync().ConfigureAwait(false);
|
||||||
|
@ -137,10 +135,10 @@ namespace JobsJobsJobs.Server.Data
|
||||||
SET skill = @skill,
|
SET skill = @skill,
|
||||||
notes = @notes
|
notes = @notes
|
||||||
WHERE skill.id = excluded.id";
|
WHERE skill.id = excluded.id";
|
||||||
cmd.Parameters.Add(new NpgsqlParameter("id", skill.Id.ToString()));
|
cmd.AddString("id", skill.Id);
|
||||||
cmd.Parameters.Add(new NpgsqlParameter("citizen_id", skill.CitizenId.ToString()));
|
cmd.AddString("citizen_id", skill.CitizenId);
|
||||||
cmd.Parameters.Add(new NpgsqlParameter("skill", skill.Description));
|
cmd.AddString("skill", skill.Description);
|
||||||
cmd.Parameters.Add(new NpgsqlParameter("notes", skill.Notes == null ? DBNull.Value : skill.Notes));
|
cmd.AddMaybeNull("notes", skill.Notes);
|
||||||
|
|
||||||
await cmd.ExecuteNonQueryAsync().ConfigureAwait(false);
|
await cmd.ExecuteNonQueryAsync().ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
|
@ -161,11 +159,39 @@ namespace JobsJobsJobs.Server.Data
|
||||||
.Append(string.Join(", ", ids.Select(_ => $"@id{count++}").ToArray()))
|
.Append(string.Join(", ", ids.Select(_ => $"@id{count++}").ToArray()))
|
||||||
.Append(')')
|
.Append(')')
|
||||||
.ToString();
|
.ToString();
|
||||||
cmd.Parameters.Add(new NpgsqlParameter("citizen_id", citizenId.ToString()));
|
cmd.AddString("citizen_id", citizenId);
|
||||||
count = 0;
|
count = 0;
|
||||||
foreach (var id in ids) cmd.Parameters.Add(new NpgsqlParameter($"id{count++}", id.ToString()));
|
foreach (var id in ids) cmd.AddString($"id{count++}", id);
|
||||||
|
|
||||||
await cmd.ExecuteNonQueryAsync().ConfigureAwait(false);
|
await cmd.ExecuteNonQueryAsync().ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Get a count of the citizens with profiles
|
||||||
|
/// </summary>
|
||||||
|
/// <returns>The number of citizens with profiles</returns>
|
||||||
|
public static async Task<long> CountProfiles(this NpgsqlConnection conn)
|
||||||
|
{
|
||||||
|
using var cmd = conn.CreateCommand();
|
||||||
|
cmd.CommandText = "SELECT COUNT(citizen_id) FROM profile";
|
||||||
|
|
||||||
|
var result = await cmd.ExecuteScalarAsync().ConfigureAwait(false);
|
||||||
|
return result == null ? 0L : (long)result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Count the skills for the given citizen
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="citizenId">The ID of the citizen whose skills should be counted</param>
|
||||||
|
/// <returns>The count of skills for the given citizen</returns>
|
||||||
|
public static async Task<long> CountSkills(this NpgsqlConnection conn, CitizenId citizenId)
|
||||||
|
{
|
||||||
|
using var cmd = conn.CreateCommand();
|
||||||
|
cmd.CommandText = "SELECT COUNT(id) FROM skill WHERE citizen_id = @citizen_id";
|
||||||
|
cmd.AddString("citizen_id", citizenId);
|
||||||
|
|
||||||
|
var result = await cmd.ExecuteScalarAsync().ConfigureAwait(false);
|
||||||
|
return result == null ? 0L : (long)result;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
7
src/JobsJobsJobs/Shared/Api/Count.cs
Normal file
7
src/JobsJobsJobs/Shared/Api/Count.cs
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
namespace JobsJobsJobs.Shared.Api
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// A transport mechanism to send counts across the wire via JSON
|
||||||
|
/// </summary>
|
||||||
|
public record Count(long Value);
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user