Bulk commit: Stand ende 22.01.

This commit is contained in:
2026-01-22 17:39:38 +01:00
parent cca316daf2
commit f8940b5dcc
75 changed files with 12419 additions and 6719 deletions

View File

@@ -1,9 +1,240 @@
import { cn } from '@/lib/utils'
import { createFileRoute } from '@tanstack/react-router'
import { format } from 'date-fns'
import {
BotIcon,
CircleUserIcon,
ClipboardCheckIcon,
ProjectorIcon,
ReceiptEuroIcon,
SpeakerIcon,
} from 'lucide-react'
import { ReactNode } from 'react'
import { string } from 'zod'
export const Route = createFileRoute('/_sidebar/projects/view/$id/audit')({
component: RouteComponent,
beforeLoad: () => {
return {
breadcrumb: 'Audit',
}
},
})
function RouteComponent() {
return <div>Hello "/_sidebar/projects/view/$id/audit"!</div>
enum TimelineAction {
General,
Equipment,
Personal,
Finance,
Todo,
}
enum TimelineActionType {
Good,
Bad,
Neutral,
}
type TimelineEntry = {
title: string
description: string
date: Date
action: TimelineAction
actionType: TimelineActionType
}
const timelineEntries: Array<TimelineEntry> = [
{
title: 'Payed #5',
description: 'Client payed bill number 5.',
action: TimelineAction.Finance,
date: new Date(2025, 12, 5, 10, 54, 23),
actionType: TimelineActionType.Good,
},
{
title: 'Confirmed #1',
description: 'Client confirmed project 1.',
action: TimelineAction.General,
date: new Date(2025, 12, 4, 11),
actionType: TimelineActionType.Good,
},
{
title: 'New Task',
description: 'Konstantin Hintermayer added task 1.',
action: TimelineAction.Todo,
date: new Date(),
actionType: TimelineActionType.Neutral,
},
{
title: 'Payed #5',
description: 'Client payed bill number 5.',
action: TimelineAction.Finance,
date: new Date(2025, 12, 5, 10, 54, 23),
actionType: TimelineActionType.Good,
},
{
title: 'Confirmed #1',
description: 'Client confirmed project 1.',
action: TimelineAction.General,
date: new Date(2025, 12, 4, 11),
actionType: TimelineActionType.Good,
},
{
title: 'New Task',
description: 'Konstantin Hintermayer added task 1.',
action: TimelineAction.Todo,
date: new Date(),
actionType: TimelineActionType.Neutral,
},
{
title: 'Payed #5',
description: 'Client payed bill number 5.',
action: TimelineAction.Finance,
date: new Date(2025, 12, 5, 10, 54, 23),
actionType: TimelineActionType.Good,
},
{
title: 'Confirmed #1',
description: 'Client confirmed project 1.',
action: TimelineAction.General,
date: new Date(2025, 12, 4, 11),
actionType: TimelineActionType.Good,
},
{
title: 'New Task',
description: 'Konstantin Hintermayer added task 1.',
action: TimelineAction.Todo,
date: new Date(),
actionType: TimelineActionType.Neutral,
},
{
title: 'Payed #5',
description: 'Client payed bill number 5.',
action: TimelineAction.Finance,
date: new Date(2025, 12, 5, 10, 54, 23),
actionType: TimelineActionType.Good,
},
{
title: 'Confirmed #1',
description: 'Client confirmed project 1.',
action: TimelineAction.General,
date: new Date(2025, 12, 4, 11),
actionType: TimelineActionType.Good,
},
{
title: 'New Task',
description: 'Konstantin Hintermayer added task 1.',
action: TimelineAction.Todo,
date: new Date(),
actionType: TimelineActionType.Neutral,
},
{
title: 'Payed #5',
description: 'Client payed bill number 5.',
action: TimelineAction.Finance,
date: new Date(2025, 12, 5, 10, 54, 23),
actionType: TimelineActionType.Good,
},
{
title: 'Confirmed #1',
description: 'Client confirmed project 1.',
action: TimelineAction.General,
date: new Date(2025, 12, 4, 11),
actionType: TimelineActionType.Good,
},
{
title: 'New Task',
description: 'Konstantin Hintermayer added task 1.',
action: TimelineAction.Todo,
date: new Date(),
actionType: TimelineActionType.Neutral,
},
{
title: 'Payed #5',
description: 'Client payed bill number 5.',
action: TimelineAction.Finance,
date: new Date(2025, 12, 5, 10, 54, 23),
actionType: TimelineActionType.Good,
},
{
title: 'Confirmed #1',
description: 'Client confirmed project 1.',
action: TimelineAction.General,
date: new Date(2025, 12, 4, 11),
actionType: TimelineActionType.Good,
},
{
title: 'New Task',
description: 'Konstantin Hintermayer added task 1.',
action: TimelineAction.Todo,
date: new Date(),
actionType: TimelineActionType.Neutral,
},
]
function GetIconFromAction(ta: TimelineAction) {
switch (ta) {
case TimelineAction.Equipment:
return <SpeakerIcon className="h-6" />
case TimelineAction.Finance:
return <ReceiptEuroIcon className="h-6" />
case TimelineAction.General:
return <BotIcon className="h-6" />
case TimelineAction.Personal:
return <CircleUserIcon className="h-6" />
case TimelineAction.Todo:
return <ClipboardCheckIcon className="h-6" />
default:
return <p>Icon not found</p>
}
}
function GetColorFromActionType(at: TimelineActionType) {
switch (at) {
case TimelineActionType.Bad:
return 'bg-red-500'
case TimelineActionType.Good:
return 'bg-green-500'
case TimelineActionType.Neutral:
return 'bg-muted'
}
}
function Timeline({ children }: { children: ReactNode }) {
return (
<div className="relative">
{children}
<div className="h-full absolute top-0 left-timeline-line w-[2px] bg-muted"></div>
</div>
)
}
function TimelineEntryComponent({ te }: { te: TimelineEntry }) {
return (
<div className="flex m-2 gap-3">
<div
className={cn(
`h-8 aspect-square flex items-center justify-center rounded-full border-2 border-background z-10`,
GetColorFromActionType(te.actionType),
)}
>
{GetIconFromAction(te.action)}
</div>
<div>
{format(te.date, 'hh:mm dd.MM.yyyy')} {te.title}
<br />
{te.description}
</div>
</div>
)
}
function RouteComponent() {
return (
<Timeline>
{timelineEntries.map((te, i) => {
return <TimelineEntryComponent te={te} key={i}></TimelineEntryComponent>
})}
</Timeline>
)
}

View File

@@ -2,8 +2,17 @@ import { createFileRoute } from '@tanstack/react-router'
export const Route = createFileRoute('/_sidebar/projects/view/$id/equipment')({
component: RouteComponent,
beforeLoad: () => {
return {
breadcrumb: 'Equipment',
}
},
})
function RouteComponent() {
return <div>Hello "/_sidebar/projects/view/$id/equipment"!</div>
return (
<div className="bg-emerald-500 h-full">
Hello "/_sidebar/projects/view/$id/equipment"!
</div>
)
}

View File

@@ -2,6 +2,11 @@ import { createFileRoute } from '@tanstack/react-router'
export const Route = createFileRoute('/_sidebar/projects/view/$id/finance')({
component: RouteComponent,
beforeLoad: () => {
return {
breadcrumb: 'Finanzen',
}
},
})
function RouteComponent() {

View File

@@ -27,9 +27,19 @@ import {
SelectTrigger,
SelectValue,
} from '@/components/ui/select'
import { useSuspenseQuery } from '@connectrpc/connect-query'
import { getProject } from '@/gen/project/v1/project-ProjectService_connectquery'
import { Breadcrumb } from '@/components/ui/breadcrumb'
import { Map, MapControls, MapMarker, MapRef, MarkerContent, MarkerPopup, MarkerTooltip } from '@/components/ui/map'
import { useRef } from 'react'
export const Route = createFileRoute('/_sidebar/projects/view/$id/')({
component: RouteComponent,
beforeLoad: () => {
return {
breadcrumb: 'General',
}
},
})
const managers = [
@@ -100,6 +110,21 @@ const clients = [
},
]
const locations = [
{
id: 1,
name: 'SZU',
},
{
id: 2,
name: 'Stadthalle',
},
{
id: 3,
name: 'Gasometer',
},
]
const formSchema = z.object({
title: z
.string()
@@ -113,11 +138,12 @@ const formSchema = z.object({
type: z.string(),
status: z.string(),
client: z.number(),
location: z.number(),
})
function RouteComponent() {
const { id } = Route.useParams()
const { data } = useProject(Number(id))
const { data } = useSuspenseQuery(getProject, { id: Number(id) })
const { mutate } = useProjectEdit(Number(id))
const form = useForm({
@@ -128,6 +154,7 @@ function RouteComponent() {
type: 'Tour',
status: 'Confirmed',
client: 1,
location: 1,
},
validators: {
onSubmit: formSchema,
@@ -138,91 +165,222 @@ function RouteComponent() {
name: value.title!,
description: value.description!,
icon: '',
MandantID: data?.MandantID,
MandantID: 1,
})
},
})
return (
<>
<form
id="project-esther-graf-general-form"
className="w-2xl"
onSubmit={(e) => {
e.preventDefault()
form.handleSubmit()
}}
>
<FieldGroup>
<div className='w-full mt-2 grid grid-cols-6 lg:grid-cols-12 gap-6'>
<div className="col-span-6 rounded-md bg-sidebar p-2 overflow-hidden">
<form
id="project-esther-graf-general-form"
onSubmit={(e) => {
e.preventDefault()
form.handleSubmit()
}}
>
<FieldGroup>
<FieldLegend>Projekt Info</FieldLegend>
<FieldDescription>
Allgemeine Infos über das Projekt
</FieldDescription>
<FieldGroup>
<form.Field
name="title"
children={(field) => {
const isInvalid =
field.state.meta.isTouched && !field.state.meta.isValid
return (
<Field data-invalid={isInvalid}>
<FieldLabel htmlFor={field.name}>Projektname</FieldLabel>
<Input
id={field.name}
name={field.name}
value={field.state.value}
onBlur={field.handleBlur}
onChange={(e) => field.handleChange(e.target.value)}
aria-invalid={isInvalid}
placeholder="Login button not working on mobile"
autoComplete="off"
/>
{isInvalid && (
<FieldError errors={field.state.meta.errors} />
)}
</Field>
)
}}
/>
<form.Field
name="description"
children={(field) => {
const isInvalid =
field.state.meta.isTouched && !field.state.meta.isValid
return (
<Field data-invalid={isInvalid}>
<FieldLabel htmlFor={field.name}>Beschreibung</FieldLabel>
<InputGroup>
<InputGroupTextarea
<FieldLegend>Projekt Info</FieldLegend>
<FieldDescription>
Allgemeine Infos über das Projekt
</FieldDescription>
<FieldGroup>
<form.Field
name="title"
children={(field) => {
const isInvalid =
field.state.meta.isTouched && !field.state.meta.isValid
return (
<Field data-invalid={isInvalid}>
<FieldLabel htmlFor={field.name}>Projektname</FieldLabel>
<Input
id={field.name}
name={field.name}
value={field.state.value}
onBlur={field.handleBlur}
onChange={(e) => field.handleChange(e.target.value)}
placeholder="I'm having an issue with the login button on mobile."
rows={6}
className="min-h-24 resize-none"
aria-invalid={isInvalid}
placeholder="Login button not working on mobile"
autoComplete="off"
/>
<InputGroupAddon align="block-end">
<InputGroupText className="tabular-nums">
{field.state.value.length}/100 characters
</InputGroupText>
</InputGroupAddon>
</InputGroup>
<FieldDescription>
Allgemeine Infos zum Projekt
</FieldDescription>
{isInvalid && (
<FieldError errors={field.state.meta.errors} />
)}
</Field>
)
}}
/>
{isInvalid && (
<FieldError errors={field.state.meta.errors} />
)}
</Field>
)
}}
/>
<form.Field
name="description"
children={(field) => {
const isInvalid =
field.state.meta.isTouched && !field.state.meta.isValid
return (
<Field data-invalid={isInvalid}>
<FieldLabel htmlFor={field.name}>Beschreibung</FieldLabel>
<InputGroup>
<InputGroupTextarea
id={field.name}
name={field.name}
value={field.state.value}
onBlur={field.handleBlur}
onChange={(e) => field.handleChange(e.target.value)}
placeholder="I'm having an issue with the login button on mobile."
rows={6}
className="min-h-24 resize-none"
aria-invalid={isInvalid}
/>
<InputGroupAddon align="block-end">
<InputGroupText className="tabular-nums">
{field.state.value.length}/100 characters
</InputGroupText>
</InputGroupAddon>
</InputGroup>
<FieldDescription>
Allgemeine Infos zum Projekt
</FieldDescription>
{isInvalid && (
<FieldError errors={field.state.meta.errors} />
)}
</Field>
)
}}
/>
<form.Field
name="manager"
children={(field) => {
const isInvalid =
field.state.meta.isTouched && !field.state.meta.isValid
return (
<Field orientation="responsive" data-invalid={isInvalid}>
<FieldContent>
<FieldLabel htmlFor="form-tanstack-select-language">
Manager
</FieldLabel>
<FieldDescription>
Zuständiger für dieses Projekt
</FieldDescription>
{isInvalid && (
<FieldError errors={field.state.meta.errors} />
)}
</FieldContent>
<Select
name={field.name}
value={field.state.value.toString()}
onValueChange={(v) => field.handleChange(Number(v))}
>
<SelectTrigger
id="form-tanstack-select-language"
aria-invalid={isInvalid}
className="min-w-[120px]"
>
<SelectValue placeholder="Select" />
</SelectTrigger>
<SelectContent position="item-aligned">
{managers.map((manager) => (
<SelectItem
key={manager.id}
value={manager.id.toString()}
>
{manager.name}
</SelectItem>
))}
</SelectContent>
</Select>
</Field>
)
}}
/>
<form.Field
name="type"
children={(field) => {
const isInvalid =
field.state.meta.isTouched && !field.state.meta.isValid
return (
<Field orientation="responsive" data-invalid={isInvalid}>
<FieldContent>
<FieldLabel htmlFor="form-tanstack-select-language">
Projekttyp
</FieldLabel>
<FieldDescription>
Art des Projektes (Gibt voreinstellungen für Felder
wie zum Beispiel: Zahlungskonditionen vor)
</FieldDescription>
{isInvalid && (
<FieldError errors={field.state.meta.errors} />
)}
</FieldContent>
<Select
name={field.name}
value={field.state.value}
onValueChange={field.handleChange}
>
<SelectTrigger
id="form-tanstack-select-language"
aria-invalid={isInvalid}
className="min-w-[120px]"
>
<SelectValue placeholder="Select" />
</SelectTrigger>
<SelectContent position="item-aligned">
{projectTypes.map((prj) => (
<SelectItem key={prj.name} value={prj.name}>
{prj.name}
</SelectItem>
))}
</SelectContent>
</Select>
</Field>
)
}}
/>
<form.Field
name="status"
children={(field) => {
const isInvalid =
field.state.meta.isTouched && !field.state.meta.isValid
return (
<Field orientation="responsive" data-invalid={isInvalid}>
<FieldContent>
<FieldLabel htmlFor="form-tanstack-select-language">
Status
</FieldLabel>
{isInvalid && (
<FieldError errors={field.state.meta.errors} />
)}
</FieldContent>
<Select
name={field.name}
value={field.state.value}
onValueChange={field.handleChange}
>
<SelectTrigger
id="form-tanstack-select-language"
aria-invalid={isInvalid}
className="min-w-[120px]"
>
<SelectValue placeholder="Select" />
</SelectTrigger>
<SelectContent position="item-aligned">
{statusse.map((stat) => (
<SelectItem key={stat.name} value={stat.name}>
{stat.name}
</SelectItem>
))}
</SelectContent>
</Select>
</Field>
)
}}
/>
</FieldGroup>
</FieldGroup>
<FieldSeparator />
<FieldGroup>
<form.Field
name="manager"
name="client"
children={(field) => {
const isInvalid =
field.state.meta.isTouched && !field.state.meta.isValid
@@ -230,11 +388,8 @@ function RouteComponent() {
<Field orientation="responsive" data-invalid={isInvalid}>
<FieldContent>
<FieldLabel htmlFor="form-tanstack-select-language">
Manager
Client
</FieldLabel>
<FieldDescription>
Zuständiger für dieses Projekt
</FieldDescription>
{isInvalid && (
<FieldError errors={field.state.meta.errors} />
)}
@@ -252,12 +407,9 @@ function RouteComponent() {
<SelectValue placeholder="Select" />
</SelectTrigger>
<SelectContent position="item-aligned">
{managers.map((manager) => (
<SelectItem
key={manager.id}
value={manager.id.toString()}
>
{manager.name}
{clients.map((stat) => (
<SelectItem key={stat.id} value={stat.id.toString()}>
{stat.name}
</SelectItem>
))}
</SelectContent>
@@ -266,8 +418,10 @@ function RouteComponent() {
)
}}
/>
</FieldGroup>
<FieldGroup>
<form.Field
name="type"
name="location"
children={(field) => {
const isInvalid =
field.state.meta.isTouched && !field.state.meta.isValid
@@ -275,20 +429,16 @@ function RouteComponent() {
<Field orientation="responsive" data-invalid={isInvalid}>
<FieldContent>
<FieldLabel htmlFor="form-tanstack-select-language">
Projekttyp
Location
</FieldLabel>
<FieldDescription>
Art des Projektes (Gibt voreinstellungen für Felder
wie zum Beispiel: Zahlungskonditionen vor)
</FieldDescription>
{isInvalid && (
<FieldError errors={field.state.meta.errors} />
)}
</FieldContent>
<Select
name={field.name}
value={field.state.value}
onValueChange={field.handleChange}
value={field.state.value.toString()}
onValueChange={(v) => field.handleChange(Number(v))}
>
<SelectTrigger
id="form-tanstack-select-language"
@@ -298,47 +448,8 @@ function RouteComponent() {
<SelectValue placeholder="Select" />
</SelectTrigger>
<SelectContent position="item-aligned">
{projectTypes.map((prj) => (
<SelectItem key={prj.name} value={prj.name}>
{prj.name}
</SelectItem>
))}
</SelectContent>
</Select>
</Field>
)
}}
/>
<form.Field
name="status"
children={(field) => {
const isInvalid =
field.state.meta.isTouched && !field.state.meta.isValid
return (
<Field orientation="responsive" data-invalid={isInvalid}>
<FieldContent>
<FieldLabel htmlFor="form-tanstack-select-language">
Status
</FieldLabel>
{isInvalid && (
<FieldError errors={field.state.meta.errors} />
)}
</FieldContent>
<Select
name={field.name}
value={field.state.value}
onValueChange={field.handleChange}
>
<SelectTrigger
id="form-tanstack-select-language"
aria-invalid={isInvalid}
className="min-w-[120px]"
>
<SelectValue placeholder="Select" />
</SelectTrigger>
<SelectContent position="item-aligned">
{statusse.map((stat) => (
<SelectItem key={stat.name} value={stat.name}>
{locations.map((stat) => (
<SelectItem key={stat.id} value={stat.id.toString()}>
{stat.name}
</SelectItem>
))}
@@ -350,58 +461,66 @@ function RouteComponent() {
/>
</FieldGroup>
</FieldGroup>
<FieldSeparator />
<FieldGroup>
<form.Field
name="client"
children={(field) => {
const isInvalid =
field.state.meta.isTouched && !field.state.meta.isValid
return (
<Field orientation="responsive" data-invalid={isInvalid}>
<FieldContent>
<FieldLabel htmlFor="form-tanstack-select-language">
Client
</FieldLabel>
{isInvalid && (
<FieldError errors={field.state.meta.errors} />
)}
</FieldContent>
<Select
name={field.name}
value={field.state.value.toString()}
onValueChange={(v) => field.handleChange(Number(v))}
>
<SelectTrigger
id="form-tanstack-select-language"
aria-invalid={isInvalid}
className="min-w-[120px]"
>
<SelectValue placeholder="Select" />
</SelectTrigger>
<SelectContent position="item-aligned">
{clients.map((stat) => (
<SelectItem key={stat.id} value={stat.id.toString()}>
{stat.name}
</SelectItem>
))}
</SelectContent>
</Select>
</Field>
)
}}
/>
</FieldGroup>
</FieldGroup>
</form>
<Field orientation="horizontal" className="mt-4">
<Button type="button" variant="outline" onClick={() => form.reset()}>
Reset
</Button>
<Button type="submit" form="project-esther-graf-general-form">
Submit
</Button>
</Field>
</>
</form>
<Field orientation="horizontal" className="mt-4">
<Button type="button" variant="outline" onClick={() => form.reset()}>
Reset
</Button>
<Button type="submit" form="project-esther-graf-general-form">
Submit
</Button>
</Field>
</div>
<div className="col-span-6 rounded-md overflow-hidden h-128 max-h-128 bg-sidebar flex flex-col p-3">
<ShowEventLocation lat={48.202373} lon={16.332889} label='Wiener Stadthalle D'/>
</div>
</div>
)
}
function ShowEventLocation({lat, lon, label}: {lat: number, lon: number, label: string}) {
const map = useRef<MapRef>(null);
return (
<>
<h2 className='text-xl mb-1'>Location</h2>
<div className="flex-grow rounded-md overflow-hidden">
<Map
center={{
lat,
lon,
}}
zoom={15}
ref={map}
>
<MapControls
position='bottom-right'
showFullscreen
showLocate
showCompass
showZoom
/>
<MapMarker
latitude={lat}
longitude={lon}
>
<MarkerContent>
<div className="size-4 rounded-full bg-emerald-500 border-2 border-white shadow-lg" />
</MarkerContent>
<MarkerTooltip>{label}</MarkerTooltip>
<MarkerPopup>
<div className="space-y-1">
<p className="font-medium text-foreground">{label}</p>
<p className="text-xs text-muted-foreground">
{lat.toFixed(4)}, {lon.toFixed(4)}
</p>
</div>
</MarkerPopup>
</MapMarker>
</Map>
</div>
</>
)
}

View File

@@ -2,6 +2,11 @@ import { createFileRoute } from '@tanstack/react-router'
export const Route = createFileRoute('/_sidebar/projects/view/$id/personal')({
component: RouteComponent,
beforeLoad: () => {
return {
breadcrumb: 'Personal',
}
},
})
function RouteComponent() {

View File

@@ -1,9 +1,34 @@
import { createFileRoute } from '@tanstack/react-router'
import TimelineTable from '@/features/Projects/components/timelinetable'
import { Scheduler } from "@bitnoi.se/react-scheduler"
export const Route = createFileRoute('/_sidebar/projects/view/$id/timeline')({
component: RouteComponent,
beforeLoad: () => {
return {
breadcrumb: 'Timeline',
}
},
})
function RouteComponent() {
return <div>Hello "/_sidebar/projects/view/$id/timeline"!</div>
return <div className='h-full max-h-full relative'>
<Scheduler data={[{
id: '1',
data: [
{
id: '1',
startDate: new Date(2026, 0, 11, 16, 0, 0),
endDate: new Date(2026, 0, 11, 18, 0, 0),
title: "TEST",
occupancy: 1
}
],
label: {
title: "Hello World",
icon: "NZLL",
subtitle: "TEST"
}
}]}/>
</div>
}

View File

@@ -1,9 +1,43 @@
import TodoTable from '@/features/Projects/components/todotable'
import { getTodo, listTodos } from '@/gen/todo/v1/todo-TodoService_connectquery'
import {
createInfiniteQueryOptions,
createQueryOptions,
} from '@connectrpc/connect-query'
import { createFileRoute } from '@tanstack/react-router'
const fetchSize = 25
export const Route = createFileRoute('/_sidebar/projects/view/$id/todos')({
component: RouteComponent,
beforeLoad: () => {
return {
breadcrumb: 'Todos',
}
},
loader: async ({ params: { id }, context }) => {
await context.queryClient.ensureInfiniteQueryData(
createInfiniteQueryOptions(
listTodos,
{
page: 0,
perPage: fetchSize,
orberBy: 'id',
asc: false,
},
{
transport: context.transport,
getNextPageParam: (_, p) => {
console.log(p.length)
return p.length * fetchSize
// return ()
},
pageParamKey: 'page',
},
),
)
},
})
function RouteComponent() {
return <div>Hello "/_sidebar/projects/view/$id/todos"!</div>
return <TodoTable />
}