Bulk commit: November work
This commit is contained in:
@ -3,6 +3,7 @@
|
||||
import { BadgeCheck, Bell, ChevronsUpDown, LogOut } from 'lucide-react'
|
||||
|
||||
import { Link } from '@tanstack/react-router'
|
||||
import { useProfile } from '../queries'
|
||||
import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar'
|
||||
import {
|
||||
DropdownMenu,
|
||||
@ -19,6 +20,7 @@ import {
|
||||
SidebarMenuItem,
|
||||
useSidebar,
|
||||
} from '@/components/ui/sidebar'
|
||||
import { env } from '@/env'
|
||||
|
||||
export function NavUser({
|
||||
user,
|
||||
@ -30,6 +32,11 @@ export function NavUser({
|
||||
}
|
||||
}) {
|
||||
const { isMobile } = useSidebar()
|
||||
const { data } = useProfile()
|
||||
|
||||
if (!data) {
|
||||
return <div>Loading...</div>
|
||||
}
|
||||
|
||||
return (
|
||||
<SidebarMenu>
|
||||
@ -41,12 +48,12 @@ export function NavUser({
|
||||
className="data-[state=open]:bg-sidebar-accent data-[state=open]:text-sidebar-accent-foreground"
|
||||
>
|
||||
<Avatar className="h-8 w-8 rounded-lg">
|
||||
<AvatarImage src={user.avatar} alt={user.name} />
|
||||
<AvatarFallback className="rounded-lg">CN</AvatarFallback>
|
||||
<AvatarImage src={data.picture} alt={data.name} />
|
||||
<AvatarFallback className="rounded-lg">KH</AvatarFallback>
|
||||
</Avatar>
|
||||
<div className="grid flex-1 text-left text-sm leading-tight">
|
||||
<span className="truncate font-medium">{user.name}</span>
|
||||
<span className="truncate text-xs">{user.email}</span>
|
||||
<span className="truncate font-medium">{data.name}</span>
|
||||
<span className="truncate text-xs">{data.Email}</span>
|
||||
</div>
|
||||
<ChevronsUpDown className="ml-auto size-4" />
|
||||
</SidebarMenuButton>
|
||||
@ -60,30 +67,26 @@ export function NavUser({
|
||||
<DropdownMenuLabel className="p-0 font-normal">
|
||||
<div className="flex items-center gap-2 px-1 py-1.5 text-left text-sm">
|
||||
<Avatar className="h-8 w-8 rounded-lg">
|
||||
<AvatarImage src={user.avatar} alt={user.name} />
|
||||
<AvatarImage src={data.picture} alt={data.name} />
|
||||
<AvatarFallback className="rounded-lg">CN</AvatarFallback>
|
||||
</Avatar>
|
||||
<div className="grid flex-1 text-left text-sm leading-tight">
|
||||
<span className="truncate font-medium">{user.name}</span>
|
||||
<span className="truncate text-xs">{user.email}</span>
|
||||
<span className="truncate font-medium">{data.name}</span>
|
||||
<span className="truncate text-xs">{data.Email}</span>
|
||||
</div>
|
||||
</div>
|
||||
</DropdownMenuLabel>
|
||||
<DropdownMenuSeparator />
|
||||
{/* <DropdownMenuGroup>
|
||||
<DropdownMenuItem>
|
||||
<Sparkles />
|
||||
Upgrade to Pro
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuGroup>
|
||||
<DropdownMenuSeparator /> */}
|
||||
<DropdownMenuGroup>
|
||||
<Link to="/about">
|
||||
<a
|
||||
href="https://keycloak.kocoder.xyz/realms/che/account"
|
||||
target="_blank"
|
||||
>
|
||||
<DropdownMenuItem>
|
||||
<BadgeCheck />
|
||||
Account
|
||||
</DropdownMenuItem>
|
||||
</Link>
|
||||
</a>
|
||||
{/* <DropdownMenuItem>
|
||||
<CreditCard />
|
||||
Billing
|
||||
@ -96,7 +99,7 @@ export function NavUser({
|
||||
</Link>
|
||||
</DropdownMenuGroup>
|
||||
<DropdownMenuSeparator />
|
||||
<a href="http://localhost:3000/api/logout">
|
||||
<a href={`${env.VITE_BACKEND_URI}/api/auth/logout`}>
|
||||
<DropdownMenuItem>
|
||||
<LogOut />
|
||||
Log out
|
||||
|
||||
@ -1,4 +1,6 @@
|
||||
import { useQuery } from '@tanstack/react-query'
|
||||
import { useNavigate } from '@tanstack/react-router'
|
||||
import { useQuery, useQueryClient } from '@tanstack/react-query'
|
||||
import { env } from '@/env'
|
||||
|
||||
const sessionKeys = {
|
||||
all: ['sessions'] as const,
|
||||
@ -11,12 +13,23 @@ export type Session = {
|
||||
CreatedAt: Date
|
||||
}
|
||||
|
||||
export type User = {
|
||||
ID: number
|
||||
CreatedAt: Date
|
||||
UpdatedAt: Date
|
||||
DeletedAt: Date | undefined
|
||||
sub: string
|
||||
Email: string
|
||||
name: string
|
||||
picture: string
|
||||
}
|
||||
|
||||
export function useCurrentSession() {
|
||||
return useQuery<Session>({
|
||||
queryKey: sessionKeys.current(),
|
||||
queryFn: async () => {
|
||||
const data = await fetch(
|
||||
'http://localhost:3000/api/auth/currentSession',
|
||||
env.VITE_BACKEND_URI + '/api/auth/currentSession',
|
||||
{
|
||||
credentials: 'include',
|
||||
},
|
||||
@ -25,3 +38,22 @@ export function useCurrentSession() {
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
export function useProfile() {
|
||||
const queryClient = useQueryClient()
|
||||
const navigate = useNavigate()
|
||||
|
||||
return useQuery<User>({
|
||||
queryKey: sessionKeys.current(),
|
||||
queryFn: async () => {
|
||||
const data = await fetch(env.VITE_BACKEND_URI + '/v1/users/current', {
|
||||
credentials: 'include',
|
||||
})
|
||||
if (data.status == 401) {
|
||||
queryClient.invalidateQueries()
|
||||
navigate({ to: '/' })
|
||||
}
|
||||
return await data.json()
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
@ -4,6 +4,7 @@ import {
|
||||
useQueryClient,
|
||||
useSuspenseQuery,
|
||||
} from '@tanstack/react-query'
|
||||
import { env } from '@/env'
|
||||
|
||||
const ansprechpartnerKeys = {
|
||||
all: ['ansprechpartner'] as const,
|
||||
@ -35,9 +36,12 @@ export function useAllAnsprechpartners() {
|
||||
return useQuery<Ansprechpartner>({
|
||||
queryKey: ansprechpartnerKeys.lists(),
|
||||
queryFn: async () => {
|
||||
const data = await fetch('http://localhost:3000/v1/ansprechpartner/all', {
|
||||
credentials: 'include',
|
||||
})
|
||||
const data = await fetch(
|
||||
env.VITE_BACKEND_URI + '/v1/ansprechpartner/all',
|
||||
{
|
||||
credentials: 'include',
|
||||
},
|
||||
)
|
||||
return await data.json()
|
||||
},
|
||||
})
|
||||
@ -48,7 +52,7 @@ export function useAnsprechpartner(id: number) {
|
||||
queryKey: ansprechpartnerKeys.detail(id),
|
||||
queryFn: async () => {
|
||||
const data = await fetch(
|
||||
'http://localhost:3000/v1/ansprechpartner/' + id,
|
||||
env.VITE_BACKEND_URI + '/v1/ansprechpartner/' + id,
|
||||
{ credentials: 'include' },
|
||||
)
|
||||
return await data.json()
|
||||
@ -62,7 +66,7 @@ export function useAnsprechpartnerEditMutation() {
|
||||
return useMutation({
|
||||
mutationFn: async (ansprechpartner: Ansprechpartner) => {
|
||||
await fetch(
|
||||
'http://localhost:3000/v1/ansprechpartner/' + ansprechpartner.ID,
|
||||
env.VITE_BACKEND_URI + '/v1/ansprechpartner/' + ansprechpartner.ID,
|
||||
{
|
||||
headers: {
|
||||
'content-type': 'application/json',
|
||||
|
||||
40
src/features/Editor/tiptap.tsx
Normal file
40
src/features/Editor/tiptap.tsx
Normal file
@ -0,0 +1,40 @@
|
||||
import { ToggleGroup, ToggleGroupItem } from '@/components/ui/toggle-group'
|
||||
import { EditorContent, useEditor } from '@tiptap/react'
|
||||
import { BubbleMenu, FloatingMenu } from '@tiptap/react/menus'
|
||||
import StarterKit from '@tiptap/starter-kit'
|
||||
import { Bold, Italic, Underline } from 'lucide-react'
|
||||
|
||||
const Tiptap = () => {
|
||||
const editor = useEditor({
|
||||
extensions: [StarterKit], // define your extension array
|
||||
content: '<p>Hello World!</p>', // initial content
|
||||
immediatelyRender: false,
|
||||
})
|
||||
|
||||
if (!editor) return <></>
|
||||
|
||||
return (
|
||||
<>
|
||||
<EditorContent editor={editor} />
|
||||
<FloatingMenu editor={editor}>This is the floating menu</FloatingMenu>
|
||||
<BubbleMenu editor={editor}>
|
||||
<ToggleGroup variant="outline" type="multiple">
|
||||
<ToggleGroupItem value="bold" aria-label="Toggle bold">
|
||||
<Bold className="h-4 w-4" />
|
||||
</ToggleGroupItem>
|
||||
<ToggleGroupItem value="italic" aria-label="Toggle italic">
|
||||
<Italic className="h-4 w-4" />
|
||||
</ToggleGroupItem>
|
||||
<ToggleGroupItem
|
||||
value="strikethrough"
|
||||
aria-label="Toggle strikethrough"
|
||||
>
|
||||
<Underline className="h-4 w-4" />
|
||||
</ToggleGroupItem>
|
||||
</ToggleGroup>
|
||||
</BubbleMenu>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default Tiptap
|
||||
11
src/features/Kanban/components/Kanban.tsx
Normal file
11
src/features/Kanban/components/Kanban.tsx
Normal file
@ -0,0 +1,11 @@
|
||||
import type { ReactNode } from 'react'
|
||||
|
||||
function Kanban({ children }: { children: ReactNode }) {
|
||||
return (
|
||||
<div className="box-border flex gap-2 h-full min-w-full max-w-screen overflow-x-auto snap-x snap-mandatory md:snap-none">
|
||||
{children}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default Kanban
|
||||
15
src/features/Kanban/components/KanbanCard.tsx
Normal file
15
src/features/Kanban/components/KanbanCard.tsx
Normal file
@ -0,0 +1,15 @@
|
||||
type Card = {
|
||||
name: string
|
||||
path: string
|
||||
description: string
|
||||
labels: Array<{
|
||||
name: string
|
||||
className: string
|
||||
}>
|
||||
}
|
||||
|
||||
function KanbanCard({ card }: { card: Card }) {
|
||||
return <div className="bg-background rounded p-2">{card.name}</div>
|
||||
}
|
||||
|
||||
export default KanbanCard
|
||||
59
src/features/Kanban/components/KanbanColumn.tsx
Normal file
59
src/features/Kanban/components/KanbanColumn.tsx
Normal file
@ -0,0 +1,59 @@
|
||||
import { GripVertical, Tickets } from 'lucide-react'
|
||||
import { useRef } from 'react'
|
||||
import type { ReactNode } from 'react'
|
||||
|
||||
function KanbanColumn({
|
||||
children,
|
||||
name,
|
||||
itemCount,
|
||||
}: {
|
||||
children: ReactNode
|
||||
name: string
|
||||
itemCount: number
|
||||
}) {
|
||||
const column = useRef<HTMLDivElement>(null)
|
||||
|
||||
const handleDraggableStart = () => {
|
||||
column.current?.setAttribute('draggable', 'true')
|
||||
}
|
||||
|
||||
const handleDraggableStop = () => {
|
||||
column.current?.setAttribute('draggable', 'false')
|
||||
}
|
||||
|
||||
const handleDragStart = (e: DragEvent) => {
|
||||
e.dataTransfer?.setData('text/custom', 'ASDF')
|
||||
}
|
||||
|
||||
const handleDragStop = () => {
|
||||
column.current?.setAttribute('draggable', 'false')
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
className="min-w-96 bg-accent rounded-md p-4 snap-center"
|
||||
ref={column}
|
||||
onDragStart={handleDragStart}
|
||||
onDragEnd={handleDragStop}
|
||||
>
|
||||
<div className="flex justify-between mb-2">
|
||||
<div className="flex place-items-center">
|
||||
<GripVertical
|
||||
size={20}
|
||||
className="mr-2 cursor-grab"
|
||||
onMouseDown={handleDraggableStart}
|
||||
onMouseUp={handleDraggableStop}
|
||||
/>
|
||||
<p>{name}</p>
|
||||
</div>
|
||||
<div className="flex place-items-center gap-2">
|
||||
{itemCount}
|
||||
<Tickets size={20} />
|
||||
</div>
|
||||
</div>
|
||||
{children}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default KanbanColumn
|
||||
28
src/features/Kanban/components/KanbanDropzone.tsx
Normal file
28
src/features/Kanban/components/KanbanDropzone.tsx
Normal file
@ -0,0 +1,28 @@
|
||||
import { useRef } from 'react'
|
||||
|
||||
function KanbanDropzone() {
|
||||
const ref = useRef<HTMLDivElement>(null)
|
||||
|
||||
const handleDragEnter = () => {
|
||||
console.log('DRAGOVER')
|
||||
if (!ref.current) return
|
||||
ref.current.style.backgroundColor = '#41b2b2'
|
||||
}
|
||||
|
||||
const handleDragLeave = () => {
|
||||
console.log('DRAGOVER')
|
||||
if (!ref.current) return
|
||||
ref.current.style.backgroundColor = 'transparent'
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
className="min-w-1 h-full block"
|
||||
ref={ref}
|
||||
onDragEnter={handleDragEnter}
|
||||
onDragLeave={handleDragLeave}
|
||||
></div>
|
||||
)
|
||||
}
|
||||
|
||||
export default KanbanDropzone
|
||||
@ -1,4 +1,5 @@
|
||||
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'
|
||||
import { env } from '@/env'
|
||||
|
||||
const mandantKeys = {
|
||||
all: ['mandant'] as const,
|
||||
@ -18,7 +19,7 @@ export function useCurrentMandant() {
|
||||
return useQuery<Mandant>({
|
||||
queryKey: mandantKeys.current(),
|
||||
queryFn: async () => {
|
||||
const data = await fetch('http://localhost:3000/v1/mandant/current', {
|
||||
const data = await fetch(env.VITE_BACKEND_URI + '/v1/mandant/current', {
|
||||
credentials: 'include',
|
||||
})
|
||||
return await data.json()
|
||||
@ -30,7 +31,7 @@ export function useAllMandanten() {
|
||||
return useQuery<Array<Mandant>>({
|
||||
queryKey: mandantKeys.lists(),
|
||||
queryFn: async () => {
|
||||
const data = await fetch('http://localhost:3000/v1/mandant/all', {
|
||||
const data = await fetch(env.VITE_BACKEND_URI + '/v1/mandant/all', {
|
||||
credentials: 'include',
|
||||
})
|
||||
return await data.json()
|
||||
@ -43,7 +44,7 @@ export function useCurrentMandantMutation() {
|
||||
|
||||
return useMutation({
|
||||
mutationFn: async (mandant: Mandant) => {
|
||||
const res = await fetch('http://localhost:3000/v1/mandant/current', {
|
||||
const res = await fetch(env.VITE_BACKEND_URI + '/v1/mandant/current', {
|
||||
headers: {
|
||||
'content-type': 'application/json',
|
||||
},
|
||||
|
||||
82
src/features/Projects/components/list.tsx
Normal file
82
src/features/Projects/components/list.tsx
Normal file
@ -0,0 +1,82 @@
|
||||
import { Folder, MoreHorizontal, Share, Trash2 } from 'lucide-react'
|
||||
import { Link } from '@tanstack/react-router'
|
||||
import { useAllProjects } from '../queries'
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuSeparator,
|
||||
DropdownMenuTrigger,
|
||||
} from '@/components/ui/dropdown-menu'
|
||||
import {
|
||||
SidebarGroup,
|
||||
SidebarGroupLabel,
|
||||
SidebarMenu,
|
||||
SidebarMenuAction,
|
||||
SidebarMenuButton,
|
||||
SidebarMenuItem,
|
||||
useSidebar,
|
||||
} from '@/components/ui/sidebar'
|
||||
|
||||
export function NavProjects() {
|
||||
const { isMobile } = useSidebar()
|
||||
const { data: projects } = useAllProjects({
|
||||
fetchSize: 5,
|
||||
sorting: [],
|
||||
})
|
||||
|
||||
if (!projects) {
|
||||
return <p>Loading...</p>
|
||||
}
|
||||
|
||||
return (
|
||||
<SidebarGroup className="group-data-[collapsible=icon]:hidden">
|
||||
<SidebarGroupLabel>Projects</SidebarGroupLabel>
|
||||
<SidebarMenu>
|
||||
{projects.pages[0].data.map((item) => (
|
||||
<SidebarMenuItem key={item.name}>
|
||||
<SidebarMenuButton asChild>
|
||||
<Link to="/projects/view/$id" params={{ id: item.ID.toString() }}>
|
||||
{item.icon}
|
||||
<span>{item.name}</span>
|
||||
</Link>
|
||||
</SidebarMenuButton>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<SidebarMenuAction showOnHover>
|
||||
<MoreHorizontal />
|
||||
<span className="sr-only">More</span>
|
||||
</SidebarMenuAction>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent
|
||||
className="w-48"
|
||||
side={isMobile ? 'bottom' : 'right'}
|
||||
align={isMobile ? 'end' : 'start'}
|
||||
>
|
||||
<DropdownMenuItem>
|
||||
<Folder className="text-muted-foreground" />
|
||||
<span>View Project</span>
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem>
|
||||
<Share className="text-muted-foreground" />
|
||||
<span>Share Project</span>
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuItem>
|
||||
<Trash2 className="text-muted-foreground" />
|
||||
<span>Delete Project</span>
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</SidebarMenuItem>
|
||||
))}
|
||||
<SidebarMenuItem>
|
||||
<SidebarMenuButton>
|
||||
<MoreHorizontal />
|
||||
<span>More</span>
|
||||
</SidebarMenuButton>
|
||||
</SidebarMenuItem>
|
||||
</SidebarMenu>
|
||||
</SidebarGroup>
|
||||
)
|
||||
}
|
||||
196
src/features/Projects/components/table.tsx
Normal file
196
src/features/Projects/components/table.tsx
Normal file
@ -0,0 +1,196 @@
|
||||
import {
|
||||
CircleCheckIcon,
|
||||
CircleUserIcon,
|
||||
ClipboardCheckIcon,
|
||||
MoreHorizontalIcon,
|
||||
ReceiptEuroIcon,
|
||||
SpeakerIcon,
|
||||
} from 'lucide-react'
|
||||
import { useState } from 'react'
|
||||
import { Link } from '@tanstack/react-router'
|
||||
import { useAllProjects } from '../queries'
|
||||
import type {
|
||||
ColumnDef,
|
||||
RowSelectionState,
|
||||
SortingState,
|
||||
VisibilityState,
|
||||
} from '@tanstack/react-table'
|
||||
import type { PaginatedProject } from '../queries'
|
||||
import { Checkbox } from '@/components/ui/checkbox'
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuLabel,
|
||||
DropdownMenuSeparator,
|
||||
DropdownMenuTrigger,
|
||||
} from '@/components/ui/dropdown-menu'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { DataTableColumnHeader } from '@/components/data-table-column-header'
|
||||
import { DataTable } from '@/components/data-table'
|
||||
|
||||
const iconSize = 16
|
||||
|
||||
export const columnDefs: Array<ColumnDef<PaginatedProject>> = [
|
||||
{
|
||||
id: 'select',
|
||||
header: ({ table }) => (
|
||||
<Checkbox
|
||||
checked={
|
||||
table.getIsAllPageRowsSelected() ||
|
||||
(table.getIsSomePageRowsSelected() && 'indeterminate')
|
||||
}
|
||||
onCheckedChange={(value) => table.toggleAllPageRowsSelected(!!value)}
|
||||
aria-label="Select all"
|
||||
className="aspect-square"
|
||||
/>
|
||||
),
|
||||
cell: ({ row }) => (
|
||||
<Checkbox
|
||||
checked={row.getIsSelected()}
|
||||
onCheckedChange={(value) => row.toggleSelected(!!value)}
|
||||
aria-label="Select row"
|
||||
/>
|
||||
),
|
||||
enableSorting: false,
|
||||
enableHiding: false,
|
||||
size: 26,
|
||||
},
|
||||
{
|
||||
accessorKey: 'name',
|
||||
header: ({ column, header }) => {
|
||||
return (
|
||||
<DataTableColumnHeader
|
||||
column={column}
|
||||
title="Name"
|
||||
style={{ width: header.getSize() }}
|
||||
/>
|
||||
)
|
||||
},
|
||||
size: 200,
|
||||
},
|
||||
{
|
||||
accessorKey: 'description',
|
||||
header: ({ column }) => {
|
||||
return <DataTableColumnHeader column={column} title="Beschreibung" />
|
||||
},
|
||||
size: 400,
|
||||
},
|
||||
{
|
||||
accessorKey: 'progress',
|
||||
header: 'Project progress',
|
||||
cell: () => {
|
||||
return (
|
||||
<div className="flex flex-row gap-2 items-center h-4">
|
||||
<SpeakerIcon size={iconSize} />
|
||||
<CircleUserIcon size={iconSize} />
|
||||
<CircleCheckIcon size={iconSize} />
|
||||
<ReceiptEuroIcon size={iconSize} />
|
||||
<ClipboardCheckIcon size={iconSize} />
|
||||
</div>
|
||||
)
|
||||
},
|
||||
size: 200,
|
||||
},
|
||||
{
|
||||
accessorKey: 'client',
|
||||
header: 'Kunde',
|
||||
size: 200,
|
||||
},
|
||||
{
|
||||
accessorKey: 'location',
|
||||
header: 'Location',
|
||||
size: 200,
|
||||
},
|
||||
{
|
||||
accessorKey: 'startdate',
|
||||
header: 'Planungszeitraum Start',
|
||||
size: 200,
|
||||
},
|
||||
{
|
||||
accessorKey: 'enddate',
|
||||
header: 'Planungszeitraum Ende',
|
||||
size: 200,
|
||||
},
|
||||
{
|
||||
accessorKey: 'type',
|
||||
header: 'Projekttyp',
|
||||
size: 200,
|
||||
},
|
||||
{
|
||||
id: 'actions',
|
||||
cell: ({ row }) => {
|
||||
const project = row.original
|
||||
|
||||
return (
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button variant="ghost" className="h-8 w-8 p-0">
|
||||
<span className="sr-only">Open menu</span>
|
||||
<MoreHorizontalIcon className="h-4 w-4" />
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end">
|
||||
<DropdownMenuLabel>Actions</DropdownMenuLabel>
|
||||
<DropdownMenuItem
|
||||
onClick={() =>
|
||||
navigator.clipboard.writeText(project.ID.toString())
|
||||
}
|
||||
>
|
||||
Copy project ID
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuItem asChild>
|
||||
<Link
|
||||
to="/projects/view/$id"
|
||||
params={{ id: project.ID.toString() }}
|
||||
>
|
||||
View Project
|
||||
</Link>
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
)
|
||||
},
|
||||
size: 50,
|
||||
},
|
||||
]
|
||||
|
||||
const fetchSize = 25
|
||||
|
||||
function ProjectsTable() {
|
||||
const [sorting, setSorting] = useState<SortingState>([])
|
||||
const [columnVisibility, setColumnVisibility] = useState<VisibilityState>({
|
||||
description: false,
|
||||
client: false,
|
||||
location: false,
|
||||
type: false,
|
||||
enddate: false,
|
||||
startdate: false,
|
||||
})
|
||||
|
||||
const [selected, setSelected] = useState<RowSelectionState>({})
|
||||
|
||||
const { data, fetchNextPage, isFetching, isLoading } = useAllProjects({
|
||||
fetchSize,
|
||||
sorting,
|
||||
})
|
||||
|
||||
return (
|
||||
<DataTable
|
||||
columns={columnDefs}
|
||||
data={data}
|
||||
columnVisibility={columnVisibility}
|
||||
setColumnVisibility={setColumnVisibility}
|
||||
fetchNextPage={fetchNextPage}
|
||||
isFetching={isFetching}
|
||||
isLoading={isLoading}
|
||||
setSorting={setSorting}
|
||||
sorting={sorting}
|
||||
rowSelection={selected}
|
||||
setRowSelection={setSelected}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export default ProjectsTable
|
||||
119
src/features/Projects/queries.ts
Normal file
119
src/features/Projects/queries.ts
Normal file
@ -0,0 +1,119 @@
|
||||
import {
|
||||
keepPreviousData,
|
||||
useInfiniteQuery,
|
||||
useMutation,
|
||||
useQuery,
|
||||
useQueryClient,
|
||||
} from '@tanstack/react-query'
|
||||
import { env } from '@/env'
|
||||
|
||||
const projectKeys = {
|
||||
all: ['projects'] as const,
|
||||
lists: () => [...projectKeys.all, 'list'] as const,
|
||||
getAll: (fetchSize: number, sorting: any) =>
|
||||
[...projectKeys.lists(), 'all', fetchSize, sorting] as const,
|
||||
get: (id: number) => [...projectKeys.all, 'get', id] as const,
|
||||
}
|
||||
|
||||
export type PaginatedProject = {
|
||||
data: Array<Project>
|
||||
meta: {
|
||||
totalProjectsCount: number
|
||||
}
|
||||
}
|
||||
|
||||
export type Project = {
|
||||
ID: number
|
||||
name: string
|
||||
description: string
|
||||
icon: string
|
||||
MandantID: number
|
||||
}
|
||||
|
||||
export function getProjectQueryObject(id: number) {
|
||||
return {
|
||||
queryKey: projectKeys.get(id),
|
||||
queryFn: async () => {
|
||||
const data = await fetch(env.VITE_BACKEND_URI + '/v1/projects/' + id, {
|
||||
credentials: 'include',
|
||||
})
|
||||
return await data.json()
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
export function useProject(id: number) {
|
||||
return useQuery<Project>(getProjectQueryObject(id))
|
||||
}
|
||||
|
||||
export function useAllProjects({
|
||||
fetchSize,
|
||||
sorting,
|
||||
}: {
|
||||
fetchSize: number
|
||||
sorting: any
|
||||
}) {
|
||||
return useInfiniteQuery<PaginatedProject>({
|
||||
queryKey: projectKeys.getAll(fetchSize, sorting),
|
||||
|
||||
queryFn: async ({ pageParam = 0 }) => {
|
||||
const start = (pageParam as number) * fetchSize
|
||||
const data = await fetch(
|
||||
env.VITE_BACKEND_URI +
|
||||
'/v1/projects/all?' +
|
||||
new URLSearchParams(sorting[0]),
|
||||
{
|
||||
credentials: 'include',
|
||||
headers: {
|
||||
'X-OFFSET': start.toString(),
|
||||
'X-PER-PAGE': fetchSize.toString(),
|
||||
},
|
||||
},
|
||||
)
|
||||
return await data.json()
|
||||
},
|
||||
|
||||
initialPageParam: 0,
|
||||
getNextPageParam: (_lastGroup, groups) => groups.length,
|
||||
refetchOnWindowFocus: false,
|
||||
placeholderData: keepPreviousData,
|
||||
})
|
||||
}
|
||||
|
||||
export function useProjectEdit(id: number) {
|
||||
return useMutation({
|
||||
mutationFn: async (project: Project) => {
|
||||
await fetch(env.VITE_BACKEND_URI + '/v1/projects/' + id + '/edit', {
|
||||
headers: {
|
||||
'content-type': 'application/json',
|
||||
},
|
||||
method: 'POST',
|
||||
body: JSON.stringify(project),
|
||||
credentials: 'include',
|
||||
})
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
export function useProjectCreate() {
|
||||
const queryClient = useQueryClient()
|
||||
|
||||
return useMutation({
|
||||
mutationFn: async (project: Project) => {
|
||||
const res = await fetch(env.VITE_BACKEND_URI + '/v1/projects/new', {
|
||||
headers: {
|
||||
'content-type': 'application/json',
|
||||
},
|
||||
method: 'POST',
|
||||
body: JSON.stringify(project),
|
||||
credentials: 'include',
|
||||
})
|
||||
const newCurrentMandant = await res.json()
|
||||
queryClient.invalidateQueries({ queryKey: projectKeys.lists() })
|
||||
queryClient.setQueryData(
|
||||
projectKeys.get(newCurrentMandant.id),
|
||||
(_: Project) => newCurrentMandant,
|
||||
)
|
||||
},
|
||||
})
|
||||
}
|
||||
Reference in New Issue
Block a user