From e92e4c329fd34f1075f2b0797c0b6b9d614c4a88 Mon Sep 17 00:00:00 2001 From: Derek Holloway Date: Mon, 25 Aug 2025 17:31:49 -0700 Subject: [PATCH 1/8] Add Job Listing Skills to Job Listing --- ToDo.yaml | 3 ++- src/Client/src/app/models/Application.ts | 1 + src/Client/src/app/models/Company.ts | 1 + src/Client/src/app/models/Employee.ts | 1 + src/Client/src/app/models/JobListing.ts | 4 +++- .../jobs/editor/jobeditor.component.html | 21 +++++++++++++++++++ .../pages/jobs/editor/jobeditor.component.ts | 19 ++++++++++++++++- .../jobs/viewer/jobviewer.component.html | 10 +++++++++ .../pages/resumes/editor/editor.component.ts | 5 ----- .../Services/DatabaseService/JobListing.cs | 12 ++++++++++- .../DatabaseService/JobListingSkill.cs | 2 +- 11 files changed, 69 insertions(+), 10 deletions(-) diff --git a/ToDo.yaml b/ToDo.yaml index d0eb9cb..88a1556 100755 --- a/ToDo.yaml +++ b/ToDo.yaml @@ -12,9 +12,10 @@ Server: Need to update notification email Create page to notify cx that their work email has been verified + Verify that each resume section belongs to the resume it was created for: + Client: jobs/editor: - Job Listing Skills exists but isn't implimented in the UI Want to add completed job listing preview at end of carosel Resume: diff --git a/src/Client/src/app/models/Application.ts b/src/Client/src/app/models/Application.ts index 576fefd..73560f1 100644 --- a/src/Client/src/app/models/Application.ts +++ b/src/Client/src/app/models/Application.ts @@ -9,4 +9,5 @@ export class Application { public hasBeenViewed: boolean = false; public rating: number = 0; public notes: string = ""; + public trackUUID: string = crypto.randomUUID(); } \ No newline at end of file diff --git a/src/Client/src/app/models/Company.ts b/src/Client/src/app/models/Company.ts index a2e4a6a..1d34666 100644 --- a/src/Client/src/app/models/Company.ts +++ b/src/Client/src/app/models/Company.ts @@ -13,4 +13,5 @@ export class Company { public stateOrRegion: string = ""; public city: string = ""; public description: string = ""; + public trackUUID: string = crypto.randomUUID(); } \ No newline at end of file diff --git a/src/Client/src/app/models/Employee.ts b/src/Client/src/app/models/Employee.ts index b62a5ff..127bba2 100644 --- a/src/Client/src/app/models/Employee.ts +++ b/src/Client/src/app/models/Employee.ts @@ -6,4 +6,5 @@ export class Employee { public accountName: string = ""; public accountEmail: string = ""; public company: Company = new Company; + public trackUUID: string = crypto.randomUUID(); } \ No newline at end of file diff --git a/src/Client/src/app/models/JobListing.ts b/src/Client/src/app/models/JobListing.ts index ffd34e3..778d3b6 100644 --- a/src/Client/src/app/models/JobListing.ts +++ b/src/Client/src/app/models/JobListing.ts @@ -15,11 +15,13 @@ export class JobListing { public createdTime: Date = new Date(); public modifiedTime: Date = new Date(); public isDeleted: boolean = false; + public trackUUID: string = crypto.randomUUID(); } export class JobListingSkills { public id: number | null = null; public jobListingID: number = 0; public name: string = ""; - public Description: string = ""; + public description: string = ""; + public trackUUID: string = crypto.randomUUID(); } \ No newline at end of file diff --git a/src/Client/src/app/pages/jobs/editor/jobeditor.component.html b/src/Client/src/app/pages/jobs/editor/jobeditor.component.html index ef9f48e..93e47a4 100644 --- a/src/Client/src/app/pages/jobs/editor/jobeditor.component.html +++ b/src/Client/src/app/pages/jobs/editor/jobeditor.component.html @@ -91,6 +91,27 @@ + +
+
+
+ + +
+ @for(skill of Listing.skills; track skill.trackUUID){ +
+ + + +
+ } +
+ + +
+
+
+
diff --git a/src/Client/src/app/pages/jobs/editor/jobeditor.component.ts b/src/Client/src/app/pages/jobs/editor/jobeditor.component.ts index df01516..7a77aa3 100644 --- a/src/Client/src/app/pages/jobs/editor/jobeditor.component.ts +++ b/src/Client/src/app/pages/jobs/editor/jobeditor.component.ts @@ -4,7 +4,7 @@ import { FormsModule } from '@angular/forms'; import { Router, ActivatedRoute, RouterModule } from '@angular/router'; import { Title } from '@angular/platform-browser'; import { CommonModule } from '@angular/common'; -import { JobListing } from 'app/models/JobListing'; +import { JobListing, JobListingSkills } from 'app/models/JobListing'; import { Authentication } from 'app/services/Authentication'; @Component({ @@ -51,6 +51,9 @@ export class JobEditorComponent { if (this.mode === "edit") { this.http.get("api/joblisting/" + JobID).subscribe({ next: data => { + data.skills.forEach(element => { + element.trackUUID = crypto.randomUUID(); + }); this.Listing = data; }, error: err => { @@ -80,6 +83,20 @@ export class JobEditorComponent { }); } + addSkill(){ + this.Listing.skills.push(new JobListingSkills); + } + + remSkill(self: JobListingSkills){ + for(let i=0; i{{ selectedJob.country }}

{{ selectedJob.postalCode }}

+
+

Required Skills

+ @for(skill of selectedJob.skills; track skill.trackUUID){ +
+

{{ skill.name }}

+

{{ skill.description }}

+
+ } +
+

{{ selectedJob.description }}

} diff --git a/src/Client/src/app/pages/resumes/editor/editor.component.ts b/src/Client/src/app/pages/resumes/editor/editor.component.ts index df4bdfc..0663fa5 100644 --- a/src/Client/src/app/pages/resumes/editor/editor.component.ts +++ b/src/Client/src/app/pages/resumes/editor/editor.component.ts @@ -75,11 +75,6 @@ export class ResumesEditorComponent { }); } - // Pagnation // - //////////////////////////////// - - //////////////////////////////// - SubmitForm(resume: Resume){ resume.accountID = this.auth.loggedInUser.id; this.http.post("api/resume", resume).subscribe({ diff --git a/src/Server/Services/DatabaseService/JobListing.cs b/src/Server/Services/DatabaseService/JobListing.cs index f23b28a..8dad416 100644 --- a/src/Server/Services/DatabaseService/JobListing.cs +++ b/src/Server/Services/DatabaseService/JobListing.cs @@ -228,6 +228,8 @@ namespace BoredCareers.Services.DatabaseService { Description = @Description, ModifiedTime = @ModifiedTime, IsDeleted = @IsDeleted; + + SELECT LAST_INSERT_ID(); "; MySqlCommand cmd = new MySqlCommand(command, connection); @@ -247,9 +249,17 @@ namespace BoredCareers.Services.DatabaseService { cmd.Parameters.AddWithValue("@ModifiedTime", DateTime.UtcNow); cmd.Parameters.AddWithValue("@IsDeleted", jobListing.IsDeleted); - await cmd.ExecuteNonQueryAsync(); + object? result = await cmd.ExecuteScalarAsync(); + int jobListingID = 0; + if (jobListing.ID != null && jobListing.ID != 0) { + jobListingID = Convert.ToInt32(jobListing.ID); + } else { + cmd.CommandText = ""; + jobListingID = Convert.ToInt32(result); + } foreach (JobListingSkill cur in jobListing.Skills) { + cur.JobListingID = jobListingID; await SetJobListingSkills(cur); } } diff --git a/src/Server/Services/DatabaseService/JobListingSkill.cs b/src/Server/Services/DatabaseService/JobListingSkill.cs index 953bc0b..3e20855 100644 --- a/src/Server/Services/DatabaseService/JobListingSkill.cs +++ b/src/Server/Services/DatabaseService/JobListingSkill.cs @@ -43,7 +43,7 @@ namespace BoredCareers.Services.DatabaseService { using( MySqlConnection connection = GetConnection() ) { await connection.OpenAsync(); string command = @" - INSERT INTO JobListing + INSERT INTO JobListingSkill (ID,JobListingID,Name,Description) VALUES (@ID,@JobListingID,@Name,@Description) From 69984d735034ba4119d668f0eebb086dad979670 Mon Sep 17 00:00:00 2001 From: Derek Holloway Date: Mon, 25 Aug 2025 21:12:34 -0700 Subject: [PATCH 2/8] Add email and phone validators --- src/Client/src/app/app.ts | 1 - .../company/editor/editor.component.html | 4 +- .../pages/company/editor/editor.component.ts | 37 ++++++++++++++++++- .../resumes/editor/editor.component.html | 4 +- .../pages/resumes/editor/editor.component.ts | 17 ++++++++- src/Client/src/app/services/Validation.ts | 35 ++++++++++++++++++ 6 files changed, 91 insertions(+), 7 deletions(-) create mode 100644 src/Client/src/app/services/Validation.ts diff --git a/src/Client/src/app/app.ts b/src/Client/src/app/app.ts index 6d4ec07..5048ec6 100644 --- a/src/Client/src/app/app.ts +++ b/src/Client/src/app/app.ts @@ -20,7 +20,6 @@ export class App { this.devMode = isDevMode(); this.route.queryParams.subscribe(params => { this.loginToken = params['LoginToken']; - console.log("LoginToken : " + this.loginToken); if (this.loginToken){ this.http.post( "api/account/loginticket", JSON.stringify(this.loginToken), { headers: {'Content-Type': 'application/json'} } ).subscribe({ next: async() => { diff --git a/src/Client/src/app/pages/company/editor/editor.component.html b/src/Client/src/app/pages/company/editor/editor.component.html index 53a4e56..8b85075 100644 --- a/src/Client/src/app/pages/company/editor/editor.component.html +++ b/src/Client/src/app/pages/company/editor/editor.component.html @@ -60,11 +60,11 @@
- +
- +
diff --git a/src/Client/src/app/pages/company/editor/editor.component.ts b/src/Client/src/app/pages/company/editor/editor.component.ts index e0b5b60..41eab5e 100644 --- a/src/Client/src/app/pages/company/editor/editor.component.ts +++ b/src/Client/src/app/pages/company/editor/editor.component.ts @@ -6,6 +6,7 @@ import { Title } from '@angular/platform-browser'; import { CommonModule } from '@angular/common'; import { Authentication } from 'app/services/Authentication'; import { Company } from 'app/models/Company'; +import { Validation } from 'app/services/Validation'; @Component({ selector: 'main-company-editor', @@ -23,7 +24,7 @@ export class CompanyEditorComponent { public ErrorMsg: string = ""; MaxFileMB: number = 3; - constructor( private http: HttpClient, private router: Router, private route: ActivatedRoute, private title: Title, public auth: Authentication ) { + constructor( private http: HttpClient, private router: Router, private route: ActivatedRoute, private title: Title, public auth: Authentication, public validator: Validation ) { this.title.setTitle("Company - Editor | BoredCareers"); if (!auth.isLoggedIn){ router.navigate(["/"]); @@ -54,6 +55,18 @@ export class CompanyEditorComponent { this.updateUI(0); } + validatePhone(input: string){ + let result = this.validator.validatePhoneNumber(input); + this.newListing.phone = result[1]; + } + + validateEmail(input: string){ + let result = this.validator.validateEmail(input); + if (result[0]){ + this.newListing.email = result[1]; + } + } + @HostListener('window:keydown', ['$event']) handleGlobalKeyDown(event: KeyboardEvent){ if ( event.key === 'Tab' ){ @@ -122,51 +135,73 @@ export class CompanyEditorComponent { PostNewCompany(company: Company){ if (this.isNullOrEmpty(company.name)){ + this.ErrorMsg = "Comany name is blank"; this.focusFrame(0, 0); return; } if (this.isNullOrEmpty(company.websiteURL)){ + this.ErrorMsg = "Website URL is blank"; this.focusFrame(1, 0); return; } if (this.isNullOrEmpty(company.logo)){ + this.ErrorMsg = "Logo is blank"; this.focusFrame(2, 0); return; } if (this.isNullOrEmpty(company.email)){ + this.ErrorMsg = "Email is blank"; + this.focusFrame(3, 0); + return; + } + + if (this.validator.validateEmail(company.email)[0]){ + this.ErrorMsg = "Email is invalid"; this.focusFrame(3, 0); return; } if (this.isNullOrEmpty(company.phone)){ + this.ErrorMsg = "Phone number is blank"; + this.focusFrame(3, 1); + return; + } + + if (this.validator.validatePhoneNumber(company.phone)[0]){ + this.ErrorMsg = "Phone number is invalid"; this.focusFrame(3, 1); return; } if (this.isNullOrEmpty(company.city)){ + this.ErrorMsg = "City is blank"; this.focusFrame(4, 0); return; } if (this.isNullOrEmpty(company.country)){ + this.ErrorMsg = "Country is blank"; this.focusFrame(4, 1); return; } if (this.isNullOrEmpty(company.stateOrRegion)){ + this.ErrorMsg = "State or Region is blank"; this.focusFrame(4, 2); return; } if (this.isNullOrEmpty(company.postalCode)){ + this.ErrorMsg = "Postal Code is blank"; this.focusFrame(4, 3); return; } if (this.isNullOrEmpty(company.description)){ + this.ErrorMsg = "Description is blank"; this.focusFrame(5, 0); return; } diff --git a/src/Client/src/app/pages/resumes/editor/editor.component.html b/src/Client/src/app/pages/resumes/editor/editor.component.html index 26833a1..c3ce681 100644 --- a/src/Client/src/app/pages/resumes/editor/editor.component.html +++ b/src/Client/src/app/pages/resumes/editor/editor.component.html @@ -23,8 +23,8 @@
- - + +
diff --git a/src/Client/src/app/pages/resumes/editor/editor.component.ts b/src/Client/src/app/pages/resumes/editor/editor.component.ts index 0663fa5..f4c8ee9 100644 --- a/src/Client/src/app/pages/resumes/editor/editor.component.ts +++ b/src/Client/src/app/pages/resumes/editor/editor.component.ts @@ -6,6 +6,7 @@ import { Title } from '@angular/platform-browser'; import { CommonModule } from '@angular/common'; import { Resume, ResumeCertification, ResumeEducation, ResumeExperience, ResumeExperienceBullet, ResumeLanguage, ResumeMilitary, ResumeMilitaryBullet, ResumeProject, ResumeSkill } from 'app/models/Resume'; import { Authentication } from 'app/services/Authentication'; +import { Validation } from 'app/services/Validation'; @Component({ selector: 'main-resume-editor', @@ -20,7 +21,7 @@ export class ResumesEditorComponent { public ErrorMsg: string = ""; - constructor( private http: HttpClient, private router: Router, private route: ActivatedRoute, private title: Title, public auth: Authentication ) { + constructor( private http: HttpClient, private router: Router, private route: ActivatedRoute, private title: Title, public auth: Authentication, public validator: Validation ) { this.title.setTitle("Resume - Editor | BoredCareers"); if (!this.auth.isLoggedIn){ this.router.navigate(["/"]); @@ -87,6 +88,20 @@ export class ResumesEditorComponent { }); } + validatePhone(input: string){ + let result = this.validator.validatePhoneNumber(input); + if (result[0]){ + this.resume.phoneNumber = result[1]; + } + } + + validateEmail(input: string){ + let result = this.validator.validateEmail(input); + if (result[0]){ + this.resume.email = result[1]; + } + } + PrintResume(){ const divToPrint = document.getElementsByClassName("paper")[0]; diff --git a/src/Client/src/app/services/Validation.ts b/src/Client/src/app/services/Validation.ts new file mode 100644 index 0000000..56314ea --- /dev/null +++ b/src/Client/src/app/services/Validation.ts @@ -0,0 +1,35 @@ +import { Injectable } from "@angular/core"; + +@Injectable({ providedIn: 'root' }) +export class Validation { + + constructor(){ } + + validatePhoneNumber(input: string): [boolean, string] { + var sanitized = input.replace(/\D/g, ''); + if (sanitized.length < 10){ + let formatted = sanitized.replace(/(\d{3})(?=\d{3})/g, '$1-').replace(/(\d{4})(?=\d{1,4}$)/, '$1-'); + return [false, formatted]; + } else if (sanitized.length === 10) { + let result = `(${sanitized.slice(0, 3)})${sanitized.slice(3, 6)}-${sanitized.slice(6, 10)}`; + return [true, result]; + } else if (sanitized.length > 10 && sanitized.length < 14) { + let countryCode = sanitized.slice(0, sanitized.length - 10); + let areaCode = sanitized.slice(sanitized.length - 10, sanitized.length - 7); + let firstPart = sanitized.slice(sanitized.length - 7, sanitized.length - 4); + let secondPart = sanitized.slice(sanitized.length - 4); + let result = `+${countryCode} (${areaCode})${firstPart}-${secondPart}`; + return [true, result]; + }else{ + return [false, input]; + } + } + + emailRegex: RegExp = /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,4}$/; + validateEmail(input: string): [boolean, string] { + const corrected = input.trim().toLowerCase(); + const success = this.emailRegex.test(corrected); + return [success, corrected]; + } + +} \ No newline at end of file From f791735a1390bb6b0159605cf662c2d14c822fea Mon Sep 17 00:00:00 2001 From: Derek Holloway Date: Tue, 26 Aug 2025 16:49:54 -0700 Subject: [PATCH 3/8] Add apply to job button --- .../jobs/viewer/jobviewer.component.html | 7 +++ .../pages/jobs/viewer/jobviewer.component.ts | 43 ++++++++++++++++++- 2 files changed, 49 insertions(+), 1 deletion(-) 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 2e2b560..f5610f9 100644 --- a/src/Client/src/app/pages/jobs/viewer/jobviewer.component.html +++ b/src/Client/src/app/pages/jobs/viewer/jobviewer.component.html @@ -58,6 +58,13 @@

{{ selectedJob.description }}

+
+ @for(resume of myResumes; track resume.trackUUID){ +
+ +
+ } +
} \ No newline at end of file diff --git a/src/Client/src/app/pages/jobs/viewer/jobviewer.component.ts b/src/Client/src/app/pages/jobs/viewer/jobviewer.component.ts index e22ae7f..452db25 100644 --- a/src/Client/src/app/pages/jobs/viewer/jobviewer.component.ts +++ b/src/Client/src/app/pages/jobs/viewer/jobviewer.component.ts @@ -7,6 +7,8 @@ import { CommonModule } from '@angular/common'; import { Authentication } from 'app/services/Authentication'; import { JobListing } from 'app/models/JobListing'; import { Company } from 'app/models/Company'; +import { Resume } from 'app/models/Resume'; +import { Application } from 'app/models/Application'; @Component({ selector: 'main-jobs-viewer', @@ -17,9 +19,12 @@ import { Company } from 'app/models/Company'; export class JobViewerComponent { public selectedJob: JobListing | null = null; + public myResumes: Resume[] = []; public jobsCompany: Company | null = null; public ErrorMsg: string = ""; + JobListingID: number = -1; + constructor( private http: HttpClient, private router: Router, private route: ActivatedRoute, private title: Title, public auth: Authentication ) { this.title.setTitle("Jobs - Viewer | BoredCareers"); }; @@ -27,7 +32,8 @@ export class JobViewerComponent { ngOnInit(){ this.route.queryParams.subscribe(params => { const JobID = params['JobID']; - if (JobID){ + if (JobID) { + this.JobListingID = JobID; this.http.get( "api/joblisting/" + JobID ).subscribe({ next: data => { this.selectedJob = data; @@ -51,6 +57,41 @@ export class JobViewerComponent { } }); + + this.http.get("api/resume").subscribe({ + next: data => { + this.myResumes = data; + }, + error: err => { + console.log("Error fetching resumes: " + err.error); + } + }); + } + + applyWithResume(resume: Resume) { + var application = new Application; + + if (this.auth.loggedInUser.id != null){ + application.accountID = this.auth.loggedInUser.id; + } + + if (resume.id != null){ + application.resumeID = resume.id; + } + + application.jobListingID = this.JobListingID; + application.hasBeenViewed = false; + application.responseEmail = resume.email; + + this.http.post("api/application", application).subscribe({ + next: data => { + this.router.navigate(["/"]); + }, + error: err => { + this.ErrorMsg = err.error; + } + }); + } } \ No newline at end of file From 722f2d3651425fd8f7ab9bd9e62e824252857433 Mon Sep 17 00:00:00 2001 From: Derek Holloway Date: Tue, 26 Aug 2025 17:28:11 -0700 Subject: [PATCH 4/8] Update app controller to support multiple applicaitons --- src/Server/Controllers/ApplicationController.cs | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/src/Server/Controllers/ApplicationController.cs b/src/Server/Controllers/ApplicationController.cs index 5d74675..1017332 100644 --- a/src/Server/Controllers/ApplicationController.cs +++ b/src/Server/Controllers/ApplicationController.cs @@ -10,13 +10,19 @@ namespace BoredCareers.Controllers { public ApplicationController(DatabaseService db) : base(db) {} [HttpGet] - public async Task GetApplication(int ApplicationID) { + public async Task GetApplication(int? ApplicationID, int? JobListingID) { if (isLoggedIn()) { - Application? application = await _databaseService.GetApplication(ApplicationID); - if (application != null) { - return Ok(application); + if (ApplicationID != null) { + Application? application = await _databaseService.GetApplication(Convert.ToInt32(ApplicationID)); + if (application != null) { + return Ok(application); + } + return NotFound("Application doesn't exist"); + } else if (JobListingID != null) { + Application[] applications = await _databaseService.GetApplicationsFromJobListing(Convert.ToInt32(JobListingID)); + return Ok(applications); } - return NotFound("Application doesn't exist"); + return NotFound("No query selector supplied"); } return NotFound("Not logged in"); } From 44db1c651bbc0e552eda33d2479b76cbbb4143a0 Mon Sep 17 00:00:00 2001 From: Derek Holloway Date: Tue, 26 Aug 2025 17:28:27 -0700 Subject: [PATCH 5/8] Start application viewer --- .../pages/resumes/viewer/viewer.component.css | 105 ++++++ .../resumes/viewer/viewer.component.html | 301 ++++++++++++++++++ .../pages/resumes/viewer/viewer.component.ts | 92 ++++++ 3 files changed, 498 insertions(+) create mode 100644 src/Client/src/app/pages/resumes/viewer/viewer.component.css create mode 100644 src/Client/src/app/pages/resumes/viewer/viewer.component.html create mode 100644 src/Client/src/app/pages/resumes/viewer/viewer.component.ts diff --git a/src/Client/src/app/pages/resumes/viewer/viewer.component.css b/src/Client/src/app/pages/resumes/viewer/viewer.component.css new file mode 100644 index 0000000..be09ca3 --- /dev/null +++ b/src/Client/src/app/pages/resumes/viewer/viewer.component.css @@ -0,0 +1,105 @@ +input, textarea { + border: 1px solid #444; + background-color: transparent; +} + +textarea { + resize: vertical; +} + +h1 { + margin: 0; + font-size: 15px; +} + +.paper { + width: 800px; + aspect-ratio: 17 / 22; + background: white; + box-shadow: 0 0 10px rgba(0, 0, 0, 0.2); + margin: auto; + margin-top: 50px; + border: 1px solid #ddd; +} + +.spacer { + margin: 0 10px; +} + +.spacer-title { + display: flex; + margin: 10px 0; +} + +.columns { + columns: 2; + column-gap: 10px; +} + +.resume-header { + width: 70vw; + background: white; + box-shadow: 0 0 10px rgba(0, 0, 0, 0.2); + margin: auto; + margin-top: 50px; + border: 1px solid #ddd; +} + +.resume-section { + margin-bottom: 10px; +} + +.resume-sub-section { + border: 1px solid #666666; + break-inside: avoid; + padding: 10px; +} + +.title-text { + margin: 0; + height: 20px; +} + +.header-left { + width: 50%; + display: flex; + flex-direction: column; + justify-content: flex-start; + align-items: flex-start; +} + +.header-right { + width: 50%; + display: flex; + flex-direction: column; + justify-content: flex-start; + align-items: flex-end; +} + +.header-location input{ + width: 80px; +} + +.Add { + position: relative; + float: right; + width: 20px; + height: 20px; + background-color: #0F0; + border: none; + bottom: 20px; +} + +.Del { + position: relative; + float: right; + width: 20px; + height: 20px; + background-color: #F00; + border: none; +} + +.flex-two-row { + display: flex; + flex: 1; +} \ No newline at end of file diff --git a/src/Client/src/app/pages/resumes/viewer/viewer.component.html b/src/Client/src/app/pages/resumes/viewer/viewer.component.html new file mode 100644 index 0000000..cd65297 --- /dev/null +++ b/src/Client/src/app/pages/resumes/viewer/viewer.component.html @@ -0,0 +1,301 @@ + +
+
+ +
+

Public:

+

Is Veteran:

+
+ +
+
+ +
+
+ + +
+ + + + +
+
+
+ + +
+
+ +
+ +
+

Experience

+ @for(experience of resume.experiences; track experience.trackUUID ){ +
+ + + + + + + + + @if(!experience.stillEmployed){ + + } + @for(bullet of experience.experienceBullets; track bullet.trackUUID){ +
+ +
+ } +
+ } +
+ + + @if(resume.military !== null){ +
+

Military

+ + +

Still Serving:

+ + @if (!resume.military.stillServing){ + + } + @for(military of resume.military.militaryBullets; track military.trackUUID ){ +
+ + +
+ } +
+ } + + +
+

Education

+ @for(education of resume.educations; track education.trackUUID){ +
+ + + + + + + + + + @if (!education.stillStudying){ + + } +
+ } +
+ +
+
+ +
+

Public:

+

Is Veteran:

+
+ +
+
+ +
+
+ + +
+ + + + +
+
+
+ + +
+
+ +
+ +
+

Experience

+ @for(experience of resume.experiences; track experience.trackUUID ){ +
+ + + + + + + + + @if(!experience.stillEmployed){ + + } + @for(bullet of experience.experienceBullets; track bullet.trackUUID){ +
+ +
+ } +
+ } +
+ + + @if(resume.military !== null){ +
+

Military

+ + +

Still Serving:

+ + @if (!resume.military.stillServing){ + + } + @for(military of resume.military.militaryBullets; track military.trackUUID ){ +
+ + +
+ } +
+ } + + +
+

Education

+ @for(education of resume.educations; track education.trackUUID){ +
+ + + + + + + + + + @if (!education.stillStudying){ + + } +
+ } +
+ + +
+

Skills

+ @for(skill of resume.skills; track skill.trackUUID){ +
+
+ +
+ +
+ } +
+ + +
+

Languages

+ @for(language of resume.languages; track language.trackUUID){ +
+ + +
+ } +
+ + + +
+

Certifications

+ @for(cert of resume.certifications; track cert.trackUUID){ +
+
+ + +
+ +
+ } +
+ + +
+

Projects

+ @for(proj of resume.projects; track proj.trackUUID){ +
+
+ + +
+ +
+ } +
+
+
+
+ +
+

Skills

+ @for(skill of resume.skills; track skill.trackUUID){ +
+
+ +
+ +
+ } +
+ + +
+

Languages

+ @for(language of resume.languages; track language.trackUUID){ +
+ + +
+ } +
+ + + +
+

Certifications

+ @for(cert of resume.certifications; track cert.trackUUID){ +
+
+ + +
+ +
+ } +
+ + +
+

Projects

+ @for(proj of resume.projects; track proj.trackUUID){ +
+
+ + +
+ +
+ } +
+
+
+
\ No newline at end of file diff --git a/src/Client/src/app/pages/resumes/viewer/viewer.component.ts b/src/Client/src/app/pages/resumes/viewer/viewer.component.ts new file mode 100644 index 0000000..b40feef --- /dev/null +++ b/src/Client/src/app/pages/resumes/viewer/viewer.component.ts @@ -0,0 +1,92 @@ +import { Component } from '@angular/core'; +import { HttpClient } from '@angular/common/http'; +import { FormsModule } from '@angular/forms'; +import { Router, ActivatedRoute, RouterModule } from '@angular/router'; +import { Title } from '@angular/platform-browser'; +import { CommonModule } from '@angular/common'; +import { Resume } from 'app/models/Resume'; +import { Authentication } from 'app/services/Authentication'; +import { Validation } from 'app/services/Validation'; + +@Component({ + selector: 'main-resume-viewer', + templateUrl: './viewer.component.html', + styleUrls: [ './viewer.component.css' ], + imports: [FormsModule, CommonModule, RouterModule] +}) +export class ResumesViewerComponent { + + public resume: Resume = new Resume; + + public ErrorMsg: string = ""; + + constructor( private http: HttpClient, private router: Router, private route: ActivatedRoute, private title: Title, public auth: Authentication, public validator: Validation ) { + this.title.setTitle("Resume - Viewer | BoredCareers"); + if (!this.auth.isLoggedIn){ + this.router.navigate(["/"]); + } + }; + + ngOnInit(){ + this.route.queryParams.subscribe(params => { + const ResumeID = params['ResumeID'] ? +params['ResumeID'] : null; + if (ResumeID !== null){ + this.http.get("api/resume?ResumeID=" + ResumeID).subscribe({ + next: data => { + + data.trackUUID = crypto.randomUUID(); + data.certifications.forEach(element => { + element.trackUUID = crypto.randomUUID(); + }); + data.educations.forEach(element => { + element.trackUUID = crypto.randomUUID(); + }); + data.experiences.forEach(element => { + element.trackUUID = crypto.randomUUID(); + element.experienceBullets.forEach(subelement => { + subelement.trackUUID = crypto.randomUUID(); + }); + }); + data.languages.forEach(element => { + element.trackUUID = crypto.randomUUID(); + }); + if (data.military){ + data.military.trackUUID = crypto.randomUUID(); + data.military.militaryBullets.forEach(element => { + element.trackUUID = crypto.randomUUID(); + }); + } + data.projects.forEach(element => { + element.trackUUID = crypto.randomUUID(); + }); + data.skills.forEach(element => { + element.trackUUID = crypto.randomUUID(); + }); + + this.resume = data; + }, + error: err => { + this.ErrorMsg = err.error; + } + }); + } + }); + } + + PrintResume(){ + const divToPrint = document.getElementsByClassName("paper")[0]; + + const printContents = divToPrint.innerHTML; + const originalContents = document.body.innerHTML; // Store original body content + + // Temporarily replace the body content with the div's content + document.body.innerHTML = printContents; + + // Trigger the print dialog + window.print(); + + // Restore the original body content + document.body.innerHTML = originalContents; + } + +} \ No newline at end of file From fda27fdb4b32489355a10c4021adb13620d6528d Mon Sep 17 00:00:00 2001 From: Derek Holloway Date: Tue, 26 Aug 2025 17:28:42 -0700 Subject: [PATCH 6/8] add link to application viewer --- src/Client/src/app/pages/company/company.component.html | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Client/src/app/pages/company/company.component.html b/src/Client/src/app/pages/company/company.component.html index e299ab6..a22e49b 100644 --- a/src/Client/src/app/pages/company/company.component.html +++ b/src/Client/src/app/pages/company/company.component.html @@ -46,6 +46,7 @@

{{ listing.title }}

+ From d89b10b3455912000d679996c750527b8426dff7 Mon Sep 17 00:00:00 2001 From: Derek Holloway Date: Tue, 26 Aug 2025 17:28:55 -0700 Subject: [PATCH 7/8] Add resume viewer --- .../applicationviewer/appviewer.component.css | 134 ++++++++++++++++++ .../appviewer.component.html | 15 ++ .../applicationviewer/appviewer.component.ts | 48 +++++++ 3 files changed, 197 insertions(+) create mode 100644 src/Client/src/app/pages/applicationviewer/appviewer.component.css create mode 100644 src/Client/src/app/pages/applicationviewer/appviewer.component.html create mode 100644 src/Client/src/app/pages/applicationviewer/appviewer.component.ts diff --git a/src/Client/src/app/pages/applicationviewer/appviewer.component.css b/src/Client/src/app/pages/applicationviewer/appviewer.component.css new file mode 100644 index 0000000..8eecdb8 --- /dev/null +++ b/src/Client/src/app/pages/applicationviewer/appviewer.component.css @@ -0,0 +1,134 @@ +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); + text-decoration: none; + font: inherit; +} + + button:hover { + background-color: #00000044; + color: var(--Mistox-Light); + } + +.top-bar { + width: 100%; + height: 60px; +} + +.content-frame { + background-color: #3c3c3c; + width: calc(100% - 40px); + height: calc(100vh - 400px); + border-radius: 20px; + margin: 10px; + overflow: scroll; + padding: 10px; + color: var(--Mistox-White); +} + +.center-item { + display: flex; + width: 100%; + justify-content: center; +} + +.content-edit { + position: absolute; + right: 20px; +} + +.center-item img { + width: 300px; +} + +.content-name { + width: 300px; + text-align: center; + font-size: 30px; +} + +.content-name h1 { + margin: 0; +} + +.content-link { + display: flex; + width: 300px; + justify-content: center; +} + +.content-link a { + text-decoration: none; + color: var(--Mistox-White); + margin-top: auto; +} + +.content-desc { + border: solid 1px red; + border-radius: 5px; + padding: 20px; + margin: 0 100px; + margin-bottom: 50px; +} + +.content-desc h1 { + margin: 0; + font-size: 20px; + color: #ddd; +} + +.content-button { + display: flex; + justify-content: center; +} + +.content-button span { + align-content: center; +} + +.split-frame { + display: flex; + width: 100%; +} + +.half-frame { + width: 50%; + border-right: solid 1px var(--Mistox-Black); + border-left: solid 1px var(--Mistox-Black); +} + +.half-frame h2 { + text-align: center; +} + +.job-tile { + display: flex; + background-color: var(--Mistox-Black); + justify-content: end; + align-items: center; + border-radius: 10px; + margin: 0 5px; + margin-bottom: 10px; +} + +.center-text { + display: flex; + flex: 1; + justify-content: center; +} + +.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/applicationviewer/appviewer.component.html b/src/Client/src/app/pages/applicationviewer/appviewer.component.html new file mode 100644 index 0000000..487c6ff --- /dev/null +++ b/src/Client/src/app/pages/applicationviewer/appviewer.component.html @@ -0,0 +1,15 @@ +
+ @for (application of List; track application.trackUUID){ +
+

{{ application.responseEmail }}

+

{{ application.notes }}

+

{{ application.hasBeenViewed }}

+

{{ application.rating }}

+

{{ application.responseStatus }}

+ +

Date Applied:

{{ application.dateApplied }}

+ + +
+ } +
\ No newline at end of file diff --git a/src/Client/src/app/pages/applicationviewer/appviewer.component.ts b/src/Client/src/app/pages/applicationviewer/appviewer.component.ts new file mode 100644 index 0000000..6de59ae --- /dev/null +++ b/src/Client/src/app/pages/applicationviewer/appviewer.component.ts @@ -0,0 +1,48 @@ +import { Component } from '@angular/core'; +import { HttpClient } from '@angular/common/http'; +import { FormsModule } from '@angular/forms'; +import { Router, ActivatedRoute, RouterModule } from '@angular/router'; +import { Title } from '@angular/platform-browser'; +import { CommonModule } from '@angular/common'; +import { Authentication } from 'app/services/Authentication'; +import { Application } from 'app/models/Application'; + +@Component({ + selector: 'App-Viewer', + templateUrl: './appviewer.component.html', + styleUrls: [ './appviewer.component.css' ], + imports: [ FormsModule, CommonModule, RouterModule ] +}) +export class AppViewerComponent { + public ErrorMsg: string = ""; + + public List: Application[] = []; + + constructor( private http: HttpClient, private router: Router, private route: ActivatedRoute, private title: Title, public auth: Authentication ) { + this.title.setTitle("Applications | BoredCareers"); + if (!auth.isLoggedIn){ + router.navigate(["/"]); + } + }; + + ngOnInit(){ + this.route.queryParams.subscribe(params => { + const JobListingID = params['JobID'] ? +params['JobID'] : null; + if (JobListingID !== null){ + this.http.get("api/application?JobListingID=" + JobListingID).subscribe({ + next: data => { + this.List = data; + }, + error: err => { + this.ErrorMsg = err.error; + } + }); + } + }); + } + + viewResume(app: Application) { + window.open('/resumes/viewer?ResumeID=' + app.resumeID, '_blank'); + } + +} \ No newline at end of file From 40cd3bfeb404caedfbb24964f3724a7897f36129 Mon Sep 17 00:00:00 2001 From: Derek Holloway Date: Tue, 26 Aug 2025 17:29:04 -0700 Subject: [PATCH 8/8] Add routes and update todo --- ToDo.yaml | 9 +++++++++ src/Client/src/app/app.routes.ts | 6 ++++++ 2 files changed, 15 insertions(+) diff --git a/ToDo.yaml b/ToDo.yaml index 88a1556..23c0bc9 100755 --- a/ToDo.yaml +++ b/ToDo.yaml @@ -28,6 +28,11 @@ 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 + resume/viewer: + CSS is broken + Company needs to be able to add notes to resume + Company needs to be able to add a rating to the resume + company/editor: Keyboard Tab key does nothing Format phone number for database @@ -37,6 +42,10 @@ Client: resume/editor: There is no data validation + application/viewer: + need to make look better for companies to sort + need to show notes and rating properly + Company: Need to impliment Add employee Need to impliment Remove employee diff --git a/src/Client/src/app/app.routes.ts b/src/Client/src/app/app.routes.ts index b221d22..bd5275b 100644 --- a/src/Client/src/app/app.routes.ts +++ b/src/Client/src/app/app.routes.ts @@ -8,15 +8,21 @@ import { CompanyEditorComponent } from './pages/company/editor/editor.component' import { JobViewerComponent } from './pages/jobs/viewer/jobviewer.component'; import { CompanyComponent } from './pages/company/company.component'; import { ResumesEditorComponent } from './pages/resumes/editor/editor.component'; +import { AppViewerComponent } from './pages/applicationviewer/appviewer.component'; +import { ResumesViewerComponent } from './pages/resumes/viewer/viewer.component'; export const routes: Routes = [ // Home { path: "", component: HomeComponent }, + // Application + { path: "application/viewer", component: AppViewerComponent }, + // Resumes { path: "resumes", component: ResumesComponent }, { path: "resumes/editor", component: ResumesEditorComponent }, + { path: "resumes/viewer", component: ResumesViewerComponent }, // Jobs { path: "jobs", component: JobsComponent },