Initial commit: Port Nimara CRM (Layers 0-4)
Some checks failed
Build & Push Docker Images / build-and-push (push) Has been cancelled
Build & Push Docker Images / deploy (push) Has been cancelled
Build & Push Docker Images / lint (push) Has been cancelled

Full CRM rebuild with Next.js 15, TypeScript, Tailwind, Drizzle ORM,
PostgreSQL, Redis, BullMQ, MinIO, and Socket.io. Includes 461 source
files covering clients, berths, interests/pipeline, documents/EOI,
expenses/invoices, email, notifications, dashboard, admin, and
client portal. CI/CD via Gitea Actions with Docker builds.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-03-26 11:52:51 +01:00
commit 67d7e6e3d5
572 changed files with 86496 additions and 0 deletions

View File

@@ -0,0 +1,38 @@
'use client';
import { useEffect } from 'react';
import { useQuery } from '@tanstack/react-query';
import { apiFetch } from '@/lib/api/client';
import { usePermissionsStore } from '@/stores/permissions-store';
import type { RolePermissions } from '@/lib/db/schema/users';
interface MeResponse {
data: {
userId: string;
portId: string;
portSlug: string;
permissions: RolePermissions | null;
isSuperAdmin: boolean;
user: { email: string; name: string };
};
}
export function PermissionsProvider({ children }: { children: React.ReactNode }) {
const setPermissions = usePermissionsStore((s) => s.setPermissions);
const { data } = useQuery<MeResponse>({
queryKey: ['me'],
queryFn: () => apiFetch<MeResponse>('/api/v1/me'),
staleTime: 5 * 60 * 1000, // 5 minutes
retry: 1,
});
useEffect(() => {
if (data?.data) {
setPermissions(data.data.permissions, data.data.isSuperAdmin, data.data.userId);
}
}, [data, setPermissions]);
return <>{children}</>;
}

View File

@@ -0,0 +1,66 @@
'use client';
import { createContext, useContext, useEffect, type ReactNode } from 'react';
import { useParams } from 'next/navigation';
import { useUIStore } from '@/stores/ui-store';
import type { Port } from '@/lib/db/schema/ports';
interface PortContextValue {
ports: Port[];
currentPort: Port | null;
currentPortId: string | null;
currentPortSlug: string | null;
}
const PortContext = createContext<PortContextValue>({
ports: [],
currentPort: null,
currentPortId: null,
currentPortSlug: null,
});
interface PortProviderProps {
children: ReactNode;
ports: Port[];
defaultPortId: string | null;
}
export function PortProvider({ children, ports, defaultPortId }: PortProviderProps) {
const params = useParams();
const portSlugFromUrl = params?.portSlug as string | undefined;
const setPort = useUIStore((s) => s.setPort);
const currentPortId = useUIStore((s) => s.currentPortId);
const currentPortSlug = useUIStore((s) => s.currentPortSlug);
// Resolve current port — URL slug takes priority over stored port
const currentPort =
ports.find((p) => p.slug === portSlugFromUrl) ??
ports.find((p) => p.id === currentPortId) ??
(defaultPortId ? (ports.find((p) => p.id === defaultPortId) ?? null) : null);
// Sync Zustand store whenever the active port changes
useEffect(() => {
if (currentPort && (currentPort.id !== currentPortId || currentPort.slug !== currentPortSlug)) {
setPort(currentPort.id, currentPort.slug);
}
}, [currentPort, currentPortId, currentPortSlug, setPort]);
return (
<PortContext.Provider
value={{
ports,
currentPort: currentPort ?? null,
currentPortId: currentPort?.id ?? null,
currentPortSlug: currentPort?.slug ?? null,
}}
>
{children}
</PortContext.Provider>
);
}
export function usePortContext(): PortContextValue {
return useContext(PortContext);
}

View File

@@ -0,0 +1,34 @@
'use client';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { ReactQueryDevtools } from '@tanstack/react-query-devtools';
import { useState, type ReactNode } from 'react';
export function QueryProvider({ children }: { children: ReactNode }) {
const [queryClient] = useState(
() =>
new QueryClient({
defaultOptions: {
queries: {
staleTime: 30 * 1000,
retry: 1,
refetchOnWindowFocus: false,
},
mutations: {
onError: (error) => {
console.error('Mutation error:', error);
},
},
},
}),
);
return (
<QueryClientProvider client={queryClient}>
{children}
{process.env.NODE_ENV === 'development' && (
<ReactQueryDevtools initialIsOpen={false} />
)}
</QueryClientProvider>
);
}

View File

@@ -0,0 +1,44 @@
'use client';
import { createContext, useContext, useEffect, useState, type ReactNode } from 'react';
import { io, type Socket } from 'socket.io-client';
import { useSession } from '@/lib/auth/client';
import { usePortStore } from '@/stores/ui-store';
const SocketContext = createContext<Socket | null>(null);
export function SocketProvider({ children }: { children: ReactNode }) {
const { data: session } = useSession();
const currentPortId = usePortStore((s) => s.currentPortId);
const [socket, setSocket] = useState<Socket | null>(null);
useEffect(() => {
if (!session?.user || !currentPortId) return;
const s = io(process.env.NEXT_PUBLIC_APP_URL!, {
path: '/socket.io/',
withCredentials: true,
auth: { portId: currentPortId },
transports: ['websocket', 'polling'],
});
s.on('connect', () => setSocket(s));
s.on('disconnect', () => setSocket(null));
return () => {
s.disconnect();
setSocket(null);
};
}, [session?.user, currentPortId]);
return (
<SocketContext.Provider value={socket}>
{children}
</SocketContext.Provider>
);
}
export function useSocket() {
return useContext(SocketContext);
}