From e263636c1566e82a4b31a097119c2df4495023db Mon Sep 17 00:00:00 2001 From: Ida Marie Andreassen <43541032+idamand@users.noreply.github.com> Date: Tue, 30 Jul 2024 13:35:36 +0200 Subject: [PATCH] Feat/edit engagement name (#513) --- backend/Api/Common/ErrorResponseBody.cs | 3 + backend/Api/Projects/ProjectController.cs | 20 ++-- .../Projects/ProjectControllerValidator.cs | 12 +- backend/Api/Projects/ProjectModels.cs | 2 +- frontend/src/api-types.ts | 4 +- .../api/projects/updateProjectName/route.ts | 17 +-- .../prosjekt/[project]/page.tsx | 7 +- frontend/src/app/globals.css | 6 + .../src/components/EditEngagementName.tsx | 107 ++++++++++++++++++ frontend/src/data/apiCallsWithToken.ts | 37 ++++-- frontend/tailwind.config.ts | 1 + 11 files changed, 181 insertions(+), 35 deletions(-) create mode 100644 backend/Api/Common/ErrorResponseBody.cs create mode 100644 frontend/src/components/EditEngagementName.tsx diff --git a/backend/Api/Common/ErrorResponseBody.cs b/backend/Api/Common/ErrorResponseBody.cs new file mode 100644 index 00000000..57027d2e --- /dev/null +++ b/backend/Api/Common/ErrorResponseBody.cs @@ -0,0 +1,3 @@ + + +public record ErrorResponseBody( string code, string message); \ No newline at end of file diff --git a/backend/Api/Projects/ProjectController.cs b/backend/Api/Projects/ProjectController.cs index c6e9fe41..74dec96c 100644 --- a/backend/Api/Projects/ProjectController.cs +++ b/backend/Api/Projects/ProjectController.cs @@ -7,6 +7,7 @@ using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Caching.Memory; + namespace Api.Projects; [Authorize] @@ -172,15 +173,15 @@ public ActionResult> Put([FromRoute] string orgUrlKey, [HttpPut] [Route("updateProjectName")] public ActionResult> 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 { @@ -188,14 +189,17 @@ public ActionResult> Put([FromRoute] string orgUrlKey, 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) { diff --git a/backend/Api/Projects/ProjectControllerValidator.cs b/backend/Api/Projects/ProjectControllerValidator.cs index 0234089f..9fa6af77 100644 --- a/backend/Api/Projects/ProjectControllerValidator.cs +++ b/backend/Api/Projects/ProjectControllerValidator.cs @@ -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; diff --git a/backend/Api/Projects/ProjectModels.cs b/backend/Api/Projects/ProjectModels.cs index c95c4192..f03d4c54 100644 --- a/backend/Api/Projects/ProjectModels.cs +++ b/backend/Api/Projects/ProjectModels.cs @@ -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( diff --git a/frontend/src/api-types.ts b/frontend/src/api-types.ts index 8ce8f600..c1441569 100644 --- a/frontend/src/api-types.ts +++ b/frontend/src/api-types.ts @@ -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 { diff --git a/frontend/src/app/[organisation]/bemanning/api/projects/updateProjectName/route.ts b/frontend/src/app/[organisation]/bemanning/api/projects/updateProjectName/route.ts index 4e44e5af..9b0d6451 100644 --- a/frontend/src/app/[organisation]/bemanning/api/projects/updateProjectName/route.ts +++ b/frontend/src/app/[organisation]/bemanning/api/projects/updateProjectName/route.ts @@ -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"; @@ -10,11 +10,12 @@ export async function PUT( const orgUrlKey = params.organisation; const requestBody = (await request.json()) as updateProjectNameBody; - const project = - (await putWithToken( - `${orgUrlKey}/projects/updateProjectName`, - requestBody, - )) ?? []; - - return NextResponse.json(project); + const response = await putWithTokenNoParse( + `${orgUrlKey}/projects/updateProjectName`, + requestBody, + ); + if (!response) { + return; + } + return response; } diff --git a/frontend/src/app/[organisation]/prosjekt/[project]/page.tsx b/frontend/src/app/[organisation]/prosjekt/[project]/page.tsx index a87a4c6b..af8f4989 100644 --- a/frontend/src/app/[organisation]/prosjekt/[project]/page.tsx +++ b/frontend/src/app/[organisation]/prosjekt/[project]/page.tsx @@ -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, @@ -49,7 +50,11 @@ export default async function Project({
-

{project.projectName}

+

{project.customerName}

diff --git a/frontend/src/app/globals.css b/frontend/src/app/globals.css index 1d1edd82..9ff35eb8 100644 --- a/frontend/src/app/globals.css +++ b/frontend/src/app/globals.css @@ -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; + } } diff --git a/frontend/src/components/EditEngagementName.tsx b/frontend/src/components/EditEngagementName.tsx new file mode 100644 index 00000000..d9b6acf6 --- /dev/null +++ b/frontend/src/components/EditEngagementName.tsx @@ -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 ? ( +
{ + e.preventDefault(); + handleChange(newEngagementName); + }} + className="flex flex-col gap-2 " + > + { + 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 && ( +

{errorMessage}

+ )} +
+ ) : ( +
setInputFieldIsActive(true)} + > +

{newEngagementName}

+
+ )} + + ); +} diff --git a/frontend/src/data/apiCallsWithToken.ts b/frontend/src/data/apiCallsWithToken.ts index 02712bcf..d30a6fbd 100644 --- a/frontend/src/data/apiCallsWithToken.ts +++ b/frontend/src/data/apiCallsWithToken.ts @@ -13,15 +13,11 @@ import { ConsultantReadModel, EmployeeItemChewbacca } from "@/api-types"; type HttpMethod = "GET" | "PUT" | "POST" | "DELETE"; -export async function callApi( +export async function callApiNoParse( path: string, method: HttpMethod, bodyInit?: Body, -): Promise { - if (process.env.NEXT_PUBLIC_NO_AUTH) { - return mockedCall(path); - } - +): Promise { const session = await getCustomServerSession(authOptions); if (!session || !session.access_token) return; @@ -51,16 +47,32 @@ export async function callApi( const completeUrl = `${apiBackendUrl}/${path}`; + const response = await fetch(completeUrl, options); + return response; +} + +export async function callApi( + path: string, + method: HttpMethod, + bodyInit?: Body, +): Promise { + if (process.env.NEXT_PUBLIC_NO_AUTH) { + return mockedCall(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`); } } @@ -159,6 +171,13 @@ export async function callEmployee(path: string) { } } +export async function putWithTokenNoParse( + path: string, + body?: BodyType, +): Promise { + return callApiNoParse(path, "PUT", body); +} + export async function putWithToken( path: string, body?: BodyType, diff --git a/frontend/tailwind.config.ts b/frontend/tailwind.config.ts index ca2779d3..e24c30ae 100644 --- a/frontend/tailwind.config.ts +++ b/frontend/tailwind.config.ts @@ -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"],