Start work on filtering logic

This commit is contained in:
2025-09-02 22:31:05 -07:00
parent 72fb4f2536
commit 73c1e1ce98
10 changed files with 230 additions and 83 deletions
+13
View File
@@ -15,6 +15,19 @@ Server:
Emails:
Make emails follow theme of website better
JobListingController:
Dont refresh on every filter edit
Line 63 is terrible and need to be fixed in the JobListing DB JobListingController
bools and numbers are getting strigified which breaks the mysql parameters
Validation:
Alot of the validation is only taking place client side.
Need to validate all inputs before processing
Phone number
Email Address
City, CountryCode, PostalCode
When applying to a job, The server doesnt make sure that the company only fields are not modified
Client:
jobs/editor:
Want to add completed job listing preview at end of carosel
+2 -2
View File
@@ -193,8 +193,8 @@ CREATE TABLE IF NOT EXISTS `PostalCodes` (
`StateCode` varchar(20),
`County` varchar(100),
`CountyCode` varchar(20),
`Admin` varchar(100),
`AdminCode` varchar(20),
`Community` varchar(100),
`CommunityCode` varchar(20),
`Latitude` float,
`Longitude` float,
`Accuracy` varchar(2)
+11
View File
@@ -0,0 +1,11 @@
export class JobFilter {
public JobsPerPage: number = 20;
public CurrentPage: number = 1;
public CountryCode: string | null = null;
public PostalCode: string | null = null;
public Distance: number | null = null;
public JobType: string | null = null;
public Remote: boolean | null = null;
public SalaryMin: number | null = null;
public SalaryMax: number | null = null;
}
@@ -1,3 +1,35 @@
<!-- Filter Bar -->
<div>
<div>
<h1>Country</h1>
<input name="CountryCode" [(ngModel)]="currentFilter.CountryCode" (ngModelChange)="reloadFilters()" type="text" />
</div>
<div>
<h1>Postal Code</h1>
<input name="PostalCode" [(ngModel)]="currentFilter.PostalCode" (ngModelChange)="reloadFilters()" type="text" />
</div>
<div>
<h1>Distance</h1>
<input name="Distance" [(ngModel)]="currentFilter.Distance" (ngModelChange)="reloadFilters()" type="number" />
</div>
<div>
<h1>Job Type</h1>
<input name="CountryCode" [(ngModel)]="currentFilter.JobType" (ngModelChange)="reloadFilters()" type="text" />
</div>
<div>
<h1>Remote</h1>
<input name="Remote" [(ngModel)]="currentFilter.Remote" (ngModelChange)="reloadFilters()" type="checkbox" />
</div>
<div>
<h1>Minimum Salary</h1>
<input name="SalaryMin" [(ngModel)]="currentFilter.SalaryMin" (ngModelChange)="reloadFilters()" type="number" />
</div>
<div>
<h1>Maximum Salary</h1>
<input name="SalaryMax" [(ngModel)]="currentFilter.SalaryMax" (ngModelChange)="reloadFilters()" type="number" />
</div>
</div>
<!-- Avaliable Jobs -->
<div class="tile-frame">
@for (cur of JobListingPage; track cur.id){
@@ -21,4 +53,8 @@
</div>
</div>
}
<div>
<h1>Jobs Per Page</h1>
<input name="JobsPerPage" [(ngModel)]="currentFilter.JobsPerPage" (ngModelChange)="reloadFilters()" type="number" />
</div>
</div>
@@ -6,6 +6,7 @@ import { Title } from '@angular/platform-browser';
import { CommonModule } from '@angular/common';
import { JobListing } from 'app/models/JobListing';
import { Authentication } from 'app/services/Authentication';
import { JobFilter } from 'app/models/JobFilter';
@Component({
selector: 'main-jobs',
@@ -15,7 +16,7 @@ import { Authentication } from 'app/services/Authentication';
})
export class JobsComponent {
public MyJobListings: JobListing[] = [];
public currentFilter: JobFilter;
public JobListingPage: JobListing[] = [];
public ErrorMsg: string = "";
@@ -23,10 +24,46 @@ export class JobsComponent {
constructor( private http: HttpClient, private router: Router, private route: ActivatedRoute, private title: Title, public auth: Authentication ) {
this.title.setTitle("Jobs | BoredCareers");
this.currentFilter = new JobFilter();
};
ngOnInit(){
this.http.get<JobListing[]>("api/joblisting?PageQuantity=" + 10 + "&Page=" + 1).subscribe({
this.http.get<JobListing[]>("api/joblisting?PageQuantity=" + this.currentFilter.JobsPerPage + "&Page=" + this.currentFilter.CurrentPage).subscribe({
next: data => {
this.JobListingPage = data;
},
error: err => {
this.ErrorMsg = err.error;
}
});
}
reloadFilters(){
var queryBuilder = "api/joblisting?PageQuantity=" + this.currentFilter.JobsPerPage + "&Page=" + this.currentFilter.CurrentPage;
if (this.currentFilter.PostalCode != null){
queryBuilder += "&PC=" + this.currentFilter.PostalCode;
}
if (this.currentFilter.CountryCode != null){
queryBuilder += "&CC=" + this.currentFilter.CountryCode;
}
if (this.currentFilter.Distance != null){
queryBuilder += "&D=" + this.currentFilter.Distance;
}
if (this.currentFilter.JobType != null){
queryBuilder += "&JT=" + this.currentFilter.JobType;
}
if (this.currentFilter.Remote != null){
queryBuilder += "&R=" + this.currentFilter.Remote;
}
if (this.currentFilter.SalaryMin != null){
queryBuilder += "&SMI=" + this.currentFilter.SalaryMin;
}
if (this.currentFilter.SalaryMax != null){
queryBuilder += "&SMA=" + this.currentFilter.SalaryMax;
}
this.http.get<JobListing[]>(queryBuilder).subscribe({
next: data => {
this.JobListingPage = data;
},
+15
View File
@@ -86,4 +86,19 @@ export class Validation {
}
}
///////// GETTERS /////////
get ValidCountries(): string[] {
return ['AD', 'AE', 'AI', 'AL', 'AR', 'AS', 'AT', 'AU', 'AX', 'AZ', 'BD', 'BE', 'BG',
'BM', 'BR', 'BY', 'CA', 'CC', 'CH', 'CL', 'CN', 'CO', 'CR', 'CX', 'CY', 'CZ',
'DE', 'DK', 'DO', 'DZ', 'EC', 'EE', 'ES', 'FI', 'FK', 'FM', 'FO', 'FR', 'GB',
'GF', 'GG', 'GI', 'GL', 'GP', 'GS', 'GT', 'GU', 'HK', 'HM', 'HN', 'HR', 'HT',
'HU', 'ID', 'IE', 'IM', 'IN', 'IO', 'IS', 'IT', 'JE', 'JP', 'KE', 'KR', 'LI',
'LK', 'LT', 'LU', 'LV', 'MA', 'MC', 'MD', 'MH', 'MK', 'MO', 'MP', 'MQ', 'MT',
'MW', 'MX', 'MY', 'NC', 'NF', 'NL', 'NO', 'NR', 'NU', 'NZ', 'PA', 'PE', 'PF',
'PH', 'PK', 'PL', 'PM', 'PN', 'PR', 'PT', 'PW', 'RE', 'RO', 'RS', 'RU', 'SE',
'SG', 'SI', 'SJ', 'SK', 'SM', 'TC', 'TH', 'TR', 'UA', 'US', 'UY', 'VA', 'VI',
'WF', 'WS', 'YT', 'ZA'];
}
}
+20 -2
View File
@@ -1,6 +1,7 @@
using Microsoft.AspNetCore.Mvc;
using BoredCareers.Services.DatabaseService;
using BoredCareers.Entities;
using Org.BouncyCastle.Tls;
namespace BoredCareers.Controllers {
[ApiController]
@@ -31,8 +32,25 @@ namespace BoredCareers.Controllers {
}
[HttpGet]
public async Task<IActionResult> GetJobListings(int Page = 1, int PageQuantity = 25) {
JobListing[] jobListings = await _databaseService.GetJobListingPage(Page, PageQuantity);
public async Task<IActionResult> GetJobListings(string? CC, string? PC, float? D, string? JT, bool? R, int? SMI, int? SMA, int Page = 1, int PageQuantity = 25 ) {
// Get all relevant postal codes
List<string> PostalCodes = new List<string>();
if (PC != null && CC != null && D != null) {
Location[] nearby = await _databaseService.GetNearbyLocations(PC, CC, D.Value);
foreach (Location cur in nearby) {
PostalCodes.Add(cur.PostalCode);
}
} else if (PC != null) {
PostalCodes.Add(PC);
}
string[]? pc = null;
if (PostalCodes.Count > 0) {
pc = PostalCodes.ToArray();
}
JobListing[] jobListings = await _databaseService.GetJobListingPage(Page, PageQuantity, pc, CC, JT, R, SMI, SMA );
return Ok(jobListings);
}
+2 -8
View File
@@ -1,26 +1,20 @@
using Microsoft.AspNetCore.Mvc;
using BoredCareers.Services.DatabaseService;
using BoredCareers.Entities;
using BoredCareers.Services;
namespace BoredCareers.Controllers {
[ApiController]
[Route("api/location")]
public class LocationController : MistoxControllerBase {
public LocationController(DatabaseService db, EmailService emailContext) : base(db) {}
public LocationController(DatabaseService db) : base(db) {}
[HttpGet]
public async Task<IActionResult> GetCompany(string PostalCode, string CountryCode, float MaxDistanceKm) {
if (isLoggedIn()) {
Location? MyZIP = await _databaseService.GetLocation(PostalCode, CountryCode);
if (MyZIP != null) {
Location[] places = await _databaseService.GetNearbyLocations(MyZIP.Latitude, MyZIP.Longitude, MyZIP.CountryCode, MaxDistanceKm);
Location[] places = await _databaseService.GetNearbyLocations(PostalCode, CountryCode, MaxDistanceKm);
return Ok(places.ToArray());
}
return NotFound("Postal + CountryCode not found");
}
return NotFound("Not logged in");
}
@@ -7,21 +7,73 @@ using System.Data.Common;
namespace BoredCareers.Services.DatabaseService {
public partial class DatabaseService {
public async Task<JobListing[]> GetJobListingPage(int PageNumber, int CountPerPage) {
public async Task<JobListing[]> GetJobListingPage(int PageNumber, int CountPerPage, string[]? PostalCodes = null, string? CountryCode = null, string? JobType = null, bool? Remote = null, int? SalaryMin = null, int? SalaryMax = null ) {
List<JobListing> joblistings = new List<JobListing>();
using (MySqlConnection connection = GetConnection()) {
await connection.OpenAsync();
string command = @"
SELECT *
FROM JobListing
WHERE IsDeleted = FALSE
ORDER BY CreatedTime DESC
LIMIT @PageSize OFFSET @PageNumber;
";
MySqlCommand cmd = new MySqlCommand(command, connection);
string select = "SELECT * FROM JobListing";
string order = " ORDER BY CreatedTime DESC";
string limit = " LIMIT @PageSize OFFSET @PageNumber;";
List<string> Filters = new List<string>();
List<string> Parameters = new List<string>();
List<string> ParameterName = new List<string>();
if (PostalCodes != null) {
for (int i = 0; i < PostalCodes.Length; i++) {
Filters.Add("PostalCode");
Parameters.Add(PostalCodes[i]);
ParameterName.Add("@PostalCode" + i);
}
}
if (CountryCode != null) {
Filters.Add("Country");
Parameters.Add(CountryCode);
ParameterName.Add("@CountryCode");
}
if (JobType != null) {
Filters.Add("JobType");
Parameters.Add(JobType);
ParameterName.Add("@JobType");
}
if (Remote != null) {
Filters.Add("Remote");
Parameters.Add(Remote.Value.ToString());
ParameterName.Add("@Remote");
}
if (SalaryMin != null) {
Filters.Add("SalaryMin");
Parameters.Add(SalaryMin.Value.ToString());
ParameterName.Add("@SalaryMin");
}
if (SalaryMax != null) {
Filters.Add("SalaryMax");
Parameters.Add(SalaryMax.Value.ToString());
ParameterName.Add("@SalaryMax");
}
string filter = " WHERE IsDeleted = False";
for (int i = 0; i < Filters.Count; i++) {
if (Filters[i] == "PostalCode" && i != 0) {
filter += " OR ";
} else {
filter += " AND ";
}
filter += Filters[i] + " = " + ParameterName[i];
}
MySqlCommand cmd = new MySqlCommand(select + filter + order + limit, connection);
cmd.Parameters.AddWithValue("@PageSize", CountPerPage);
cmd.Parameters.AddWithValue("@PageNumber", (PageNumber - 1) * CountPerPage);
for (int i = 0; i < Filters.Count; i++) {
cmd.Parameters.AddWithValue( ParameterName[i], Parameters[i] );
}
using (DbDataReader reader = await cmd.ExecuteReaderAsync()) {
while (await reader.ReadAsync()) {
+17 -46
View File
@@ -7,64 +7,35 @@ using System.Text;
namespace BoredCareers.Services.DatabaseService {
public partial class DatabaseService {
public async Task<Location?> GetLocation(string PostalCode, string CountryCode) {
using (MySqlConnection connection = GetConnection()) {
await connection.OpenAsync();
string command = @"
SELECT PostalCode, CountryCode, Latitude, Longitude, City
FROM PostalCodes
WHERE PostalCode = @PostalCode
AND CountryCode = @CountryCode;
";
MySqlCommand cmd = new MySqlCommand(command, connection);
cmd.Parameters.AddWithValue("@PostalCode", PostalCode);
cmd.Parameters.AddWithValue("@CountryCode", CountryCode);
using (DbDataReader reader = await cmd.ExecuteReaderAsync()) {
while (await reader.ReadAsync()) {
string _city = reader.GetString("City");
string _postalCode = reader.GetString("PostalCode");
string _countryCode = reader.GetString("CountryCode");
float _latitude = reader.GetFloat("Latitude");
float _longitude = reader.GetFloat("Longitude");
return new Location() {
City = _city,
PostalCode = _postalCode,
CountryCode = _countryCode,
Latitude = _latitude,
Longitude = _longitude
};
}
}
}
return null;
}
public async Task<Location[]> GetNearbyLocations(float Latitude, float Longitude, string CountryCode, float MaxDistanceKm) {
public async Task<Location[]> GetNearbyLocations(string PostalCode, string CountryCode, float MaxDistanceKm) {
List<Location> closePostalCodes = new List<Location>();
using (MySqlConnection connection = GetConnection()) {
await connection.OpenAsync();
string command = @"
SELECT PostalCode, CountryCode, Latitude, Longitude, City, (
SELECT
pc2.PostalCode,
pc2.CountryCode,
pc2.Latitude,
pc2.Longitude,
pc2.City,
(
6371 * acos(
cos(radians(@Latitude)) *
cos(radians(Latitude)) *
cos(radians(Longitude) - radians(@Longitude)) +
sin(radians(@Latitude)) *
sin(radians(Latitude))
cos(radians(pc1.Latitude)) *
cos(radians(pc2.Latitude)) *
cos(radians(pc2.Longitude) - radians(pc1.Longitude)) +
sin(radians(pc1.Latitude)) *
sin(radians(pc2.Latitude))
)
) AS distance_km
FROM PostalCodes
WHERE countrycode = @CountryCode
FROM PostalCodes pc1
JOIN PostalCodes pc2 ON pc2.CountryCode = 'us'
WHERE pc1.PostalCode = @PostalCode AND pc1.CountryCode = @CountryCode
HAVING distance_km <= @MaxDistanceKm
ORDER BY distance_km;
";
MySqlCommand cmd = new MySqlCommand(command, connection);
cmd.Parameters.AddWithValue("@Latitude", Latitude);
cmd.Parameters.AddWithValue("@Longitude", Longitude);
cmd.Parameters.AddWithValue("@PostalCode", PostalCode);
cmd.Parameters.AddWithValue("@CountryCode", CountryCode);
cmd.Parameters.AddWithValue("@MaxDistanceKm", MaxDistanceKm);