,
+ coordinates: [number, number]
+ ) => void;
+ /** Callback when a cluster is clicked. If not provided, zooms into the cluster */
+ onClusterClick?: (
+ clusterId: number,
+ coordinates: [number, number],
+ pointCount: number
+ ) => void;
+};
+
+function MapClusterLayer<
+ P extends GeoJSON.GeoJsonProperties = GeoJSON.GeoJsonProperties
+>({
+ data,
+ clusterMaxZoom = 14,
+ clusterRadius = 50,
+ clusterColors = ["#51bbd6", "#f1f075", "#f28cb1"],
+ clusterThresholds = [100, 750],
+ pointColor = "#3b82f6",
+ onPointClick,
+ onClusterClick,
+}: MapClusterLayerProps) {
+ const { map, isLoaded } = useMap();
+ const id = useId();
+ const sourceId = `cluster-source-${id}`;
+ const clusterLayerId = `clusters-${id}`;
+ const clusterCountLayerId = `cluster-count-${id}`;
+ const unclusteredLayerId = `unclustered-point-${id}`;
+
+ const stylePropsRef = useRef({
+ clusterColors,
+ clusterThresholds,
+ pointColor,
+ });
+
+ // Add source and layers on mount
+ useEffect(() => {
+ if (!isLoaded || !map) return;
+
+ // Add clustered GeoJSON source
+ map.addSource(sourceId, {
+ type: "geojson",
+ data,
+ cluster: true,
+ clusterMaxZoom,
+ clusterRadius,
+ });
+
+ // Add cluster circles layer
+ map.addLayer({
+ id: clusterLayerId,
+ type: "circle",
+ source: sourceId,
+ filter: ["has", "point_count"],
+ paint: {
+ "circle-color": [
+ "step",
+ ["get", "point_count"],
+ clusterColors[0],
+ clusterThresholds[0],
+ clusterColors[1],
+ clusterThresholds[1],
+ clusterColors[2],
+ ],
+ "circle-radius": [
+ "step",
+ ["get", "point_count"],
+ 20,
+ clusterThresholds[0],
+ 30,
+ clusterThresholds[1],
+ 40,
+ ],
+ },
+ });
+
+ // Add cluster count text layer
+ map.addLayer({
+ id: clusterCountLayerId,
+ type: "symbol",
+ source: sourceId,
+ filter: ["has", "point_count"],
+ layout: {
+ "text-field": "{point_count_abbreviated}",
+ "text-size": 12,
+ },
+ paint: {
+ "text-color": "#fff",
+ },
+ });
+
+ // Add unclustered point layer
+ map.addLayer({
+ id: unclusteredLayerId,
+ type: "circle",
+ source: sourceId,
+ filter: ["!", ["has", "point_count"]],
+ paint: {
+ "circle-color": pointColor,
+ "circle-radius": 6,
+ },
+ });
+
+ return () => {
+ try {
+ if (map.getLayer(clusterCountLayerId))
+ map.removeLayer(clusterCountLayerId);
+ if (map.getLayer(unclusteredLayerId))
+ map.removeLayer(unclusteredLayerId);
+ if (map.getLayer(clusterLayerId)) map.removeLayer(clusterLayerId);
+ if (map.getSource(sourceId)) map.removeSource(sourceId);
+ } catch {
+ // ignore
+ }
+ };
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, [isLoaded, map, sourceId]);
+
+ // Update source data when data prop changes (only for non-URL data)
+ useEffect(() => {
+ if (!isLoaded || !map || typeof data === "string") return;
+
+ const source = map.getSource(sourceId) as MapLibreGL.GeoJSONSource;
+ if (source) {
+ source.setData(data);
+ }
+ }, [isLoaded, map, data, sourceId]);
+
+ // Update layer styles when props change
+ useEffect(() => {
+ if (!isLoaded || !map) return;
+
+ const prev = stylePropsRef.current;
+ const colorsChanged =
+ prev.clusterColors !== clusterColors ||
+ prev.clusterThresholds !== clusterThresholds;
+
+ // Update cluster layer colors and sizes
+ if (map.getLayer(clusterLayerId) && colorsChanged) {
+ map.setPaintProperty(clusterLayerId, "circle-color", [
+ "step",
+ ["get", "point_count"],
+ clusterColors[0],
+ clusterThresholds[0],
+ clusterColors[1],
+ clusterThresholds[1],
+ clusterColors[2],
+ ]);
+ map.setPaintProperty(clusterLayerId, "circle-radius", [
+ "step",
+ ["get", "point_count"],
+ 20,
+ clusterThresholds[0],
+ 30,
+ clusterThresholds[1],
+ 40,
+ ]);
+ }
+
+ // Update unclustered point layer color
+ if (map.getLayer(unclusteredLayerId) && prev.pointColor !== pointColor) {
+ map.setPaintProperty(unclusteredLayerId, "circle-color", pointColor);
+ }
+
+ stylePropsRef.current = { clusterColors, clusterThresholds, pointColor };
+ }, [
+ isLoaded,
+ map,
+ clusterLayerId,
+ unclusteredLayerId,
+ clusterColors,
+ clusterThresholds,
+ pointColor,
+ ]);
+
+ // Handle click events
+ useEffect(() => {
+ if (!isLoaded || !map) return;
+
+ // Cluster click handler - zoom into cluster
+ const handleClusterClick = async (
+ e: MapLibreGL.MapMouseEvent & {
+ features?: MapLibreGL.MapGeoJSONFeature[];
+ }
+ ) => {
+ const features = map.queryRenderedFeatures(e.point, {
+ layers: [clusterLayerId],
+ });
+ if (!features.length) return;
+
+ const feature = features[0];
+ const clusterId = feature.properties?.cluster_id as number;
+ const pointCount = feature.properties?.point_count as number;
+ const coordinates = (feature.geometry as GeoJSON.Point).coordinates as [
+ number,
+ number
+ ];
+
+ if (onClusterClick) {
+ onClusterClick(clusterId, coordinates, pointCount);
+ } else {
+ // Default behavior: zoom to cluster expansion zoom
+ const source = map.getSource(sourceId) as MapLibreGL.GeoJSONSource;
+ const zoom = await source.getClusterExpansionZoom(clusterId);
+ map.easeTo({
+ center: coordinates,
+ zoom,
+ });
+ }
+ };
+
+ // Unclustered point click handler
+ const handlePointClick = (
+ e: MapLibreGL.MapMouseEvent & {
+ features?: MapLibreGL.MapGeoJSONFeature[];
+ }
+ ) => {
+ if (!onPointClick || !e.features?.length) return;
+
+ const feature = e.features[0];
+ const coordinates = (
+ feature.geometry as GeoJSON.Point
+ ).coordinates.slice() as [number, number];
+
+ // Handle world copies
+ while (Math.abs(e.lngLat.lng - coordinates[0]) > 180) {
+ coordinates[0] += e.lngLat.lng > coordinates[0] ? 360 : -360;
+ }
+
+ onPointClick(
+ feature as unknown as GeoJSON.Feature,
+ coordinates
+ );
+ };
+
+ // Cursor style handlers
+ const handleMouseEnterCluster = () => {
+ map.getCanvas().style.cursor = "pointer";
+ };
+ const handleMouseLeaveCluster = () => {
+ map.getCanvas().style.cursor = "";
+ };
+ const handleMouseEnterPoint = () => {
+ if (onPointClick) {
+ map.getCanvas().style.cursor = "pointer";
+ }
+ };
+ const handleMouseLeavePoint = () => {
+ map.getCanvas().style.cursor = "";
+ };
+
+ map.on("click", clusterLayerId, handleClusterClick);
+ map.on("click", unclusteredLayerId, handlePointClick);
+ map.on("mouseenter", clusterLayerId, handleMouseEnterCluster);
+ map.on("mouseleave", clusterLayerId, handleMouseLeaveCluster);
+ map.on("mouseenter", unclusteredLayerId, handleMouseEnterPoint);
+ map.on("mouseleave", unclusteredLayerId, handleMouseLeavePoint);
+
+ return () => {
+ map.off("click", clusterLayerId, handleClusterClick);
+ map.off("click", unclusteredLayerId, handlePointClick);
+ map.off("mouseenter", clusterLayerId, handleMouseEnterCluster);
+ map.off("mouseleave", clusterLayerId, handleMouseLeaveCluster);
+ map.off("mouseenter", unclusteredLayerId, handleMouseEnterPoint);
+ map.off("mouseleave", unclusteredLayerId, handleMouseLeavePoint);
+ };
+ }, [
+ isLoaded,
+ map,
+ clusterLayerId,
+ unclusteredLayerId,
+ sourceId,
+ onClusterClick,
+ onPointClick,
+ ]);
+
+ return null;
+}
+
+export {
+ Map,
+ useMap,
+ MapMarker,
+ MarkerContent,
+ MarkerPopup,
+ MarkerTooltip,
+ MarkerLabel,
+ MapPopup,
+ MapControls,
+ MapRoute,
+ MapClusterLayer,
+};
+
+export type { MapRef };
diff --git a/src/components/ui/resizable.tsx b/src/components/ui/resizable.tsx
new file mode 100644
index 0000000..b9432bd
--- /dev/null
+++ b/src/components/ui/resizable.tsx
@@ -0,0 +1,54 @@
+import * as React from "react"
+import { GripVerticalIcon } from "lucide-react"
+import * as ResizablePrimitive from "react-resizable-panels"
+
+import { cn } from "@/lib/utils"
+
+function ResizablePanelGroup({
+ className,
+ ...props
+}: React.ComponentProps) {
+ return (
+
+ )
+}
+
+function ResizablePanel({
+ ...props
+}: React.ComponentProps) {
+ return
+}
+
+function ResizableHandle({
+ withHandle,
+ className,
+ ...props
+}: React.ComponentProps & {
+ withHandle?: boolean
+}) {
+ return (
+ div]:rotate-90",
+ className
+ )}
+ {...props}
+ >
+ {withHandle && (
+
+
+
+ )}
+
+ )
+}
+
+export { ResizablePanelGroup, ResizablePanel, ResizableHandle }
diff --git a/src/env.ts b/src/env.ts
index 9b2dd96..c1706c1 100644
--- a/src/env.ts
+++ b/src/env.ts
@@ -1,4 +1,5 @@
import { createEnv } from '@t3-oss/env-core'
+import { vite } from '@t3-oss/env-core/presets-zod'
import { z } from 'zod'
export const env = createEnv({
@@ -14,14 +15,17 @@ export const env = createEnv({
client: {
VITE_APP_TITLE: z.string().min(1).optional(),
- VITE_BACKEND_URI: z.string(),
+ VITE_RPC_URI: z.string().optional(),
+ VITE_BACKEND_URI: z.string().optional(),
+ VITE_ENVIRONMENT: z.string().optional(),
},
/**
* What object holds the environment variables at runtime. This is usually
* `process.env` or `import.meta.env`.
*/
- runtimeEnv: import.meta.env,
+ runtimeEnv: process.env || import.meta.env,
+ // extends: [vite()],
/**
* By default, this library will feed the environment variables directly to
@@ -38,3 +42,5 @@ export const env = createEnv({
*/
emptyStringAsUndefined: true,
})
+
+console.log(env)
diff --git a/src/features/Auth/components/nav-user.tsx b/src/features/Auth/components/nav-user.tsx
index 62ae914..596c4d9 100644
--- a/src/features/Auth/components/nav-user.tsx
+++ b/src/features/Auth/components/nav-user.tsx
@@ -1,9 +1,6 @@
-'use client'
-
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,
@@ -20,23 +17,14 @@ import {
SidebarMenuItem,
useSidebar,
} from '@/components/ui/sidebar'
-import { env } from '@/env'
+import { Button } from '@/components/ui/button'
+import { useOidc } from '@/lib/oidc'
-export function NavUser({
- user,
-}: {
- user: {
- name: string
- email: string
- avatar: string
- }
-}) {
+export function NavUser() {
+ const { logout, decodedIdToken } = useOidc()
const { isMobile } = useSidebar()
- const { data } = useProfile()
- if (!data) {
- return Loading...
- }
+ if (!logout) return
return (
@@ -48,12 +36,17 @@ export function NavUser({
className="data-[state=open]:bg-sidebar-accent data-[state=open]:text-sidebar-accent-foreground"
>
-
+
KH
- {data.name}
- {data.Email}
+
+ {decodedIdToken.name}
+
+ {decodedIdToken.email}
@@ -67,12 +60,19 @@ export function NavUser({
-
+
CN
- {data.name}
- {data.Email}
+
+ {decodedIdToken.name}
+
+
+ {decodedIdToken.email}
+
@@ -99,12 +99,17 @@ export function NavUser({
-
+
+
diff --git a/src/features/Auth/queries.ts b/src/features/Auth/queries.ts
index 29ccc1f..4926e63 100644
--- a/src/features/Auth/queries.ts
+++ b/src/features/Auth/queries.ts
@@ -1,6 +1,7 @@
-import { useNavigate } from '@tanstack/react-router'
import { useQuery, useQueryClient } from '@tanstack/react-query'
import { env } from '@/env'
+import { fetchWithAuth } from '@/lib/oidc'
+import { getBackendURI } from '@/routes'
const sessionKeys = {
all: ['sessions'] as const,
@@ -28,8 +29,8 @@ export function useCurrentSession() {
return useQuery({
queryKey: sessionKeys.current(),
queryFn: async () => {
- const data = await fetch(
- env.VITE_BACKEND_URI + '/api/auth/currentSession',
+ const data = await fetchWithAuth(
+ (await getBackendURI()) + '/api/auth/currentSession',
{
credentials: 'include',
},
@@ -41,17 +42,18 @@ export function useCurrentSession() {
export function useProfile() {
const queryClient = useQueryClient()
- const navigate = useNavigate()
return useQuery({
queryKey: sessionKeys.current(),
queryFn: async () => {
- const data = await fetch(env.VITE_BACKEND_URI + '/v1/users/current', {
- credentials: 'include',
- })
+ const data = await fetchWithAuth(
+ (await getBackendURI()) + '/v1/users/current',
+ {
+ credentials: 'include',
+ },
+ )
if (data.status == 401) {
queryClient.invalidateQueries()
- navigate({ to: '/' })
}
return await data.json()
},
diff --git a/src/features/CRM/queries.ts b/src/features/CRM/queries.ts
index 87a5e34..dbf3e77 100644
--- a/src/features/CRM/queries.ts
+++ b/src/features/CRM/queries.ts
@@ -4,7 +4,8 @@ import {
useQueryClient,
useSuspenseQuery,
} from '@tanstack/react-query'
-import { env } from '@/env'
+import { fetchWithAuth } from '@/lib/oidc'
+import { getBackendURI } from '@/routes'
const ansprechpartnerKeys = {
all: ['ansprechpartner'] as const,
@@ -36,8 +37,8 @@ export function useAllAnsprechpartners() {
return useQuery({
queryKey: ansprechpartnerKeys.lists(),
queryFn: async () => {
- const data = await fetch(
- env.VITE_BACKEND_URI + '/v1/ansprechpartner/all',
+ const data = await fetchWithAuth(
+ (await getBackendURI()) + '/v1/ansprechpartner/all',
{
credentials: 'include',
},
@@ -51,8 +52,8 @@ export function useAnsprechpartner(id: number) {
return useSuspenseQuery({
queryKey: ansprechpartnerKeys.detail(id),
queryFn: async () => {
- const data = await fetch(
- env.VITE_BACKEND_URI + '/v1/ansprechpartner/' + id,
+ const data = await fetchWithAuth(
+ (await getBackendURI()) + '/v1/ansprechpartner/' + id,
{ credentials: 'include' },
)
return await data.json()
@@ -65,8 +66,8 @@ export function useAnsprechpartnerEditMutation() {
return useMutation({
mutationFn: async (ansprechpartner: Ansprechpartner) => {
- await fetch(
- env.VITE_BACKEND_URI + '/v1/ansprechpartner/' + ansprechpartner.ID,
+ await fetchWithAuth(
+ (await getBackendURI()) + '/v1/ansprechpartner/' + ansprechpartner.ID,
{
headers: {
'content-type': 'application/json',
diff --git a/src/features/Kanban/components/Kanban.tsx b/src/features/Kanban/components/Kanban.tsx
index eec55c3..163566a 100644
--- a/src/features/Kanban/components/Kanban.tsx
+++ b/src/features/Kanban/components/Kanban.tsx
@@ -1,11 +1,135 @@
-import type { ReactNode } from 'react'
+import { createContext, Fragment, useContext, type ReactElement, type ReactNode } from 'react'
+import KanbanDropzone from './KanbanDropzone'
+import { DndContext } from '@dnd-kit/core'
+import {
+ Sortable,
+ SortableContent,
+ SortableItem,
+ SortableOverlay,
+} from "@/components/ui/sortable";
+import { CSS } from '@dnd-kit/utilities'
-function Kanban({ children }: { children: ReactNode }) {
+type ColumnRendererProps = {
+ name: string
+ itemCount: number
+}
+
+type KanbanContextType = {
+ columns: any[]
+ setColumns: (cols: any[]) => void
+ columnRenderer: ReactElement
+}
+
+const kanbanContext = createContext({ columns: [], setColumns: () => {}, columnRenderer: <>> });
+
+export const useKanban = () => {
+ const ctx = useContext(kanbanContext)
+
+ const reorderColumns = (name: string, toIndex: number) => {
+ const fromIndex = ctx.columns.findIndex((col: any) => col.name == name);
+ console.log(fromIndex, toIndex, ctx.columns)
+ let fromCompIndex;
+ if (toIndex < fromIndex) {
+ fromCompIndex = fromIndex - 1;
+ } else {
+ fromCompIndex = 0;
+ }
+
+ if (fromIndex === -1 || toIndex < 0 || toIndex >= ctx.columns.length) { console.log("invalid"); return; }
+
+
+ const updatedColumns = [...ctx.columns];
+
+ const movedColumn = ctx.columns[fromIndex];
+ updatedColumns.splice(toIndex, 0, movedColumn);
+ console.log("valid", movedColumn, fromIndex, fromCompIndex, toIndex, updatedColumns);
+ updatedColumns.splice(fromCompIndex, 1);
+ console.log(movedColumn, fromIndex, fromCompIndex, toIndex, updatedColumns);
+
+ ctx.setColumns(updatedColumns);
+ }
+
+ return { reorderColumns, ...ctx }
+}
+
+export type KanbanColumn = {
+ id: number,
+ name: string
+}
+
+interface TrickCardProps
+ extends Omit, "value"> {
+ trick: KanbanColumn;
+}
+
+function TrickCard({ trick, ...props }: TrickCardProps) {
return (
-
- {children}
-
+
+
+
+ {trick.name}
+
+
+ {trick.id}
+
+
+
+ );
+}
+
+function KanbanOLD({ columns, setColumns, columnRenderer }: { columns: KanbanColumn[], setColumns: (v: KanbanColumn[]) => void, columnRenderer: ReactElement }) {
+ return (
+ c.id} orientation='horizontal'>
+
+ {columns.map((c) => )}
+
+
+ {(activeItem) => {
+ const c = columns.find((c) => c.id === activeItem.value);
+
+ if (!c) return null;
+
+ return ;
+ }}
+
+
)
}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+function KanbanInner() {
+ const { columns, columnRenderer } = useKanban();
+
+ return (
+
+
+ {columns.map((column, index) => (
+
+
+ {}
+
+
+
+ ))}
+
+ );
+}
+
export default Kanban
diff --git a/src/features/Kanban/components/KanbanColumn.tsx b/src/features/Kanban/components/KanbanColumn.tsx
index d067588..a78c780 100644
--- a/src/features/Kanban/components/KanbanColumn.tsx
+++ b/src/features/Kanban/components/KanbanColumn.tsx
@@ -1,59 +1,91 @@
+import { listTodos } from '@/gen/todo/v1/todo-TodoService_connectquery'
+import { Field, Operation } from '@/gen/todo/v1/todo_pb'
+import { useSuspenseQuery } from '@connectrpc/connect-query'
+import { useDroppable } from '@dnd-kit/core'
import { GripVertical, Tickets } from 'lucide-react'
-import { useRef } from 'react'
-import type { ReactNode } from 'react'
+import { Suspense, useRef } from 'react'
function KanbanColumn({
- children,
name,
- itemCount,
+ value,
}: {
- children: ReactNode
- name: string
- itemCount: number
+ name: string,
+ value: string,
}) {
- const column = useRef(null)
+ // const column = useRef(null)
+ const { isOver, setNodeRef } = useDroppable({
+ id: name
+ })
- const handleDraggableStart = () => {
- column.current?.setAttribute('draggable', 'true')
- }
+ // const handleDragStart = (e: any) => {
+ // e.dataTransfer?.setData('text/custom', name)
+ // }
- const handleDraggableStop = () => {
- column.current?.setAttribute('draggable', 'false')
- }
-
- const handleDragStart = (e: DragEvent) => {
- e.dataTransfer?.setData('text/custom', 'ASDF')
- }
-
- const handleDragStop = () => {
- column.current?.setAttribute('draggable', 'false')
- }
+ // const handleDragStop = () => {
+ // column.current?.setAttribute('draggable', 'false')
+ // }
return (
-
- {children}
+
Loading... }>
+
+