N-FIN-42: add default category to entity (#53)

Resolves #42
This commit is contained in:
Markus Thielker 2024-03-17 12:52:44 +01:00 committed by GitHub
commit 1b0c5bf6e1
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
10 changed files with 118 additions and 16 deletions

View file

@ -0,0 +1,7 @@
-- AlterTable
ALTER TABLE "entities"
ADD COLUMN "default_category_id" INTEGER;
-- AddForeignKey
ALTER TABLE "entities"
ADD CONSTRAINT "entities_default_category_id_fkey" FOREIGN KEY ("default_category_id") REFERENCES "categories" ("id") ON DELETE SET NULL ON UPDATE CASCADE;

View file

@ -34,13 +34,15 @@ model Session {
} }
model Entity { model Entity {
id Int @id @default(autoincrement()) id Int @id @default(autoincrement())
userId String @map("user_id") userId String @map("user_id")
user User @relation(fields: [userId], references: [id]) user User @relation(fields: [userId], references: [id])
name String name String
type EntityType type EntityType
createdAt DateTime @default(now()) @map("created_at") defaultCategory Category? @relation(fields: [defaultCategoryId], references: [id])
updatedAt DateTime @updatedAt @map("updated_at") defaultCategoryId Int? @map("default_category_id")
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @updatedAt @map("updated_at")
paymentsAsPayor Payment[] @relation("PayorEntity") paymentsAsPayor Payment[] @relation("PayorEntity")
paymentsAsPayee Payment[] @relation("PayeeEntity") paymentsAsPayee Payment[] @relation("PayeeEntity")
@ -84,6 +86,7 @@ model Category {
updatedAt DateTime @updatedAt @map("updated_at") updatedAt DateTime @updatedAt @map("updated_at")
payments Payment[] payments Payment[]
Entity Entity[]
@@unique(fields: [userId, name]) @@unique(fields: [userId, name])
@@map("categories") @@map("categories")

View file

@ -1,12 +1,13 @@
'use client'; 'use client';
import { ColumnDef } from '@tanstack/react-table'; import { ColumnDef } from '@tanstack/react-table';
import { Entity } from '@prisma/client'; import { Category, Entity } from '@prisma/client';
import { CellContext, ColumnDefTemplate } from '@tanstack/table-core'; import { CellContext, ColumnDefTemplate } from '@tanstack/table-core';
import { format } from 'date-fns'; import { format } from 'date-fns';
export const columns = ( export const columns = (
actionCell: ColumnDefTemplate<CellContext<Entity, unknown>>, actionCell: ColumnDefTemplate<CellContext<Entity, unknown>>,
categories: Category[],
) => { ) => {
return [ return [
@ -19,6 +20,30 @@ export const columns = (
header: 'Type', header: 'Type',
size: 100, size: 100,
}, },
{
accessorKey: 'defaultCategoryId',
header: 'Default Category',
cell: ({row}) => {
const category = categories.find((category) => category.id === row.original.defaultCategoryId);
return (
<>
{
category && (
<div className="flex items-center space-x-4">
<svg className="h-5" fill={category?.color} viewBox="0 0 20 20"
xmlns="http://www.w3.org/2000/svg">
<circle cx="10" cy="10" r="10"/>
</svg>
<p>{category?.name ?? '-'}</p>
</div>
)
}
</>
);
},
size: 200,
},
{ {
accessorKey: 'createdAt', accessorKey: 'createdAt',
header: 'Created at', header: 'Created at',

View file

@ -23,9 +23,24 @@ export default async function EntitiesPage() {
], ],
}); });
const categories = await prismaClient.category.findMany({
where: {
userId: user?.id,
},
orderBy: [
{
name: 'asc',
},
{
id: 'asc',
},
],
});
return ( return (
<EntityPageClientContent <EntityPageClientContent
entities={entities} entities={entities}
categories={categories}
onSubmit={entityCreateUpdate} onSubmit={entityCreateUpdate}
onDelete={entityDelete} onDelete={entityDelete}
className="flex flex-col justify-center space-y-4"/> className="flex flex-col justify-center space-y-4"/>

View file

@ -1,6 +1,6 @@
'use client'; 'use client';
import { Entity } from '@prisma/client'; import { Category, Entity } from '@prisma/client';
import React, { useState } from 'react'; import React, { useState } from 'react';
import { CellContext } from '@tanstack/table-core'; import { CellContext } from '@tanstack/table-core';
import { Button } from '@/components/ui/button'; import { Button } from '@/components/ui/button';
@ -27,8 +27,9 @@ import {
import { useMediaQuery } from '@/lib/hooks/useMediaQuery'; import { useMediaQuery } from '@/lib/hooks/useMediaQuery';
import { Drawer, DrawerContent, DrawerHeader, DrawerTitle, DrawerTrigger } from '@/components/ui/drawer'; import { Drawer, DrawerContent, DrawerHeader, DrawerTitle, DrawerTrigger } from '@/components/ui/drawer';
export default function EntityPageClientContent({entities, onSubmit, onDelete, className}: { export default function EntityPageClientContent({entities, categories, onSubmit, onDelete, className}: {
entities: Entity[], entities: Entity[],
categories: Category[],
onSubmit: (data: z.infer<typeof entityFormSchema>) => Promise<ActionResponse>, onSubmit: (data: z.infer<typeof entityFormSchema>) => Promise<ActionResponse>,
onDelete: (id: number) => Promise<ActionResponse>, onDelete: (id: number) => Promise<ActionResponse>,
className: string, className: string,
@ -146,6 +147,7 @@ export default function EntityPageClientContent({entities, onSubmit, onDelete, c
</DialogHeader> </DialogHeader>
<EntityForm <EntityForm
value={selectedEntity} value={selectedEntity}
categories={categories}
onSubmit={handleSubmit} onSubmit={handleSubmit}
className="grid grid-cols-1 md:grid-cols-2 gap-4 py-4"/> className="grid grid-cols-1 md:grid-cols-2 gap-4 py-4"/>
</DialogContent> </DialogContent>
@ -167,6 +169,7 @@ export default function EntityPageClientContent({entities, onSubmit, onDelete, c
</DrawerHeader> </DrawerHeader>
<EntityForm <EntityForm
value={selectedEntity} value={selectedEntity}
categories={categories}
onSubmit={handleSubmit} onSubmit={handleSubmit}
className="grid grid-cols-1 md:grid-cols-2 gap-4 py-4"/> className="grid grid-cols-1 md:grid-cols-2 gap-4 py-4"/>
</DrawerContent> </DrawerContent>
@ -184,7 +187,7 @@ export default function EntityPageClientContent({entities, onSubmit, onDelete, c
{/* Data Table */} {/* Data Table */}
<DataTable <DataTable
className="w-full" className="w-full"
columns={columns(actionCell)} columns={columns(actionCell, categories)}
data={filterEntities(entities, filter)} data={filterEntities(entities, filter)}
pagination/> pagination/>

View file

@ -12,11 +12,13 @@ import { useRouter } from 'next/navigation';
import { toast } from 'sonner'; import { toast } from 'sonner';
import { sonnerContent } from '@/components/ui/sonner'; import { sonnerContent } from '@/components/ui/sonner';
import { entityFormSchema } from '@/lib/form-schemas/entityFormSchema'; import { entityFormSchema } from '@/lib/form-schemas/entityFormSchema';
import { Entity, EntityType } from '@prisma/client'; import { Category, Entity, EntityType } from '@prisma/client';
import { Select, SelectContent, SelectGroup, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'; import { Select, SelectContent, SelectGroup, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
import { AutoCompleteInput } from '@/components/ui/auto-complete-input';
export default function EntityForm({value, onSubmit, className}: { export default function EntityForm({value, categories, onSubmit, className}: {
value: Entity | undefined, value: Entity | undefined,
categories: Category[],
onSubmit: (data: z.infer<typeof entityFormSchema>) => Promise<ActionResponse> onSubmit: (data: z.infer<typeof entityFormSchema>) => Promise<ActionResponse>
className?: string className?: string
}) { }) {
@ -29,6 +31,7 @@ export default function EntityForm({value, onSubmit, className}: {
id: value?.id ?? undefined, id: value?.id ?? undefined,
name: value?.name ?? '', name: value?.name ?? '',
type: value?.type ?? EntityType.Entity, type: value?.type ?? EntityType.Entity,
defaultCategoryId: value?.defaultCategoryId ?? undefined,
}, },
}); });
@ -40,6 +43,13 @@ export default function EntityForm({value, onSubmit, className}: {
} }
}; };
const categoriesMapped = categories?.map((category) => {
return {
label: category.name,
value: category.id,
};
}) ?? [];
return ( return (
<Form {...form}> <Form {...form}>
<form autoComplete="off" onSubmit={form.handleSubmit(handleSubmit)}> <form autoComplete="off" onSubmit={form.handleSubmit(handleSubmit)}>
@ -94,6 +104,22 @@ export default function EntityForm({value, onSubmit, className}: {
</FormItem> </FormItem>
)} )}
/> />
<FormField
control={form.control}
name="defaultCategoryId"
render={({field}) => (
<FormItem>
<FormLabel>Category</FormLabel>
<FormControl>
<AutoCompleteInput
placeholder="Select category"
items={categoriesMapped}
{...field} />
</FormControl>
<FormMessage/>
</FormItem>
)}
/>
</div> </div>
<Button type="submit" className="w-full">{value?.id ? 'Update Entity' : 'Create Entity'}</Button> <Button type="submit" className="w-full">{value?.id ? 'Update Entity' : 'Create Entity'}</Button>
</form> </form>

View file

@ -166,7 +166,17 @@ export default function PaymentForm({value, entities, categories, onSubmit, clas
placeholder="Select payee" placeholder="Select payee"
items={entitiesMapped} items={entitiesMapped}
next={categoryRef} next={categoryRef}
{...field} /> {...field}
onChange={(e) => {
field.onChange(e);
if (e && e.target.value) {
const entity = entities.find((entity) => entity.id === Number(e.target.value));
console.log(entity?.defaultCategoryId);
if (entity?.defaultCategoryId !== null) {
form.setValue('categoryId', entity?.defaultCategoryId);
}
}
}}/>
</FormControl> </FormControl>
<FormMessage/> <FormMessage/>
</FormItem> </FormItem>

View file

@ -13,12 +13,12 @@ export interface AutoCompleteInputProps
const AutoCompleteInput = React.forwardRef<HTMLInputElement, AutoCompleteInputProps>( const AutoCompleteInput = React.forwardRef<HTMLInputElement, AutoCompleteInputProps>(
({className, type, ...props}, ref) => { ({className, type, ...props}, ref) => {
const [value, setValue] = useState(getInitialValue()); const [value, setValue] = useState(getNameOfPropValue());
const [open, setOpen] = useState(false); const [open, setOpen] = useState(false);
const [lastKey, setLastKey] = useState(''); const [lastKey, setLastKey] = useState('');
const [filteredItems, setFilteredItems] = useState(props.items); const [filteredItems, setFilteredItems] = useState(props.items);
function getInitialValue() { function getNameOfPropValue() {
if (!props.items) { if (!props.items) {
return ''; return '';
@ -50,6 +50,15 @@ const AutoCompleteInput = React.forwardRef<HTMLInputElement, AutoCompleteInputPr
} }
}, [filteredItems]); }, [filteredItems]);
useEffect(() => {
console.log('Prop value changed', value, props.value);
if (props.value) {
setValue(getNameOfPropValue());
} else {
setValue('');
}
}, [props.value]);
return ( return (
<div className="relative"> <div className="relative">
<input <input

View file

@ -9,6 +9,7 @@ export default async function entityCreateUpdate({
id, id,
name, name,
type, type,
defaultCategoryId,
}: z.infer<typeof entityFormSchema>): Promise<ActionResponse> { }: z.infer<typeof entityFormSchema>): Promise<ActionResponse> {
'use server'; 'use server';
@ -32,6 +33,7 @@ export default async function entityCreateUpdate({
data: { data: {
name: name, name: name,
type: type, type: type,
defaultCategoryId: defaultCategoryId,
}, },
}, },
); );
@ -47,6 +49,7 @@ export default async function entityCreateUpdate({
userId: user.id, userId: user.id,
name: name, name: name,
type: type, type: type,
defaultCategoryId: defaultCategoryId,
}, },
}); });

View file

@ -5,4 +5,5 @@ export const entityFormSchema = z.object({
id: z.number().positive().optional(), id: z.number().positive().optional(),
name: z.string().min(1).max(32), name: z.string().min(1).max(32),
type: z.nativeEnum(EntityType), type: z.nativeEnum(EntityType),
defaultCategoryId: z.number().positive().optional(),
}); });