Skip to content

Commit

Permalink
Feat/edit engagement name (#513)
Browse files Browse the repository at this point in the history
  • Loading branch information
idamand authored Jul 30, 2024
1 parent db9c466 commit e263636
Show file tree
Hide file tree
Showing 11 changed files with 181 additions and 35 deletions.
3 changes: 3 additions & 0 deletions backend/Api/Common/ErrorResponseBody.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@


public record ErrorResponseBody( string code, string message);
20 changes: 12 additions & 8 deletions backend/Api/Projects/ProjectController.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Caching.Memory;


namespace Api.Projects;

[Authorize]
Expand Down Expand Up @@ -172,30 +173,33 @@ public ActionResult<List<StaffingReadModel>> Put([FromRoute] string orgUrlKey,
[HttpPut]
[Route("updateProjectName")]
public ActionResult<List<EngagementReadModel>> Put([FromRoute] string orgUrlKey,
[FromBody] UpdateProjectNameWriteModel projectWriteModel)
[FromBody] UpdateEngagementNameWriteModel engagementWriteModel)
{
// Merk: Service kommer snart via Dependency Injection, da slipper å lage ny hele tiden
var service = new StorageService(_cache, _context);

if (!ProjectControllerValidator.ValidateUpdateProjectNameWriteModel(projectWriteModel, service, orgUrlKey))
return BadRequest("Error in data");
if (ProjectControllerValidator.ValidateUpdateProjectNameAlreadyExist(projectWriteModel, service, orgUrlKey))
{return BadRequest("Name already in use");}
if (!ProjectControllerValidator.ValidateUpdateEngagementNameWriteModel(engagementWriteModel, service, orgUrlKey))
return BadRequest(new ErrorResponseBody("1","Invalid body"));
if (ProjectControllerValidator.ValidateUpdateEngagementNameAlreadyExist(engagementWriteModel, service, orgUrlKey))
{return Conflict(new ErrorResponseBody("1872","Name already in use"));}

try
{
Engagement engagement;
engagement = _context.Project
.Include(p => p.Consultants)
.Include(p => p.Staffings)
.Single(p => p.Id == projectWriteModel.EngagementId);
.Single(p => p.Id == engagementWriteModel.EngagementId);

engagement.Name = projectWriteModel.EngagementName;
engagement.Name = engagementWriteModel.EngagementName;
_context.SaveChanges();

service.ClearConsultantCache(orgUrlKey);

return Ok();
var responseModel =
new EngagementReadModel(engagement.Id, engagement.Name, engagement.State, engagement.IsBillable);

return Ok(responseModel);
}
catch (Exception e)
{
Expand Down
12 changes: 6 additions & 6 deletions backend/Api/Projects/ProjectControllerValidator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,23 +12,23 @@ public static bool ValidateUpdateProjectWriteModel(UpdateProjectWriteModel updat
CheckIfEngagementIsInOrganisation(updateProjectWriteModel.EngagementId, storageService, orgUrlKey);
}

public static bool ValidateUpdateProjectNameWriteModel(UpdateProjectNameWriteModel updateProjectNameWriteModel, StorageService storageService, string orgUrlKey)
public static bool ValidateUpdateEngagementNameWriteModel(UpdateEngagementNameWriteModel updateEngagementNameWriteModel, StorageService storageService, string orgUrlKey)
{
return CheckIfEngagementExists(updateProjectNameWriteModel.EngagementId, storageService) &&
CheckIfEngagementIsInOrganisation(updateProjectNameWriteModel.EngagementId, storageService, orgUrlKey) && !string.IsNullOrWhiteSpace(updateProjectNameWriteModel.EngagementName);
return CheckIfEngagementExists(updateEngagementNameWriteModel.EngagementId, storageService) &&
CheckIfEngagementIsInOrganisation(updateEngagementNameWriteModel.EngagementId, storageService, orgUrlKey) && !string.IsNullOrWhiteSpace(updateEngagementNameWriteModel.EngagementName);
}

public static bool ValidateUpdateProjectNameAlreadyExist(UpdateProjectNameWriteModel updateProjectNameWriteModel,
public static bool ValidateUpdateEngagementNameAlreadyExist(UpdateEngagementNameWriteModel updateEngagementNameWriteModel,
StorageService storageService, string orgUrlKey)
{
var updatedEngagement = storageService.GetProjectById(updateProjectNameWriteModel.EngagementId);
var updatedEngagement = storageService.GetProjectById(updateEngagementNameWriteModel.EngagementId);
if (updatedEngagement is not null)
{
var customer = storageService.GetCustomerFromId(orgUrlKey, updatedEngagement.CustomerId);
if (customer is not null)
{
return customer.Projects.Any(engagement => string.Equals(engagement.Name,
updateProjectNameWriteModel.EngagementName, StringComparison.OrdinalIgnoreCase));
updateEngagementNameWriteModel.EngagementName, StringComparison.OrdinalIgnoreCase));
}
}
return false;
Expand Down
2 changes: 1 addition & 1 deletion backend/Api/Projects/ProjectModels.cs
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ public record ProjectWithCustomerModel(
public record UpdateProjectWriteModel(int EngagementId, EngagementState ProjectState, int StartYear, int StartWeek,
int WeekSpan);

public record UpdateProjectNameWriteModel(int EngagementId, string EngagementName);
public record UpdateEngagementNameWriteModel(int EngagementId, string EngagementName);


public record CustomersWithProjectsReadModel(
Expand Down
4 changes: 2 additions & 2 deletions frontend/src/api-types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -231,11 +231,11 @@ export interface UpdateProjectWriteModel {
weekSpan?: number;
}

export interface UpdateProjectNameWriteModel {
export interface UpdateEngagementNameWriteModel {
/** @format int32 */
engagementId?: number;
/** @minLength 1 */
projectName: string;
engagementName: string;
}

export interface VacationMetaData {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { putWithToken } from "@/data/apiCallsWithToken";
import { putWithToken, putWithTokenNoParse } from "@/data/apiCallsWithToken";
import { updateProjectNameBody } from "@/types";
import { NextResponse } from "next/server";
import { EngagementReadModel } from "@/api-types";
Expand All @@ -10,11 +10,12 @@ export async function PUT(
const orgUrlKey = params.organisation;
const requestBody = (await request.json()) as updateProjectNameBody;

const project =
(await putWithToken<EngagementReadModel, updateProjectNameBody>(
`${orgUrlKey}/projects/updateProjectName`,
requestBody,
)) ?? [];

return NextResponse.json(project);
const response = await putWithTokenNoParse<updateProjectNameBody>(
`${orgUrlKey}/projects/updateProjectName`,
requestBody,
);
if (!response) {
return;
}
return response;
}
7 changes: 6 additions & 1 deletion frontend/src/app/[organisation]/prosjekt/[project]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import Sidebar from "./Sidebar";
import { ConsultantFilterProvider } from "@/hooks/ConsultantFilterProvider";
import { parseYearWeekFromUrlString } from "@/data/urlUtils";
import { fetchWorkHoursPerWeek } from "@/hooks/fetchWorkHoursPerDay";
import EditEngagementName from "@/components/EditEngagementName";

export default async function Project({
params,
Expand Down Expand Up @@ -49,7 +50,11 @@ export default async function Project({
<Sidebar project={project} />
<div className="main p-4 pt-5 w-full flex flex-col gap-8">
<div className="flex flex-col gap-2">
<h1>{project.projectName}</h1>
<EditEngagementName
engagementName={project.projectName}
engagementId={project.projectId}
organisationName={params.organisation}
/>
<h2>{project.customerName}</h2>
</div>

Expand Down
6 changes: 6 additions & 0 deletions frontend/src/app/globals.css
Original file line number Diff line number Diff line change
Expand Up @@ -150,4 +150,10 @@
.rmdp-container .custom-calendar.ep-arrow::after {
box-shadow: none;
}

.h1 {
font-size: 1.625rem;
font-family: "Graphik-Regular", sans-serif;
line-height: 2.5rem;
}
}
107 changes: 107 additions & 0 deletions frontend/src/components/EditEngagementName.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
"use client";
import { UpdateEngagementNameWriteModel } from "@/api-types";
import { useState } from "react";
import { Edit3 } from "react-feather";

export default function EditEngagementName({
engagementName,
engagementId,
organisationName,
}: {
engagementName: string;
engagementId: number;
organisationName: string;
}) {
const [newEngagementName, setNewEngagementName] = useState(engagementName);
const [inputFieldIsActive, setInputFieldIsActive] = useState(false);
const [lastUpdatedName, setLastUpdatedName] = useState(engagementName);
const [inputIsInvalid, setInputIsInvalid] = useState(false);
const [errorMessage, setErrorMessage] = useState("");

async function handleChange(newName: string) {
if (newName === lastUpdatedName) {
setInputFieldIsActive(false);
return;
}
setNewEngagementName(newName);

const body: UpdateEngagementNameWriteModel = {
engagementName: newName,
engagementId: engagementId,
};

const res = await submitAddEngagementForm(body);
const data = await res.json();

if (res.ok) {
setInputFieldIsActive(false);
setLastUpdatedName(newName);
setInputIsInvalid(false);
} else {
setInputIsInvalid(true);
setInputFieldIsActive(true);
if (data.code === "1872") {
setErrorMessage("Prosjektnavnet eksisterer hos kunden fra før");
} else if (data.code === "1") {
setErrorMessage("Prosjektnavn kan ikke være tomt");
} else {
setErrorMessage("Noe gikk galt, spør på slack");
}
}
}

async function submitAddEngagementForm(body: UpdateEngagementNameWriteModel) {
const url = `/${organisationName}/bemanning/api/projects/updateProjectName`;
const res = await fetch(url, {
method: "PUT",
body: JSON.stringify({
...body,
}),
});
return res;
}

return (
<>
{" "}
{inputFieldIsActive ? (
<form
onSubmit={(e) => {
e.preventDefault();
handleChange(newEngagementName);
}}
className="flex flex-col gap-2 "
>
<input
value={newEngagementName}
onChange={(e) => {
setNewEngagementName(e.target.value);
setInputIsInvalid(false);
}}
className={`h1 w-full px-2 ${
inputIsInvalid ? " text-error focus:outline-error" : ""
}`}
autoFocus
onBlur={() => {
setInputFieldIsActive(false);
handleChange(newEngagementName);
}}
/>
{inputIsInvalid && (
<p className="small text-error/80 mb-2 italic">{errorMessage}</p>
)}
</form>
) : (
<div
className="flex flex-row gap-2 items-center"
onClick={() => setInputFieldIsActive(true)}
>
<h1 className="w-fit peer">{newEngagementName} </h1>
<Edit3
className={`text-primary/50 h-6 w-6 m-2 hidden peer-hover:flex`}
/>
</div>
)}
</>
);
}
37 changes: 28 additions & 9 deletions frontend/src/data/apiCallsWithToken.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,15 +13,11 @@ import { ConsultantReadModel, EmployeeItemChewbacca } from "@/api-types";

type HttpMethod = "GET" | "PUT" | "POST" | "DELETE";

export async function callApi<T, Body>(
export async function callApiNoParse<Body>(
path: string,
method: HttpMethod,
bodyInit?: Body,
): Promise<T | undefined> {
if (process.env.NEXT_PUBLIC_NO_AUTH) {
return mockedCall<T>(path);
}

): Promise<Response | undefined> {
const session = await getCustomServerSession(authOptions);

if (!session || !session.access_token) return;
Expand Down Expand Up @@ -51,16 +47,32 @@ export async function callApi<T, Body>(

const completeUrl = `${apiBackendUrl}/${path}`;

const response = await fetch(completeUrl, options);
return response;
}

export async function callApi<T, Body>(
path: string,
method: HttpMethod,
bodyInit?: Body,
): Promise<T | undefined> {
if (process.env.NEXT_PUBLIC_NO_AUTH) {
return mockedCall<T>(path);
}
const session = await getCustomServerSession(authOptions);

if (!session || !session.access_token) return;

try {
const response = await fetch(completeUrl, options);
if (response.status == 204) {
const response = await callApiNoParse(path, method, bodyInit);
if (!response || response.status == 204) {
return;
}
const json = await response.json();
return json as T;
} catch (e) {
console.error(e);
throw new Error(`${options.method} ${completeUrl} failed`);
throw new Error(`${method} ${path} failed`);
}
}

Expand Down Expand Up @@ -159,6 +171,13 @@ export async function callEmployee(path: string) {
}
}

export async function putWithTokenNoParse<BodyType>(
path: string,
body?: BodyType,
): Promise<Response | undefined> {
return callApiNoParse<BodyType>(path, "PUT", body);
}

export async function putWithToken<ReturnType, BodyType>(
path: string,
body?: BodyType,
Expand Down
1 change: 1 addition & 0 deletions frontend/tailwind.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ export default {
background_light_purple: "#423D8908",
background_light_purple_hover: "#423D891A",
text_light_black: "#333333BF",
error: "#B91456",
},
fontSize: {
h1: ["1.625rem", "2.5rem"],
Expand Down

0 comments on commit e263636

Please sign in to comment.