diff --git a/ToDo.yaml b/ToDo.yaml index 883180f..93ccd47 100755 --- a/ToDo.yaml +++ b/ToDo.yaml @@ -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 @@ -29,6 +42,8 @@ Client: Allow users to look up jobs and apply [ Boost visibility | Completely manual ] Mark ghost listings to allow users to be informed and put companies on blast Dont allow users to apply to the same job more than once + Need to add 'job field' to job for better filtering + Need to add 'clear filter button' resume/viewer: CSS is broken diff --git a/database/mistox.sql b/database/mistox.sql index 59032b3..068b5b0 100755 --- a/database/mistox.sql +++ b/database/mistox.sql @@ -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) @@ -206,10 +206,7 @@ FIELDS TERMINATED BY '\t' ENCLOSED BY '"' LINES TERMINATED BY '\n'; -CREATE INDEX idx_country_code ON PostalCodes(CountryCode); -CREATE INDEX idx_postal_code ON PostalCodes(PostalCode); -CREATE INDEX idx_latitude ON PostalCodes(Latitude); -CREATE INDEX idx_longitude ON PostalCodes(Longitude); +CREATE INDEX idx_postal_country ON PostalCodes (City, PostalCode, CountryCode, Latitude, Longitude); -- Application Section diff --git a/src/Client/src/app/models/JobFilter.ts b/src/Client/src/app/models/JobFilter.ts new file mode 100644 index 0000000..7ccd085 --- /dev/null +++ b/src/Client/src/app/models/JobFilter.ts @@ -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; +} \ No newline at end of file diff --git a/src/Client/src/app/pages/company/company.component.css b/src/Client/src/app/pages/company/company.component.css index 8eecdb8..d7264b8 100644 --- a/src/Client/src/app/pages/company/company.component.css +++ b/src/Client/src/app/pages/company/company.component.css @@ -1,36 +1,56 @@ -button { +.primary-button { height: 45px; border-radius: 5px; margin: 10px; text-align: center; padding: 15px 20px; transition: 0.5s; - background-color: #00000000; - border: 1px solid var(--Mistox-Black); - color: var(--Mistox-Black); + background-color: var(--mistox-button-primary); + border: 1px solid var(--mistox-button-primary); + color: var(--mistox-button-text); text-decoration: none; font: inherit; } - button:hover { - background-color: #00000044; - color: var(--Mistox-Light); + .primary-button:hover { + background-color: var(--mistox-button-primary-click); + } + +.secondary-button { + height: 45px; + border-radius: 5px; + margin: 10px; + text-align: center; + padding: 15px 20px; + transition: 0.5s; + background-color: var(--mistox-button-secondary); + border: 1px solid var(--mistox-button-secondary); + color: var(--mistox-button-text); + text-decoration: none; + font: inherit; +} + + .secondary-button:hover { + background-color: var(--mistox-button-secondary-click); } .top-bar { - width: 100%; - height: 60px; + display: flex; + break-inside: avoid; + padding: 20px; + border-radius: 20px; + margin: 20px; + background-color: var(--mistox-bg-medium); + border: 1px solid var(--mistox-border); + box-shadow: var(--mistox-shadow); + color: var(--mistox-text); } .content-frame { - background-color: #3c3c3c; - width: calc(100% - 40px); - height: calc(100vh - 400px); + max-width: 1800px; border-radius: 20px; - margin: 10px; - overflow: scroll; padding: 10px; - color: var(--Mistox-White); + margin: 0 auto; } .center-item { @@ -40,7 +60,7 @@ button { } .content-edit { - position: absolute; + position: relative; right: 20px; } @@ -66,22 +86,23 @@ button { .content-link a { text-decoration: none; - color: var(--Mistox-White); margin-top: auto; } .content-desc { - border: solid 1px red; + border: solid 1px var(--mistox-border); border-radius: 5px; padding: 20px; margin: 0 100px; + margin-bottom: 0px; margin-bottom: 50px; + background-color: var(--mistox-bg-medium); + color: var(--mistox-text); } .content-desc h1 { margin: 0; font-size: 20px; - color: #ddd; } .content-button { @@ -100,8 +121,6 @@ button { .half-frame { width: 50%; - border-right: solid 1px var(--Mistox-Black); - border-left: solid 1px var(--Mistox-Black); } .half-frame h2 { @@ -110,7 +129,8 @@ button { .job-tile { display: flex; - background-color: var(--Mistox-Black); + background-color: var(--mistox-bg-medium); + border: solid 1px var(--mistox-border-dark); justify-content: end; align-items: center; border-radius: 10px; @@ -126,9 +146,4 @@ button { .job-tile h1 { margin: 0; -} - -.job-tile button { - color: white; - border-color: white; } \ No newline at end of file diff --git a/src/Client/src/app/pages/company/company.component.html b/src/Client/src/app/pages/company/company.component.html index 456916f..3387cc5 100644 --- a/src/Client/src/app/pages/company/company.component.html +++ b/src/Client/src/app/pages/company/company.component.html @@ -1,13 +1,13 @@
@for(company of Employers; track company.accountID){ - + } - +
@if(Comp != null){
- +
@@ -31,11 +31,11 @@
@if (Comp.emailVerified){
- +
} @else {
- VERIFY EMAIL> + VERIFY EMAIL> You must verify your company email before you can post job listings.
} @@ -46,21 +46,21 @@

{{ listing.title }}

- - - - + + + +
}
@if (Comp.emailVerified){
- +
} @else {
- VERIFY EMAIL> + VERIFY EMAIL> You must verify your company email before you can post job listings.
} @@ -72,9 +72,9 @@

{{ listing.accountName }}

@if (listing.accountID != auth.loggedInUser.id){ - + } @else { - + }
} diff --git a/src/Client/src/app/pages/jobs/jobs.component.css b/src/Client/src/app/pages/jobs/jobs.component.css index 4bbf54f..edfca32 100644 --- a/src/Client/src/app/pages/jobs/jobs.component.css +++ b/src/Client/src/app/pages/jobs/jobs.component.css @@ -14,6 +14,34 @@ button { background-color: var(--mistox-button-primary-click); } +.top-bar { + display: flex; + break-inside: avoid; + padding: 20px; + border-radius: 20px; + margin: 20px; + background-color: var(--mistox-bg-medium); + border: 1px solid var(--mistox-border); + box-shadow: var(--mistox-shadow); + color: var(--mistox-text); +} + +.top-bar-sub { + display: flex; + height: 20px; +} + +.top-bar-sub :nth-child(1) { + margin: 0; + font-size: 20px; +} + +.top-bar-sub :nth-child(2) { + margin-right: 50px; + margin-left: 5px; + height: 15px; +} + .full-width { display: block; width: 100%; diff --git a/src/Client/src/app/pages/jobs/jobs.component.html b/src/Client/src/app/pages/jobs/jobs.component.html index cdfd17c..d57768b 100644 --- a/src/Client/src/app/pages/jobs/jobs.component.html +++ b/src/Client/src/app/pages/jobs/jobs.component.html @@ -1,3 +1,37 @@ + +
+
+

Country

+ +
+
+

Postal Code

+ +
+ @if (currentFilter.CountryCode != null && currentFilter.CountryCode !== "" && currentFilter.PostalCode != null && currentFilter.PostalCode !== ""){ +
+

Distance

+ +
+ } +
+

Job Type

+ +
+
+

Remote

+ +
+
+

Minimum Salary

+ +
+
+

Maximum Salary

+ +
+
+
@for (cur of JobListingPage; track cur.id){ @@ -21,4 +55,8 @@
} +
+

Jobs Per Page

+ +
\ No newline at end of file diff --git a/src/Client/src/app/pages/jobs/jobs.component.ts b/src/Client/src/app/pages/jobs/jobs.component.ts index 827bd86..b677b2a 100644 --- a/src/Client/src/app/pages/jobs/jobs.component.ts +++ b/src/Client/src/app/pages/jobs/jobs.component.ts @@ -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,50 @@ 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("api/joblisting?PageQuantity=" + 10 + "&Page=" + 1).subscribe({ + this.http.get("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 === "" || this.currentFilter.CountryCode === "" ){ + this.currentFilter.Distance = null; + } + + 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(queryBuilder).subscribe({ next: data => { this.JobListingPage = data; }, diff --git a/src/Client/src/app/pages/jobs/viewer/jobviewer.component.css b/src/Client/src/app/pages/jobs/viewer/jobviewer.component.css index 1ce3c33..3644a9c 100644 --- a/src/Client/src/app/pages/jobs/viewer/jobviewer.component.css +++ b/src/Client/src/app/pages/jobs/viewer/jobviewer.component.css @@ -1,5 +1,5 @@ .company-details { - background-color: #5c3030; + background-color: var(--mistox-bg-light); } .company-details::after { @@ -82,7 +82,7 @@ } .job-details { - background-color: #3c3c3c; + padding-bottom: 40px; } .job-timestamp { diff --git a/src/Client/src/app/pages/jobs/viewer/jobviewer.component.html b/src/Client/src/app/pages/jobs/viewer/jobviewer.component.html index b0f8daf..cba83bb 100644 --- a/src/Client/src/app/pages/jobs/viewer/jobviewer.component.html +++ b/src/Client/src/app/pages/jobs/viewer/jobviewer.component.html @@ -1,26 +1,5 @@
- @if (jobsCompany != null){ -
-
- - - -
-
- -

{{ jobsCompany.name }}

- -
-
-

{{ jobsCompany.city }}, {{ jobsCompany.stateOrRegion }} {{ jobsCompany.postalCode }}

-
-
- @for(line of jobsCompany.description.split('\n'); track line.length){ -

{{ line }}

- } -
-
- } + @if (selectedJob != null) {
@if (selectedJob.isDeleted){ @@ -32,6 +11,12 @@

{{ selectedJob.title }}

+
+

Job Description:

+
+

{{ selectedJob.description }}

+
+

Job Details:

@@ -78,12 +63,6 @@ Opened: {{ selectedJob.createdTime }} | Last Updated: {{ selectedJob.modifiedTime }}
-
-

Job Description:

-
-

{{ selectedJob.description }}

-
-
@@ -93,4 +72,28 @@
} + + @if (jobsCompany != null){ +
+
+ + + +
+
+ +

{{ jobsCompany.name }}

+ +
+
+

{{ jobsCompany.city }}, {{ jobsCompany.stateOrRegion }} {{ jobsCompany.postalCode }}

+
+
+ @for(line of jobsCompany.description.split('\n'); track line.length){ +

{{ line }}

+ } +
+
+ } + \ No newline at end of file diff --git a/src/Client/src/app/services/Validation.ts b/src/Client/src/app/services/Validation.ts index 0887ad9..ddec9c9 100644 --- a/src/Client/src/app/services/Validation.ts +++ b/src/Client/src/app/services/Validation.ts @@ -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']; } -} } \ No newline at end of file diff --git a/src/Client/src/styles.css b/src/Client/src/styles.css index 81dd7be..d9bb939 100644 --- a/src/Client/src/styles.css +++ b/src/Client/src/styles.css @@ -12,6 +12,8 @@ --mistox-border: oklch(0.6 0.13 264); --mistox-border-dark: oklch(0.7 0.13 264); + --mistox-button-text: oklch(1 0.00011 271.152); + --mistox-button-primary: oklch(0.4 0.13 264); --mistox-button-primary-click: oklch(0.3 0.13 264); @@ -27,28 +29,34 @@ font-family: Arial, Helvetica, sans-serif; } -dark-mode { - --mistox-bg-dark: oklch(0.1 0.065 264); - --mistox-bg-medium: oklch(0.15 0.065 264); - --mistox-bg-light: oklch(0.2 0.065 264); +@media (prefers-color-scheme: dark) { + :root { + --mistox-bg-dark: oklch(0.1 0.065 264); + --mistox-bg-medium: oklch(0.15 0.065 264); + --mistox-bg-light: oklch(0.2 0.065 264); - --mistox-text: oklch(0.96 0.1 264); - --mistox-text-sub: oklch(0.76 0.1 264); + --mistox-text: oklch(0.96 0.1 264); + --mistox-text-sub: oklch(0.76 0.1 264); - --mistox-border-light: oklch(0.5 0.13 264); - --mistox-border: oklch(0.4 0.13 264); - --mistox-border-dark: oklch(0.3 0.13 264); + --mistox-border-light: oklch(0.5 0.13 264); + --mistox-border: oklch(0.4 0.13 264); + --mistox-border-dark: oklch(0.3 0.13 264); - --mistox-button-primary: oklch(0.76 0.13 264); - --mistox-button-secondary: oklch(0.76 0.13 84); + --mistox-button-text: oklch(0 0.00011 271.152); - --mistox-alert-danger: oklch(0.7 0.13 30); - --mistox-alert-warning: oklch(0.7 0.13 100); - --mistox-alert-success: oklch(0.7 0.13 160); - --mistox-alert-info: oklch(0.7 0.13 260); + --mistox-button-primary: oklch(0.76 0.13 264); + --mistox-button-secondary: oklch(0.76 0.13 84); - --mistox-shadow: 0px 2px 2px oklch(0 0 0 / 0.2), 0px 4px 4px oklch(0 0 0 / 0.1); - font-family: Arial, Helvetica, sans-serif; + --mistox-alert-danger: oklch(0.7 0.13 30); + --mistox-alert-warning: oklch(0.7 0.13 100); + --mistox-alert-success: oklch(0.7 0.13 160); + --mistox-alert-info: oklch(0.7 0.13 260); + + --mistox-shadow: 0px 2px 2px oklch(0 0 0 / 0.2), 0px 4px 4px oklch(0 0 0 / 0.1); + + color: #fff; + font-family: Arial, Helvetica, sans-serif; + } } html { @@ -56,6 +64,5 @@ html { } body { - background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='28' height='49' viewBox='0 0 28 49'%3E%3Cg fill-rule='evenodd'%3E%3Cg id='hexagons' fill='%23999999' fill-opacity='0.2' fill-rule='nonzero'%3E%3Cpath d='M13.99 9.25l13 7.5v15l-13 7.5L1 31.75v-15l12.99-7.5zM3 17.9v12.7l10.99 6.34 11-6.35V17.9l-11-6.34L3 17.9zM0 15l12.98-7.5V0h-2v6.35L0 12.69v2.3zm0 18.5L12.98 41v8h-2v-6.85L0 35.81v-2.3zM15 0v7.5L27.99 15H28v-2.31h-.01L17 6.35V0h-2zm0 49v-8l12.99-7.5H28v2.31h-.01L17 42.15V49h-2z'/%3E%3C/g%3E%3C/g%3E%3C/svg%3E"); background-color: var(--mistox-bg-dark); } \ No newline at end of file diff --git a/src/Server/Controllers/JobListingController.cs b/src/Server/Controllers/JobListingController.cs index 18e90c3..d4f4ddf 100644 --- a/src/Server/Controllers/JobListingController.cs +++ b/src/Server/Controllers/JobListingController.cs @@ -31,8 +31,25 @@ namespace BoredCareers.Controllers { } [HttpGet] - public async Task GetJobListings(int Page = 1, int PageQuantity = 25) { - JobListing[] jobListings = await _databaseService.GetJobListingPage(Page, PageQuantity); + public async Task 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 PostalCodes = new List(); + 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); } diff --git a/src/Server/Controllers/LocationController.cs b/src/Server/Controllers/LocationController.cs new file mode 100644 index 0000000..90ab731 --- /dev/null +++ b/src/Server/Controllers/LocationController.cs @@ -0,0 +1,22 @@ +using Microsoft.AspNetCore.Mvc; +using BoredCareers.Services.DatabaseService; +using BoredCareers.Entities; + +namespace BoredCareers.Controllers { + [ApiController] + [Route("api/location")] + public class LocationController : MistoxControllerBase { + + public LocationController(DatabaseService db) : base(db) {} + + [HttpGet] + public async Task GetCompany(string PostalCode, string CountryCode, float MaxDistanceKm) { + if (isLoggedIn()) { + Location[] places = await _databaseService.GetNearbyLocations(PostalCode, CountryCode, MaxDistanceKm); + return Ok(places.ToArray()); + } + return NotFound("Not logged in"); + } + + } +} diff --git a/src/Server/Entities/Location.cs b/src/Server/Entities/Location.cs new file mode 100644 index 0000000..0fa6711 --- /dev/null +++ b/src/Server/Entities/Location.cs @@ -0,0 +1,10 @@ +namespace BoredCareers.Entities { + public class Location { + public string City { get; set; } = ""; + public string PostalCode { get; set; } = ""; + public string CountryCode { get; set; } = ""; + public float Latitude { get; set; } + public float Longitude { get; set; } + public float DistanceKM { get; set; } + } +} \ No newline at end of file diff --git a/src/Server/Services/DatabaseService/JobListing.cs b/src/Server/Services/DatabaseService/JobListing.cs index 8dad416..5acab3d 100644 --- a/src/Server/Services/DatabaseService/JobListing.cs +++ b/src/Server/Services/DatabaseService/JobListing.cs @@ -7,21 +7,73 @@ using System.Data.Common; namespace BoredCareers.Services.DatabaseService { public partial class DatabaseService { - public async Task GetJobListingPage(int PageNumber, int CountPerPage) { + public async Task GetJobListingPage(int PageNumber, int CountPerPage, string[]? PostalCodes = null, string? CountryCode = null, string? JobType = null, bool? Remote = null, int? SalaryMin = null, int? SalaryMax = null ) { List joblistings = new List(); 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 Filters = new List(); + List Parameters = new List(); + List ParameterName = new List(); + + 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); + ParameterName.Add(" = @Remote"); + } + + if (SalaryMin != null) { + Filters.Add("SalaryMin"); + Parameters.Add(SalaryMin.Value); + ParameterName.Add(" >= @SalaryMin"); + } + + if (SalaryMax != null) { + Filters.Add("SalaryMax"); + Parameters.Add(SalaryMax.Value); + 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].Split(' ').Last(), Parameters[i] ); + } using (DbDataReader reader = await cmd.ExecuteReaderAsync()) { while (await reader.ReadAsync()) { diff --git a/src/Server/Services/DatabaseService/Location.cs b/src/Server/Services/DatabaseService/Location.cs new file mode 100644 index 0000000..16efe83 --- /dev/null +++ b/src/Server/Services/DatabaseService/Location.cs @@ -0,0 +1,66 @@ +using BoredCareers.Entities; +using MySql.Data.MySqlClient; +using System.Data; +using System.Data.Common; +using System.Text; + +namespace BoredCareers.Services.DatabaseService { + public partial class DatabaseService { + + public async Task GetNearbyLocations(string PostalCode, string CountryCode, float MaxDistanceKm) { + List closePostalCodes = new List(); + using (MySqlConnection connection = GetConnection()) { + await connection.OpenAsync(); + string command = @" + 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("@PostalCode", PostalCode); + cmd.Parameters.AddWithValue("@CountryCode", CountryCode); + cmd.Parameters.AddWithValue("@MaxDistanceKm", MaxDistanceKm); + + 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"); + float _distanceKm = reader.GetFloat("distance_km"); + + closePostalCodes.Add(new Location() { + City = _city, + PostalCode = _postalCode, + CountryCode = _countryCode, + Latitude = _latitude, + Longitude = _longitude, + DistanceKM = _distanceKm + }); + } + } + } + return closePostalCodes.ToArray(); + } + + } +}