mirror of
https://codeberg.org/MarkusThielker/finances.git
synced 2025-04-12 05:08:43 +00:00
N-FIN-5: add entity page UI
This commit is contained in:
parent
6e449ac603
commit
8dcc99d1f8
3 changed files with 248 additions and 3 deletions
41
src/app/entities/columns.tsx
Normal file
41
src/app/entities/columns.tsx
Normal file
|
@ -0,0 +1,41 @@
|
||||||
|
'use client';
|
||||||
|
|
||||||
|
import { ColumnDef } from '@tanstack/react-table';
|
||||||
|
import { Entity } from '@prisma/client';
|
||||||
|
import { CellContext, ColumnDefTemplate } from '@tanstack/table-core';
|
||||||
|
|
||||||
|
export const columns = (
|
||||||
|
actionCell: ColumnDefTemplate<CellContext<Entity, unknown>>,
|
||||||
|
) => {
|
||||||
|
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
accessorKey: 'name',
|
||||||
|
header: 'Name',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
accessorKey: 'type',
|
||||||
|
header: 'Type',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
accessorKey: 'createdAt',
|
||||||
|
header: 'Created at',
|
||||||
|
cell: ({row}) => {
|
||||||
|
const date = row.getValue('createdAt') as Date;
|
||||||
|
return date.toDateString();
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
accessorKey: 'updatedAt',
|
||||||
|
header: 'Updated at',
|
||||||
|
cell: ({row}) => {
|
||||||
|
const date = row.getValue('updatedAt') as Date;
|
||||||
|
return date.toDateString();
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'actions',
|
||||||
|
cell: actionCell,
|
||||||
|
},
|
||||||
|
] as ColumnDef<Entity>[];
|
||||||
|
};
|
|
@ -1,7 +1,33 @@
|
||||||
|
import { prismaClient } from '@/prisma';
|
||||||
|
import { getUser } from '@/auth';
|
||||||
|
import React from 'react';
|
||||||
|
import EntityPageClientContent from '@/components/EntityPageClientComponents';
|
||||||
|
import entityCreateUpdate from '@/lib/actions/entityCreateUpdate';
|
||||||
|
import entityDelete from '@/lib/actions/entityDelete';
|
||||||
|
|
||||||
export default async function EntitiesPage() {
|
export default async function EntitiesPage() {
|
||||||
|
|
||||||
|
const user = await getUser();
|
||||||
|
|
||||||
|
const entities = await prismaClient.entity.findMany({
|
||||||
|
where: {
|
||||||
|
userId: user?.id,
|
||||||
|
},
|
||||||
|
orderBy: [
|
||||||
|
{
|
||||||
|
type: 'desc',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'asc',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<main className="flex items-center justify-center min-h-screen text-3xl">
|
<EntityPageClientContent
|
||||||
Entities
|
entities={entities}
|
||||||
</main>
|
onSubmit={entityCreateUpdate}
|
||||||
|
onDelete={entityDelete}
|
||||||
|
className="flex flex-col justify-center space-y-4 p-10"/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
178
src/components/EntityPageClientComponents.tsx
Normal file
178
src/components/EntityPageClientComponents.tsx
Normal file
|
@ -0,0 +1,178 @@
|
||||||
|
'use client';
|
||||||
|
|
||||||
|
import { Entity } from '@prisma/client';
|
||||||
|
import React, { useState } from 'react';
|
||||||
|
import { CellContext } from '@tanstack/table-core';
|
||||||
|
import { Button } from '@/components/ui/button';
|
||||||
|
import { Edit, Trash } from 'lucide-react';
|
||||||
|
import { Dialog, DialogContent, DialogFooter, DialogHeader, DialogTitle, DialogTrigger } from '@/components/ui/dialog';
|
||||||
|
import EntityForm from '@/components/form/entityForm';
|
||||||
|
import { DataTable } from '@/components/ui/data-table';
|
||||||
|
import { columns } from '@/app/entities/columns';
|
||||||
|
import { z } from 'zod';
|
||||||
|
import { entityFormSchema } from '@/lib/form-schemas/entityFormSchema';
|
||||||
|
import { ActionResponse } from '@/lib/types/ActionResponse';
|
||||||
|
import { Input } from '@/components/ui/input';
|
||||||
|
import { useRouter } from 'next/navigation';
|
||||||
|
import { toast } from 'sonner';
|
||||||
|
import { sonnerContent } from '@/components/ui/sonner';
|
||||||
|
|
||||||
|
export default function EntityPageClientContent({entities, onSubmit, onDelete, className}: {
|
||||||
|
entities: Entity[],
|
||||||
|
onSubmit: (data: z.infer<typeof entityFormSchema>) => Promise<ActionResponse>,
|
||||||
|
onDelete: (id: number) => Promise<ActionResponse>,
|
||||||
|
className: string,
|
||||||
|
}) {
|
||||||
|
|
||||||
|
const router = useRouter();
|
||||||
|
|
||||||
|
const [isEditDialogOpen, setIsEditDialogOpen] = useState(false);
|
||||||
|
const [isDeleteDialogOpen, setIsDeleteDialogOpen] = useState(false);
|
||||||
|
|
||||||
|
const [selectedEntity, setSelectedEntity] = useState<Entity | undefined>(undefined);
|
||||||
|
|
||||||
|
async function handleSubmit(data: z.infer<typeof entityFormSchema>) {
|
||||||
|
const response = await onSubmit(data);
|
||||||
|
router.refresh();
|
||||||
|
setIsEditDialogOpen(false);
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleDelete(id: number | undefined) {
|
||||||
|
|
||||||
|
if (!id) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const response = await onDelete(id);
|
||||||
|
toast(sonnerContent(response));
|
||||||
|
if (response.redirect) {
|
||||||
|
router.push(response.redirect);
|
||||||
|
}
|
||||||
|
router.refresh();
|
||||||
|
setIsDeleteDialogOpen(false);
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
|
||||||
|
function filterEntities(entities: Entity[], filter: string) {
|
||||||
|
const filterChars = filter.toLowerCase().split('');
|
||||||
|
const filterCharCounts: Record<string, number> = {};
|
||||||
|
|
||||||
|
// Count character occurrences in the filter
|
||||||
|
filterChars.forEach(char => {
|
||||||
|
filterCharCounts[char] = (filterCharCounts[char] || 0) + 1;
|
||||||
|
});
|
||||||
|
|
||||||
|
return entities.filter(entity => {
|
||||||
|
const entityChars = entity.name.toLowerCase().split('');
|
||||||
|
const entityCharCounts: Record<string, number> = {};
|
||||||
|
|
||||||
|
// Check if entity has enough of each character
|
||||||
|
for (const char of entityChars) {
|
||||||
|
entityCharCounts[char] = (entityCharCounts[char] || 0) + 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ensure all filter characters were found
|
||||||
|
return Object.keys(filterCharCounts).every(char => {
|
||||||
|
return entityCharCounts[char] >= filterCharCounts[char];
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const actionCell = ({row}: CellContext<Entity, unknown>) => {
|
||||||
|
const entity = row.original as Entity;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex items-center space-x-4">
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
className="h-8 w-8 p-0"
|
||||||
|
onClick={() => {
|
||||||
|
setSelectedEntity(entity);
|
||||||
|
setIsEditDialogOpen(true);
|
||||||
|
}}>
|
||||||
|
<span className="sr-only">Edit entity</span>
|
||||||
|
<Edit className="h-4 w-4"/>
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
className="h-8 w-8 p-0"
|
||||||
|
onClick={() => {
|
||||||
|
setSelectedEntity(entity);
|
||||||
|
setIsDeleteDialogOpen(true);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<span className="sr-only">Delete entity</span>
|
||||||
|
<Trash className="h-4 w-4"/>
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const [filter, setFilter] = useState<string>('');
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={className}>
|
||||||
|
<div className="flex items-center justify-between w-full">
|
||||||
|
<p className="text-3xl font-semibold">Entities</p>
|
||||||
|
|
||||||
|
{/* Edit dialog */}
|
||||||
|
<Dialog open={isEditDialogOpen} onOpenChange={setIsEditDialogOpen}>
|
||||||
|
<DialogTrigger asChild>
|
||||||
|
<Button
|
||||||
|
onClick={() => {
|
||||||
|
setSelectedEntity(undefined);
|
||||||
|
setIsEditDialogOpen(true);
|
||||||
|
}}>
|
||||||
|
Create Entity
|
||||||
|
</Button>
|
||||||
|
</DialogTrigger>
|
||||||
|
<DialogContent>
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle>{selectedEntity?.id ? 'Update Entity' : 'Create Entity'}</DialogTitle>
|
||||||
|
</DialogHeader>
|
||||||
|
<EntityForm
|
||||||
|
value={selectedEntity}
|
||||||
|
onSubmit={handleSubmit}
|
||||||
|
className="grid grid-cols-1 md:grid-cols-2 gap-4 py-4"/>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Filter input */}
|
||||||
|
<Input
|
||||||
|
value={filter}
|
||||||
|
onChange={(event) => setFilter(event.target.value)}
|
||||||
|
placeholder="Filter entities"/>
|
||||||
|
|
||||||
|
{/* Data Table */}
|
||||||
|
<DataTable
|
||||||
|
className="w-full"
|
||||||
|
columns={columns(actionCell)}
|
||||||
|
data={filterEntities(entities, filter)}
|
||||||
|
pagination/>
|
||||||
|
|
||||||
|
{/* Delete confirmation dialog */}
|
||||||
|
<Dialog open={isDeleteDialogOpen} onOpenChange={setIsDeleteDialogOpen}>
|
||||||
|
<DialogContent>
|
||||||
|
<DialogHeader>Delete Entity?</DialogHeader>
|
||||||
|
<p>Are your sure you want to delete the entity {selectedEntity?.name}?</p>
|
||||||
|
<DialogFooter>
|
||||||
|
<Button
|
||||||
|
className="w-full"
|
||||||
|
onClick={() => {
|
||||||
|
setIsDeleteDialogOpen(false);
|
||||||
|
}}>
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
className="w-full"
|
||||||
|
onClick={() => handleDelete(selectedEntity?.id)}>
|
||||||
|
Delete
|
||||||
|
</Button>
|
||||||
|
</DialogFooter>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
Loading…
Add table
Reference in a new issue