mirror of
https://codeberg.org/MarkusThielker/finances.git
synced 2025-04-12 05:08:43 +00:00
commit
1b0c5bf6e1
10 changed files with 118 additions and 16 deletions
|
@ -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;
|
|
@ -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")
|
||||||
|
|
|
@ -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',
|
||||||
|
|
|
@ -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"/>
|
||||||
|
|
|
@ -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/>
|
||||||
|
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -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(),
|
||||||
});
|
});
|
||||||
|
|
Loading…
Add table
Reference in a new issue