Fix all ESLint errors: remove unused imports, replace any types
- Remove ~60 unused imports and variables across 88 files - Replace ~80 `any` type annotations with proper types (unknown, Record<string, unknown>, or specific types) - Prefix unused callback args with underscore - Fix unescaped JSX entities - Lint now passes cleanly (0 errors, 2 intentional img warnings) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,7 +1,6 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import { useParams } from 'next/navigation';
|
|
||||||
import { Grid, List, Upload } from 'lucide-react';
|
import { Grid, List, Upload } from 'lucide-react';
|
||||||
import { useQueryClient } from '@tanstack/react-query';
|
import { useQueryClient } from '@tanstack/react-query';
|
||||||
|
|
||||||
@@ -19,13 +18,12 @@ import { apiFetch } from '@/lib/api/client';
|
|||||||
import type { FileRow } from '@/components/files/file-grid';
|
import type { FileRow } from '@/components/files/file-grid';
|
||||||
|
|
||||||
export default function DocumentsPage() {
|
export default function DocumentsPage() {
|
||||||
const params = useParams<{ portSlug: string }>();
|
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
|
|
||||||
const { viewMode, setViewMode, currentFolder, setCurrentFolder } = useFileBrowserStore();
|
const { viewMode, setViewMode, currentFolder, setCurrentFolder } = useFileBrowserStore();
|
||||||
const [showUpload, setShowUpload] = useState(false);
|
const [showUpload, setShowUpload] = useState(false);
|
||||||
const [previewFile, setPreviewFile] = useState<FileRow | null>(null);
|
const [previewFile, setPreviewFile] = useState<FileRow | null>(null);
|
||||||
const [renameFile, setRenameFile] = useState<FileRow | null>(null);
|
const [, setRenameFile] = useState<FileRow | null>(null);
|
||||||
|
|
||||||
const { data, isLoading } = usePaginatedQuery<FileRow & { storagePath: string }>({
|
const { data, isLoading } = usePaginatedQuery<FileRow & { storagePath: string }>({
|
||||||
queryKey: ['files'],
|
queryKey: ['files'],
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import { useParams, useRouter } from 'next/navigation';
|
import { useParams } from 'next/navigation';
|
||||||
import { Plus, Download, FileText, FileSpreadsheet } from 'lucide-react';
|
import { Plus, Download, FileText, FileSpreadsheet } from 'lucide-react';
|
||||||
import { useMutation, useQueryClient } from '@tanstack/react-query';
|
import { useMutation, useQueryClient } from '@tanstack/react-query';
|
||||||
|
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
import { useState, useRef } from 'react';
|
import { useState, useRef } from 'react';
|
||||||
import { useParams, useRouter } from 'next/navigation';
|
import { useParams, useRouter } from 'next/navigation';
|
||||||
import { useMutation } from '@tanstack/react-query';
|
import { useMutation } from '@tanstack/react-query';
|
||||||
import { Upload, Loader2, Camera, ScanLine } from 'lucide-react';
|
import { Upload, Loader2, ScanLine } from 'lucide-react';
|
||||||
|
|
||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
import { Input } from '@/components/ui/input';
|
import { Input } from '@/components/ui/input';
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import { useState } from 'react';
|
|||||||
import { useParams, useRouter } from 'next/navigation';
|
import { useParams, useRouter } from 'next/navigation';
|
||||||
import { useForm, FormProvider } from 'react-hook-form';
|
import { useForm, FormProvider } from 'react-hook-form';
|
||||||
import { zodResolver } from '@hookform/resolvers/zod';
|
import { zodResolver } from '@hookform/resolvers/zod';
|
||||||
import { useMutation, useQuery } from '@tanstack/react-query';
|
import { useMutation } from '@tanstack/react-query';
|
||||||
import { ChevronLeft, ChevronRight, Check, Loader2 } from 'lucide-react';
|
import { ChevronLeft, ChevronRight, Check, Loader2 } from 'lucide-react';
|
||||||
|
|
||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
@@ -19,7 +19,6 @@ import {
|
|||||||
SelectValue,
|
SelectValue,
|
||||||
} from '@/components/ui/select';
|
} from '@/components/ui/select';
|
||||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
||||||
import { Badge } from '@/components/ui/badge';
|
|
||||||
import { InvoiceLineItems } from '@/components/invoices/invoice-line-items';
|
import { InvoiceLineItems } from '@/components/invoices/invoice-line-items';
|
||||||
import { apiFetch } from '@/lib/api/client';
|
import { apiFetch } from '@/lib/api/client';
|
||||||
import { createInvoiceSchema, type CreateInvoiceInput } from '@/lib/validators/invoices';
|
import { createInvoiceSchema, type CreateInvoiceInput } from '@/lib/validators/invoices';
|
||||||
@@ -75,7 +74,7 @@ export default function NewInvoicePage() {
|
|||||||
method: 'POST',
|
method: 'POST',
|
||||||
body: data,
|
body: data,
|
||||||
}),
|
}),
|
||||||
onSuccess: (res: any) => {
|
onSuccess: (res: { data?: { id?: string } }) => {
|
||||||
const id = res?.data?.id;
|
const id = res?.data?.id;
|
||||||
if (id) {
|
if (id) {
|
||||||
router.push(`/${portSlug}/invoices/${id}`);
|
router.push(`/${portSlug}/invoices/${id}`);
|
||||||
@@ -217,7 +216,7 @@ export default function NewInvoicePage() {
|
|||||||
<Label>Payment Terms</Label>
|
<Label>Payment Terms</Label>
|
||||||
<Select
|
<Select
|
||||||
defaultValue="net30"
|
defaultValue="net30"
|
||||||
onValueChange={(v) => setValue('paymentTerms', v as any)}
|
onValueChange={(v) => setValue('paymentTerms', v as CreateInvoiceInput['paymentTerms'])}
|
||||||
>
|
>
|
||||||
<SelectTrigger>
|
<SelectTrigger>
|
||||||
<SelectValue placeholder="Select terms" />
|
<SelectValue placeholder="Select terms" />
|
||||||
@@ -266,7 +265,7 @@ export default function NewInvoicePage() {
|
|||||||
<InvoiceLineItems name="lineItems" />
|
<InvoiceLineItems name="lineItems" />
|
||||||
{errors.lineItems && !Array.isArray(errors.lineItems) && (
|
{errors.lineItems && !Array.isArray(errors.lineItems) && (
|
||||||
<p className="text-xs text-destructive mt-2">
|
<p className="text-xs text-destructive mt-2">
|
||||||
{(errors.lineItems as any).message}
|
{(errors.lineItems as { message?: string }).message}
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
{errors.root && (
|
{errors.root && (
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import type { Metadata } from 'next';
|
import type { Metadata } from 'next';
|
||||||
import { redirect } from 'next/navigation';
|
|
||||||
|
|
||||||
import { getPortalSession } from '@/lib/portal/auth';
|
import { getPortalSession } from '@/lib/portal/auth';
|
||||||
import { getPortalDashboard } from '@/lib/services/portal.service';
|
import { getPortalDashboard } from '@/lib/services/portal.service';
|
||||||
@@ -13,8 +12,6 @@ export const metadata: Metadata = {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
const PUBLIC_PORTAL_PATHS = ['/portal/login', '/portal/verify'];
|
|
||||||
|
|
||||||
export default async function PortalLayout({
|
export default async function PortalLayout({
|
||||||
children,
|
children,
|
||||||
}: {
|
}: {
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ export default function PortalLoginPage() {
|
|||||||
|
|
||||||
if (!res.ok) {
|
if (!res.ok) {
|
||||||
const data = await res.json().catch(() => ({}));
|
const data = await res.json().catch(() => ({}));
|
||||||
setError((data as any).error ?? 'Something went wrong. Please try again.');
|
setError((data as { error?: string }).error ?? 'Something went wrong. Please try again.');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ export default function PortalVerifyPage() {
|
|||||||
const token = searchParams.get('token');
|
const token = searchParams.get('token');
|
||||||
|
|
||||||
if (!token) {
|
if (!token) {
|
||||||
router.replace('/portal/login?error=missing_token' as any);
|
router.replace('/portal/login?error=missing_token');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import { errorResponse } from '@/lib/errors';
|
|||||||
import { getMergeFields } from '@/lib/services/document-templates';
|
import { getMergeFields } from '@/lib/services/document-templates';
|
||||||
|
|
||||||
export const GET = withAuth(
|
export const GET = withAuth(
|
||||||
withPermission('documents', 'view', async (req, ctx) => {
|
withPermission('documents', 'view', async (_req, _ctx) => {
|
||||||
try {
|
try {
|
||||||
const mergeFields = getMergeFields();
|
const mergeFields = getMergeFields();
|
||||||
return NextResponse.json({ data: mergeFields });
|
return NextResponse.json({ data: mergeFields });
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
import { NextResponse } from 'next/server';
|
import { NextResponse } from 'next/server';
|
||||||
|
|
||||||
import { withAuth, withPermission } from '@/lib/api/helpers';
|
import { withAuth, withPermission } from '@/lib/api/helpers';
|
||||||
import { parseBody } from '@/lib/api/route-helpers';
|
|
||||||
import { errorResponse } from '@/lib/errors';
|
import { errorResponse } from '@/lib/errors';
|
||||||
import { exportCsv } from '@/lib/services/expense-export';
|
import { exportCsv } from '@/lib/services/expense-export';
|
||||||
import { listExpensesSchema } from '@/lib/validators/expenses';
|
import { listExpensesSchema } from '@/lib/validators/expenses';
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ import { NextRequest, NextResponse } from 'next/server';
|
|||||||
import { createHash } from 'crypto';
|
import { createHash } from 'crypto';
|
||||||
|
|
||||||
import { db } from '@/lib/db';
|
import { db } from '@/lib/db';
|
||||||
import { documentEvents } from '@/lib/db/schema/documents';
|
|
||||||
import { verifyDocumensoSignature } from '@/lib/services/documenso-webhook';
|
import { verifyDocumensoSignature } from '@/lib/services/documenso-webhook';
|
||||||
import {
|
import {
|
||||||
handleRecipientSigned,
|
handleRecipientSigned,
|
||||||
|
|||||||
@@ -38,7 +38,7 @@ export function QueueOverview({ queues }: QueueOverviewProps) {
|
|||||||
const params = useParams<{ portSlug: string }>();
|
const params = useParams<{ portSlug: string }>();
|
||||||
|
|
||||||
function handleQueueClick(queueName: string) {
|
function handleQueueClick(queueName: string) {
|
||||||
router.push(`/${params.portSlug}/admin/monitoring/${queueName}` as any);
|
router.push(`/${params.portSlug}/admin/monitoring/${queueName}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
import { Checkbox } from '@/components/ui/checkbox';
|
import { Checkbox } from '@/components/ui/checkbox';
|
||||||
import { Label } from '@/components/ui/label';
|
import { Label } from '@/components/ui/label';
|
||||||
import { WEBHOOK_EVENTS, type WebhookEvent } from '@/lib/services/webhook-event-map';
|
import { type WebhookEvent } from '@/lib/services/webhook-event-map';
|
||||||
|
|
||||||
// ─── Event Groups ─────────────────────────────────────────────────────────────
|
// ─── Event Groups ─────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ import { type ColumnDef } from '@tanstack/react-table';
|
|||||||
import { MoreHorizontal, Pencil, Activity } from 'lucide-react';
|
import { MoreHorizontal, Pencil, Activity } from 'lucide-react';
|
||||||
import { useRouter, useParams } from 'next/navigation';
|
import { useRouter, useParams } from 'next/navigation';
|
||||||
|
|
||||||
import { Badge } from '@/components/ui/badge';
|
|
||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
import {
|
import {
|
||||||
DropdownMenu,
|
DropdownMenu,
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import { Pencil, RefreshCw } from 'lucide-react';
|
import { Pencil, RefreshCw } from 'lucide-react';
|
||||||
import { useMutation, useQueryClient } from '@tanstack/react-query';
|
import { useQueryClient } from '@tanstack/react-query';
|
||||||
import { toast } from 'sonner';
|
import { toast } from 'sonner';
|
||||||
import { useForm } from 'react-hook-form';
|
import { useForm } from 'react-hook-form';
|
||||||
import { zodResolver } from '@hookform/resolvers/zod';
|
import { zodResolver } from '@hookform/resolvers/zod';
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ export function BerthDetail({ berthId }: BerthDetailProps) {
|
|||||||
'berth:statusChanged': [['berth', berthId]],
|
'berth:statusChanged': [['berth', berthId]],
|
||||||
});
|
});
|
||||||
|
|
||||||
const berth = data as any;
|
const berth = data as Record<string, unknown>;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<DetailLayout
|
<DetailLayout
|
||||||
|
|||||||
@@ -68,8 +68,7 @@ export function BerthForm({ berth, open, onOpenChange }: BerthFormProps) {
|
|||||||
handleSubmit,
|
handleSubmit,
|
||||||
setValue,
|
setValue,
|
||||||
watch,
|
watch,
|
||||||
formState: { errors, isSubmitting },
|
formState: { isSubmitting },
|
||||||
reset,
|
|
||||||
} = useForm<UpdateBerthInput>({
|
} = useForm<UpdateBerthInput>({
|
||||||
resolver: zodResolver(updateBerthSchema),
|
resolver: zodResolver(updateBerthSchema),
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
|
|||||||
@@ -58,7 +58,7 @@ export function BerthList() {
|
|||||||
entityType="berths"
|
entityType="berths"
|
||||||
currentFilters={filters}
|
currentFilters={filters}
|
||||||
currentSort={sort}
|
currentSort={sort}
|
||||||
onApplyView={(savedFilters, savedSort) => {
|
onApplyView={(savedFilters, _savedSort) => {
|
||||||
clearFilters();
|
clearFilters();
|
||||||
Object.entries(savedFilters).forEach(([key, value]) => setFilter(key, value));
|
Object.entries(savedFilters).forEach(([key, value]) => setFilter(key, value));
|
||||||
}}
|
}}
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { useState } from 'react';
|
|
||||||
import { useMutation } from '@tanstack/react-query';
|
import { useMutation } from '@tanstack/react-query';
|
||||||
import { ArrowRight, Loader2 } from 'lucide-react';
|
import { ArrowRight, Loader2 } from 'lucide-react';
|
||||||
|
|
||||||
|
|||||||
@@ -2,8 +2,6 @@
|
|||||||
|
|
||||||
import { type DetailTab } from '@/components/shared/detail-layout';
|
import { type DetailTab } from '@/components/shared/detail-layout';
|
||||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
||||||
import { Separator } from '@/components/ui/separator';
|
|
||||||
import { Badge } from '@/components/ui/badge';
|
|
||||||
import { TagBadge } from '@/components/shared/tag-badge';
|
import { TagBadge } from '@/components/shared/tag-badge';
|
||||||
|
|
||||||
type BerthData = {
|
type BerthData = {
|
||||||
|
|||||||
@@ -33,6 +33,25 @@ interface ClientDetailHeaderProps {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type ClientFormClient = {
|
||||||
|
id: string;
|
||||||
|
fullName: string;
|
||||||
|
companyName?: string | null;
|
||||||
|
nationality?: string | null;
|
||||||
|
isProxy?: boolean;
|
||||||
|
proxyType?: string | null;
|
||||||
|
actualOwnerName?: string | null;
|
||||||
|
yachtName?: string | null;
|
||||||
|
berthSizeDesired?: string | null;
|
||||||
|
preferredContactMethod?: string | null;
|
||||||
|
preferredLanguage?: string | null;
|
||||||
|
timezone?: string | null;
|
||||||
|
source?: string | null;
|
||||||
|
sourceDetails?: string | null;
|
||||||
|
contacts?: Array<{ channel: string; value: string; label?: string | null; isPrimary?: boolean }>;
|
||||||
|
tags?: Array<{ id: string }>;
|
||||||
|
};
|
||||||
|
|
||||||
const SOURCE_LABELS: Record<string, string> = {
|
const SOURCE_LABELS: Record<string, string> = {
|
||||||
website: 'Website',
|
website: 'Website',
|
||||||
manual: 'Manual',
|
manual: 'Manual',
|
||||||
@@ -67,7 +86,6 @@ export function ClientDetailHeader({ client }: ClientDetailHeaderProps) {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const primaryContact = client.contacts?.find((c) => c.isPrimary);
|
|
||||||
const primaryEmail = client.contacts?.find((c) => c.channel === 'email' && c.isPrimary)
|
const primaryEmail = client.contacts?.find((c) => c.channel === 'email' && c.isPrimary)
|
||||||
?? client.contacts?.find((c) => c.channel === 'email');
|
?? client.contacts?.find((c) => c.channel === 'email');
|
||||||
const primaryPhone = client.contacts?.find((c) => c.channel === 'phone' && c.isPrimary)
|
const primaryPhone = client.contacts?.find((c) => c.channel === 'phone' && c.isPrimary)
|
||||||
@@ -162,7 +180,7 @@ export function ClientDetailHeader({ client }: ClientDetailHeaderProps) {
|
|||||||
<ClientForm
|
<ClientForm
|
||||||
open={editOpen}
|
open={editOpen}
|
||||||
onOpenChange={setEditOpen}
|
onOpenChange={setEditOpen}
|
||||||
client={client as any}
|
client={client as unknown as ClientFormClient}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<ArchiveConfirmDialog
|
<ArchiveConfirmDialog
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { useQuery } from '@tanstack/react-query';
|
import { useQuery } from '@tanstack/react-query';
|
||||||
import { useParams } from 'next/navigation';
|
|
||||||
|
|
||||||
import { DetailLayout } from '@/components/shared/detail-layout';
|
import { DetailLayout } from '@/components/shared/detail-layout';
|
||||||
import { ClientDetailHeader } from '@/components/clients/client-detail-header';
|
import { ClientDetailHeader } from '@/components/clients/client-detail-header';
|
||||||
|
|||||||
@@ -96,15 +96,15 @@ export function ClientForm({ open, onOpenChange, client }: ClientFormProps) {
|
|||||||
actualOwnerName: client.actualOwnerName ?? undefined,
|
actualOwnerName: client.actualOwnerName ?? undefined,
|
||||||
yachtName: client.yachtName ?? undefined,
|
yachtName: client.yachtName ?? undefined,
|
||||||
berthSizeDesired: client.berthSizeDesired ?? undefined,
|
berthSizeDesired: client.berthSizeDesired ?? undefined,
|
||||||
preferredContactMethod: (client.preferredContactMethod as any) ?? undefined,
|
preferredContactMethod: (client.preferredContactMethod as string) ?? undefined,
|
||||||
preferredLanguage: client.preferredLanguage ?? undefined,
|
preferredLanguage: client.preferredLanguage ?? undefined,
|
||||||
timezone: client.timezone ?? undefined,
|
timezone: client.timezone ?? undefined,
|
||||||
source: (client.source as any) ?? undefined,
|
source: (client.source as string) ?? undefined,
|
||||||
sourceDetails: client.sourceDetails ?? undefined,
|
sourceDetails: client.sourceDetails ?? undefined,
|
||||||
contacts:
|
contacts:
|
||||||
client.contacts && client.contacts.length > 0
|
client.contacts && client.contacts.length > 0
|
||||||
? client.contacts.map((c) => ({
|
? client.contacts.map((c) => ({
|
||||||
channel: c.channel as any,
|
channel: c.channel as 'email' | 'phone' | 'whatsapp' | 'other',
|
||||||
value: c.value,
|
value: c.value,
|
||||||
label: c.label ?? undefined,
|
label: c.label ?? undefined,
|
||||||
isPrimary: c.isPrimary ?? false,
|
isPrimary: c.isPrimary ?? false,
|
||||||
@@ -125,6 +125,7 @@ export function ClientForm({ open, onOpenChange, client }: ClientFormProps) {
|
|||||||
const mutation = useMutation({
|
const mutation = useMutation({
|
||||||
mutationFn: async (data: CreateClientInput) => {
|
mutationFn: async (data: CreateClientInput) => {
|
||||||
if (isEdit) {
|
if (isEdit) {
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
const { contacts, tagIds: tIds, ...rest } = data;
|
const { contacts, tagIds: tIds, ...rest } = data;
|
||||||
await apiFetch(`/api/v1/clients/${client!.id}`, { method: 'PATCH', body: rest });
|
await apiFetch(`/api/v1/clients/${client!.id}`, { method: 'PATCH', body: rest });
|
||||||
if (tIds) {
|
if (tIds) {
|
||||||
@@ -217,7 +218,7 @@ export function ClientForm({ open, onOpenChange, client }: ClientFormProps) {
|
|||||||
<Select
|
<Select
|
||||||
value={watch(`contacts.${index}.channel`)}
|
value={watch(`contacts.${index}.channel`)}
|
||||||
onValueChange={(v) =>
|
onValueChange={(v) =>
|
||||||
setValue(`contacts.${index}.channel`, v as any)
|
setValue(`contacts.${index}.channel`, v as 'email' | 'phone' | 'whatsapp' | 'other')
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<SelectTrigger className="h-8">
|
<SelectTrigger className="h-8">
|
||||||
@@ -356,7 +357,7 @@ export function ClientForm({ open, onOpenChange, client }: ClientFormProps) {
|
|||||||
<Label>Source</Label>
|
<Label>Source</Label>
|
||||||
<Select
|
<Select
|
||||||
value={watch('source') ?? ''}
|
value={watch('source') ?? ''}
|
||||||
onValueChange={(v) => setValue('source', v as any)}
|
onValueChange={(v) => setValue('source', v as 'website' | 'manual' | 'referral' | 'broker')}
|
||||||
>
|
>
|
||||||
<SelectTrigger>
|
<SelectTrigger>
|
||||||
<SelectValue placeholder="Select source" />
|
<SelectValue placeholder="Select source" />
|
||||||
@@ -373,7 +374,7 @@ export function ClientForm({ open, onOpenChange, client }: ClientFormProps) {
|
|||||||
<Label>Preferred Contact Method</Label>
|
<Label>Preferred Contact Method</Label>
|
||||||
<Select
|
<Select
|
||||||
value={watch('preferredContactMethod') ?? ''}
|
value={watch('preferredContactMethod') ?? ''}
|
||||||
onValueChange={(v) => setValue('preferredContactMethod', v as any)}
|
onValueChange={(v) => setValue('preferredContactMethod', v as 'email' | 'phone' | 'whatsapp')}
|
||||||
>
|
>
|
||||||
<SelectTrigger>
|
<SelectTrigger>
|
||||||
<SelectValue placeholder="Select method" />
|
<SelectValue placeholder="Select method" />
|
||||||
|
|||||||
@@ -96,7 +96,7 @@ export function ClientList() {
|
|||||||
entityType="clients"
|
entityType="clients"
|
||||||
currentFilters={filters}
|
currentFilters={filters}
|
||||||
currentSort={sort}
|
currentSort={sort}
|
||||||
onApplyView={(savedFilters, savedSort) => {
|
onApplyView={(savedFilters, _savedSort) => {
|
||||||
clearFilters();
|
clearFilters();
|
||||||
Object.entries(savedFilters).forEach(([key, val]) => setFilter(key, val));
|
Object.entries(savedFilters).forEach(([key, val]) => setFilter(key, val));
|
||||||
}}
|
}}
|
||||||
@@ -137,7 +137,7 @@ export function ClientList() {
|
|||||||
<ClientForm
|
<ClientForm
|
||||||
open={!!editClient}
|
open={!!editClient}
|
||||||
onOpenChange={(open) => !open && setEditClient(null)}
|
onOpenChange={(open) => !open && setEditClient(null)}
|
||||||
client={editClient as any}
|
client={editClient as unknown as NonNullable<Parameters<typeof ClientForm>[0]['client']>}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { useState } from 'react';
|
|
||||||
import { useQueryClient } from '@tanstack/react-query';
|
import { useQueryClient } from '@tanstack/react-query';
|
||||||
|
|
||||||
import { Badge } from '@/components/ui/badge';
|
import { Badge } from '@/components/ui/badge';
|
||||||
|
|||||||
@@ -41,7 +41,7 @@ export function SigningProgress({ documentId, signers }: SigningProgressProps) {
|
|||||||
|
|
||||||
const sorted = [...signers].sort((a, b) => a.signingOrder - b.signingOrder);
|
const sorted = [...signers].sort((a, b) => a.signingOrder - b.signingOrder);
|
||||||
|
|
||||||
const handleResend = async (signer: Signer) => {
|
const handleResend = async (_signer: Signer) => {
|
||||||
try {
|
try {
|
||||||
await apiFetch(`/api/v1/documents/${documentId}/remind`, { method: 'POST' });
|
await apiFetch(`/api/v1/documents/${documentId}/remind`, { method: 'POST' });
|
||||||
queryClient.invalidateQueries({ queryKey: ['documents', documentId, 'signers'] });
|
queryClient.invalidateQueries({ queryKey: ['documents', documentId, 'signers'] });
|
||||||
|
|||||||
@@ -31,12 +31,6 @@ export interface ExpenseRow {
|
|||||||
createdAt: string;
|
createdAt: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
const PAYMENT_STATUS_VARIANTS: Record<string, string> = {
|
|
||||||
unpaid: 'destructive',
|
|
||||||
paid: 'default',
|
|
||||||
partial: 'secondary',
|
|
||||||
};
|
|
||||||
|
|
||||||
const PAYMENT_STATUS_COLORS: Record<string, string> = {
|
const PAYMENT_STATUS_COLORS: Record<string, string> = {
|
||||||
unpaid: 'bg-red-100 text-red-700 border-red-200',
|
unpaid: 'bg-red-100 text-red-700 border-red-200',
|
||||||
paid: 'bg-green-100 text-green-700 border-green-200',
|
paid: 'bg-green-100 text-green-700 border-green-200',
|
||||||
|
|||||||
@@ -8,16 +8,6 @@ import { Loader2, Receipt, Edit, Archive } from 'lucide-react';
|
|||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
import { Badge } from '@/components/ui/badge';
|
import { Badge } from '@/components/ui/badge';
|
||||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
||||||
import { Label } from '@/components/ui/label';
|
|
||||||
import { Input } from '@/components/ui/input';
|
|
||||||
import { Textarea } from '@/components/ui/textarea';
|
|
||||||
import {
|
|
||||||
Select,
|
|
||||||
SelectContent,
|
|
||||||
SelectItem,
|
|
||||||
SelectTrigger,
|
|
||||||
SelectValue,
|
|
||||||
} from '@/components/ui/select';
|
|
||||||
import { ArchiveConfirmDialog } from '@/components/shared/archive-confirm-dialog';
|
import { ArchiveConfirmDialog } from '@/components/shared/archive-confirm-dialog';
|
||||||
import { apiFetch } from '@/lib/api/client';
|
import { apiFetch } from '@/lib/api/client';
|
||||||
import type { ExpenseRow } from './expense-columns';
|
import type { ExpenseRow } from './expense-columns';
|
||||||
|
|||||||
@@ -43,7 +43,6 @@ export function ExpenseFormDialog({ open, onOpenChange, expense }: ExpenseFormDi
|
|||||||
register,
|
register,
|
||||||
handleSubmit,
|
handleSubmit,
|
||||||
setValue,
|
setValue,
|
||||||
watch,
|
|
||||||
reset,
|
reset,
|
||||||
formState: { errors, isSubmitting },
|
formState: { errors, isSubmitting },
|
||||||
} = useForm<CreateExpenseInput>({
|
} = useForm<CreateExpenseInput>({
|
||||||
@@ -60,11 +59,11 @@ export function ExpenseFormDialog({ open, onOpenChange, expense }: ExpenseFormDi
|
|||||||
establishmentName: expense.establishmentName ?? undefined,
|
establishmentName: expense.establishmentName ?? undefined,
|
||||||
amount: Number(expense.amount),
|
amount: Number(expense.amount),
|
||||||
currency: expense.currency,
|
currency: expense.currency,
|
||||||
category: expense.category as any,
|
category: expense.category as string,
|
||||||
paymentMethod: expense.paymentMethod as any,
|
paymentMethod: expense.paymentMethod as string,
|
||||||
payer: expense.payer ?? undefined,
|
payer: expense.payer ?? undefined,
|
||||||
expenseDate: new Date(expense.expenseDate),
|
expenseDate: new Date(expense.expenseDate),
|
||||||
paymentStatus: (expense.paymentStatus as any) ?? 'unpaid',
|
paymentStatus: (expense.paymentStatus as string) ?? 'unpaid',
|
||||||
});
|
});
|
||||||
} else if (open && !expense) {
|
} else if (open && !expense) {
|
||||||
reset({
|
reset({
|
||||||
@@ -162,7 +161,7 @@ export function ExpenseFormDialog({ open, onOpenChange, expense }: ExpenseFormDi
|
|||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<Label htmlFor="category">Category</Label>
|
<Label htmlFor="category">Category</Label>
|
||||||
<Select
|
<Select
|
||||||
onValueChange={(v) => setValue('category', v as any)}
|
onValueChange={(v) => setValue('category', v as string)}
|
||||||
defaultValue={expense?.category ?? undefined}
|
defaultValue={expense?.category ?? undefined}
|
||||||
>
|
>
|
||||||
<SelectTrigger id="category">
|
<SelectTrigger id="category">
|
||||||
@@ -181,7 +180,7 @@ export function ExpenseFormDialog({ open, onOpenChange, expense }: ExpenseFormDi
|
|||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<Label htmlFor="paymentMethod">Payment Method</Label>
|
<Label htmlFor="paymentMethod">Payment Method</Label>
|
||||||
<Select
|
<Select
|
||||||
onValueChange={(v) => setValue('paymentMethod', v as any)}
|
onValueChange={(v) => setValue('paymentMethod', v as string)}
|
||||||
defaultValue={expense?.paymentMethod ?? undefined}
|
defaultValue={expense?.paymentMethod ?? undefined}
|
||||||
>
|
>
|
||||||
<SelectTrigger id="paymentMethod">
|
<SelectTrigger id="paymentMethod">
|
||||||
@@ -209,7 +208,7 @@ export function ExpenseFormDialog({ open, onOpenChange, expense }: ExpenseFormDi
|
|||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<Label htmlFor="paymentStatus">Payment Status</Label>
|
<Label htmlFor="paymentStatus">Payment Status</Label>
|
||||||
<Select
|
<Select
|
||||||
onValueChange={(v) => setValue('paymentStatus', v as any)}
|
onValueChange={(v) => setValue('paymentStatus', v as 'unpaid' | 'paid' | 'partial')}
|
||||||
defaultValue={expense?.paymentStatus ?? 'unpaid'}
|
defaultValue={expense?.paymentStatus ?? 'unpaid'}
|
||||||
>
|
>
|
||||||
<SelectTrigger id="paymentStatus">
|
<SelectTrigger id="paymentStatus">
|
||||||
|
|||||||
@@ -45,7 +45,7 @@ function formatBytes(bytes: string | null): string {
|
|||||||
|
|
||||||
function FileIcon({ mimeType }: { mimeType: string | null }) {
|
function FileIcon({ mimeType }: { mimeType: string | null }) {
|
||||||
if (!mimeType) return <FileText className="h-8 w-8 text-muted-foreground" />;
|
if (!mimeType) return <FileText className="h-8 w-8 text-muted-foreground" />;
|
||||||
if (mimeType.startsWith('image/')) return <Image className="h-8 w-8 text-blue-500" />;
|
if (mimeType.startsWith('image/')) return <Image className="h-8 w-8 text-blue-500" alt="" />;
|
||||||
if (mimeType === 'application/pdf') return <FileText className="h-8 w-8 text-red-500" />;
|
if (mimeType === 'application/pdf') return <FileText className="h-8 w-8 text-red-500" />;
|
||||||
if (
|
if (
|
||||||
mimeType === 'application/vnd.ms-excel' ||
|
mimeType === 'application/vnd.ms-excel' ||
|
||||||
|
|||||||
@@ -181,7 +181,7 @@ export function InterestDetailHeader({ portSlug, interest }: InterestDetailHeade
|
|||||||
<InterestForm
|
<InterestForm
|
||||||
open={editOpen}
|
open={editOpen}
|
||||||
onOpenChange={setEditOpen}
|
onOpenChange={setEditOpen}
|
||||||
interest={interest as any}
|
interest={interest as unknown as Parameters<typeof InterestForm>[0]['interest']}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<InterestStagePicker
|
<InterestStagePicker
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import { useQuery, useQueryClient } from '@tanstack/react-query';
|
import { useQuery } from '@tanstack/react-query';
|
||||||
|
|
||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
import { DocumentList } from '@/components/documents/document-list';
|
import { DocumentList } from '@/components/documents/document-list';
|
||||||
@@ -25,7 +25,6 @@ interface InterestData {
|
|||||||
|
|
||||||
export function InterestDocumentsTab({ interestId }: InterestDocumentsTabProps) {
|
export function InterestDocumentsTab({ interestId }: InterestDocumentsTabProps) {
|
||||||
const [eoiDialogOpen, setEoiDialogOpen] = useState(false);
|
const [eoiDialogOpen, setEoiDialogOpen] = useState(false);
|
||||||
const queryClient = useQueryClient();
|
|
||||||
|
|
||||||
const { data: interestRes } = useQuery({
|
const { data: interestRes } = useQuery({
|
||||||
queryKey: ['interests', interestId],
|
queryKey: ['interests', interestId],
|
||||||
|
|||||||
@@ -164,7 +164,7 @@ export function InterestList() {
|
|||||||
<InterestForm
|
<InterestForm
|
||||||
open={!!editInterest}
|
open={!!editInterest}
|
||||||
onOpenChange={(open) => !open && setEditInterest(null)}
|
onOpenChange={(open) => !open && setEditInterest(null)}
|
||||||
interest={editInterest as any}
|
interest={editInterest as unknown as Parameters<typeof InterestForm>[0]['interest']}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,6 @@
|
|||||||
|
|
||||||
import { useQuery } from '@tanstack/react-query';
|
import { useQuery } from '@tanstack/react-query';
|
||||||
|
|
||||||
import { Badge } from '@/components/ui/badge';
|
|
||||||
import {
|
import {
|
||||||
Tooltip,
|
Tooltip,
|
||||||
TooltipContent,
|
TooltipContent,
|
||||||
|
|||||||
@@ -66,7 +66,7 @@ export function InterestTimeline({ interestId }: InterestTimelineProps) {
|
|||||||
{/* Vertical line */}
|
{/* Vertical line */}
|
||||||
<div className="absolute left-4 top-2 bottom-2 w-px bg-border" />
|
<div className="absolute left-4 top-2 bottom-2 w-px bg-border" />
|
||||||
|
|
||||||
{events.map((event, idx) => (
|
{events.map((event, _idx) => (
|
||||||
<div key={event.id} className="relative flex gap-4 pb-6">
|
<div key={event.id} className="relative flex gap-4 pb-6">
|
||||||
{/* Icon */}
|
{/* Icon */}
|
||||||
<div className="relative z-10 flex h-8 w-8 shrink-0 items-center justify-center rounded-full bg-background border">
|
<div className="relative z-10 flex h-8 w-8 shrink-0 items-center justify-center rounded-full bg-background border">
|
||||||
|
|||||||
@@ -55,7 +55,7 @@ export function PipelineCard({
|
|||||||
|
|
||||||
<div className="flex items-center justify-between gap-2">
|
<div className="flex items-center justify-between gap-2">
|
||||||
{leadCategory && (
|
{leadCategory && (
|
||||||
<Badge variant={(LEAD_CATEGORY_COLORS[leadCategory] as any) ?? 'secondary'}>
|
<Badge variant={(LEAD_CATEGORY_COLORS[leadCategory] as 'default' | 'secondary' | 'destructive' | 'outline') ?? 'secondary'}>
|
||||||
{leadCategory.replace(/_/g, ' ')}
|
{leadCategory.replace(/_/g, ' ')}
|
||||||
</Badge>
|
</Badge>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -1,8 +1,7 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { useState } from 'react';
|
|
||||||
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
||||||
import { Sparkles, Link, Loader2 } from 'lucide-react';
|
import { Sparkles, Loader2 } from 'lucide-react';
|
||||||
|
|
||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
import { Badge } from '@/components/ui/badge';
|
import { Badge } from '@/components/ui/badge';
|
||||||
@@ -75,7 +74,7 @@ export function RecommendationList({ interestId }: RecommendationListProps) {
|
|||||||
</div>
|
</div>
|
||||||
) : recommendations.length === 0 ? (
|
) : recommendations.length === 0 ? (
|
||||||
<div className="text-center py-12 text-muted-foreground">
|
<div className="text-center py-12 text-muted-foreground">
|
||||||
<p>No recommendations yet. Click "Generate Recommendations" to get started.</p>
|
<p>No recommendations yet. Click "Generate Recommendations" to get started.</p>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
||||||
import { format } from 'date-fns';
|
|
||||||
import { Loader2, Send, CreditCard } from 'lucide-react';
|
import { Loader2, Send, CreditCard } from 'lucide-react';
|
||||||
import { useForm } from 'react-hook-form';
|
import { useForm } from 'react-hook-form';
|
||||||
import { zodResolver } from '@hookform/resolvers/zod';
|
import { zodResolver } from '@hookform/resolvers/zod';
|
||||||
@@ -33,7 +33,7 @@ export function InvoiceDetail({ invoiceId }: InvoiceDetailProps) {
|
|||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
const [tab, setTab] = useState('overview');
|
const [tab, setTab] = useState('overview');
|
||||||
|
|
||||||
const { data, isLoading, error } = useQuery<{ data: any }>({
|
const { data, isLoading, error } = useQuery<{ data: Record<string, unknown> }>({
|
||||||
queryKey: ['invoices', invoiceId],
|
queryKey: ['invoices', invoiceId],
|
||||||
queryFn: () => apiFetch(`/api/v1/invoices/${invoiceId}`),
|
queryFn: () => apiFetch(`/api/v1/invoices/${invoiceId}`),
|
||||||
});
|
});
|
||||||
@@ -172,7 +172,7 @@ export function InvoiceDetail({ invoiceId }: InvoiceDetailProps) {
|
|||||||
<span className="col-span-2 text-right">Unit Price</span>
|
<span className="col-span-2 text-right">Unit Price</span>
|
||||||
<span className="col-span-2 text-right">Total</span>
|
<span className="col-span-2 text-right">Total</span>
|
||||||
</div>
|
</div>
|
||||||
{invoice.lineItems.map((li: any) => (
|
{(invoice.lineItems as Record<string, unknown>[]).map((li) => (
|
||||||
<div key={li.id} className="grid grid-cols-12 gap-2 text-sm">
|
<div key={li.id} className="grid grid-cols-12 gap-2 text-sm">
|
||||||
<span className="col-span-6">{li.description}</span>
|
<span className="col-span-6">{li.description}</span>
|
||||||
<span className="col-span-2 text-right tabular-nums">{li.quantity}</span>
|
<span className="col-span-2 text-right tabular-nums">{li.quantity}</span>
|
||||||
@@ -239,7 +239,7 @@ export function InvoiceDetail({ invoiceId }: InvoiceDetailProps) {
|
|||||||
<TabsContent value="expenses" className="pt-4">
|
<TabsContent value="expenses" className="pt-4">
|
||||||
{invoice.linkedExpenses && invoice.linkedExpenses.length > 0 ? (
|
{invoice.linkedExpenses && invoice.linkedExpenses.length > 0 ? (
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
{invoice.linkedExpenses.map((exp: any) => (
|
{(invoice.linkedExpenses as Record<string, unknown>[]).map((exp) => (
|
||||||
<div
|
<div
|
||||||
key={exp.id}
|
key={exp.id}
|
||||||
className="flex items-center justify-between p-3 border rounded-md text-sm"
|
className="flex items-center justify-between p-3 border rounded-md text-sm"
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ import { Plus, Trash2 } from 'lucide-react';
|
|||||||
|
|
||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
import { Input } from '@/components/ui/input';
|
import { Input } from '@/components/ui/input';
|
||||||
import { Label } from '@/components/ui/label';
|
|
||||||
|
|
||||||
interface LineItem {
|
interface LineItem {
|
||||||
description: string;
|
description: string;
|
||||||
@@ -18,7 +17,7 @@ interface InvoiceLineItemsProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function InvoiceLineItems({ name = 'lineItems' }: InvoiceLineItemsProps) {
|
export function InvoiceLineItems({ name = 'lineItems' }: InvoiceLineItemsProps) {
|
||||||
const { register, watch, formState: { errors } } = useFormContext();
|
const { register, watch } = useFormContext();
|
||||||
const { fields, append, remove } = useFieldArray({ name });
|
const { fields, append, remove } = useFieldArray({ name });
|
||||||
|
|
||||||
const lineItems: LineItem[] = watch(name) ?? [];
|
const lineItems: LineItem[] = watch(name) ?? [];
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ export function InvoicePdfPreview({ invoiceId, pdfFileId: initialPdfFileId }: In
|
|||||||
const regenerateMutation = useMutation({
|
const regenerateMutation = useMutation({
|
||||||
mutationFn: () =>
|
mutationFn: () =>
|
||||||
apiFetch(`/api/v1/invoices/${invoiceId}/generate-pdf`, { method: 'POST' }),
|
apiFetch(`/api/v1/invoices/${invoiceId}/generate-pdf`, { method: 'POST' }),
|
||||||
onSuccess: (data: any) => {
|
onSuccess: (data: { data?: { id?: string } }) => {
|
||||||
const fileId = data?.data?.id;
|
const fileId = data?.data?.id;
|
||||||
if (fileId) {
|
if (fileId) {
|
||||||
setPdfFileId(fileId);
|
setPdfFileId(fileId);
|
||||||
|
|||||||
@@ -83,7 +83,7 @@ export function Breadcrumbs() {
|
|||||||
<BreadcrumbItem>
|
<BreadcrumbItem>
|
||||||
<BreadcrumbLink asChild>
|
<BreadcrumbLink asChild>
|
||||||
<Link
|
<Link
|
||||||
href={`/${currentPortSlug}/dashboard` as any}
|
href={`/${currentPortSlug}/dashboard`}
|
||||||
className="text-muted-foreground hover:text-foreground transition-colors"
|
className="text-muted-foreground hover:text-foreground transition-colors"
|
||||||
>
|
>
|
||||||
{currentPort.name}
|
{currentPort.name}
|
||||||
@@ -98,7 +98,7 @@ export function Breadcrumbs() {
|
|||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{crumbs.map((crumb, index) => (
|
{crumbs.map((crumb, _index) => (
|
||||||
<Fragment key={crumb.href}>
|
<Fragment key={crumb.href}>
|
||||||
<BreadcrumbItem>
|
<BreadcrumbItem>
|
||||||
{crumb.isLast ? (
|
{crumb.isLast ? (
|
||||||
@@ -108,7 +108,7 @@ export function Breadcrumbs() {
|
|||||||
) : (
|
) : (
|
||||||
<BreadcrumbLink asChild>
|
<BreadcrumbLink asChild>
|
||||||
<Link
|
<Link
|
||||||
href={crumb.href as any}
|
href={crumb.href}
|
||||||
className="text-muted-foreground hover:text-foreground transition-colors"
|
className="text-muted-foreground hover:text-foreground transition-colors"
|
||||||
>
|
>
|
||||||
{crumb.label}
|
{crumb.label}
|
||||||
|
|||||||
@@ -36,7 +36,7 @@ export function PortSwitcher({ ports }: PortSwitcherProps) {
|
|||||||
queryClient.invalidateQueries();
|
queryClient.invalidateQueries();
|
||||||
|
|
||||||
// Navigate to the selected port's dashboard
|
// Navigate to the selected port's dashboard
|
||||||
router.push(`/${port.slug}/dashboard` as any);
|
router.push(`/${port.slug}/dashboard`);
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -20,7 +20,6 @@ import {
|
|||||||
Menu,
|
Menu,
|
||||||
ChevronDown,
|
ChevronDown,
|
||||||
ChevronUp,
|
ChevronUp,
|
||||||
LogOut,
|
|
||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
|
|
||||||
import { cn } from '@/lib/utils';
|
import { cn } from '@/lib/utils';
|
||||||
@@ -113,7 +112,7 @@ function NavItemLink({
|
|||||||
}) {
|
}) {
|
||||||
const content = (
|
const content = (
|
||||||
<Link
|
<Link
|
||||||
href={item.href as any}
|
href={item.href}
|
||||||
className={cn(
|
className={cn(
|
||||||
'flex items-center gap-3 px-3 py-2 rounded-md text-sm font-medium transition-colors duration-150',
|
'flex items-center gap-3 px-3 py-2 rounded-md text-sm font-medium transition-colors duration-150',
|
||||||
'text-[#cdcfd6] hover:bg-[#171f35] hover:text-white',
|
'text-[#cdcfd6] hover:bg-[#171f35] hover:text-white',
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ import {
|
|||||||
} from '@/components/ui/dropdown-menu';
|
} from '@/components/ui/dropdown-menu';
|
||||||
import { PortSwitcher } from '@/components/layout/port-switcher';
|
import { PortSwitcher } from '@/components/layout/port-switcher';
|
||||||
import { Breadcrumbs } from '@/components/layout/breadcrumbs';
|
import { Breadcrumbs } from '@/components/layout/breadcrumbs';
|
||||||
import { CommandSearch, SearchTrigger } from '@/components/search/command-search';
|
import { CommandSearch } from '@/components/search/command-search';
|
||||||
import { NotificationBell } from '@/components/notifications/notification-bell';
|
import { NotificationBell } from '@/components/notifications/notification-bell';
|
||||||
import type { Port } from '@/lib/db/schema/ports';
|
import type { Port } from '@/lib/db/schema/ports';
|
||||||
|
|
||||||
@@ -64,16 +64,16 @@ export function Topbar({ ports }: TopbarProps) {
|
|||||||
<DropdownMenuContent align="end" className="w-44">
|
<DropdownMenuContent align="end" className="w-44">
|
||||||
<DropdownMenuLabel className="text-xs text-muted-foreground">Create</DropdownMenuLabel>
|
<DropdownMenuLabel className="text-xs text-muted-foreground">Create</DropdownMenuLabel>
|
||||||
<DropdownMenuSeparator />
|
<DropdownMenuSeparator />
|
||||||
<DropdownMenuItem onClick={() => router.push(`${base}/clients/new` as any)}>
|
<DropdownMenuItem onClick={() => router.push(`${base}/clients/new`)}>
|
||||||
New Client
|
New Client
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
<DropdownMenuItem onClick={() => router.push(`${base}/interests/new` as any)}>
|
<DropdownMenuItem onClick={() => router.push(`${base}/interests/new`)}>
|
||||||
New Interest
|
New Interest
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
<DropdownMenuItem onClick={() => router.push(`${base}/expenses/new` as any)}>
|
<DropdownMenuItem onClick={() => router.push(`${base}/expenses/new`)}>
|
||||||
New Expense
|
New Expense
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
<DropdownMenuItem onClick={() => router.push(`${base}/reminders/new` as any)}>
|
<DropdownMenuItem onClick={() => router.push(`${base}/reminders/new`)}>
|
||||||
New Reminder
|
New Reminder
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
</DropdownMenuContent>
|
</DropdownMenuContent>
|
||||||
@@ -99,11 +99,11 @@ export function Topbar({ ports }: TopbarProps) {
|
|||||||
<DropdownMenuContent align="end" className="w-52">
|
<DropdownMenuContent align="end" className="w-52">
|
||||||
<DropdownMenuLabel>My Account</DropdownMenuLabel>
|
<DropdownMenuLabel>My Account</DropdownMenuLabel>
|
||||||
<DropdownMenuSeparator />
|
<DropdownMenuSeparator />
|
||||||
<DropdownMenuItem onClick={() => router.push(`${base}/settings/profile` as any)}>
|
<DropdownMenuItem onClick={() => router.push(`${base}/settings/profile`)}>
|
||||||
<User className="w-4 h-4 mr-2" />
|
<User className="w-4 h-4 mr-2" />
|
||||||
Profile
|
Profile
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
<DropdownMenuItem onClick={() => router.push(`${base}/settings` as any)}>
|
<DropdownMenuItem onClick={() => router.push(`${base}/settings`)}>
|
||||||
<Settings className="w-4 h-4 mr-2" />
|
<Settings className="w-4 h-4 mr-2" />
|
||||||
Settings
|
Settings
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ export function NotificationItem({ notification, onMarkRead }: NotificationItemP
|
|||||||
onMarkRead(notification.id);
|
onMarkRead(notification.id);
|
||||||
}
|
}
|
||||||
if (notification.link) {
|
if (notification.link) {
|
||||||
router.push(notification.link as any);
|
router.push(notification.link);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -41,7 +41,7 @@ export function PortalCard({
|
|||||||
);
|
);
|
||||||
|
|
||||||
if (href) {
|
if (href) {
|
||||||
return <Link href={href as any}>{content}</Link>;
|
return <Link href={href}>{content}</Link>;
|
||||||
}
|
}
|
||||||
|
|
||||||
return content;
|
return content;
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ export function PortalHeader({ portName, portLogoUrl, clientName }: PortalHeader
|
|||||||
|
|
||||||
async function handleLogout() {
|
async function handleLogout() {
|
||||||
await fetch('/api/portal/auth/logout', { method: 'POST' });
|
await fetch('/api/portal/auth/logout', { method: 'POST' });
|
||||||
router.push('/portal/login' as any);
|
router.push('/portal/login');
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ export function PortalNav() {
|
|||||||
return (
|
return (
|
||||||
<Link
|
<Link
|
||||||
key={item.href}
|
key={item.href}
|
||||||
href={item.href as any}
|
href={item.href}
|
||||||
className={cn(
|
className={cn(
|
||||||
'flex items-center gap-2 px-4 py-3 text-sm font-medium border-b-2 transition-colors whitespace-nowrap',
|
'flex items-center gap-2 px-4 py-3 text-sm font-medium border-b-2 transition-colors whitespace-nowrap',
|
||||||
isActive
|
isActive
|
||||||
|
|||||||
@@ -53,7 +53,7 @@ export function CommandSearch() {
|
|||||||
setFocused(false);
|
setFocused(false);
|
||||||
setQuery('');
|
setQuery('');
|
||||||
inputRef.current?.blur();
|
inputRef.current?.blur();
|
||||||
router.push(path as any);
|
router.push(path);
|
||||||
},
|
},
|
||||||
[router],
|
[router],
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { useCallback, useRef, useState } from 'react';
|
import { useRef, useState } from 'react';
|
||||||
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
||||||
import { ChevronDown, ChevronRight } from 'lucide-react';
|
import { ChevronDown, ChevronRight } from 'lucide-react';
|
||||||
|
|
||||||
@@ -15,7 +15,6 @@ import {
|
|||||||
SelectTrigger,
|
SelectTrigger,
|
||||||
SelectValue,
|
SelectValue,
|
||||||
} from '@/components/ui/select';
|
} from '@/components/ui/select';
|
||||||
import { Button } from '@/components/ui/button';
|
|
||||||
import { apiFetch } from '@/lib/api/client';
|
import { apiFetch } from '@/lib/api/client';
|
||||||
|
|
||||||
// ─── Types ────────────────────────────────────────────────────────────────────
|
// ─── Types ────────────────────────────────────────────────────────────────────
|
||||||
|
|||||||
@@ -6,9 +6,7 @@ import {
|
|||||||
getCoreRowModel,
|
getCoreRowModel,
|
||||||
useReactTable,
|
useReactTable,
|
||||||
type ColumnDef,
|
type ColumnDef,
|
||||||
type SortingState,
|
|
||||||
type RowSelectionState,
|
type RowSelectionState,
|
||||||
type PaginationState,
|
|
||||||
} from '@tanstack/react-table';
|
} from '@tanstack/react-table';
|
||||||
import { ArrowDown, ArrowUp, ArrowUpDown, Loader2 } from 'lucide-react';
|
import { ArrowDown, ArrowUp, ArrowUpDown, Loader2 } from 'lucide-react';
|
||||||
|
|
||||||
|
|||||||
@@ -37,7 +37,7 @@ export function DetailLayout({
|
|||||||
function handleTabChange(tabId: string) {
|
function handleTabChange(tabId: string) {
|
||||||
const params = new URLSearchParams(searchParams.toString());
|
const params = new URLSearchParams(searchParams.toString());
|
||||||
params.set('tab', tabId);
|
params.set('tab', tabId);
|
||||||
router.replace(`${pathname}?${params.toString()}` as any, { scroll: false });
|
router.replace(`${pathname}?${params.toString()}`, { scroll: false });
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isLoading) {
|
if (isLoading) {
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { type ReactNode, type ElementType } from 'react';
|
import { type ElementType } from 'react';
|
||||||
import { cn } from '@/lib/utils';
|
import { cn } from '@/lib/utils';
|
||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
|
|
||||||
|
|||||||
@@ -48,7 +48,7 @@ export function useEntityOptions({
|
|||||||
|
|
||||||
const options: EntityOption[] = useMemo(() => {
|
const options: EntityOption[] = useMemo(() => {
|
||||||
if (!data) return [];
|
if (!data) return [];
|
||||||
return data.map((item: any) => ({
|
return data.map((item: Record<string, unknown>) => ({
|
||||||
value: String(item[valueKey]),
|
value: String(item[valueKey]),
|
||||||
label: String(item[labelKey]),
|
label: String(item[labelKey]),
|
||||||
...item,
|
...item,
|
||||||
|
|||||||
@@ -34,12 +34,12 @@ export function useNotifications() {
|
|||||||
setUnreadCount(payload.count);
|
setUnreadCount(payload.count);
|
||||||
};
|
};
|
||||||
|
|
||||||
socket.on('notification:new' as any, handleNew);
|
socket.on('notification:new', handleNew);
|
||||||
socket.on('notification:unreadCount' as any, handleCount);
|
socket.on('notification:unreadCount', handleCount);
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
socket.off('notification:new' as any, handleNew);
|
socket.off('notification:new', handleNew);
|
||||||
socket.off('notification:unreadCount' as any, handleCount);
|
socket.off('notification:unreadCount', handleCount);
|
||||||
};
|
};
|
||||||
}, [socket, queryClient]);
|
}, [socket, queryClient]);
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { useState, useCallback, useMemo, useEffect } from 'react';
|
import { useState, useCallback, useMemo } from 'react';
|
||||||
import { useQuery, useQueryClient, type QueryKey } from '@tanstack/react-query';
|
import { useQuery, useQueryClient, type QueryKey } from '@tanstack/react-query';
|
||||||
import { useSearchParams, useRouter, usePathname } from 'next/navigation';
|
import { useSearchParams, useRouter, usePathname } from 'next/navigation';
|
||||||
|
|
||||||
@@ -13,7 +13,7 @@ import {
|
|||||||
type FilterValues,
|
type FilterValues,
|
||||||
} from '@/components/shared/filter-bar';
|
} from '@/components/shared/filter-bar';
|
||||||
|
|
||||||
interface UsePaginatedQueryOptions<T> {
|
interface UsePaginatedQueryOptions {
|
||||||
queryKey: QueryKey;
|
queryKey: QueryKey;
|
||||||
endpoint: string;
|
endpoint: string;
|
||||||
initialPage?: number;
|
initialPage?: number;
|
||||||
@@ -27,7 +27,7 @@ export function usePaginatedQuery<T>({
|
|||||||
initialPage = 1,
|
initialPage = 1,
|
||||||
initialPageSize = 25,
|
initialPageSize = 25,
|
||||||
filterDefinitions = [],
|
filterDefinitions = [],
|
||||||
}: UsePaginatedQueryOptions<T>) {
|
}: UsePaginatedQueryOptions) {
|
||||||
const searchParams = useSearchParams();
|
const searchParams = useSearchParams();
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const pathname = usePathname();
|
const pathname = usePathname();
|
||||||
@@ -67,7 +67,7 @@ export function usePaginatedQuery<T>({
|
|||||||
if (tab) params.set('tab', tab);
|
if (tab) params.set('tab', tab);
|
||||||
|
|
||||||
const qs = params.toString();
|
const qs = params.toString();
|
||||||
router.replace(`${pathname}${qs ? `?${qs}` : ''}` as any, { scroll: false });
|
router.replace(`${pathname}${qs ? `?${qs}` : ''}`, { scroll: false });
|
||||||
},
|
},
|
||||||
[pathname, router, searchParams, initialPageSize],
|
[pathname, router, searchParams, initialPageSize],
|
||||||
);
|
);
|
||||||
@@ -147,7 +147,7 @@ export function usePaginatedQuery<T>({
|
|||||||
if (!old) return old;
|
if (!old) return old;
|
||||||
return {
|
return {
|
||||||
...old,
|
...old,
|
||||||
data: old.data.filter((item: any) => item.id !== id),
|
data: old.data.filter((item: Record<string, unknown>) => item.id !== id),
|
||||||
pagination: {
|
pagination: {
|
||||||
...old.pagination,
|
...old.pagination,
|
||||||
total: old.pagination.total - 1,
|
total: old.pagination.total - 1,
|
||||||
|
|||||||
@@ -34,13 +34,13 @@ export function useRealtimeInvalidation(
|
|||||||
queryClient.invalidateQueries({ queryKey: key });
|
queryClient.invalidateQueries({ queryKey: key });
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
socket.on(event as any, handler);
|
socket.on(event, handler);
|
||||||
handlers.push({ event, handler });
|
handlers.push({ event, handler });
|
||||||
}
|
}
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
for (const { event, handler } of handlers) {
|
for (const { event, handler } of handlers) {
|
||||||
socket.off(event as any, handler);
|
socket.off(event, handler);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}, [socket, queryClient, eventMap]);
|
}, [socket, queryClient, eventMap]);
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ import { betterAuth } from 'better-auth';
|
|||||||
import { drizzleAdapter } from 'better-auth/adapters/drizzle';
|
import { drizzleAdapter } from 'better-auth/adapters/drizzle';
|
||||||
|
|
||||||
import { db } from '@/lib/db';
|
import { db } from '@/lib/db';
|
||||||
import { logger } from '@/lib/logger';
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Better Auth server configuration.
|
* Better Auth server configuration.
|
||||||
|
|||||||
@@ -6,7 +6,6 @@ import {
|
|||||||
numeric,
|
numeric,
|
||||||
jsonb,
|
jsonb,
|
||||||
index,
|
index,
|
||||||
uniqueIndex,
|
|
||||||
primaryKey,
|
primaryKey,
|
||||||
} from 'drizzle-orm/pg-core';
|
} from 'drizzle-orm/pg-core';
|
||||||
import { sql } from 'drizzle-orm';
|
import { sql } from 'drizzle-orm';
|
||||||
|
|||||||
@@ -67,7 +67,6 @@ import {
|
|||||||
savedViews,
|
savedViews,
|
||||||
scratchpadNotes,
|
scratchpadNotes,
|
||||||
userNotificationPreferences,
|
userNotificationPreferences,
|
||||||
currencyRates,
|
|
||||||
customFieldDefinitions,
|
customFieldDefinitions,
|
||||||
customFieldValues,
|
customFieldValues,
|
||||||
} from './system';
|
} from './system';
|
||||||
|
|||||||
@@ -8,7 +8,6 @@ import {
|
|||||||
jsonb,
|
jsonb,
|
||||||
index,
|
index,
|
||||||
uniqueIndex,
|
uniqueIndex,
|
||||||
primaryKey,
|
|
||||||
} from 'drizzle-orm/pg-core';
|
} from 'drizzle-orm/pg-core';
|
||||||
import { ports } from './ports';
|
import { ports } from './ports';
|
||||||
import { clients } from './clients';
|
import { clients } from './clients';
|
||||||
@@ -29,7 +28,7 @@ export const auditLogs = pgTable(
|
|||||||
userAgent: text('user_agent'),
|
userAgent: text('user_agent'),
|
||||||
revertedBy: text('reverted_by'), // user ID if this change was reverted
|
revertedBy: text('reverted_by'), // user ID if this change was reverted
|
||||||
revertedAt: timestamp('reverted_at', { withTimezone: true }),
|
revertedAt: timestamp('reverted_at', { withTimezone: true }),
|
||||||
revertOf: text('revert_of').references((): any => auditLogs.id),
|
revertOf: text('revert_of').references((): ReturnType<typeof text> => auditLogs.id),
|
||||||
metadata: jsonb('metadata').default({}),
|
metadata: jsonb('metadata').default({}),
|
||||||
createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(),
|
createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(),
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ import { db } from './index';
|
|||||||
export async function withTransaction<T>(
|
export async function withTransaction<T>(
|
||||||
callback: (tx: typeof db) => Promise<T>,
|
callback: (tx: typeof db) => Promise<T>,
|
||||||
): Promise<T> {
|
): Promise<T> {
|
||||||
return db.transaction(callback as any) as Promise<T>;
|
return db.transaction(callback as unknown as Parameters<typeof db.transaction>[0]) as Promise<T>;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -33,7 +33,7 @@ export async function softDelete<TTable extends PgTable>(
|
|||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
await db
|
await db
|
||||||
.update(table)
|
.update(table)
|
||||||
.set({ archived_at: sql`now()` } as any)
|
.set({ archived_at: sql`now()` } as Record<string, unknown>)
|
||||||
.where(eq(idColumn, id));
|
.where(eq(idColumn, id));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -51,6 +51,6 @@ export async function restore<TTable extends PgTable>(
|
|||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
await db
|
await db
|
||||||
.update(table)
|
.update(table)
|
||||||
.set({ archived_at: null } as any)
|
.set({ archived_at: null } as Record<string, unknown>)
|
||||||
.where(eq(idColumn, id));
|
.where(eq(idColumn, id));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import type { Template } from '@pdfme/common';
|
import type { Template } from '@pdfme/common';
|
||||||
|
|
||||||
export const berthSpecTemplate: Template = {
|
export const berthSpecTemplate: Template = {
|
||||||
basePdf: 'BLANK_PDF' as any,
|
basePdf: 'BLANK_PDF' as unknown as string,
|
||||||
schemas: [
|
schemas: [
|
||||||
[
|
[
|
||||||
{ name: 'portName', type: 'text', position: { x: 20, y: 15 }, width: 100, height: 10, fontSize: 16 },
|
{ name: 'portName', type: 'text', position: { x: 20, y: 15 }, width: 100, height: 10, fontSize: 16 },
|
||||||
@@ -19,11 +19,11 @@ export const berthSpecTemplate: Template = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export function buildBerthSpecInputs(
|
export function buildBerthSpecInputs(
|
||||||
berth: any,
|
berth: Record<string, unknown>,
|
||||||
waitingList: any[],
|
waitingList: Record<string, unknown>[],
|
||||||
maintenance: any[],
|
maintenance: Record<string, unknown>[],
|
||||||
linkedInterests: any[],
|
linkedInterests: Record<string, unknown>[],
|
||||||
port: any,
|
port: Record<string, unknown>,
|
||||||
): Record<string, string> {
|
): Record<string, string> {
|
||||||
const berthInfo = [
|
const berthInfo = [
|
||||||
`Mooring: ${berth.mooringNumber}`,
|
`Mooring: ${berth.mooringNumber}`,
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import type { Template } from '@pdfme/common';
|
import type { Template } from '@pdfme/common';
|
||||||
|
|
||||||
export const clientSummaryTemplate: Template = {
|
export const clientSummaryTemplate: Template = {
|
||||||
basePdf: 'BLANK_PDF' as any,
|
basePdf: 'BLANK_PDF' as unknown as string,
|
||||||
schemas: [
|
schemas: [
|
||||||
[
|
[
|
||||||
{ name: 'portName', type: 'text', position: { x: 20, y: 15 }, width: 100, height: 10, fontSize: 16 },
|
{ name: 'portName', type: 'text', position: { x: 20, y: 15 }, width: 100, height: 10, fontSize: 16 },
|
||||||
@@ -17,11 +17,11 @@ export const clientSummaryTemplate: Template = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export function buildClientSummaryInputs(
|
export function buildClientSummaryInputs(
|
||||||
client: any,
|
client: Record<string, unknown>,
|
||||||
contacts: any[],
|
contacts: Record<string, unknown>[],
|
||||||
interestList: any[],
|
interestList: Record<string, unknown>[],
|
||||||
activity: any[],
|
activity: Record<string, unknown>[],
|
||||||
port: any,
|
port: Record<string, unknown>,
|
||||||
): Record<string, string> {
|
): Record<string, string> {
|
||||||
const clientInfo = [
|
const clientInfo = [
|
||||||
`Name: ${client.fullName ?? 'N/A'}`,
|
`Name: ${client.fullName ?? 'N/A'}`,
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import type { Template } from '@pdfme/common';
|
import type { Template } from '@pdfme/common';
|
||||||
|
|
||||||
export const eoiTemplate: Template = {
|
export const eoiTemplate: Template = {
|
||||||
basePdf: 'BLANK_PDF' as any,
|
basePdf: 'BLANK_PDF' as unknown as string,
|
||||||
schemas: [
|
schemas: [
|
||||||
[
|
[
|
||||||
{ name: 'portName', type: 'text', position: { x: 20, y: 20 }, width: 170, height: 10, fontSize: 18 },
|
{ name: 'portName', type: 'text', position: { x: 20, y: 20 }, width: 170, height: 10, fontSize: 18 },
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import type { Template } from '@pdfme/common';
|
import type { Template } from '@pdfme/common';
|
||||||
|
|
||||||
export const interestSummaryTemplate: Template = {
|
export const interestSummaryTemplate: Template = {
|
||||||
basePdf: 'BLANK_PDF' as any,
|
basePdf: 'BLANK_PDF' as unknown as string,
|
||||||
schemas: [
|
schemas: [
|
||||||
[
|
[
|
||||||
{ name: 'portName', type: 'text', position: { x: 20, y: 15 }, width: 100, height: 10, fontSize: 16 },
|
{ name: 'portName', type: 'text', position: { x: 20, y: 15 }, width: 100, height: 10, fontSize: 16 },
|
||||||
@@ -23,11 +23,11 @@ function formatDate(d: Date | string | null | undefined): string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function buildInterestSummaryInputs(
|
export function buildInterestSummaryInputs(
|
||||||
interest: any,
|
interest: Record<string, unknown>,
|
||||||
client: any,
|
client: Record<string, unknown>,
|
||||||
berth: any,
|
berth: Record<string, unknown> | null,
|
||||||
timeline: any[],
|
timeline: Record<string, unknown>[],
|
||||||
port: any,
|
port: Record<string, unknown>,
|
||||||
): Record<string, string> {
|
): Record<string, string> {
|
||||||
const clientInfo = [
|
const clientInfo = [
|
||||||
`Name: ${client?.fullName ?? 'N/A'}`,
|
`Name: ${client?.fullName ?? 'N/A'}`,
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import type { Template } from '@pdfme/common';
|
import type { Template } from '@pdfme/common';
|
||||||
|
|
||||||
export const invoiceTemplate: Template = {
|
export const invoiceTemplate: Template = {
|
||||||
basePdf: 'BLANK_PDF' as any,
|
basePdf: 'BLANK_PDF' as unknown as string,
|
||||||
schemas: [
|
schemas: [
|
||||||
[
|
[
|
||||||
// Header fields
|
// Header fields
|
||||||
@@ -86,9 +86,9 @@ export const invoiceTemplate: Template = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export function buildInvoiceInputs(
|
export function buildInvoiceInputs(
|
||||||
invoice: any,
|
invoice: Record<string, unknown>,
|
||||||
lineItems: any[],
|
lineItems: Record<string, unknown>[],
|
||||||
port: any,
|
port: Record<string, unknown>,
|
||||||
): Record<string, string> {
|
): Record<string, string> {
|
||||||
const itemLines = lineItems
|
const itemLines = lineItems
|
||||||
.map(
|
.map(
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import type { Template } from '@pdfme/common';
|
|||||||
import type { ActivityData } from '@/lib/services/report-generators';
|
import type { ActivityData } from '@/lib/services/report-generators';
|
||||||
|
|
||||||
export const activityReportTemplate: Template = {
|
export const activityReportTemplate: Template = {
|
||||||
basePdf: 'BLANK_PDF' as any,
|
basePdf: 'BLANK_PDF' as unknown as string,
|
||||||
schemas: [
|
schemas: [
|
||||||
[
|
[
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import type { Template } from '@pdfme/common';
|
|||||||
import type { OccupancyData } from '@/lib/services/report-generators';
|
import type { OccupancyData } from '@/lib/services/report-generators';
|
||||||
|
|
||||||
export const occupancyReportTemplate: Template = {
|
export const occupancyReportTemplate: Template = {
|
||||||
basePdf: 'BLANK_PDF' as any,
|
basePdf: 'BLANK_PDF' as unknown as string,
|
||||||
schemas: [
|
schemas: [
|
||||||
[
|
[
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import type { Template } from '@pdfme/common';
|
|||||||
import type { PipelineData } from '@/lib/services/report-generators';
|
import type { PipelineData } from '@/lib/services/report-generators';
|
||||||
|
|
||||||
export const pipelineReportTemplate: Template = {
|
export const pipelineReportTemplate: Template = {
|
||||||
basePdf: 'BLANK_PDF' as any,
|
basePdf: 'BLANK_PDF' as unknown as string,
|
||||||
schemas: [
|
schemas: [
|
||||||
[
|
[
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import type { Template } from '@pdfme/common';
|
|||||||
import type { RevenueData } from '@/lib/services/report-generators';
|
import type { RevenueData } from '@/lib/services/report-generators';
|
||||||
|
|
||||||
export const revenueReportTemplate: Template = {
|
export const revenueReportTemplate: Template = {
|
||||||
basePdf: 'BLANK_PDF' as any,
|
basePdf: 'BLANK_PDF' as unknown as string,
|
||||||
schemas: [
|
schemas: [
|
||||||
[
|
[
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -33,8 +33,8 @@ async function generateEmailDraft(payload: GenerateEmailDraftPayload): Promise<D
|
|||||||
const { clients } = await import('@/lib/db/schema/clients');
|
const { clients } = await import('@/lib/db/schema/clients');
|
||||||
const { berths } = await import('@/lib/db/schema/berths');
|
const { berths } = await import('@/lib/db/schema/berths');
|
||||||
const { interestNotes } = await import('@/lib/db/schema/interests');
|
const { interestNotes } = await import('@/lib/db/schema/interests');
|
||||||
const { emailThreads, emailMessages } = await import('@/lib/db/schema/email');
|
const { emailThreads } = await import('@/lib/db/schema/email');
|
||||||
const { and, eq, desc, inArray } = await import('drizzle-orm');
|
const { and, eq, desc } = await import('drizzle-orm');
|
||||||
|
|
||||||
// Fetch interest, client, berth
|
// Fetch interest, client, berth
|
||||||
const [interest, client] = await Promise.all([
|
const [interest, client] = await Promise.all([
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { and, eq, gte, lte, inArray, sql } from 'drizzle-orm';
|
import { and, eq, gte, lte, inArray } from 'drizzle-orm';
|
||||||
|
|
||||||
import { db } from '@/lib/db';
|
import { db } from '@/lib/db';
|
||||||
import {
|
import {
|
||||||
@@ -6,7 +6,6 @@ import {
|
|||||||
berthTags,
|
berthTags,
|
||||||
berthWaitingList,
|
berthWaitingList,
|
||||||
berthMaintenanceLog,
|
berthMaintenanceLog,
|
||||||
berthMapData,
|
|
||||||
} from '@/lib/db/schema/berths';
|
} from '@/lib/db/schema/berths';
|
||||||
import { tags } from '@/lib/db/schema/system';
|
import { tags } from '@/lib/db/schema/system';
|
||||||
import { createAuditLog } from '@/lib/audit';
|
import { createAuditLog } from '@/lib/audit';
|
||||||
|
|||||||
@@ -192,8 +192,8 @@ export async function updateClient(
|
|||||||
action: 'update',
|
action: 'update',
|
||||||
entityType: 'client',
|
entityType: 'client',
|
||||||
entityId: id,
|
entityId: id,
|
||||||
oldValue: diff as any,
|
oldValue: diff as Record<string, unknown>,
|
||||||
newValue: data as any,
|
newValue: data as Record<string, unknown>,
|
||||||
ipAddress: meta.ipAddress,
|
ipAddress: meta.ipAddress,
|
||||||
userAgent: meta.userAgent,
|
userAgent: meta.userAgent,
|
||||||
});
|
});
|
||||||
@@ -312,7 +312,7 @@ export async function updateContact(
|
|||||||
clientId: string,
|
clientId: string,
|
||||||
portId: string,
|
portId: string,
|
||||||
data: Partial<{ channel: string; value: string; label: string; isPrimary: boolean; notes: string }>,
|
data: Partial<{ channel: string; value: string; label: string; isPrimary: boolean; notes: string }>,
|
||||||
meta: AuditMeta,
|
_meta: AuditMeta,
|
||||||
) {
|
) {
|
||||||
const client = await db.query.clients.findFirst({
|
const client = await db.query.clients.findFirst({
|
||||||
where: eq(clients.id, clientId),
|
where: eq(clients.id, clientId),
|
||||||
@@ -339,7 +339,7 @@ export async function removeContact(
|
|||||||
contactId: string,
|
contactId: string,
|
||||||
clientId: string,
|
clientId: string,
|
||||||
portId: string,
|
portId: string,
|
||||||
meta: AuditMeta,
|
_meta: AuditMeta,
|
||||||
) {
|
) {
|
||||||
const client = await db.query.clients.findFirst({
|
const client = await db.query.clients.findFirst({
|
||||||
where: eq(clients.id, clientId),
|
where: eq(clients.id, clientId),
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { and, count, desc, eq, inArray, isNull, sql, sum } from 'drizzle-orm';
|
import { and, count, desc, eq, isNull, sql } from 'drizzle-orm';
|
||||||
|
|
||||||
import { db } from '@/lib/db';
|
import { db } from '@/lib/db';
|
||||||
import { clients } from '@/lib/db/schema/clients';
|
import { clients } from '@/lib/db/schema/clients';
|
||||||
|
|||||||
@@ -22,7 +22,6 @@ import type {
|
|||||||
UpdateTemplateInput,
|
UpdateTemplateInput,
|
||||||
ListTemplatesInput,
|
ListTemplatesInput,
|
||||||
GenerateInput,
|
GenerateInput,
|
||||||
GenerateAndSendInput,
|
|
||||||
GenerateAndSignInput,
|
GenerateAndSignInput,
|
||||||
} from '@/lib/validators/document-templates';
|
} from '@/lib/validators/document-templates';
|
||||||
|
|
||||||
@@ -343,7 +342,7 @@ export async function resolveTemplate(
|
|||||||
|
|
||||||
// BR-140: Check required merge fields have values
|
// BR-140: Check required merge fields have values
|
||||||
const missing: string[] = [];
|
const missing: string[] = [];
|
||||||
for (const [_category, fields] of Object.entries(MERGE_FIELDS)) {
|
for (const [, fields] of Object.entries(MERGE_FIELDS)) {
|
||||||
for (const field of fields) {
|
for (const field of fields) {
|
||||||
if (field.required) {
|
if (field.required) {
|
||||||
const value = tokenMap[field.token];
|
const value = tokenMap[field.token];
|
||||||
@@ -381,7 +380,7 @@ export async function generateFromTemplate(
|
|||||||
portId: string,
|
portId: string,
|
||||||
context: GenerateInput,
|
context: GenerateInput,
|
||||||
meta: AuditMeta,
|
meta: AuditMeta,
|
||||||
): Promise<{ document: any; file: any }> {
|
): Promise<{ document: unknown; file: unknown }> {
|
||||||
const template = await getTemplateById(templateId, portId);
|
const template = await getTemplateById(templateId, portId);
|
||||||
|
|
||||||
const resolvedHtml = await resolveTemplate(templateId, { ...context, portId });
|
const resolvedHtml = await resolveTemplate(templateId, { ...context, portId });
|
||||||
@@ -396,7 +395,7 @@ export async function generateFromTemplate(
|
|||||||
|
|
||||||
// Use a simple single-field pdfme template for the HTML body
|
// Use a simple single-field pdfme template for the HTML body
|
||||||
const pdfTemplate = {
|
const pdfTemplate = {
|
||||||
basePdf: 'BLANK_PDF' as any,
|
basePdf: 'BLANK_PDF' as unknown as string,
|
||||||
schemas: [
|
schemas: [
|
||||||
[
|
[
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ import { createAuditLog } from '@/lib/audit';
|
|||||||
import { diffEntity } from '@/lib/entity-diff';
|
import { diffEntity } from '@/lib/entity-diff';
|
||||||
import { NotFoundError, ValidationError, ConflictError } from '@/lib/errors';
|
import { NotFoundError, ValidationError, ConflictError } from '@/lib/errors';
|
||||||
import { emitToRoom } from '@/lib/socket/server';
|
import { emitToRoom } from '@/lib/socket/server';
|
||||||
import { minioClient, getPresignedUrl, buildStoragePath } from '@/lib/minio';
|
import { minioClient, buildStoragePath } from '@/lib/minio';
|
||||||
import { env } from '@/lib/env';
|
import { env } from '@/lib/env';
|
||||||
import { logger } from '@/lib/logger';
|
import { logger } from '@/lib/logger';
|
||||||
import { generatePdf } from '@/lib/pdf/generate';
|
import { generatePdf } from '@/lib/pdf/generate';
|
||||||
@@ -20,15 +20,12 @@ import { evaluateRule } from '@/lib/services/berth-rules-engine';
|
|||||||
import {
|
import {
|
||||||
createDocument as documensoCreate,
|
createDocument as documensoCreate,
|
||||||
sendDocument as documensoSend,
|
sendDocument as documensoSend,
|
||||||
getDocument as documensoGet,
|
|
||||||
sendReminder as documensoRemind,
|
|
||||||
downloadSignedPdf,
|
downloadSignedPdf,
|
||||||
} from '@/lib/services/documenso-client';
|
} from '@/lib/services/documenso-client';
|
||||||
import type {
|
import type {
|
||||||
CreateDocumentInput,
|
CreateDocumentInput,
|
||||||
UpdateDocumentInput,
|
UpdateDocumentInput,
|
||||||
ListDocumentsInput,
|
ListDocumentsInput,
|
||||||
GenerateEoiInput,
|
|
||||||
} from '@/lib/validators/documents';
|
} from '@/lib/validators/documents';
|
||||||
|
|
||||||
// ─── Types ────────────────────────────────────────────────────────────────────
|
// ─── Types ────────────────────────────────────────────────────────────────────
|
||||||
@@ -144,7 +141,7 @@ export async function updateDocument(
|
|||||||
.where(and(eq(documents.id, id), eq(documents.portId, portId)))
|
.where(and(eq(documents.id, id), eq(documents.portId, portId)))
|
||||||
.returning();
|
.returning();
|
||||||
|
|
||||||
const diff = diffEntity(existing, updated!);
|
diffEntity(existing, updated!);
|
||||||
|
|
||||||
void createAuditLog({
|
void createAuditLog({
|
||||||
userId: meta.userId,
|
userId: meta.userId,
|
||||||
|
|||||||
@@ -8,42 +8,42 @@ import { logger } from '@/lib/logger';
|
|||||||
import type { ListExpensesInput } from '@/lib/validators/expenses';
|
import type { ListExpensesInput } from '@/lib/validators/expenses';
|
||||||
|
|
||||||
async function fetchAllExpenses(portId: string, query: ListExpensesInput) {
|
async function fetchAllExpenses(portId: string, query: ListExpensesInput) {
|
||||||
const conditions: ReturnType<typeof eq>[] = [eq(expenses.portId, portId) as any];
|
const conditions: ReturnType<typeof eq>[] = [eq(expenses.portId, portId) as ReturnType<typeof eq>];
|
||||||
|
|
||||||
if (!query.includeArchived) {
|
if (!query.includeArchived) {
|
||||||
conditions.push(isNull(expenses.archivedAt) as any);
|
conditions.push(isNull(expenses.archivedAt) as unknown as ReturnType<typeof eq>);
|
||||||
}
|
}
|
||||||
if (query.category) {
|
if (query.category) {
|
||||||
conditions.push(eq(expenses.category, query.category) as any);
|
conditions.push(eq(expenses.category, query.category) as ReturnType<typeof eq>);
|
||||||
}
|
}
|
||||||
if (query.paymentStatus) {
|
if (query.paymentStatus) {
|
||||||
conditions.push(eq(expenses.paymentStatus, query.paymentStatus) as any);
|
conditions.push(eq(expenses.paymentStatus, query.paymentStatus) as ReturnType<typeof eq>);
|
||||||
}
|
}
|
||||||
if (query.currency) {
|
if (query.currency) {
|
||||||
conditions.push(eq(expenses.currency, query.currency) as any);
|
conditions.push(eq(expenses.currency, query.currency) as ReturnType<typeof eq>);
|
||||||
}
|
}
|
||||||
if (query.payer) {
|
if (query.payer) {
|
||||||
conditions.push(eq(expenses.payer, query.payer) as any);
|
conditions.push(eq(expenses.payer, query.payer) as ReturnType<typeof eq>);
|
||||||
}
|
}
|
||||||
if (query.dateFrom) {
|
if (query.dateFrom) {
|
||||||
conditions.push(gte(expenses.expenseDate, new Date(query.dateFrom)) as any);
|
conditions.push(gte(expenses.expenseDate, new Date(query.dateFrom)) as unknown as ReturnType<typeof eq>);
|
||||||
}
|
}
|
||||||
if (query.dateTo) {
|
if (query.dateTo) {
|
||||||
conditions.push(lte(expenses.expenseDate, new Date(query.dateTo)) as any);
|
conditions.push(lte(expenses.expenseDate, new Date(query.dateTo)) as unknown as ReturnType<typeof eq>);
|
||||||
}
|
}
|
||||||
if (query.search) {
|
if (query.search) {
|
||||||
conditions.push(
|
conditions.push(
|
||||||
or(
|
or(
|
||||||
ilike(expenses.establishmentName, `%${query.search}%`),
|
ilike(expenses.establishmentName, `%${query.search}%`),
|
||||||
ilike(expenses.description, `%${query.search}%`),
|
ilike(expenses.description, `%${query.search}%`),
|
||||||
) as any,
|
) as unknown as ReturnType<typeof eq>,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return db
|
return db
|
||||||
.select()
|
.select()
|
||||||
.from(expenses)
|
.from(expenses)
|
||||||
.where(and(...(conditions as any[])));
|
.where(and(...conditions));
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function exportCsv(portId: string, query: ListExpensesInput): Promise<string> {
|
export async function exportCsv(portId: string, query: ListExpensesInput): Promise<string> {
|
||||||
@@ -122,7 +122,7 @@ export async function exportPdf(portId: string, query: ListExpensesInput): Promi
|
|||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
return generatePdf(template as any, inputs);
|
return generatePdf(template as unknown as Parameters<typeof generatePdf>[0], inputs);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function exportParentCompany(
|
export async function exportParentCompany(
|
||||||
@@ -207,5 +207,5 @@ export async function exportParentCompany(
|
|||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
return generatePdf(template as any, inputs);
|
return generatePdf(template as unknown as Parameters<typeof generatePdf>[0], inputs);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -58,7 +58,7 @@ export async function listExpenses(portId: string, query: ListExpensesInput) {
|
|||||||
includeArchived: query.includeArchived,
|
includeArchived: query.includeArchived,
|
||||||
archivedAtColumn: expenses.archivedAt,
|
archivedAtColumn: expenses.archivedAt,
|
||||||
sort: query.sort
|
sort: query.sort
|
||||||
? { column: expenses[query.sort as keyof typeof expenses] as any, direction: query.order }
|
? { column: expenses[query.sort as keyof typeof expenses] as unknown, direction: query.order }
|
||||||
: undefined,
|
: undefined,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -176,7 +176,7 @@ export async function updateExpense(
|
|||||||
|
|
||||||
const [updated] = await db
|
const [updated] = await db
|
||||||
.update(expenses)
|
.update(expenses)
|
||||||
.set(updateData as any)
|
.set(updateData as Record<string, unknown>)
|
||||||
.where(and(eq(expenses.id, id), eq(expenses.portId, portId)))
|
.where(and(eq(expenses.id, id), eq(expenses.portId, portId)))
|
||||||
.returning();
|
.returning();
|
||||||
|
|
||||||
@@ -286,7 +286,7 @@ export async function addReceiptFiles(
|
|||||||
.set({
|
.set({
|
||||||
receiptFileIds: sql`array_cat(receipt_file_ids, ${fileIds}::text[])`,
|
receiptFileIds: sql`array_cat(receipt_file_ids, ${fileIds}::text[])`,
|
||||||
updatedAt: new Date(),
|
updatedAt: new Date(),
|
||||||
} as any)
|
} as Record<string, unknown>)
|
||||||
.where(and(eq(expenses.id, id), eq(expenses.portId, portId)))
|
.where(and(eq(expenses.id, id), eq(expenses.portId, portId)))
|
||||||
.returning();
|
.returning();
|
||||||
|
|
||||||
|
|||||||
@@ -132,7 +132,7 @@ export async function listInterests(portId: string, query: ListInterestsInput) {
|
|||||||
|
|
||||||
let clientsMap: Record<string, string> = {};
|
let clientsMap: Record<string, string> = {};
|
||||||
let berthsMap: Record<string, string> = {};
|
let berthsMap: Record<string, string> = {};
|
||||||
let tagsByInterestId: Record<string, Array<{ id: string; name: string; color: string }>> = {};
|
const tagsByInterestId: Record<string, Array<{ id: string; name: string; color: string }>> = {};
|
||||||
|
|
||||||
if (clientIds.length > 0) {
|
if (clientIds.length > 0) {
|
||||||
const clientRows = await db
|
const clientRows = await db
|
||||||
|
|||||||
@@ -96,7 +96,7 @@ export async function listInvoices(portId: string, query: ListInvoicesInput) {
|
|||||||
archivedAtColumn: invoices.archivedAt,
|
archivedAtColumn: invoices.archivedAt,
|
||||||
sort: query.sort
|
sort: query.sort
|
||||||
? {
|
? {
|
||||||
column: invoices[query.sort as keyof typeof invoices] as any,
|
column: invoices[query.sort as keyof typeof invoices] as unknown,
|
||||||
direction: query.order,
|
direction: query.order,
|
||||||
}
|
}
|
||||||
: undefined,
|
: undefined,
|
||||||
@@ -379,7 +379,7 @@ export async function updateInvoice(
|
|||||||
|
|
||||||
const [result] = await tx
|
const [result] = await tx
|
||||||
.update(invoices)
|
.update(invoices)
|
||||||
.set(updateData as any)
|
.set(updateData as Record<string, unknown>)
|
||||||
.where(and(eq(invoices.id, id), eq(invoices.portId, portId)))
|
.where(and(eq(invoices.id, id), eq(invoices.portId, portId)))
|
||||||
.returning();
|
.returning();
|
||||||
|
|
||||||
|
|||||||
@@ -13,14 +13,6 @@ type EntityType = 'clients' | 'interests';
|
|||||||
|
|
||||||
// ─── Helpers ─────────────────────────────────────────────────────────────────
|
// ─── Helpers ─────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
function getTable(entityType: EntityType) {
|
|
||||||
return entityType === 'clients' ? clientNotes : interestNotes;
|
|
||||||
}
|
|
||||||
|
|
||||||
function getEntityIdField(entityType: EntityType) {
|
|
||||||
return entityType === 'clients' ? clientNotes.clientId : interestNotes.interestId;
|
|
||||||
}
|
|
||||||
|
|
||||||
async function verifyParentBelongsToPort(
|
async function verifyParentBelongsToPort(
|
||||||
entityType: EntityType,
|
entityType: EntityType,
|
||||||
entityId: string,
|
entityId: string,
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ import { and, count, eq, gt, sql } from 'drizzle-orm';
|
|||||||
import { db } from '@/lib/db';
|
import { db } from '@/lib/db';
|
||||||
import { notifications } from '@/lib/db/schema/operations';
|
import { notifications } from '@/lib/db/schema/operations';
|
||||||
import { userNotificationPreferences } from '@/lib/db/schema/system';
|
import { userNotificationPreferences } from '@/lib/db/schema/system';
|
||||||
import { userProfiles } from '@/lib/db/schema/users';
|
|
||||||
import { emitToRoom } from '@/lib/socket/server';
|
import { emitToRoom } from '@/lib/socket/server';
|
||||||
import { getQueue } from '@/lib/queue';
|
import { getQueue } from '@/lib/queue';
|
||||||
import { NotFoundError } from '@/lib/errors';
|
import { NotFoundError } from '@/lib/errors';
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import { and, eq, count } from 'drizzle-orm';
|
|||||||
import { db } from '@/lib/db';
|
import { db } from '@/lib/db';
|
||||||
import { clients, clientContacts } from '@/lib/db/schema/clients';
|
import { clients, clientContacts } from '@/lib/db/schema/clients';
|
||||||
import { interests } from '@/lib/db/schema/interests';
|
import { interests } from '@/lib/db/schema/interests';
|
||||||
import { documents, documentSigners, files } from '@/lib/db/schema/documents';
|
import { documents, files } from '@/lib/db/schema/documents';
|
||||||
import { invoices } from '@/lib/db/schema/financial';
|
import { invoices } from '@/lib/db/schema/financial';
|
||||||
import { berths } from '@/lib/db/schema/berths';
|
import { berths } from '@/lib/db/schema/berths';
|
||||||
import { ports } from '@/lib/db/schema/ports';
|
import { ports } from '@/lib/db/schema/ports';
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { and, eq, isNull } from 'drizzle-orm';
|
import { and, eq } from 'drizzle-orm';
|
||||||
|
|
||||||
import { db } from '@/lib/db';
|
import { db } from '@/lib/db';
|
||||||
import { interests } from '@/lib/db/schema/interests';
|
import { interests } from '@/lib/db/schema/interests';
|
||||||
@@ -28,7 +28,6 @@ function scoreBerth(
|
|||||||
if (yachtLengthFt && berth.lengthFt) {
|
if (yachtLengthFt && berth.lengthFt) {
|
||||||
const berthLen = parseFloat(berth.lengthFt);
|
const berthLen = parseFloat(berth.lengthFt);
|
||||||
if (berthLen >= yachtLengthFt) {
|
if (berthLen >= yachtLengthFt) {
|
||||||
const fit = Math.min(100, (berthLen / yachtLengthFt) * 100);
|
|
||||||
// Prefer berths that are not too oversized (within 20% extra is ideal)
|
// Prefer berths that are not too oversized (within 20% extra is ideal)
|
||||||
const score = berthLen <= yachtLengthFt * 1.2 ? 100 : Math.max(50, 100 - (berthLen / yachtLengthFt - 1.2) * 100);
|
const score = berthLen <= yachtLengthFt * 1.2 ? 100 : Math.max(50, 100 - (berthLen / yachtLengthFt - 1.2) * 100);
|
||||||
reasons['length_fit'] = Math.round(score);
|
reasons['length_fit'] = Math.round(score);
|
||||||
@@ -148,7 +147,7 @@ export async function generateRecommendations(
|
|||||||
|
|
||||||
// ─── List Recommendations ─────────────────────────────────────────────────────
|
// ─── List Recommendations ─────────────────────────────────────────────────────
|
||||||
|
|
||||||
export async function listRecommendations(interestId: string, portId: string) {
|
export async function listRecommendations(interestId: string, _portId: string) {
|
||||||
const rows = await db
|
const rows = await db
|
||||||
.select({
|
.select({
|
||||||
id: berthRecommendations.id,
|
id: berthRecommendations.id,
|
||||||
|
|||||||
@@ -208,7 +208,7 @@ export async function generateReport(reportJobId: string): Promise<void> {
|
|||||||
const portSlug = port?.slug ?? 'port';
|
const portSlug = port?.slug ?? 'port';
|
||||||
|
|
||||||
// 6. Build inputs (pass portName)
|
// 6. Build inputs (pass portName)
|
||||||
const inputs = (config.buildInputs as (data: any, portName: string) => Record<string, string>[])(data, portName);
|
const inputs = (config.buildInputs as (data: unknown, portName: string) => Record<string, string>[])(data, portName);
|
||||||
|
|
||||||
// 7. Generate PDF
|
// 7. Generate PDF
|
||||||
const pdfBytes = await generatePdf(config.template, inputs);
|
const pdfBytes = await generatePdf(config.template, inputs);
|
||||||
|
|||||||
@@ -16,7 +16,6 @@ export function sanitizeFilename(name: string): string {
|
|||||||
return name
|
return name
|
||||||
.replace(/[/\\:]/g, '') // strip path chars
|
.replace(/[/\\:]/g, '') // strip path chars
|
||||||
.replace(/\x00/g, '') // strip null bytes
|
.replace(/\x00/g, '') // strip null bytes
|
||||||
// eslint-disable-next-line no-control-regex
|
|
||||||
.replace(/[\x01-\x1f\x7f]/g, '') // strip control chars
|
.replace(/[\x01-\x1f\x7f]/g, '') // strip control chars
|
||||||
.trim()
|
.trim()
|
||||||
.slice(0, 255);
|
.slice(0, 255);
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import { minioClient } from '@/lib/minio/index';
|
|||||||
import { getQueue, QUEUE_CONFIGS, type QueueName } from '@/lib/queue';
|
import { getQueue, QUEUE_CONFIGS, type QueueName } from '@/lib/queue';
|
||||||
import { createAuditLog } from '@/lib/audit';
|
import { createAuditLog } from '@/lib/audit';
|
||||||
import { env } from '@/lib/env';
|
import { env } from '@/lib/env';
|
||||||
import { sql, desc, or, eq } from 'drizzle-orm';
|
import { sql, desc, eq } from 'drizzle-orm';
|
||||||
import { logger } from '@/lib/logger';
|
import { logger } from '@/lib/logger';
|
||||||
|
|
||||||
// ─── Types ────────────────────────────────────────────────────────────────────
|
// ─── Types ────────────────────────────────────────────────────────────────────
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ import { createCipheriv, createDecipheriv, randomBytes } from 'node:crypto';
|
|||||||
|
|
||||||
const ALGORITHM = 'aes-256-gcm';
|
const ALGORITHM = 'aes-256-gcm';
|
||||||
const IV_LENGTH = 12;
|
const IV_LENGTH = 12;
|
||||||
const TAG_LENGTH = 16;
|
|
||||||
|
|
||||||
function getKey(): Buffer {
|
function getKey(): Buffer {
|
||||||
const hex = process.env.EMAIL_CREDENTIAL_KEY;
|
const hex = process.env.EMAIL_CREDENTIAL_KEY;
|
||||||
|
|||||||
Reference in New Issue
Block a user