working #40
@@ -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
@@ -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)
|
||||
|
||||
@@ -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;
|
||||
},
|
||||
|
||||
@@ -74,16 +74,31 @@ export class Validation {
|
||||
///////// HELPER FUNCTIONS /////////
|
||||
|
||||
isPrivateIPv6(ip: string): boolean {
|
||||
try {
|
||||
const normalized = ip.replace(/^\[|\]$/g, '').toLowerCase();
|
||||
if (normalized === '::1') return true;
|
||||
if (normalized.startsWith('fc') || normalized.startsWith('fd')) return true;
|
||||
const first4 = normalized.slice(0, 4);
|
||||
if (first4 >= 'fe80' && first4 <= 'febf') return true;
|
||||
return false;
|
||||
} catch {
|
||||
return true;
|
||||
try {
|
||||
const normalized = ip.replace(/^\[|\]$/g, '').toLowerCase();
|
||||
if (normalized === '::1') return true;
|
||||
if (normalized.startsWith('fc') || normalized.startsWith('fd')) return true;
|
||||
const first4 = normalized.slice(0, 4);
|
||||
if (first4 >= 'fe80' && first4 <= 'febf') return true;
|
||||
return false;
|
||||
} catch {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
///////// 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'];
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,25 +1,19 @@
|
||||
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);
|
||||
return Ok(places.ToArray());
|
||||
}
|
||||
return NotFound("Postal + CountryCode not found");
|
||||
Location[] places = await _databaseService.GetNearbyLocations(PostalCode, CountryCode, MaxDistanceKm);
|
||||
return Ok(places.ToArray());
|
||||
}
|
||||
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()) {
|
||||
|
||||
@@ -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, (
|
||||
6371 * acos(
|
||||
cos(radians(@Latitude)) *
|
||||
cos(radians(Latitude)) *
|
||||
cos(radians(Longitude) - radians(@Longitude)) +
|
||||
sin(radians(@Latitude)) *
|
||||
sin(radians(Latitude))
|
||||
)
|
||||
) AS distance_km
|
||||
FROM PostalCodes
|
||||
WHERE countrycode = @CountryCode
|
||||
SELECT
|
||||
pc2.PostalCode,
|
||||
pc2.CountryCode,
|
||||
pc2.Latitude,
|
||||
pc2.Longitude,
|
||||
pc2.City,
|
||||
(
|
||||
6371 * acos(
|
||||
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 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);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user