Repository: ls1intum/tum-apply
Pull request #115: `Development`: Add job creation form for professors
Author: login: ani-ib
Labels: name: server
description: Pull requests that update Java code. (Added Automatically!)
name: ready for review
name: client
description: Pull requests that update TypeScript code. (Added Automatically!)
Created: 2025-05-09T16:57:46Z | Merged: 2025-05-11T14:05:50Z

<!-- Thanks for contributing to TumApply! Before you submit your pull request, please make sure to check all tasks by putting an x in the [ ] (don't: [x ], [ x], do: [x]). Remove not applicable tasks and do not leave them unchecked -->
<!-- If your pull request is not ready for review yet, create a draft pull request! -->

### Checklist

#### General

<!-- Remove tasks that are not applicable for your PR. Please only put the PR into ready for review, if all relevant tasks are checked! -->
<!-- You only need to choose one of the first two check items: Generally, test on the test servers. -->
<!-- If it's only a small change, testing it locally is acceptable, and you may remove the first checkmark. If you are unsure, please test on the test servers. -->

- [x] I tested **all** changes and their related features with **all** corresponding user types.
- [x] Language: I followed the [guidelines for inclusive, diversity-sensitive, and appreciative language](https://confluence.aet.cit.tum.de/spaces/AP/pages/257786006/Language+Guidelines+Client).
- [x] I chose a title conforming to the [naming conventions for pull requests](https://confluence.aet.cit.tum.de/spaces/AP/pages/256311714/PR+Guidelines).

#### Client

- [x] **Important**: I implemented the changes with a very good performance, prevented too many (unnecessary) REST calls and made sure the UI is responsive, even with large data (e.g. using paging).
- [x] I **strictly** followed the principle of **data economy** for all client-server REST calls.
- [x] I **strictly** followed the [client coding and design guidelines](https://confluence.aet.cit.tum.de/spaces/AP/pages/256311716/Client+Guidelines).
- [x] I added multiple integration tests (Jest) related to the features (with a high test coverage), while following the [test guidelines](https://confluence.aet.cit.tum.de/spaces/AP/pages/256311925/Client+Test+Guidelines).
- [x] I documented the TypeScript code using JSDoc style.
- [x] I added multiple screenshots/screencasts of my UI changes.

### Motivation and Context

<!-- Why is this change required? What problem does it solve? -->
<!-- If it fixes an open issue, please link to the issue here. -->

This PR adds the job creation form for professors. It allows them to create new doctorate job postings by filling out basic information, position details, and additional information in a structured, step-by-step form.

The form connects to the server using the OpenAPI-generated client and prepares the data in the correct format. This is an important step toward allowing professors to manage their own job postings directly in the system.

This is a basic version of the form — some UI elements like dropdowns and the datepicker are improved in a follow-up PR.

### Description

<!-- Describe your changes in detail -->

This PR implements the Job Creation Form for professors, enabling them to create and publish doctorate job postings through a structured, 3-step form:
	1.	Basic Information
Includes job title, research area, field of studies, supervising professor (read-only), location, start date, workload, contract duration, and funding type.
	2.	Position Details
Captures the job description, main tasks, and eligibility requirements expected from candidates.
	3.	Additional Information
Contains a placeholder for future additions (e.g. custom questions) and includes a data privacy notice referencing TUM’s data protection policy.

The data is submitted via the OpenAPI-generated JobResourceService in two ways:
	•	saveDraft() – saves the job as a draft
	•	publishJob() – submits and publishes the job


### Steps for Testing

<!-- Please describe in detail how reviewers can test your changes. Make sure to take all related features and views into account! Below is an example that you can refine. -->

Steps for testing:

1. Run the client-side using `npm start` and navigate to http://localhost:4200/job-creation
2. Verify that the job creation form functions as required

Limitations:
	•	The “Cancel” button does not yet navigate away from the form.
	•	The current implementation uses native dropdowns and date inputs — these are replaced with customized PrimeNG components for improved styling and functionality in another PR.
	•	Due to a misconfiguration in the OpenAPI-generated code, the job submission currently targets http://localhost:4200/... instead of the correct server API on port 8080.

### Review Progress

<!-- Each PR should be reviewed by at least one other developers. The code, the functionality (= manual test). -->
<!-- The reviewer or author check the following boxes depending on what was reviewed or tested. All boxes should be checked before merge. -->
<!-- You can add additional checkboxes if it makes sense to only review parts of the code or functionality. -->
<!-- When changes are pushed, uncheck the affected boxes. (Not all changes require full re-reviews.) -->

#### Code Review

- [ ] Code Review 1

#### Manual Tests

- [ ] Test 1


Diff:
diff --git a/openapi/openapi.yaml b/openapi/openapi.yaml
index 92daf55a5d..22241a8614 100644
--- a/openapi/openapi.yaml
+++ b/openapi/openapi.yaml
@@ -685,13 +685,19 @@
       "JobFormDTO": {
         "type": "object",
         "properties": {
+          "title": {
+            "type": "string"
+          },
+          "researchArea": {
+            "type": "string"
+          },
+          "fieldOfStudies": {
+            "type": "string"
+          },
           "supervisingProfessor": {
             "type": "string",
             "format": "uuid"
           },
-          "title": {
-            "type": "string"
-          },
           "location": {
             "type": "string",
             "enum": [
@@ -704,6 +710,18 @@
               "SINGAPORE"
             ]
           },
+          "startDate": {
+            "type": "string",
+            "format": "date"
+          },
+          "workload": {
+            "type": "integer",
+            "format": "int32"
+          },
+          "contractDuration": {
+            "type": "integer",
+            "format": "int32"
+          },
           "fundingType": {
             "type": "string",
             "enum": [
@@ -716,6 +734,15 @@
               "RESEARCH_GRANT"
             ]
           },
+          "description": {
+            "type": "string"
+          },
+          "tasks": {
+            "type": "string"
+          },
+          "requirements": {
+            "type": "string"
+          },
           "state": {
             "type": "string",
             "enum": [
@@ -725,7 +752,13 @@
               "APPLICANT_FOUND"
             ]
           }
-        }
+        },
+        "required": [
+          "fundingType",
+          "location",
+          "state",
+          "supervisingProfessor"
+        ]
       },
       "CreateApplicationDTO": {
         "type": "object",
diff --git a/src/main/java/de/tum/cit/aet/job/domain/Job.java b/src/main/java/de/tum/cit/aet/job/domain/Job.java
index 82a6f17421..fbd5e010d8 100644
--- a/src/main/java/de/tum/cit/aet/job/domain/Job.java
+++ b/src/main/java/de/tum/cit/aet/job/domain/Job.java
@@ -8,7 +8,7 @@
 import de.tum.cit.aet.usermanagement.domain.ResearchGroup;
 import de.tum.cit.aet.usermanagement.domain.User;
 import jakarta.persistence.*;
-import java.time.Instant;
+import java.time.LocalDate;
 import java.util.List;
 import java.util.Set;
 import java.util.UUID;
@@ -75,7 +75,7 @@ public class Job extends AbstractAuditingEntity {
     private JobState state;
 
     @Column(name = "start_date")
-    private Instant startDate;
+    private LocalDate startDate;
 
     @OneToMany(mappedBy = "job", cascade = CascadeType.ALL, orphanRemoval = true)
     @OrderBy("sequence ASC")
diff --git a/src/main/java/de/tum/cit/aet/job/dto/JobFormDTO.java b/src/main/java/de/tum/cit/aet/job/dto/JobFormDTO.java
index 921b54f3ef..57327c816d 100644
--- a/src/main/java/de/tum/cit/aet/job/dto/JobFormDTO.java
+++ b/src/main/java/de/tum/cit/aet/job/dto/JobFormDTO.java
@@ -4,7 +4,23 @@
 import de.tum.cit.aet.job.constants.Campus;
 import de.tum.cit.aet.job.constants.FundingType;
 import de.tum.cit.aet.job.constants.JobState;
+import jakarta.validation.constraints.NotNull;
+import java.time.LocalDate;
 import java.util.UUID;
 
 @JsonInclude(JsonInclude.Include.NON_EMPTY)
-public record JobFormDTO(UUID supervisingProfessor, String title, Campus location, FundingType fundingType, JobState state) {}
+public record JobFormDTO(
+    String title,
+    String researchArea,
+    String fieldOfStudies,
+    @NotNull UUID supervisingProfessor,
+    @NotNull Campus location,
+    LocalDate startDate,
+    Integer workload,
+    Integer contractDuration,
+    @NotNull FundingType fundingType,
+    String description,
+    String tasks,
+    String requirements,
+    @NotNull JobState state
+) {}
diff --git a/src/main/java/de/tum/cit/aet/job/service/JobService.java b/src/main/java/de/tum/cit/aet/job/service/JobService.java
index c3c3d992c0..abacb47613 100644
--- a/src/main/java/de/tum/cit/aet/job/service/JobService.java
+++ b/src/main/java/de/tum/cit/aet/job/service/JobService.java
@@ -52,8 +52,16 @@ public void createJob(JobFormDTO dto) {
         job.setSupervisingProfessor(supervisingProfessor);
         job.setResearchGroup(supervisingProfessor.getResearchGroup());
         job.setTitle(dto.title());
+        job.setResearchArea(dto.researchArea());
+        job.setFieldOfStudies(dto.fieldOfStudies());
         job.setLocation(dto.location());
+        job.setStartDate(dto.startDate());
+        job.setWorkload(dto.workload());
+        job.setContractDuration(dto.contractDuration());
         job.setFundingType(dto.fundingType());
+        job.setDescription(dto.description());
+        job.setTasks(dto.tasks());
+        job.setRequirements(dto.requirements());
         job.setState(dto.state());
         jobRepository.save(job);
     }
diff --git a/src/main/resources/testdata/04_jobs.sql b/src/main/resources/testdata/04_jobs.sql
index 60dea14609..908ee49d8e 100644
--- a/src/main/resources/testdata/04_jobs.sql
+++ b/src/main/resources/testdata/04_jobs.sql
@@ -47,7 +47,7 @@ VALUES
  'Assist in AI research projects, publish papers, collaborate with partners.',
  'Bachelor in Computer Science or related field, experience with ML.',
  'PUBLISHED',
- '2025-10-01 00:00:00'),
+ '2025-10-01'),
 
 -- Job 2 (Some NULLs: no description or requirements)
 ('00000000-0000-0000-0000-000000010002',
@@ -64,7 +64,7 @@ VALUES
  'Design robotic systems, prototype mechanical solutions, publish research.',
  NULL,
  'DRAFT',
- '2025-10-01 00:00:00'),
+ '2025-10-01'),
 
 -- Job 3 (All NULLs except the mandatory ones)
 ('00000000-0000-0000-0000-000000010003',
@@ -98,7 +98,7 @@ VALUES
  NULL,
  'Ongoing Bachelor in Informatics or related, enthusiasm for UX.',
  'DRAFT',
- '2025-10-01 00:00:00'),
+ '2025-10-01'),
 
 -- Job 5 (Fully filled out again, normal)
 ('00000000-0000-0000-0000-000000010005',
@@ -115,4 +115,4 @@ VALUES
  'Develop data pipelines, analyze massive datasets, publish results.',
  'Master in Computer Science or Data Science, SQL and Python skills.',
  'APPLICANT_FOUND',
- '2025-10-01 00:00:00');
+ '2025-10-01');
diff --git a/src/main/webapp/app/app.routes.ts b/src/main/webapp/app/app.routes.ts
index 7a7a8282f9..61463179b4 100644
--- a/src/main/webapp/app/app.routes.ts
+++ b/src/main/webapp/app/app.routes.ts
@@ -5,6 +5,11 @@ import { UserRouteAccessService } from 'app/core/auth/user-route-access.service'
 import { errorRoute } from './layouts/error/error.route';
 
 const routes: Routes = [
+  {
+    path: 'job-creation',
+    loadComponent: () => import('./pages/job/jobCreationForm/job-creation-form.component').then(m => m.JobCreationFormComponent),
+    title: 'home.title',
+  },
   {
     path: '',
     loadComponent: () => import('./pages/misc/home/home.component'),
diff --git a/src/main/webapp/app/generated/model/jobFormDTO.ts b/src/main/webapp/app/generated/model/jobFormDTO.ts
index c9ad62abbb..18a56a398a 100644
--- a/src/main/webapp/app/generated/model/jobFormDTO.ts
+++ b/src/main/webapp/app/generated/model/jobFormDTO.ts
@@ -9,11 +9,19 @@
  */
 
 export interface JobFormDTO {
-  supervisingProfessor?: string;
   title?: string;
-  location?: JobFormDTO.LocationEnum;
-  fundingType?: JobFormDTO.FundingTypeEnum;
-  state?: JobFormDTO.StateEnum;
+  researchArea?: string;
+  fieldOfStudies?: string;
+  supervisingProfessor: string;
+  location: JobFormDTO.LocationEnum;
+  startDate?: string;
+  workload?: number;
+  contractDuration?: number;
+  fundingType: JobFormDTO.FundingTypeEnum;
+  description?: string;
+  tasks?: string;
+  requirements?: string;
+  state: JobFormDTO.StateEnum;
 }
 export namespace JobFormDTO {
   export type LocationEnum = 'GARCHING' | 'GARCHING_HOCHBRUECK' | 'HEILBRONN' | 'MUNICH' | 'STRAUBING' | 'WEIHENSTEPHAN' | 'SINGAPORE';
diff --git a/src/main/webapp/app/pages/job/job.module.ts b/src/main/webapp/app/pages/job/job.module.ts
deleted file mode 100644
index e8b68e73c3..0000000000
--- a/src/main/webapp/app/pages/job/job.module.ts
+++ /dev/null
@@ -1,8 +0,0 @@
-import { NgModule } from '@angular/core';
-import { CommonModule } from '@angular/common';
-
-@NgModule({
-  declarations: [],
-  imports: [CommonModule],
-})
-export class JobModule {}
diff --git a/src/main/webapp/app/pages/job/jobCreationForm/job-creation-form.component.html b/src/main/webapp/app/pages/job/jobCreationForm/job-creation-form.component.html
new file mode 100644
index 0000000000..41d0fd9719
--- /dev/null
+++ b/src/main/webapp/app/pages/job/jobCreationForm/job-creation-form.component.html
@@ -0,0 +1,292 @@
+<div class="doctorate-form-container">
+  <header>
+    <h1>Create a new Doctorate Position</h1>
+  </header>
+  <div class="stepper">
+    <div (click)="currentStep = 1" [ngClass]="{ active: currentStep === 1, completed: currentStep > 1 }" class="step">
+      <div class="step-number">1</div>
+      <div class="step-label">Basic Information</div>
+    </div>
+    <div class="step-connector"></div>
+    <div (click)="currentStep = 2" [ngClass]="{ active: currentStep === 2, completed: currentStep > 2 }" class="step">
+      <div class="step-number">2</div>
+      <div class="step-label">Position Details</div>
+    </div>
+    <div class="step-connector"></div>
+    <div (click)="currentStep = 3" [ngClass]="{ active: currentStep === 3 }" class="step">
+      <div class="step-number">3</div>
+      <div class="step-label">Additional Information</div>
+    </div>
+  </div>
+  <!-- Step 1: Basic Information -->
+  @if (currentStep === 1) {
+    <div class="form-step">
+      <form [formGroup]="basicInfoForm">
+        <h2>Basic Information</h2>
+        <p>Provide the key details about the doctoral position, such as title, field of study, and supervising professor.</p>
+
+        <div class="form-field">
+          <label for="title">Job Title</label>
+          <input formControlName="title" id="title" placeholder="e.g. Doctoral Position in Robotics" type="text" />
+        </div>
+
+        <div class="form-row">
+          <div class="form-field">
+            <label for="researchArea"
+              >Research Area
+              <fa-icon [icon]="faCircleInfo" />
+            </label>
+            <div class="input-with-icon">
+              <input
+                formControlName="researchArea"
+                id="researchArea"
+                placeholder="e.g. Machine Learning, Human-Computer Interaction"
+                type="text"
+              />
+            </div>
+          </div>
+
+          <div class="form-field">
+            <label for="fieldOfStudies"
+              >Field of Studies
+              <fa-icon [icon]="faCircleInfo" />
+            </label>
+            <div class="input-with-icon">
+              <div class="select-wrapper">
+                <select formControlName="fieldOfStudies" id="fieldOfStudies">
+                  <option disabled selected value="">Select a Field of Studies ...</option>
+                  <option value="Computer Science">Computer Science</option>
+                  <option value="Mathematics">Mathematics</option>
+                  <option value="Physics">Physics</option>
+                  <option value="Chemistry">Chemistry</option>
+                  <option value="Engineering">Engineering</option>
+                </select>
+              </div>
+            </div>
+          </div>
+        </div>
+
+        <div class="form-row">
+          <div class="form-field input-with-icon">
+            <label for="supervisingProfessor"> Supervising Professor<span class="required">*</span> </label>
+            <div class="icon-input-wrapper">
+              <fa-icon [icon]="faUser" class="input-icon" />
+              <input [readonly]="true" formControlName="supervisingProfessor" id="supervisingProfessor" type="text" />
+            </div>
+          </div>
+
+          <div class="form-field">
+            <label for="location">Location<span class="required">*</span></label>
+            <div class="select-wrapper">
+              <select formControlName="location" id="location">
+                <option disabled selected value="">
+                  <fa-icon [icon]="faLocationDot" />
+                  Select a Location ...
+                </option>
+                @for (location of locations; track location) {
+                  <option [value]="location.value">
+                    <fa-icon [icon]="faLocationDot" />
+                    {{ location.label }}
+                  </option>
+                }
+              </select>
+            </div>
+          </div>
+        </div>
+
+        <div class="form-row">
+          <div class="form-field">
+            <label for="startDate">Start Date</label>
+            <div class="date-picker-wrapper">
+              <input formControlName="startDate" id="startDate" placeholder="Select a Start Date ..." type="date" />
+            </div>
+          </div>
+          <div class="form-field">
+            <label for="workload">Workload</label>
+            <div class="select-wrapper">
+              <select formControlName="workload" id="workload">
+                <option disabled selected value="">Select a Workload ...</option>
+                @for (option of workloadOptions; track option) {
+                  <option [value]="option.value">
+                    {{ option.label }}
+                  </option>
+                }
+              </select>
+            </div>
+          </div>
+        </div>
+
+        <div class="form-row">
+          <div class="form-field">
+            <label for="contractDuration">Contract Duration</label>
+            <div class="select-wrapper">
+              <select formControlName="contractDuration" id="contractDuration">
+                <option disabled selected value="">Select a Contract Duration ...</option>
+                @for (duration of contractDurations; track duration) {
+                  <option [value]="duration.value">
+                    {{ duration.label }}
+                  </option>
+                }
+              </select>
+            </div>
+          </div>
+
+          <div class="form-field">
+            <label for="fundingType">Funding Type<span class="required">*</span></label>
+            <div class="select-wrapper">
+              <select formControlName="fundingType" id="fundingType">
+                <option disabled selected value="">Select a Funding Type ...</option>
+                @for (type of fundingTypes; track type) {
+                  <option [value]="type.value">
+                    {{ type.label }}
+                  </option>
+                }
+              </select>
+            </div>
+          </div>
+        </div>
+      </form>
+
+      <div class="form-actions">
+        <button class="btn-cancel">
+          <span class="btn-icon">
+            <fa-icon [icon]="faChevronLeft" />
+          </span>
+          Cancel
+        </button>
+        <div class="action-right">
+          <button (click)="saveDraft()" class="btn-save-draft">
+            <span class="btn-icon">
+              <fa-icon [icon]="faFloppyDisk" />
+            </span>
+            Save Draft
+          </button>
+          <button (click)="nextStep()" class="btn-next">Next</button>
+        </div>
+      </div>
+    </div>
+  }
+  <!-- Step 2: Position Details -->
+  @if (currentStep === 2) {
+    <div class="form-step">
+      <form [formGroup]="positionDetailsForm">
+        <h2>Position Details</h2>
+        <p>
+          Describe the research focus, main tasks, and expectations for the candidate. Define the minimum requirements and any preferred
+          qualifications for applicants.
+        </p>
+        <div class="form-field">
+          <label for="description"
+            >Job Description
+            <fa-icon [icon]="faCircleInfo" />
+          </label>
+          <div class="input-with-icon">
+            <textarea
+              formControlName="description"
+              id="description"
+              placeholder="Describe the scope and focus of the position..."
+              rows="10"
+            ></textarea>
+          </div>
+          <div class="form-footer">
+            <small class="description-helper"
+              >Describe the research context, goals, and overall scope of the position. Keep it concise but informative.</small
+            >
+            <small class="char-counter"> {{ descriptionLength }}/1000 </small>
+          </div>
+        </div>
+        <div class="form-field">
+          <label for="tasks"
+            >Tasks
+            <fa-icon [icon]="faCircleInfo" />
+          </label>
+          <div class="input-with-icon">
+            <textarea formControlName="tasks" id="tasks" placeholder="List the main responsibilities..." rows="8"></textarea>
+          </div>
+          <div class="form-footer">
+            <small class="description-helper"
+              >List the main responsibilities or activities the candidate will be involved in. Bullet points are recommended.</small
+            >
+            <small class="char-counter"> {{ tasksLength }}/1000 </small>
+          </div>
+        </div>
+        <div class="form-field">
+          <label for="requirements"
+            >Eligibility Criteria
+            <fa-icon [icon]="faCircleInfo" />
+          </label>
+          <div class="input-with-icon">
+            <textarea
+              formControlName="requirements"
+              id="requirements"
+              placeholder="List the minimum requirements for applicants..."
+              rows="10"
+            ></textarea>
+          </div>
+          <div class="form-footer">
+            <small class="description-helper"
+              >Specify the minimum requirements (e.g., degree, grades, language proficiency) applicants must fulfill.</small
+            >
+            <small class="char-counter"> {{ requirementsLength }}/1000 </small>
+          </div>
+        </div>
+      </form>
+      <div class="form-actions">
+        <button (click)="previousStep()" class="btn-back">
+          <span class="btn-icon">
+            <fa-icon [icon]="faChevronLeft" />
+          </span>
+          Back
+        </button>
+        <div class="action-right">
+          <button (click)="saveDraft()" class="btn-save-draft">
+            <span class="btn-icon">
+              <fa-icon [icon]="['fas', 'floppy-disk']" />
+            </span>
+            Save Draft
+          </button>
+          <button class="btn-next" (click)="nextStep()">Next</button>
+        </div>
+      </div>
+    </div>
+  }
+  <!-- Step 3: Additional Information -->
+  @if (currentStep === 3) {
+    <div class="form-step">
+      <form [formGroup]="additionalInformationForm">
+        <section class="extras-section">
+          <h2>Extras</h2>
+          <p>Add custom questions that applicants will see in the application form.</p>
+          <!--        Not implemented yet-->
+          <!--        <div class="custom-questions-dropdown">-->
+          <!--          <div class="dropdown-header">-->
+          <!--            <span>Custom Questions</span>-->
+          <!--            <span class="dropdown-icon">▼</span>-->
+          <!--          </div>-->
+          <!--        </div>-->
+          <div class="data-privacy-notice">
+            <p>
+              A Data Privacy Statement from the Technical University of Munich (TUM) will automatically be attached to this job. For more
+              information, please visit:
+              <a class="tum-link" href="https://www.community.tum.de/en/privacy-policy/">TUM Data Protection Information</a>.
+            </p>
+          </div>
+        </section>
+      </form>
+      <div class="form-actions">
+        <button (click)="previousStep()" class="btn-back">
+          <span class="btn-icon">
+            <fa-icon [icon]="faChevronLeft" />
+          </span>
+          Back
+        </button>
+        <div class="action-right">
+          <button (click)="saveDraft()" class="btn-save-draft">
+            <span class="btn-icon"><fa-icon [icon]="['fas', 'floppy-disk']" /></span> Save Draft
+          </button>
+          <button (click)="publishJob()" class="btn-publish">Publish Job</button>
+        </div>
+      </div>
+    </div>
+  }
+</div>
diff --git a/src/main/webapp/app/pages/job/jobCreationForm/job-creation-form.component.scss b/src/main/webapp/app/pages/job/jobCreationForm/job-creation-form.component.scss
new file mode 100644
index 0000000000..7df2716fb6
--- /dev/null
+++ b/src/main/webapp/app/pages/job/jobCreationForm/job-creation-form.component.scss
@@ -0,0 +1,410 @@
+/* app.component.scss */
+
+// Variables
+
+$shadow: 0 0.125rem 0.3125rem rgba(0, 0, 0, 0.1);
+
+// Base styles
+body {
+  background-color: var(--p-background-surface);
+  color: var(--p-text-primary);
+  line-height: 1.6;
+}
+
+.doctorate-form-container {
+  max-width: 75rem;
+  margin: 0 auto;
+  padding: 1.25rem;
+  background-color: var(--p-background-default);
+  box-shadow: $shadow;
+  border-radius: 0.25rem;
+}
+
+// Header
+header {
+  padding-bottom: 1.25rem;
+
+  h1 {
+    color: var(--p-primary-color);
+    font-size: 1.5rem;
+    margin-bottom: 0.5rem;
+  }
+
+  p {
+    color: var(--p-background-disabled);
+    font-size: 0.875rem;
+  }
+}
+
+// Stepper
+.stepper {
+  display: flex;
+  align-items: center;
+  margin: 1.875rem 0;
+
+  .step {
+    display: flex;
+    align-items: center;
+
+    .step-number {
+      width: 1.875rem;
+      height: 1.875rem;
+      border-radius: 50%;
+      background-color: var(--p-background-disabled);
+      color: var(--p-text-disabled);
+      display: flex;
+      align-items: center;
+      justify-content: center;
+      font-weight: bold;
+      margin-right: 0.625rem;
+    }
+
+    .step-label {
+      font-size: 0.875rem;
+      color: var(--p-text-disabled);
+    }
+
+    &.active,
+    &.completed {
+      .step-number {
+        background-color: var(--p-primary-color);
+        color: var(--p-text-on-primary);
+      }
+
+      .step-label {
+        color: var(--p-text-primary);
+        font-weight: bold;
+      }
+    }
+
+    &.completed {
+      .step-number {
+        background-color: var(--p-primary-color);
+      }
+    }
+  }
+
+  .step-connector {
+    flex: 1;
+    height: 0.125rem;
+    background-color: var(--p-text-disabled);
+    margin: 0 0.625rem;
+  }
+}
+
+// Form styles
+.form-step {
+  h2 {
+    font-size: 1.25rem;
+    margin-bottom: 0.625rem;
+  }
+
+  p {
+    color: var(--p-text-disabled);
+    font-size: 0.875rem;
+    margin-bottom: 1.25rem;
+  }
+}
+
+.form-field {
+  margin-bottom: 1.25rem;
+
+  label {
+    display: block;
+    font-size: 0.875rem;
+    margin-bottom: 0.5rem;
+
+    .required {
+      color: var(--p-danger-color);
+      margin-left: 0.125rem;
+    }
+
+    .disabled {
+      color: var(--p-background-disabled);
+    }
+  }
+
+  input,
+  textarea,
+  select {
+    width: 100%;
+    padding: 0.625rem 0.75rem;
+    border: 0.0625rem solid var(--p-border-default);
+    border-radius: 0.5rem;
+    font-size: 0.875rem;
+
+    &:focus {
+      outline: none;
+      border-color: var(--p-primary-color);
+    }
+
+    &.invalid {
+      border-color: var(--p-danger-color);
+    }
+
+    &::placeholder {
+      color: var(--p-background-disabled);
+    }
+  }
+
+  textarea {
+    resize: vertical;
+  }
+
+  .form-footer {
+    display: flex;
+    justify-content: space-between;
+    align-items: center;
+    margin-top: 0.1rem;
+    font-size: 0.9rem;
+
+    .description-helper {
+      color: var(--p-background-disabled);
+    }
+
+    .char-counter {
+      color: var(--p-text-primary);
+    }
+  }
+}
+
+.form-row {
+  display: flex;
+  gap: 1.25rem;
+
+  .form-field {
+    flex: 1;
+  }
+}
+
+.input-with-icon {
+  position: relative;
+
+  .icon-input-wrapper {
+    display: flex;
+    align-items: center;
+
+    input {
+      padding-left: 2.25rem;
+    }
+
+    .ng-fa-icon.input-icon {
+      position: absolute;
+      left: 0.75rem;
+      color: var(--p-text-disabled);
+    }
+  }
+
+  .info-icon {
+    position: absolute;
+    right: 0.625rem;
+    top: 0.625rem;
+    width: 1.125rem;
+    height: 1.125rem;
+    border-radius: 50%;
+    background-color: var(--p-secondary-color);
+    color: var(--p-background-default);
+    font-size: 0.75rem;
+    display: flex;
+    align-items: center;
+    justify-content: center;
+    cursor: pointer;
+    font-style: italic;
+  }
+}
+
+.select-wrapper {
+  position: relative;
+
+  &::after {
+    content: '▼';
+    position: absolute;
+    right: 0.9375rem;
+    top: 50%;
+    transform: translateY(-50%);
+    pointer-events: none;
+    font-size: 0.75rem;
+    color: var(--p-text-disabled);
+  }
+
+  select {
+    appearance: none;
+    padding-right: 1.875rem;
+  }
+}
+
+.date-picker-wrapper {
+  position: relative;
+
+  .calendar-button {
+    position: absolute;
+    right: 0.625rem;
+    top: 50%;
+    transform: translateY(-50%);
+    background: none;
+    border: none;
+    cursor: pointer;
+
+    .calendar-icon {
+      display: inline-block;
+      width: 1.25rem;
+      height: 1.25rem;
+      background-color: var(--p-primary-color);
+      mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24'%3E%3Cpath d='M20 3h-1V1h-2v2H7V1H5v2H4c-1.1 0-2 .9-2 2v16c0 1.1.9 2 2 2h16c1.1 0 2-.9 2-2V5c0-1.1-.9-2-2-2zm0 18H4V10h16v11zM4 8V5h16v3H4z'/%3E%3C/svg%3E")
+        no-repeat center;
+      -webkit-mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24'%3E%3Cpath d='M20 3h-1V1h-2v2H7V1H5v2H4c-1.1 0-2 .9-2 2v16c0 1.1.9 2 2 2h16c1.1 0 2-.9 2-2V5c0-1.1-.9-2-2-2zm0 18H4V10h16v11zM4 8V5h16v3H4z'/%3E%3C/svg%3E")
+        no-repeat center;
+    }
+  }
+}
+
+// Custom questions section
+.custom-questions-dropdown {
+  border: 0.0625rem solid var(--p-border-default);
+  border-radius: 0.25rem;
+  margin-bottom: 1.875rem;
+
+  .dropdown-header {
+    padding: 0.75rem 0.9375rem;
+    display: flex;
+    justify-content: space-between;
+    align-items: center;
+    background-color: var(--p-background-default);
+    border-radius: 0.25rem;
+    cursor: pointer;
+
+    .dropdown-icon {
+      color: var(--p-text-disabled);
+      font-size: 0.75rem;
+    }
+  }
+}
+
+.data-privacy-notice {
+  margin-top: 1.25rem;
+  padding: 0.9375rem;
+  background-color: var(--p-background-default);
+  border-radius: 0.25rem;
+  font-size: 0.875rem;
+
+  p {
+    margin: 0;
+    color: var(--p-text-disabled);
+  }
+
+  .tum-link {
+    color: var(--p-primary-color);
+    text-decoration: none;
+    font-weight: bold;
+
+    &:hover {
+      text-decoration: underline;
+    }
+  }
+}
+
+// Form actions
+.form-actions {
+  display: flex;
+  justify-content: space-between;
+  margin-top: 1rem;
+  padding-top: 1rem;
+
+  .action-right {
+    display: flex;
+    gap: 0.625rem;
+  }
+
+  button {
+    padding: 0.625rem 0.9375rem;
+    border-radius: 0.25rem;
+    cursor: pointer;
+    font-size: 0.875rem;
+    font-weight: bold;
+    display: flex;
+    align-items: center;
+    border: none;
+
+    &:disabled {
+      opacity: 0.6;
+      cursor: not-allowed;
+    }
+
+    .btn-icon {
+      margin-right: 0.5rem;
+    }
+  }
+
+  .btn-cancel,
+  .btn-back {
+    background-color: transparent;
+    color: var(--p-text-disabled);
+    border: 0.0625rem solid var(--p-text-disabled);
+
+    &:hover {
+      background-color: var(--p-background-surface-alt);
+    }
+  }
+
+  .btn-save-draft {
+    background-color: transparent;
+    color: var(--p-primary-color);
+    border: 0.0625rem solid var(--p-primary-color);
+
+    &:hover {
+      background-color: var(--p-primary-color);
+      color: var(--p-text-on-primary);
+
+      .btn-icon .ng-fa-icon {
+        color: var(--p-text-on-primary);
+      }
+    }
+  }
+
+  .btn-next {
+    background-color: var(--p-primary-color);
+    color: var(--p-background-default);
+    border-radius: 0.25rem;
+
+    &:hover {
+      background-color: var(--p-primary-hover-color);
+    }
+  }
+
+  .btn-publish {
+    background-color: var(--p-primary-color);
+    color: var(--p-background-default);
+
+    &:hover {
+      background-color: var(--p-primary-hover-color);
+    }
+  }
+}
+
+// Extras section
+.extras-section {
+  h2 {
+    font-size: 1.25rem;
+    margin-bottom: 0.625rem;
+  }
+
+  p {
+    color: var(--p-text-disabled);
+    font-size: 0.875rem;
+    margin-bottom: 1.25rem;
+  }
+}
+
+.ng-fa-icon {
+  color: var(--p-primary-color);
+}
+
+.btn-back .ng-fa-icon,
+.btn-cancel .ng-fa-icon {
+  color: var(--p-text-disabled);
+}
+
+input:disabled {
+  color: var(--p-text-disabled);
+  background-color: var(--p-background-disabled);
+}
diff --git a/src/main/webapp/app/pages/job/jobCreationForm/job-creation-form.component.ts b/src/main/webapp/app/pages/job/jobCreationForm/job-creation-form.component.ts
new file mode 100644
index 0000000000..c4e3a573ec
--- /dev/null
+++ b/src/main/webapp/app/pages/job/jobCreationForm/job-creation-form.component.ts
@@ -0,0 +1,168 @@
+import { Component, inject } from '@angular/core';
+import { FormBuilder, FormGroup, ReactiveFormsModule, Validators } from '@angular/forms';
+import { FontAwesomeModule } from '@fortawesome/angular-fontawesome';
+import { faChevronLeft, faCircleInfo, faFloppyDisk, faLocationDot, faUser } from '@fortawesome/free-solid-svg-icons';
+import { CommonModule } from '@angular/common';
+import { JobResourceService } from 'app/generated/api/jobResource.service';
+
+import { JobFormDTO } from '../../../generated';
+
+@Component({
+  selector: 'jhi-job-creation-form',
+  standalone: true,
+  templateUrl: './job-creation-form.component.html',
+  styleUrls: ['./job-creation-form.component.scss'],
+  imports: [CommonModule, ReactiveFormsModule, FontAwesomeModule],
+  providers: [JobResourceService],
+})
+export class JobCreationFormComponent {
+  readonly faCircleInfo = faCircleInfo;
+  readonly faChevronLeft = faChevronLeft;
+  readonly faLocationDot = faLocationDot;
+  readonly faFloppyDisk = faFloppyDisk;
+  readonly faUser = faUser;
+
+  currentStep = 1;
+
+  // Form groups for each step
+  basicInfoForm: FormGroup = this.fb.group({});
+  positionDetailsForm: FormGroup = this.fb.group({});
+  additionalInformationForm: FormGroup = this.fb.group({});
+
+  // Options for dropdowns
+  locations = [
+    { label: 'Munich Campus', value: JobFormDTO.LocationEnum.Munich },
+    { label: 'Garching Campus', value: JobFormDTO.LocationEnum.Garching },
+    { label: 'Weihenstephan Campus', value: JobFormDTO.LocationEnum.Weihenstephan },
+  ];
+  workloadOptions = [
+    { label: '100% (Full-time)', value: 100 },
+    { label: '60%', value: 60 },
+    { label: '40%', value: 40 },
+    { label: '20%', value: 20 },
+    { label: '10%', value: 10 },
+  ];
+  contractDurations = [
+    { label: '1 year', value: 1 },
+    { label: '2 years', value: 2 },
+    { label: '3 years', value: 3 },
+    { label: '4 years', value: 4 },
+    { label: '5+ years', value: 5 },
+  ];
+  fundingTypes = [
+    { label: 'University Budget', value: JobFormDTO.FundingTypeEnum.FullyFunded },
+    { label: 'Government Funding', value: JobFormDTO.FundingTypeEnum.GovernmentFunded },
+    { label: 'Self Funding', value: JobFormDTO.FundingTypeEnum.SelfFunded },
+    { label: 'Industry Sponsored', value: JobFormDTO.FundingTypeEnum.IndustrySponsored },
+    { label: 'Scholarship', value: JobFormDTO.FundingTypeEnum.Scholarship },
+    { label: 'Research Grant', value: JobFormDTO.FundingTypeEnum.ResearchGrant },
+  ];
+  private jobResourceService = inject(JobResourceService);
+
+  constructor(private fb: FormBuilder) {
+    this.initForms();
+  }
+
+  get descriptionLength(): number {
+    return this.positionDetailsForm.get('description')?.value?.length ?? 0;
+  }
+
+  get tasksLength(): number {
+    return this.positionDetailsForm.get('tasks')?.value?.length ?? 0;
+  }
+
+  get requirementsLength(): number {
+    return this.positionDetailsForm.get('requirements')?.value?.length ?? 0;
+  }
+
+  initForms(): void {
+    // Basic Information form
+    this.basicInfoForm = this.fb.group({
+      title: [''],
+      researchArea: [''],
+      fieldOfStudies: [''],
+      supervisingProfessor: this.fb.control(
+        {
+          value: 'Prof. Dr. Stephan Krusche',
+          disabled: true,
+        },
+        Validators.required,
+      ),
+      location: ['', Validators.required],
+      startDate: [''],
+      workload: [''],
+      contractDuration: [''],
+      fundingType: ['', Validators.required],
+    });
+
+    // Position Details form
+    this.positionDetailsForm = this.fb.group({
+      description: ['', [Validators.maxLength(1000)]],
+      tasks: ['', [Validators.maxLength(1000)]],
+      requirements: ['', [Validators.maxLength(1000)]],
+    });
+
+    // Additional Information form
+    this.additionalInformationForm = this.fb.group({});
+  }
+
+  nextStep(): void {
+    if (this.currentStep < 3) {
+      this.currentStep++;
+    }
+  }
+
+  previousStep(): void {
+    if (this.currentStep > 1) {
+      this.currentStep--;
+    }
+  }
+
+  saveDraft(): void {
+    const jobFormDto: JobFormDTO = {
+      title: this.basicInfoForm.value.title,
+      researchArea: this.basicInfoForm.value.researchArea,
+      fieldOfStudies: this.basicInfoForm.value.fieldOfStudies,
+      supervisingProfessor: '00000000-0000-0000-0000-000000000102',
+      location: this.basicInfoForm.value.location,
+      startDate: this.basicInfoForm.value.startDate,
+      workload: this.basicInfoForm.value.workload,
+      contractDuration: this.basicInfoForm.value.contractDuration,
+      fundingType: this.basicInfoForm.value.fundingType,
+      description: this.positionDetailsForm.value.description,
+      tasks: this.positionDetailsForm.value.tasks,
+      requirements: this.positionDetailsForm.value.requirements,
+      state: JobFormDTO.StateEnum.Draft,
+    };
+    this.jobResourceService.createJob(jobFormDto).subscribe({
+      next() {},
+      error(err) {
+        console.error('Failed to create job draft:', err);
+      },
+    });
+  }
+
+  publishJob(): void {
+    const jobFormDto: JobFormDTO = {
+      title: this.basicInfoForm.value.title,
+      researchArea: this.basicInfoForm.value.researchArea,
+      fieldOfStudies: this.basicInfoForm.value.fieldOfStudies,
+      supervisingProfessor: '00000000-0000-0000-0000-000000000102',
+      location: this.basicInfoForm.value.location,
+      startDate: this.basicInfoForm.value.startDate,
+      workload: this.basicInfoForm.value.workload,
+      contractDuration: this.basicInfoForm.value.contractDuration,
+      fundingType: this.basicInfoForm.value.fundingType,
+      description: this.positionDetailsForm.value.description,
+      tasks: this.positionDetailsForm.value.tasks,
+      requirements: this.positionDetailsForm.value.requirements,
+      state: JobFormDTO.StateEnum.Published,
+    };
+    this.jobResourceService.createJob(jobFormDto).subscribe({
+      next() {},
+      error(err) {
+        console.error('Failed to publish job:', err);
+      },
+    });
+  }
+}


Reviews:
user: login: az108
body: small comments
state: CHANGES_REQUESTED
author_association: COLLABORATOR
submitted_at: 2025-05-11T13:33:45Z
user: login: az108
body: LGTM for the prototype, we need to adjust this in the future with the atoms!
state: APPROVED
author_association: COLLABORATOR
submitted_at: 2025-05-11T14:05:45Z

Review comments:
diff_hunk: @@ -0,0 +1,182 @@
+import { Component, inject } from '@angular/core';
+import { FormBuilder, FormGroup, ReactiveFormsModule, Validators } from '@angular/forms';
+import { FontAwesomeModule } from '@fortawesome/angular-fontawesome';
+import { faChevronLeft, faCircleInfo, faFloppyDisk, faLocationDot, faUser } from '@fortawesome/free-solid-svg-icons';
+import { CommonModule } from '@angular/common';
+import { JobResourceService } from 'app/generated/api/jobResource.service';
+
+import { JobFormDTO } from '../../../generated';
+
+@Component({
+  selector: 'jhi-job-creation-form',
+  standalone: true,
+  templateUrl: './job-creation-form.component.html',
+  styleUrls: ['./job-creation-form.component.scss'],
+  imports: [CommonModule, ReactiveFormsModule, FontAwesomeModule],
+  providers: [JobResourceService],
+})
+export class JobCreationFormComponent {
+  readonly faCircleInfo = faCircleInfo;
+  readonly faChevronLeft = faChevronLeft;
+  readonly faLocationDot = faLocationDot;
+  readonly faFloppyDisk = faFloppyDisk;
+  readonly faUser = faUser;
+
+  currentStep = 1;
+
+  // Form groups for each step
+  basicInfoForm: FormGroup = this.fb.group({});
+  positionDetailsForm: FormGroup = this.fb.group({});
+  additionalInformationForm: FormGroup = this.fb.group({});
+
+  // Options for dropdowns
+  locations = [
+    { label: 'Munich Campus', value: JobFormDTO.LocationEnum.Munich },
+    { label: 'Garching Campus', value: JobFormDTO.LocationEnum.Garching },
+    { label: 'Weihenstephan Campus', value: JobFormDTO.LocationEnum.Weihenstephan },
+  ];
+  workloadOptions = [
+    { label: '100% (Full-time)', value: 100 },
+    { label: '60%', value: 60 },
+    { label: '40%', value: 40 },
+    { label: '20%', value: 20 },
+    { label: '10%', value: 10 },
+  ];
+  contractDurations = [
+    { label: '1 year', value: 1 },
+    { label: '2 years', value: 2 },
+    { label: '3 years', value: 3 },
+    { label: '4 years', value: 4 },
+    { label: '5+ years', value: 5 },
+  ];
+  fundingTypes = [
+    { label: 'University Budget', value: JobFormDTO.FundingTypeEnum.FullyFunded },
+    { label: 'Government Funding', value: JobFormDTO.FundingTypeEnum.GovernmentFunded },
+    { label: 'Self Funding', value: JobFormDTO.FundingTypeEnum.SelfFunded },
+    { label: 'Industry Sponsored', value: JobFormDTO.FundingTypeEnum.IndustrySponsored },
+    { label: 'Scholarship', value: JobFormDTO.FundingTypeEnum.Scholarship },
+    { label: 'Research Grant', value: JobFormDTO.FundingTypeEnum.ResearchGrant },
+  ];
+  private jobResourceService = inject(JobResourceService);
+
+  constructor(private fb: FormBuilder) {
+    this.initForms();
+  }
+
+  get descriptionLength(): number {
+    return this.positionDetailsForm.get('description')?.value?.length ?? 0;
+  }
+
+  get tasksLength(): number {
+    return this.positionDetailsForm.get('tasks')?.value?.length ?? 0;
+  }
+
+  get requirementsLength(): number {
+    return this.positionDetailsForm.get('requirements')?.value?.length ?? 0;
+  }
+
+  initForms(): void {
+    // Basic Information form
+    this.basicInfoForm = this.fb.group({
+      title: [''],
+      researchArea: [''],
+      fieldOfStudies: [''],
+      supervisingProfessor: this.fb.control(
+        {
+          value: 'Prof. Dr. Stephan Krusche',
+          disabled: true,
+        },
+        Validators.required,
+      ),
+      location: ['', Validators.required],
+      startDate: [''],
+      workload: [''],
+      contractDuration: [''],
+      fundingType: ['', Validators.required],
+    });
+
+    // Position Details form
+    this.positionDetailsForm = this.fb.group({
+      description: ['', [Validators.maxLength(1000)]],
+      tasks: ['', [Validators.maxLength(1000)]],
+      requirements: ['', [Validators.maxLength(1000)]],
+    });
+
+    // Additional Information form
+    this.additionalInformationForm = this.fb.group({});
+  }
+
+  nextStep(): void {
+    if (this.currentStep < 3) {
+      this.currentStep++;
+    }
+  }
+
+  previousStep(): void {
+    if (this.currentStep > 1) {
+      this.currentStep--;
+    }
+  }
+
+  saveDraft(): void {
+    const jobFormDto: JobFormDTO = {
+      title: this.basicInfoForm.value.title,
+      researchArea: this.basicInfoForm.value.researchArea,
+      fieldOfStudies: this.basicInfoForm.value.fieldOfStudies,
+      supervisingProfessor: '00000000-0000-0000-0000-000000000102',
+      location: this.basicInfoForm.value.location,
+      startDate: this.basicInfoForm.value.startDate,
+      workload: this.basicInfoForm.value.workload,
+      contractDuration: this.basicInfoForm.value.contractDuration,
+      fundingType: this.basicInfoForm.value.fundingType,
+      description: this.positionDetailsForm.value.description,
+      tasks: this.positionDetailsForm.value.tasks,
+      requirements: this.positionDetailsForm.value.requirements,
+      state: JobFormDTO.StateEnum.Draft,
+    };
+    this.jobResourceService.createJob(jobFormDto).subscribe({
+      next() {
+        console.warn('Draft Job successfully created!');
+      },
+      error(err) {
+        console.error('Failed to create job draft:', err);
+      },
+    });
+    console.warn({
+      ...this.basicInfoForm.value,
+      ...this.positionDetailsForm.value,
+      // ...this.additionalInfoForm.value,
path: src/main/webapp/app/pages/job/jobCreationForm/job-creation-form.component.ts
user: login: az108
body: remove these console errors
created_at: 2025-05-11T13:32:58Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,182 @@
+import { Component, inject } from '@angular/core';
+import { FormBuilder, FormGroup, ReactiveFormsModule, Validators } from '@angular/forms';
+import { FontAwesomeModule } from '@fortawesome/angular-fontawesome';
+import { faChevronLeft, faCircleInfo, faFloppyDisk, faLocationDot, faUser } from '@fortawesome/free-solid-svg-icons';
+import { CommonModule } from '@angular/common';
+import { JobResourceService } from 'app/generated/api/jobResource.service';
+
+import { JobFormDTO } from '../../../generated';
+
+@Component({
+  selector: 'jhi-job-creation-form',
+  standalone: true,
+  templateUrl: './job-creation-form.component.html',
+  styleUrls: ['./job-creation-form.component.scss'],
+  imports: [CommonModule, ReactiveFormsModule, FontAwesomeModule],
+  providers: [JobResourceService],
+})
+export class JobCreationFormComponent {
+  readonly faCircleInfo = faCircleInfo;
+  readonly faChevronLeft = faChevronLeft;
+  readonly faLocationDot = faLocationDot;
+  readonly faFloppyDisk = faFloppyDisk;
+  readonly faUser = faUser;
+
+  currentStep = 1;
+
+  // Form groups for each step
+  basicInfoForm: FormGroup = this.fb.group({});
+  positionDetailsForm: FormGroup = this.fb.group({});
+  additionalInformationForm: FormGroup = this.fb.group({});
+
+  // Options for dropdowns
+  locations = [
+    { label: 'Munich Campus', value: JobFormDTO.LocationEnum.Munich },
+    { label: 'Garching Campus', value: JobFormDTO.LocationEnum.Garching },
+    { label: 'Weihenstephan Campus', value: JobFormDTO.LocationEnum.Weihenstephan },
+  ];
+  workloadOptions = [
+    { label: '100% (Full-time)', value: 100 },
+    { label: '60%', value: 60 },
+    { label: '40%', value: 40 },
+    { label: '20%', value: 20 },
+    { label: '10%', value: 10 },
+  ];
+  contractDurations = [
+    { label: '1 year', value: 1 },
+    { label: '2 years', value: 2 },
+    { label: '3 years', value: 3 },
+    { label: '4 years', value: 4 },
+    { label: '5+ years', value: 5 },
+  ];
+  fundingTypes = [
+    { label: 'University Budget', value: JobFormDTO.FundingTypeEnum.FullyFunded },
+    { label: 'Government Funding', value: JobFormDTO.FundingTypeEnum.GovernmentFunded },
+    { label: 'Self Funding', value: JobFormDTO.FundingTypeEnum.SelfFunded },
+    { label: 'Industry Sponsored', value: JobFormDTO.FundingTypeEnum.IndustrySponsored },
+    { label: 'Scholarship', value: JobFormDTO.FundingTypeEnum.Scholarship },
+    { label: 'Research Grant', value: JobFormDTO.FundingTypeEnum.ResearchGrant },
+  ];
+  private jobResourceService = inject(JobResourceService);
+
+  constructor(private fb: FormBuilder) {
+    this.initForms();
+  }
+
+  get descriptionLength(): number {
+    return this.positionDetailsForm.get('description')?.value?.length ?? 0;
+  }
+
+  get tasksLength(): number {
+    return this.positionDetailsForm.get('tasks')?.value?.length ?? 0;
+  }
+
+  get requirementsLength(): number {
+    return this.positionDetailsForm.get('requirements')?.value?.length ?? 0;
+  }
+
+  initForms(): void {
+    // Basic Information form
+    this.basicInfoForm = this.fb.group({
+      title: [''],
+      researchArea: [''],
+      fieldOfStudies: [''],
+      supervisingProfessor: this.fb.control(
+        {
+          value: 'Prof. Dr. Stephan Krusche',
+          disabled: true,
+        },
+        Validators.required,
+      ),
+      location: ['', Validators.required],
+      startDate: [''],
+      workload: [''],
+      contractDuration: [''],
+      fundingType: ['', Validators.required],
+    });
+
+    // Position Details form
+    this.positionDetailsForm = this.fb.group({
+      description: ['', [Validators.maxLength(1000)]],
+      tasks: ['', [Validators.maxLength(1000)]],
+      requirements: ['', [Validators.maxLength(1000)]],
+    });
+
+    // Additional Information form
+    this.additionalInformationForm = this.fb.group({});
+  }
+
+  nextStep(): void {
+    if (this.currentStep < 3) {
+      this.currentStep++;
+    }
+  }
+
+  previousStep(): void {
+    if (this.currentStep > 1) {
+      this.currentStep--;
+    }
+  }
+
+  saveDraft(): void {
+    const jobFormDto: JobFormDTO = {
+      title: this.basicInfoForm.value.title,
+      researchArea: this.basicInfoForm.value.researchArea,
+      fieldOfStudies: this.basicInfoForm.value.fieldOfStudies,
+      supervisingProfessor: '00000000-0000-0000-0000-000000000102',
+      location: this.basicInfoForm.value.location,
+      startDate: this.basicInfoForm.value.startDate,
+      workload: this.basicInfoForm.value.workload,
+      contractDuration: this.basicInfoForm.value.contractDuration,
+      fundingType: this.basicInfoForm.value.fundingType,
+      description: this.positionDetailsForm.value.description,
+      tasks: this.positionDetailsForm.value.tasks,
+      requirements: this.positionDetailsForm.value.requirements,
+      state: JobFormDTO.StateEnum.Draft,
+    };
+    this.jobResourceService.createJob(jobFormDto).subscribe({
+      next() {
+        console.warn('Draft Job successfully created!');
+      },
+      error(err) {
+        console.error('Failed to create job draft:', err);
+      },
+    });
+    console.warn({
+      ...this.basicInfoForm.value,
+      ...this.positionDetailsForm.value,
+      // ...this.additionalInfoForm.value,
+    });
+  }
+
+  publishJob(): void {
+    const jobFormDto: JobFormDTO = {
+      title: this.basicInfoForm.value.title,
+      researchArea: this.basicInfoForm.value.researchArea,
+      fieldOfStudies: this.basicInfoForm.value.fieldOfStudies,
+      supervisingProfessor: '00000000-0000-0000-0000-000000000102',
+      location: this.basicInfoForm.value.location,
+      startDate: this.basicInfoForm.value.startDate,
+      workload: this.basicInfoForm.value.workload,
+      contractDuration: this.basicInfoForm.value.contractDuration,
+      fundingType: this.basicInfoForm.value.fundingType,
+      description: this.positionDetailsForm.value.description,
+      tasks: this.positionDetailsForm.value.tasks,
+      requirements: this.positionDetailsForm.value.requirements,
+      state: JobFormDTO.StateEnum.Published,
+    };
+    this.jobResourceService.createJob(jobFormDto).subscribe({
+      next() {
+        console.warn('Job successfully published!');
path: src/main/webapp/app/pages/job/jobCreationForm/job-creation-form.component.ts
user: login: az108
body: ideally we use alert service in the future
created_at: 2025-05-11T13:33:26Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,182 @@
+import { Component, inject } from '@angular/core';
+import { FormBuilder, FormGroup, ReactiveFormsModule, Validators } from '@angular/forms';
+import { FontAwesomeModule } from '@fortawesome/angular-fontawesome';
+import { faChevronLeft, faCircleInfo, faFloppyDisk, faLocationDot, faUser } from '@fortawesome/free-solid-svg-icons';
+import { CommonModule } from '@angular/common';
+import { JobResourceService } from 'app/generated/api/jobResource.service';
+
+import { JobFormDTO } from '../../../generated';
+
+@Component({
+  selector: 'jhi-job-creation-form',
+  standalone: true,
+  templateUrl: './job-creation-form.component.html',
+  styleUrls: ['./job-creation-form.component.scss'],
+  imports: [CommonModule, ReactiveFormsModule, FontAwesomeModule],
+  providers: [JobResourceService],
+})
+export class JobCreationFormComponent {
+  readonly faCircleInfo = faCircleInfo;
+  readonly faChevronLeft = faChevronLeft;
+  readonly faLocationDot = faLocationDot;
+  readonly faFloppyDisk = faFloppyDisk;
+  readonly faUser = faUser;
+
+  currentStep = 1;
+
+  // Form groups for each step
+  basicInfoForm: FormGroup = this.fb.group({});
+  positionDetailsForm: FormGroup = this.fb.group({});
+  additionalInformationForm: FormGroup = this.fb.group({});
+
+  // Options for dropdowns
+  locations = [
+    { label: 'Munich Campus', value: JobFormDTO.LocationEnum.Munich },
+    { label: 'Garching Campus', value: JobFormDTO.LocationEnum.Garching },
+    { label: 'Weihenstephan Campus', value: JobFormDTO.LocationEnum.Weihenstephan },
+  ];
+  workloadOptions = [
+    { label: '100% (Full-time)', value: 100 },
+    { label: '60%', value: 60 },
+    { label: '40%', value: 40 },
+    { label: '20%', value: 20 },
+    { label: '10%', value: 10 },
+  ];
+  contractDurations = [
+    { label: '1 year', value: 1 },
+    { label: '2 years', value: 2 },
+    { label: '3 years', value: 3 },
+    { label: '4 years', value: 4 },
+    { label: '5+ years', value: 5 },
+  ];
+  fundingTypes = [
+    { label: 'University Budget', value: JobFormDTO.FundingTypeEnum.FullyFunded },
+    { label: 'Government Funding', value: JobFormDTO.FundingTypeEnum.GovernmentFunded },
+    { label: 'Self Funding', value: JobFormDTO.FundingTypeEnum.SelfFunded },
+    { label: 'Industry Sponsored', value: JobFormDTO.FundingTypeEnum.IndustrySponsored },
+    { label: 'Scholarship', value: JobFormDTO.FundingTypeEnum.Scholarship },
+    { label: 'Research Grant', value: JobFormDTO.FundingTypeEnum.ResearchGrant },
+  ];
+  private jobResourceService = inject(JobResourceService);
+
+  constructor(private fb: FormBuilder) {
+    this.initForms();
+  }
+
+  get descriptionLength(): number {
+    return this.positionDetailsForm.get('description')?.value?.length ?? 0;
+  }
+
+  get tasksLength(): number {
+    return this.positionDetailsForm.get('tasks')?.value?.length ?? 0;
+  }
+
+  get requirementsLength(): number {
+    return this.positionDetailsForm.get('requirements')?.value?.length ?? 0;
+  }
+
+  initForms(): void {
+    // Basic Information form
+    this.basicInfoForm = this.fb.group({
+      title: [''],
+      researchArea: [''],
+      fieldOfStudies: [''],
+      supervisingProfessor: this.fb.control(
+        {
+          value: 'Prof. Dr. Stephan Krusche',
+          disabled: true,
+        },
+        Validators.required,
+      ),
+      location: ['', Validators.required],
+      startDate: [''],
+      workload: [''],
+      contractDuration: [''],
+      fundingType: ['', Validators.required],
+    });
+
+    // Position Details form
+    this.positionDetailsForm = this.fb.group({
+      description: ['', [Validators.maxLength(1000)]],
+      tasks: ['', [Validators.maxLength(1000)]],
+      requirements: ['', [Validators.maxLength(1000)]],
+    });
+
+    // Additional Information form
+    this.additionalInformationForm = this.fb.group({});
+  }
+
+  nextStep(): void {
+    if (this.currentStep < 3) {
+      this.currentStep++;
+    }
+  }
+
+  previousStep(): void {
+    if (this.currentStep > 1) {
+      this.currentStep--;
+    }
+  }
+
+  saveDraft(): void {
+    const jobFormDto: JobFormDTO = {
+      title: this.basicInfoForm.value.title,
+      researchArea: this.basicInfoForm.value.researchArea,
+      fieldOfStudies: this.basicInfoForm.value.fieldOfStudies,
+      supervisingProfessor: '00000000-0000-0000-0000-000000000102',
+      location: this.basicInfoForm.value.location,
+      startDate: this.basicInfoForm.value.startDate,
+      workload: this.basicInfoForm.value.workload,
+      contractDuration: this.basicInfoForm.value.contractDuration,
+      fundingType: this.basicInfoForm.value.fundingType,
+      description: this.positionDetailsForm.value.description,
+      tasks: this.positionDetailsForm.value.tasks,
+      requirements: this.positionDetailsForm.value.requirements,
+      state: JobFormDTO.StateEnum.Draft,
+    };
+    this.jobResourceService.createJob(jobFormDto).subscribe({
+      next() {
+        console.warn('Draft Job successfully created!');
+      },
+      error(err) {
+        console.error('Failed to create job draft:', err);
+      },
+    });
+    console.warn({
+      ...this.basicInfoForm.value,
+      ...this.positionDetailsForm.value,
+      // ...this.additionalInfoForm.value,
+    });
+  }
+
+  publishJob(): void {
+    const jobFormDto: JobFormDTO = {
+      title: this.basicInfoForm.value.title,
+      researchArea: this.basicInfoForm.value.researchArea,
+      fieldOfStudies: this.basicInfoForm.value.fieldOfStudies,
+      supervisingProfessor: '00000000-0000-0000-0000-000000000102',
+      location: this.basicInfoForm.value.location,
+      startDate: this.basicInfoForm.value.startDate,
+      workload: this.basicInfoForm.value.workload,
+      contractDuration: this.basicInfoForm.value.contractDuration,
+      fundingType: this.basicInfoForm.value.fundingType,
+      description: this.positionDetailsForm.value.description,
+      tasks: this.positionDetailsForm.value.tasks,
+      requirements: this.positionDetailsForm.value.requirements,
+      state: JobFormDTO.StateEnum.Published,
+    };
+    this.jobResourceService.createJob(jobFormDto).subscribe({
+      next() {
+        console.warn('Job successfully published!');
+      },
+      error(err) {
+        console.error('Failed to publish job:', err);
+      },
+    });
+    console.warn({
+      ...this.basicInfoForm.value,
+      ...this.positionDetailsForm.value,
+      // ...this.additionalInfoForm.value,
path: src/main/webapp/app/pages/job/jobCreationForm/job-creation-form.component.ts
user: login: az108
body: remove
created_at: 2025-05-11T13:33:36Z
author_association: COLLABORATOR

PR comments: