diff --git a/src/app/api/newspaper/single/[catalog_id]/route.ts b/src/app/api/newspaper/single/[catalog_id]/route.ts index 57316e3..cab1842 100644 --- a/src/app/api/newspaper/single/[catalog_id]/route.ts +++ b/src/app/api/newspaper/single/[catalog_id]/route.ts @@ -1,6 +1,9 @@ import {NextRequest, NextResponse} from 'next/server'; import prisma from '@/lib/prisma'; -import {deletePhysicalItemFromCatalog} from '@/services/catalog.data'; +import {deletePhysicalItemFromCatalog, postItemToCatalog, putPhysicalItemInCatalog} from '@/services/catalog.data'; +import {newspaper} from '@prisma/client'; +import {createCatalogNewspaperDtoFromIssue} from '@/models/CatalogNewspaperDto'; +import {createCatalogNewspaperEditDtoFromIssue} from '@/models/CatalogNewspaperEditDto'; // eslint-disable-next-line @typescript-eslint/naming-convention interface IdParams { params: { catalog_id: string} } @@ -8,10 +11,11 @@ interface IdParams { params: { catalog_id: string} } // DELETE api/newspaper/single/[catalog_id] export async function DELETE(req: NextRequest, params: IdParams): Promise { const catalog_id = params.params.catalog_id; - await deletePhysicalItemFromCatalog(catalog_id) + const catalogResponse = await deletePhysicalItemFromCatalog(catalog_id) .catch((e: Error) => { return NextResponse.json({error: `Failed to delete newspaper in catalog: ${e.message}`}, {status: 500}); }); + if (catalogResponse instanceof NextResponse) return catalogResponse; return prisma.newspaper.delete({ // eslint-disable-next-line @typescript-eslint/naming-convention @@ -24,3 +28,52 @@ export async function DELETE(req: NextRequest, params: IdParams): Promise { + const catalog_id = params.params.catalog_id; + + // eslint-disable-next-line @typescript-eslint/naming-convention + const oldIssue = await prisma.newspaper.findUniqueOrThrow({where: {catalog_id}}) + .catch((e: Error) => NextResponse.json({error: `Failed to find newspaper with id ${catalog_id}: ${e.message}`}, {status: 500})); + if (oldIssue instanceof NextResponse) return oldIssue; + + const box = await prisma.box.findUniqueOrThrow({where: {id: oldIssue.box_id}}) + .catch((e: Error) => NextResponse.json({error: `Failed to find box with id ${oldIssue.box_id}: ${e.message}`}, {status: 500})); + if (box instanceof NextResponse) return box; + + const updatedIssue: newspaper = await req.json() as newspaper; + + // If notes or edition is changed, update the manifestation in catalog + if (oldIssue.notes !== updatedIssue.notes || oldIssue.edition !== updatedIssue.edition) { + const catalogPutResponse = await putPhysicalItemInCatalog(createCatalogNewspaperEditDtoFromIssue(updatedIssue)) + .catch((e: Error) => NextResponse.json({error: `Could not update item in catalog: ${e.message}`}, {status: 500})); + if (catalogPutResponse instanceof NextResponse) return catalogPutResponse; + } + + if (oldIssue.received && !updatedIssue.received) { + // Must delete item (but not manifestation) in catalog if issue is changed from received to missing + + const catalogDeleteResponse = await deletePhysicalItemFromCatalog(params.params.catalog_id, false) + .catch((e: Error) => { + return NextResponse.json({error: `Could not update item in catalog: ${e.message}`}, {status: 500}); + }); + if (catalogDeleteResponse instanceof NextResponse) return catalogDeleteResponse; + } else if (!oldIssue.received && updatedIssue.received) { + // If issue was missing, but is now received, add item to catalog + + const catalogPostResponse = await postItemToCatalog(createCatalogNewspaperDtoFromIssue(updatedIssue, String(box.title_id))) + .catch((e: Error) => NextResponse.json({error: `Could not update item in catalog: ${e.message}`}, {status: 500})); + if (catalogPostResponse instanceof NextResponse) return catalogPostResponse; + } + + // Update in database + return prisma.newspaper.update({ + // eslint-disable-next-line @typescript-eslint/naming-convention + where: {catalog_id}, + data: updatedIssue + }) + .then(() => new NextResponse(null, {status: 204})) + .catch((e: Error) => { + return NextResponse.json({error: `Failed to update newspaper in database: ${e.message}`}, {status: 500}); + }); +} diff --git a/src/app/globals.css b/src/app/globals.css index e235df0..f92e016 100644 --- a/src/app/globals.css +++ b/src/app/globals.css @@ -36,6 +36,12 @@ input[type=number] { @apply disabled:bg-gray-400; } +.delete-button-style { + @apply bg-red-400 enabled:hover:bg-red-600; + @apply text-medium font-bold text-black; + @apply disabled:bg-gray-400; +} + .top-title-style { @apply text-4xl font-bold text-black; } diff --git a/src/components/IssueList.tsx b/src/components/IssueList.tsx index 557d5cb..b57e7bb 100644 --- a/src/components/IssueList.tsx +++ b/src/components/IssueList.tsx @@ -1,14 +1,16 @@ import {box, newspaper, title} from '@prisma/client'; import React, {ChangeEvent, useCallback, useEffect, useState} from 'react'; -import {deleteIssue, getNewspapersForBoxOnTitle, postNewIssuesForTitle} from '@/services/local.data'; +import {deleteIssue, getNewspapersForBoxOnTitle, postNewIssuesForTitle, putIssue} from '@/services/local.data'; import {ErrorMessage, Field, FieldArray, Form, Formik, FormikErrors, FormikValues} from 'formik'; -import {FaTrash} from 'react-icons/fa'; -import {Button, CalendarDate, DatePicker, Spinner, Switch, Table} from '@nextui-org/react'; +import {FaSave, FaTrash} from 'react-icons/fa'; +import {Button, CalendarDate, DatePicker, Spinner, Switch, Table, Tooltip} from '@nextui-org/react'; import {TableBody, TableCell, TableColumn, TableHeader, TableRow} from '@nextui-org/table'; import ErrorModal from '@/components/ErrorModal'; import {newNewspapersContainsDuplicateEditions, newspapersContainsEdition} from '@/utils/validationUtils'; import {parseDate} from '@internationalized/date'; import ConfirmationModal from '@/components/ConfirmationModal'; +import {FiEdit} from 'react-icons/fi'; +import {ImCross} from 'react-icons/im'; export default function IssueList(props: {title: title; box: box}) { @@ -20,6 +22,8 @@ export default function IssueList(props: {title: title; box: box}) { const [showSuccess, setShowSuccess] = useState(false); const [saveWarning, setSaveWarning] = useState(''); const [issueToDelete, setIssueToDelete] = useState(''); + const [issueIndexToEdit, setIssueIndexToEdit] = useState(undefined); + const [issueBeingSaved, setIssueBeingSaved] = useState(false); const initialValues = { issues }; @@ -132,10 +136,27 @@ export default function IssueList(props: {title: title; box: box}) { return {issues: errors} as FormikErrors; } - function newspaperIsSaved(index: number, arrayLength: number) { + function newspaperIsSaved(index: number, arrayLength: number): boolean { return index >= arrayLength - nIssuesInDb; } + function isEditingIssue(index?: number): boolean { + if (index || index === 0) return issueIndexToEdit === index; + return issueIndexToEdit !== undefined; + } + + function shouldDisableIssue(index: number, arrayLength: number): boolean { + return newspaperIsSaved(index, arrayLength) && !isEditingIssue(index); + } + + function startEditingIssue(index: number) { + if (issueIndexToEdit === undefined) setIssueIndexToEdit(index); + } + + function stopEditingIssue() { + setIssueIndexToEdit(undefined); + } + function showSuccessMessage() { setShowSuccess(true); setTimeout(() => { @@ -158,6 +179,20 @@ export default function IssueList(props: {title: title; box: box}) { return parseDate(new Date(usedDate).toISOString().split('T')[0]); } + function updateIssue(issue: newspaper) { + setIssueBeingSaved(true); + void putIssue(issue) + .then(res => { + if (res.ok) { + stopEditingIssue(); + } else { + setErrorText('Kunne ikke lagre avisutgave.'); + } + }) + .catch(() => setErrorText('Kunne ikke lagre avisutgave.')) + .finally(() => setIssueBeingSaved(false)); + } + return (
{ loading ? ( @@ -208,12 +243,18 @@ export default function IssueList(props: {title: title; box: box}) {

{saveWarning}

- + + + +
@@ -225,11 +266,11 @@ export default function IssueList(props: {title: title; box: box}) { Nummer Mottatt Kommentar - Slett + Slett {values.issues.map((issue, index) => ( - + {dayOfWeek(issue.date)} @@ -239,7 +280,7 @@ export default function IssueList(props: {title: title; box: box}) { id={`issues.${index}.date`} value={dateToCalendarDate(issue.date)} onChange={val => void setFieldValue(`issues.${index}.date`, val.toDate('UTC'))} - isDisabled={newspaperIsSaved(index, values.issues.length)} + isDisabled={shouldDisableIssue(index, values.issues.length)} popoverProps={{placement: 'right'}} /> { checkForDuplicateEditionsAndShowWarning((e.nativeEvent as InputEvent).data ?? '', values.issues); handleChange(e); @@ -270,7 +311,7 @@ export default function IssueList(props: {title: title; box: box}) { void setFieldValue(`issues.${index}.received`, value.target.checked)} > {issue.received ? 'Mottatt' : 'Ikke mottatt'} @@ -279,13 +320,49 @@ export default function IssueList(props: {title: title; box: box}) { name={`issues.${index}.notes`} className="border" type="text" - disabled={newspaperIsSaved(index, values.issues.length)} + disabled={shouldDisableIssue(index, values.issues.length)} value={issue.notes || ''} /> - + + + } + + : + + } + + } + + + ))} diff --git a/src/models/CatalogNewspaperEditDto.ts b/src/models/CatalogNewspaperEditDto.ts new file mode 100644 index 0000000..b416eab --- /dev/null +++ b/src/models/CatalogNewspaperEditDto.ts @@ -0,0 +1,22 @@ +import {newspaper} from '@prisma/client'; + + +export interface CatalogNewspaperEditDto { + manifestationId: string; + username: string; + notes: string; + // eslint-disable-next-line id-denylist + number: string; +} + +export function createCatalogNewspaperEditDtoFromIssue( + issue: newspaper +): CatalogNewspaperEditDto { + return { + manifestationId: issue.catalog_id, + username: 'Hugin stage', // TODO replace with actual username when auth is present + notes: issue.notes ?? '', + // eslint-disable-next-line id-denylist + number: issue.edition ?? '' + }; +} diff --git a/src/services/catalog.data.ts b/src/services/catalog.data.ts index 6c11458..8680e78 100644 --- a/src/services/catalog.data.ts +++ b/src/services/catalog.data.ts @@ -4,6 +4,7 @@ import {CatalogNewspaperDto} from '@/models/CatalogNewspaperDto'; import {CatalogMissingNewspaperDto} from '@/models/CatalogMissingNewspaperDto'; import {CatalogItem} from '@/models/CatalogItem'; import {KeycloakToken} from '@/models/KeycloakToken'; +import {CatalogNewspaperEditDto} from '@/models/CatalogNewspaperEditDto'; export async function searchNewspaperTitlesInCatalog(searchTerm: string, signal: AbortSignal): Promise { return fetch( @@ -49,11 +50,11 @@ export async function postItemToCatalog(issue: CatalogNewspaperDto): Promise; } else { - return Promise.reject(new Error(`Failed to create title in catalog: ${response.status} - ${await response.json()}`)); + return Promise.reject(new Error(`Failed to create issue in catalog: ${response.status} - ${await response.json()}`)); } }) .catch((e: Error) => { - return Promise.reject(new Error(`Failed to create title in catalog: ${e.message}`)); + return Promise.reject(new Error(`Failed to create issue in catalog: ${e.message}`)); }); } @@ -81,10 +82,11 @@ export async function postMissingItemToCatalog(issue: CatalogMissingNewspaperDto }); } -export async function deletePhysicalItemFromCatalog(catalog_id: string): Promise { +export async function deletePhysicalItemFromCatalog(catalog_id: string, deleteManifestation?: boolean): Promise { const token = await getKeycloakTekstToken(); + const queryParams = deleteManifestation !== undefined ? `?deleteManifestation=${deleteManifestation}` : ''; - return fetch(`${process.env.CATALOGUE_API_PATH}/newspapers/items/physical/${catalog_id}`, { + return fetch(`${process.env.CATALOGUE_API_PATH}/newspapers/items/physical/${catalog_id}${queryParams}`, { method: 'DELETE', headers: { 'Content-Type': 'application/json', @@ -104,6 +106,30 @@ export async function deletePhysicalItemFromCatalog(catalog_id: string): Promise }); } +export async function putPhysicalItemInCatalog(issue: CatalogNewspaperEditDto): Promise { + const token = await getKeycloakTekstToken(); + + return fetch(`${process.env.CATALOGUE_API_PATH}/newspapers/items`, { + method: 'PUT', + headers: { + 'Content-Type': 'application/json', + // eslint-disable-next-line @typescript-eslint/naming-convention + Authorization: `Bearer ${token.access_token}` + }, + body: JSON.stringify(issue) + }) + .then(async response => { + if (response.ok) { + return Promise.resolve(); + } else { + return Promise.reject(new Error(`Failed to update issue in catalog: ${response.status} - ${await response.json()}`)); + } + }) + .catch((e: Error) => { + return Promise.reject(new Error(`Failed to update issue in catalog: ${e.message}`)); + }); +} + async function getKeycloakTekstToken(): Promise { const body = `client_id=${process.env.KEYCLOAK_TEKST_CLIENT_ID}` + `&client_secret=${process.env.KEYCLOAK_TEKST_CLIENT_SECRET}` + diff --git a/src/services/local.data.ts b/src/services/local.data.ts index 8d4c5c7..3387507 100644 --- a/src/services/local.data.ts +++ b/src/services/local.data.ts @@ -167,3 +167,13 @@ export async function deleteIssue(catalog_id: string): Promise { return Promise.reject(new Error(`Failed to delete newspaper issue: ${e.message}`)); }); } + +export async function putIssue(issue: newspaper): Promise { + return await fetch(`${process.env.NEXT_PUBLIC_BASE_PATH}/api/newspaper/single/${issue.catalog_id}`, { + method: 'PUT', + headers: {'Content-Type': 'application/json'}, + body: JSON.stringify(issue) + }).catch((e: Error) => { + return Promise.reject(new Error(`Failed to update newspaper issue: ${e.message}`)); + }); +}