Skip to content

Commit

Permalink
feat: Add editing of newspaper items (TT-1706) (#48)
Browse files Browse the repository at this point in the history
Co-authored-by: Fredrik Monsen <[email protected]>
  • Loading branch information
MariusLevang and fredrikmonsen authored Sep 6, 2024
1 parent dfeb2f9 commit cca1e4a
Show file tree
Hide file tree
Showing 6 changed files with 219 additions and 25 deletions.
57 changes: 55 additions & 2 deletions src/app/api/newspaper/single/[catalog_id]/route.ts
Original file line number Diff line number Diff line change
@@ -1,17 +1,21 @@
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} }

// DELETE api/newspaper/single/[catalog_id]
export async function DELETE(req: NextRequest, params: IdParams): Promise<NextResponse> {
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
Expand All @@ -24,3 +28,52 @@ export async function DELETE(req: NextRequest, params: IdParams): Promise<NextRe
return NextResponse.json({error: `Failed to delete newspapers: ${e.message}`}, {status: 500});
});
}

export async function PUT(req: NextRequest, params: IdParams): Promise<NextResponse> {
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});
});
}
6 changes: 6 additions & 0 deletions src/app/globals.css
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Expand Down
115 changes: 96 additions & 19 deletions src/components/IssueList.tsx
Original file line number Diff line number Diff line change
@@ -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}) {
Expand All @@ -20,6 +22,8 @@ export default function IssueList(props: {title: title; box: box}) {
const [showSuccess, setShowSuccess] = useState<boolean>(false);
const [saveWarning, setSaveWarning] = useState<string>('');
const [issueToDelete, setIssueToDelete] = useState<string>('');
const [issueIndexToEdit, setIssueIndexToEdit] = useState<number|undefined>(undefined);
const [issueBeingSaved, setIssueBeingSaved] = useState<boolean>(false);

const initialValues = { issues };

Expand Down Expand Up @@ -132,10 +136,27 @@ export default function IssueList(props: {title: title; box: box}) {
return {issues: errors} as FormikErrors<newspaper>;
}

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(() => {
Expand All @@ -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 (
<div className='w-full mb-6 mt-4 py-10 border-style m-30'>
{ loading ? (
Expand Down Expand Up @@ -208,12 +243,18 @@ export default function IssueList(props: {title: title; box: box}) {

<div className='flex flex-row items-center'>
<p className='mr-2'>{saveWarning}</p>
<Button
className="save-button-style min-w-28"
type="submit"
disabled={isSubmitting}
startContent={isSubmitting && <Spinner className='ml-1' size='sm'/>}
>Lagre</Button>

<Tooltip
content='Lagre aller avbryt endring av avisutgave først'
isDisabled={!isEditingIssue()}
>
<Button
className="save-button-style min-w-28"
type="submit"
disabled={isSubmitting || isEditingIssue()}
startContent={isSubmitting && <Spinner className='ml-1' size='sm'/>}
>Lagre</Button>
</Tooltip>
</div>
</div>

Expand All @@ -225,11 +266,11 @@ export default function IssueList(props: {title: title; box: box}) {
<TableColumn align='center' className="text-lg">Nummer</TableColumn>
<TableColumn align='center' className="text-lg">Mottatt</TableColumn>
<TableColumn align='center' className="text-lg">Kommentar</TableColumn>
<TableColumn align='center' hideHeader={true} className="text-lg">Slett</TableColumn>
<TableColumn align='end' hideHeader={true} className="text-lg">Slett</TableColumn>
</TableHeader>
<TableBody>
{values.issues.map((issue, index) => (
<TableRow key={index}>
<TableRow key={index} className={isEditingIssue(index) ? 'bg-blue-200' : ''}>
<TableCell className="text-lg">
{dayOfWeek(issue.date)}
</TableCell>
Expand All @@ -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'}}
/>
<ErrorMessage
Expand All @@ -254,7 +295,7 @@ export default function IssueList(props: {title: title; box: box}) {
className="max-w-16 border text-center"
type="text"
width='40'
disabled={newspaperIsSaved(index, values.issues.length)}
disabled={shouldDisableIssue(index, values.issues.length)}
onChange={(e: ChangeEvent) => {
checkForDuplicateEditionsAndShowWarning((e.nativeEvent as InputEvent).data ?? '', values.issues);
handleChange(e);
Expand All @@ -270,7 +311,7 @@ export default function IssueList(props: {title: title; box: box}) {
<Switch
name={`issues.${index}.received`}
isSelected={issue.received ?? false}
isDisabled={newspaperIsSaved(index, values.issues.length)}
isDisabled={shouldDisableIssue(index, values.issues.length)}
onChange={value => void setFieldValue(`issues.${index}.received`, value.target.checked)}
> {issue.received ? 'Mottatt' : 'Ikke mottatt'} </Switch>
</TableCell>
Expand All @@ -279,22 +320,58 @@ 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 || ''}
/>
</TableCell>
<TableCell className="text-lg">
<button
{newspaperIsSaved(index, values.issues.length) &&
<>
{isEditingIssue(index) ?
<>
{issueBeingSaved ?
<Spinner size='sm' className='mr-2'/>
:
<>
<Button isIconOnly
className='save-button-style mr-1 [&]:text-medium [&]:bg-green-400'
type='button'
onClick={() => updateIssue(issue)}>
<FaSave/>
</Button>
<Button isIconOnly
className='abort-button-style mr-1'
type='button'
onClick={() => stopEditingIssue()}>
<ImCross/>
</Button>
</>
}
</>
:
<Button isIconOnly
className={isEditingIssue() ? 'opacity-25 mr-0.5' : 'edit-button-style [&]:text-medium mr-0.5'}
type='button'
disabled={isEditingIssue()}
onClick={() => startEditingIssue(index)}
>
<FiEdit/>
</Button>
}
</>
}
<Button isIconOnly
type="button"
className='delete-button-style'
onClick={() => {
if (!newspaperIsSaved(index, values.issues.length)) {
remove(index);
} else {
setIssueToDelete(issue.catalog_id);
}
}}>
<FaTrash/>
</button>
<FaTrash size={16}/>
</Button>
</TableCell>
</TableRow>
))}
Expand Down
22 changes: 22 additions & 0 deletions src/models/CatalogNewspaperEditDto.ts
Original file line number Diff line number Diff line change
@@ -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 ?? ''
};
}
34 changes: 30 additions & 4 deletions src/services/catalog.data.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<CatalogTitle[]> {
return fetch(
Expand Down Expand Up @@ -49,11 +50,11 @@ export async function postItemToCatalog(issue: CatalogNewspaperDto): Promise<Cat
if (response.ok) {
return await response.json() as Promise<CatalogItem>;
} 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}`));
});
}

Expand Down Expand Up @@ -81,10 +82,11 @@ export async function postMissingItemToCatalog(issue: CatalogMissingNewspaperDto
});
}

export async function deletePhysicalItemFromCatalog(catalog_id: string): Promise<void> {
export async function deletePhysicalItemFromCatalog(catalog_id: string, deleteManifestation?: boolean): Promise<void> {
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',
Expand All @@ -104,6 +106,30 @@ export async function deletePhysicalItemFromCatalog(catalog_id: string): Promise
});
}

export async function putPhysicalItemInCatalog(issue: CatalogNewspaperEditDto): Promise<void> {
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<KeycloakToken> {
const body = `client_id=${process.env.KEYCLOAK_TEKST_CLIENT_ID}` +
`&client_secret=${process.env.KEYCLOAK_TEKST_CLIENT_SECRET}` +
Expand Down
Loading

0 comments on commit cca1e4a

Please sign in to comment.