Initial commit: Port Nimara CRM (Layers 0-4)
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>
2026-03-26 11:52:51 +01:00
|
|
|
'use client';
|
|
|
|
|
|
2026-05-05 05:11:26 +02:00
|
|
|
import { useEffect, useRef, useState } from 'react';
|
Initial commit: Port Nimara CRM (Layers 0-4)
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>
2026-03-26 11:52:51 +01:00
|
|
|
import { useForm } from 'react-hook-form';
|
|
|
|
|
import { zodResolver } from '@hookform/resolvers/zod';
|
feat(pipeline): 9→7 stage refactor + v1.1 hardening wave
Replaces the legacy 9-stage pipeline with 7 canonical stages
(enquiry → qualified → eoi → reservation → deposit_paid → contract →
nurturing) plus three doc sub-status columns (eoi_doc_status,
reservation_doc_status, contract_doc_status) that track sent/signed
within a single stage instead of branching it.
Schema (migration 0062):
- interests gains assigned_to, deposit_expected_amount/currency,
three doc-status columns, two documenso-id columns, and
date_reservation_signed.
- New tables: qualification_criteria (per-port admin-configurable),
interest_qualifications (per-interest state), payments (deposit /
balance / refund records keyed to interest + client).
- Default qualification criteria seeded for every existing port.
- Dummy-data UPDATEs collapse Sent/Signed pairs and 'completed' into
the new stage + doc-status + outcome shape.
Migration 0063 adds interest_contact_log.voice_transcript and
template_used columns for v1.1-A/B (quick-template buttons + voice
transcription via Web Speech API).
v1.1 phase work bundled here:
- A/B: Quick-template buttons (Call / Visit / Email) + mic toggle on
the contact-log compose dialog (useVoiceTranscription hook).
- C: berth-rules-engine wraps state writes in pg_advisory_xact_lock
with an idempotent re-read; emits rule_evaluated audit traces.
- D: Documenso webhook: reservation/contract sub-status stamping
moved out of the PDF-download try-block so a download failure
no longer swallows the stamp. New integration test coverage.
- E: /admin/qualification-criteria CRUD page + admin component.
- F: default_new_interest_owner exposed in System Settings.
- G: recentActivityCount + active_engagement deal-pulse signal
surfaced as a chip on interests + hot-deals card.
- H: interest_assigned notification on assignedTo change (skips
self-assign, uses a dedupe key).
Plus the supporting components: AssignedToChip, DealPulseChip,
PaymentsSection, QualificationChecklist, MultiEoiChip,
SkipAheadBanner, WonStatusPanel, InterestBerthStatusBanner,
SupplementalInfoRequestButton, UserPicker.
Tests: 1370/1370 vitest pass (added deal-health unit suite +
expanded constants/validators/pipeline-transitions coverage). tsc
clean, eslint clean.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-14 03:39:21 +02:00
|
|
|
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
|
2026-05-05 05:11:26 +02:00
|
|
|
import { AlertTriangle, Loader2, Upload, X } from 'lucide-react';
|
Initial commit: Port Nimara CRM (Layers 0-4)
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>
2026-03-26 11:52:51 +01:00
|
|
|
|
|
|
|
|
import { Button } from '@/components/ui/button';
|
2026-05-05 05:11:26 +02:00
|
|
|
import { Checkbox } from '@/components/ui/checkbox';
|
Initial commit: Port Nimara CRM (Layers 0-4)
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>
2026-03-26 11:52:51 +01:00
|
|
|
import { Input } from '@/components/ui/input';
|
|
|
|
|
import { Label } from '@/components/ui/label';
|
|
|
|
|
import { Textarea } from '@/components/ui/textarea';
|
|
|
|
|
import {
|
|
|
|
|
Select,
|
|
|
|
|
SelectContent,
|
|
|
|
|
SelectItem,
|
|
|
|
|
SelectTrigger,
|
|
|
|
|
SelectValue,
|
|
|
|
|
} from '@/components/ui/select';
|
2026-05-05 05:11:26 +02:00
|
|
|
import { Sheet, SheetContent, SheetHeader, SheetTitle, SheetFooter } from '@/components/ui/sheet';
|
2026-05-09 04:24:46 +02:00
|
|
|
import { CurrencyInput } from '@/components/shared/currency-input';
|
|
|
|
|
import { CurrencySelect } from '@/components/shared/currency-select';
|
2026-05-09 18:36:16 +02:00
|
|
|
import { TripLabelCombobox } from '@/components/expenses/trip-label-combobox';
|
feat(pipeline): 9→7 stage refactor + v1.1 hardening wave
Replaces the legacy 9-stage pipeline with 7 canonical stages
(enquiry → qualified → eoi → reservation → deposit_paid → contract →
nurturing) plus three doc sub-status columns (eoi_doc_status,
reservation_doc_status, contract_doc_status) that track sent/signed
within a single stage instead of branching it.
Schema (migration 0062):
- interests gains assigned_to, deposit_expected_amount/currency,
three doc-status columns, two documenso-id columns, and
date_reservation_signed.
- New tables: qualification_criteria (per-port admin-configurable),
interest_qualifications (per-interest state), payments (deposit /
balance / refund records keyed to interest + client).
- Default qualification criteria seeded for every existing port.
- Dummy-data UPDATEs collapse Sent/Signed pairs and 'completed' into
the new stage + doc-status + outcome shape.
Migration 0063 adds interest_contact_log.voice_transcript and
template_used columns for v1.1-A/B (quick-template buttons + voice
transcription via Web Speech API).
v1.1 phase work bundled here:
- A/B: Quick-template buttons (Call / Visit / Email) + mic toggle on
the contact-log compose dialog (useVoiceTranscription hook).
- C: berth-rules-engine wraps state writes in pg_advisory_xact_lock
with an idempotent re-read; emits rule_evaluated audit traces.
- D: Documenso webhook: reservation/contract sub-status stamping
moved out of the PDF-download try-block so a download failure
no longer swallows the stamp. New integration test coverage.
- E: /admin/qualification-criteria CRUD page + admin component.
- F: default_new_interest_owner exposed in System Settings.
- G: recentActivityCount + active_engagement deal-pulse signal
surfaced as a chip on interests + hot-deals card.
- H: interest_assigned notification on assignedTo change (skips
self-assign, uses a dedupe key).
Plus the supporting components: AssignedToChip, DealPulseChip,
PaymentsSection, QualificationChecklist, MultiEoiChip,
SkipAheadBanner, WonStatusPanel, InterestBerthStatusBanner,
SupplementalInfoRequestButton, UserPicker.
Tests: 1370/1370 vitest pass (added deal-health unit suite +
expanded constants/validators/pipeline-transitions coverage). tsc
clean, eslint clean.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-14 03:39:21 +02:00
|
|
|
import { UserPicker } from '@/components/shared/user-picker';
|
Initial commit: Port Nimara CRM (Layers 0-4)
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>
2026-03-26 11:52:51 +01:00
|
|
|
import { apiFetch } from '@/lib/api/client';
|
feat(deps): bump zod 3→4 + @hookform/resolvers 3→5
Resolved 65 type errors across the codebase via these v4 migration
patterns:
- `ZodError.errors` renamed to `ZodError.issues` (4 call sites in auth
routes + central error handler).
- `z.record(value)` now requires explicit key type: `z.record(z.string(),
value)`. Updated 7 sites across templates / forms / saved-views /
website-inquiries.
- `.refine(check, msgFn)` second-arg shape changed — now requires an
`{ error: (issue) => ... }` object form. Updated
`mergeFieldsSchema` in document-templates validator.
- `.transform(...).default(...)` chains: v4 enforces default value type
matches transform OUTPUT. Reordered to `.default(...).transform(...)`
in list-query / company-memberships handlers.
- `z.coerce.*()` INPUT type widened to `unknown` in v4. Service signatures
using `z.input<typeof schema>` (kept for caller flexibility around
defaults) now re-parse via `schema.parse(data)` to recover the
post-coercion shape Drizzle needs. Done in berth-reservations service.
Invoice service narrows `lineItems` locally with a typed cast since
re-parsing would double-validate.
- `.optional().transform(...)` no longer propagates the optional marker
through v4's new ZodPipe. Moved `.optional()` to the END of chain in
`optionalDesiredDimSchema` (interests) and documents list query
(folderId, signatureOnly).
- ZodIssue subtype shapes simplified: `received` removed from
invalid_type, `type` renamed to `origin` on too_small. Test fixtures
updated.
- @hookform/resolvers v5 splits Resolver into 3-generic form (Input,
Context, Output). useForm calls in 6 forms (client, yacht, berth,
interest, expense, invoices-new-page) now pass explicit generics:
`useForm<z.input<typeof schema>, unknown, z.infer<typeof schema>>`.
Verified: tsc clean (0 errors), vitest 1293/1293 pass.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-12 18:29:03 +02:00
|
|
|
import type { z } from 'zod';
|
Initial commit: Port Nimara CRM (Layers 0-4)
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>
2026-03-26 11:52:51 +01:00
|
|
|
import { createExpenseSchema, type CreateExpenseInput } from '@/lib/validators/expenses';
|
2026-05-13 11:50:07 +02:00
|
|
|
import { EXPENSE_CATEGORIES, PAYMENT_METHODS, formatEnum } from '@/lib/constants';
|
Initial commit: Port Nimara CRM (Layers 0-4)
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>
2026-03-26 11:52:51 +01:00
|
|
|
import type { ExpenseRow } from './expense-columns';
|
|
|
|
|
|
2026-05-05 05:11:26 +02:00
|
|
|
interface UploadedReceipt {
|
|
|
|
|
id: string;
|
|
|
|
|
filename: string;
|
|
|
|
|
}
|
|
|
|
|
|
Initial commit: Port Nimara CRM (Layers 0-4)
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>
2026-03-26 11:52:51 +01:00
|
|
|
interface ExpenseFormDialogProps {
|
|
|
|
|
open: boolean;
|
|
|
|
|
onOpenChange: (open: boolean) => void;
|
|
|
|
|
expense?: ExpenseRow;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export function ExpenseFormDialog({ open, onOpenChange, expense }: ExpenseFormDialogProps) {
|
|
|
|
|
const queryClient = useQueryClient();
|
|
|
|
|
const isEdit = !!expense;
|
feat(pipeline): 9→7 stage refactor + v1.1 hardening wave
Replaces the legacy 9-stage pipeline with 7 canonical stages
(enquiry → qualified → eoi → reservation → deposit_paid → contract →
nurturing) plus three doc sub-status columns (eoi_doc_status,
reservation_doc_status, contract_doc_status) that track sent/signed
within a single stage instead of branching it.
Schema (migration 0062):
- interests gains assigned_to, deposit_expected_amount/currency,
three doc-status columns, two documenso-id columns, and
date_reservation_signed.
- New tables: qualification_criteria (per-port admin-configurable),
interest_qualifications (per-interest state), payments (deposit /
balance / refund records keyed to interest + client).
- Default qualification criteria seeded for every existing port.
- Dummy-data UPDATEs collapse Sent/Signed pairs and 'completed' into
the new stage + doc-status + outcome shape.
Migration 0063 adds interest_contact_log.voice_transcript and
template_used columns for v1.1-A/B (quick-template buttons + voice
transcription via Web Speech API).
v1.1 phase work bundled here:
- A/B: Quick-template buttons (Call / Visit / Email) + mic toggle on
the contact-log compose dialog (useVoiceTranscription hook).
- C: berth-rules-engine wraps state writes in pg_advisory_xact_lock
with an idempotent re-read; emits rule_evaluated audit traces.
- D: Documenso webhook: reservation/contract sub-status stamping
moved out of the PDF-download try-block so a download failure
no longer swallows the stamp. New integration test coverage.
- E: /admin/qualification-criteria CRUD page + admin component.
- F: default_new_interest_owner exposed in System Settings.
- G: recentActivityCount + active_engagement deal-pulse signal
surfaced as a chip on interests + hot-deals card.
- H: interest_assigned notification on assignedTo change (skips
self-assign, uses a dedupe key).
Plus the supporting components: AssignedToChip, DealPulseChip,
PaymentsSection, QualificationChecklist, MultiEoiChip,
SkipAheadBanner, WonStatusPanel, InterestBerthStatusBanner,
SupplementalInfoRequestButton, UserPicker.
Tests: 1370/1370 vitest pass (added deal-health unit suite +
expanded constants/validators/pipeline-transitions coverage). tsc
clean, eslint clean.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-14 03:39:21 +02:00
|
|
|
|
|
|
|
|
// Per-port vocabulary override for expense categories. Falls back to
|
|
|
|
|
// the shipped EXPENSE_CATEGORIES constant when /api/v1/vocabularies
|
|
|
|
|
// hasn't loaded yet or returns malformed data — keeps the picker
|
|
|
|
|
// populated during the first render.
|
|
|
|
|
const { data: vocab } = useQuery<{ data: Record<string, readonly string[]> }>({
|
|
|
|
|
queryKey: ['vocabularies'],
|
|
|
|
|
queryFn: () => apiFetch('/api/v1/vocabularies'),
|
|
|
|
|
staleTime: 5 * 60_000,
|
|
|
|
|
});
|
|
|
|
|
const categoryList = vocab?.data?.expense_categories ?? EXPENSE_CATEGORIES;
|
2026-05-05 05:11:26 +02:00
|
|
|
const fileInputRef = useRef<HTMLInputElement>(null);
|
|
|
|
|
const [uploadedReceipt, setUploadedReceipt] = useState<UploadedReceipt | null>(null);
|
|
|
|
|
const [previewUrl, setPreviewUrl] = useState<string | null>(null);
|
|
|
|
|
const [noReceipt, setNoReceipt] = useState(false);
|
|
|
|
|
const [uploadError, setUploadError] = useState<string | null>(null);
|
|
|
|
|
const [isUploading, setIsUploading] = useState(false);
|
Initial commit: Port Nimara CRM (Layers 0-4)
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>
2026-03-26 11:52:51 +01:00
|
|
|
|
|
|
|
|
const {
|
|
|
|
|
register,
|
|
|
|
|
handleSubmit,
|
|
|
|
|
setValue,
|
|
|
|
|
reset,
|
2026-05-09 04:24:46 +02:00
|
|
|
watch,
|
Initial commit: Port Nimara CRM (Layers 0-4)
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>
2026-03-26 11:52:51 +01:00
|
|
|
formState: { errors, isSubmitting },
|
feat(deps): bump zod 3→4 + @hookform/resolvers 3→5
Resolved 65 type errors across the codebase via these v4 migration
patterns:
- `ZodError.errors` renamed to `ZodError.issues` (4 call sites in auth
routes + central error handler).
- `z.record(value)` now requires explicit key type: `z.record(z.string(),
value)`. Updated 7 sites across templates / forms / saved-views /
website-inquiries.
- `.refine(check, msgFn)` second-arg shape changed — now requires an
`{ error: (issue) => ... }` object form. Updated
`mergeFieldsSchema` in document-templates validator.
- `.transform(...).default(...)` chains: v4 enforces default value type
matches transform OUTPUT. Reordered to `.default(...).transform(...)`
in list-query / company-memberships handlers.
- `z.coerce.*()` INPUT type widened to `unknown` in v4. Service signatures
using `z.input<typeof schema>` (kept for caller flexibility around
defaults) now re-parse via `schema.parse(data)` to recover the
post-coercion shape Drizzle needs. Done in berth-reservations service.
Invoice service narrows `lineItems` locally with a typed cast since
re-parsing would double-validate.
- `.optional().transform(...)` no longer propagates the optional marker
through v4's new ZodPipe. Moved `.optional()` to the END of chain in
`optionalDesiredDimSchema` (interests) and documents list query
(folderId, signatureOnly).
- ZodIssue subtype shapes simplified: `received` removed from
invalid_type, `type` renamed to `origin` on too_small. Test fixtures
updated.
- @hookform/resolvers v5 splits Resolver into 3-generic form (Input,
Context, Output). useForm calls in 6 forms (client, yacht, berth,
interest, expense, invoices-new-page) now pass explicit generics:
`useForm<z.input<typeof schema>, unknown, z.infer<typeof schema>>`.
Verified: tsc clean (0 errors), vitest 1293/1293 pass.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-12 18:29:03 +02:00
|
|
|
} = useForm<z.input<typeof createExpenseSchema>, unknown, CreateExpenseInput>({
|
Initial commit: Port Nimara CRM (Layers 0-4)
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>
2026-03-26 11:52:51 +01:00
|
|
|
resolver: zodResolver(createExpenseSchema),
|
|
|
|
|
defaultValues: {
|
|
|
|
|
currency: 'USD',
|
|
|
|
|
paymentStatus: 'unpaid',
|
|
|
|
|
},
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
if (open && expense) {
|
|
|
|
|
reset({
|
|
|
|
|
establishmentName: expense.establishmentName ?? undefined,
|
|
|
|
|
amount: Number(expense.amount),
|
|
|
|
|
currency: expense.currency,
|
2026-03-26 12:29:55 +01:00
|
|
|
category: expense.category as CreateExpenseInput['category'],
|
|
|
|
|
paymentMethod: expense.paymentMethod as CreateExpenseInput['paymentMethod'],
|
Initial commit: Port Nimara CRM (Layers 0-4)
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>
2026-03-26 11:52:51 +01:00
|
|
|
payer: expense.payer ?? undefined,
|
|
|
|
|
expenseDate: new Date(expense.expenseDate),
|
2026-03-26 12:29:55 +01:00
|
|
|
paymentStatus: (expense.paymentStatus as CreateExpenseInput['paymentStatus']) ?? 'unpaid',
|
2026-05-05 13:46:54 +02:00
|
|
|
tripLabel: expense.tripLabel ?? undefined,
|
Initial commit: Port Nimara CRM (Layers 0-4)
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>
2026-03-26 11:52:51 +01:00
|
|
|
});
|
2026-05-05 05:11:26 +02:00
|
|
|
setUploadedReceipt(null);
|
|
|
|
|
setPreviewUrl(null);
|
|
|
|
|
setNoReceipt(Boolean(expense.noReceiptAcknowledged));
|
|
|
|
|
setUploadError(null);
|
Initial commit: Port Nimara CRM (Layers 0-4)
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>
2026-03-26 11:52:51 +01:00
|
|
|
} else if (open && !expense) {
|
|
|
|
|
reset({
|
|
|
|
|
currency: 'USD',
|
|
|
|
|
paymentStatus: 'unpaid',
|
|
|
|
|
expenseDate: new Date(),
|
|
|
|
|
});
|
2026-05-05 05:11:26 +02:00
|
|
|
setUploadedReceipt(null);
|
|
|
|
|
setPreviewUrl(null);
|
|
|
|
|
setNoReceipt(false);
|
|
|
|
|
setUploadError(null);
|
Initial commit: Port Nimara CRM (Layers 0-4)
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>
2026-03-26 11:52:51 +01:00
|
|
|
}
|
|
|
|
|
}, [open, expense, reset]);
|
|
|
|
|
|
2026-05-05 05:11:26 +02:00
|
|
|
// Capture the URL inside the effect closure so the cleanup revokes the
|
|
|
|
|
// URL it observed at mount, not the one captured by a later render.
|
|
|
|
|
// Audit caught a bug where the cleanup ran on every change and revoked
|
|
|
|
|
// the URL that was still being shown.
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
const url = previewUrl;
|
|
|
|
|
return () => {
|
|
|
|
|
if (url) URL.revokeObjectURL(url);
|
|
|
|
|
};
|
|
|
|
|
}, [previewUrl]);
|
|
|
|
|
|
|
|
|
|
// Reset upload state whenever the sheet closes — re-opening on the same
|
|
|
|
|
// expense was carrying stale state from the prior session.
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
if (!open) {
|
|
|
|
|
setUploadedReceipt(null);
|
|
|
|
|
setPreviewUrl(null);
|
|
|
|
|
setNoReceipt(false);
|
|
|
|
|
setUploadError(null);
|
|
|
|
|
setIsUploading(false);
|
|
|
|
|
if (fileInputRef.current) fileInputRef.current.value = '';
|
|
|
|
|
}
|
|
|
|
|
}, [open]);
|
|
|
|
|
|
Initial commit: Port Nimara CRM (Layers 0-4)
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>
2026-03-26 11:52:51 +01:00
|
|
|
const mutation = useMutation({
|
|
|
|
|
mutationFn: (data: CreateExpenseInput) => {
|
|
|
|
|
if (isEdit) {
|
|
|
|
|
return apiFetch(`/api/v1/expenses/${expense.id}`, {
|
|
|
|
|
method: 'PATCH',
|
|
|
|
|
body: data,
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
return apiFetch('/api/v1/expenses', { method: 'POST', body: data });
|
|
|
|
|
},
|
|
|
|
|
onSuccess: () => {
|
|
|
|
|
queryClient.invalidateQueries({ queryKey: ['expenses'] });
|
|
|
|
|
onOpenChange(false);
|
|
|
|
|
},
|
|
|
|
|
});
|
|
|
|
|
|
2026-05-05 05:11:26 +02:00
|
|
|
async function handleFileChange(e: React.ChangeEvent<HTMLInputElement>) {
|
|
|
|
|
const file = e.target.files?.[0];
|
|
|
|
|
if (!file) return;
|
|
|
|
|
setUploadError(null);
|
|
|
|
|
if (previewUrl) URL.revokeObjectURL(previewUrl);
|
|
|
|
|
setPreviewUrl(URL.createObjectURL(file));
|
|
|
|
|
setIsUploading(true);
|
|
|
|
|
try {
|
|
|
|
|
const formData = new FormData();
|
|
|
|
|
formData.append('file', file);
|
|
|
|
|
formData.append('category', 'receipt');
|
|
|
|
|
const res = await fetch('/api/v1/files/upload', {
|
|
|
|
|
method: 'POST',
|
|
|
|
|
body: formData,
|
|
|
|
|
credentials: 'include',
|
|
|
|
|
});
|
|
|
|
|
if (!res.ok) throw new Error('Upload failed');
|
|
|
|
|
const json = (await res.json()) as { data: { id: string; filename: string } };
|
|
|
|
|
setUploadedReceipt({ id: json.data.id, filename: json.data.filename });
|
|
|
|
|
setNoReceipt(false);
|
|
|
|
|
} catch (err) {
|
|
|
|
|
setUploadError(err instanceof Error ? err.message : 'Upload failed');
|
|
|
|
|
setUploadedReceipt(null);
|
|
|
|
|
} finally {
|
|
|
|
|
setIsUploading(false);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function clearReceipt() {
|
|
|
|
|
if (previewUrl) URL.revokeObjectURL(previewUrl);
|
|
|
|
|
setPreviewUrl(null);
|
|
|
|
|
setUploadedReceipt(null);
|
|
|
|
|
setUploadError(null);
|
|
|
|
|
if (fileInputRef.current) fileInputRef.current.value = '';
|
|
|
|
|
}
|
|
|
|
|
|
Initial commit: Port Nimara CRM (Layers 0-4)
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>
2026-03-26 11:52:51 +01:00
|
|
|
function onSubmit(data: CreateExpenseInput) {
|
2026-05-05 05:11:26 +02:00
|
|
|
mutation.mutate({
|
|
|
|
|
...data,
|
|
|
|
|
receiptFileIds: uploadedReceipt ? [uploadedReceipt.id] : undefined,
|
|
|
|
|
noReceiptAcknowledged: Boolean(noReceipt && !uploadedReceipt),
|
|
|
|
|
});
|
Initial commit: Port Nimara CRM (Layers 0-4)
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>
2026-03-26 11:52:51 +01:00
|
|
|
}
|
|
|
|
|
|
2026-05-05 05:11:26 +02:00
|
|
|
const canSubmit = isEdit || Boolean(uploadedReceipt) || noReceipt;
|
|
|
|
|
|
Initial commit: Port Nimara CRM (Layers 0-4)
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>
2026-03-26 11:52:51 +01:00
|
|
|
return (
|
|
|
|
|
<Sheet open={open} onOpenChange={onOpenChange}>
|
|
|
|
|
<SheetContent className="w-full sm:max-w-lg overflow-y-auto">
|
|
|
|
|
<SheetHeader>
|
|
|
|
|
<SheetTitle>{isEdit ? 'Edit Expense' : 'New Expense'}</SheetTitle>
|
|
|
|
|
</SheetHeader>
|
|
|
|
|
|
|
|
|
|
<form onSubmit={handleSubmit(onSubmit)} className="space-y-4 mt-4">
|
|
|
|
|
<div className="space-y-2">
|
|
|
|
|
<Label htmlFor="expenseDate">Date *</Label>
|
|
|
|
|
<Input
|
|
|
|
|
id="expenseDate"
|
|
|
|
|
type="date"
|
|
|
|
|
{...register('expenseDate', {
|
|
|
|
|
setValueAs: (v) => (v ? new Date(v) : undefined),
|
|
|
|
|
})}
|
2026-05-05 05:11:26 +02:00
|
|
|
defaultValue={
|
|
|
|
|
expense?.expenseDate
|
|
|
|
|
? new Date(expense.expenseDate).toISOString().split('T')[0]
|
|
|
|
|
: new Date().toISOString().split('T')[0]
|
|
|
|
|
}
|
Initial commit: Port Nimara CRM (Layers 0-4)
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>
2026-03-26 11:52:51 +01:00
|
|
|
/>
|
|
|
|
|
{errors.expenseDate && (
|
|
|
|
|
<p className="text-xs text-destructive">{errors.expenseDate.message}</p>
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div className="grid grid-cols-2 gap-3">
|
|
|
|
|
<div className="space-y-2">
|
|
|
|
|
<Label htmlFor="amount">Amount *</Label>
|
2026-05-09 04:24:46 +02:00
|
|
|
<CurrencyInput
|
Initial commit: Port Nimara CRM (Layers 0-4)
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>
2026-03-26 11:52:51 +01:00
|
|
|
id="amount"
|
feat(deps): bump zod 3→4 + @hookform/resolvers 3→5
Resolved 65 type errors across the codebase via these v4 migration
patterns:
- `ZodError.errors` renamed to `ZodError.issues` (4 call sites in auth
routes + central error handler).
- `z.record(value)` now requires explicit key type: `z.record(z.string(),
value)`. Updated 7 sites across templates / forms / saved-views /
website-inquiries.
- `.refine(check, msgFn)` second-arg shape changed — now requires an
`{ error: (issue) => ... }` object form. Updated
`mergeFieldsSchema` in document-templates validator.
- `.transform(...).default(...)` chains: v4 enforces default value type
matches transform OUTPUT. Reordered to `.default(...).transform(...)`
in list-query / company-memberships handlers.
- `z.coerce.*()` INPUT type widened to `unknown` in v4. Service signatures
using `z.input<typeof schema>` (kept for caller flexibility around
defaults) now re-parse via `schema.parse(data)` to recover the
post-coercion shape Drizzle needs. Done in berth-reservations service.
Invoice service narrows `lineItems` locally with a typed cast since
re-parsing would double-validate.
- `.optional().transform(...)` no longer propagates the optional marker
through v4's new ZodPipe. Moved `.optional()` to the END of chain in
`optionalDesiredDimSchema` (interests) and documents list query
(folderId, signatureOnly).
- ZodIssue subtype shapes simplified: `received` removed from
invalid_type, `type` renamed to `origin` on too_small. Test fixtures
updated.
- @hookform/resolvers v5 splits Resolver into 3-generic form (Input,
Context, Output). useForm calls in 6 forms (client, yacht, berth,
interest, expense, invoices-new-page) now pass explicit generics:
`useForm<z.input<typeof schema>, unknown, z.infer<typeof schema>>`.
Verified: tsc clean (0 errors), vitest 1293/1293 pass.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-12 18:29:03 +02:00
|
|
|
value={(watch('amount') as number | null | undefined) ?? ''}
|
2026-05-09 04:24:46 +02:00
|
|
|
currency={watch('currency') ?? 'USD'}
|
|
|
|
|
onChange={(v) =>
|
|
|
|
|
setValue('amount', v ?? Number.NaN, { shouldDirty: true, shouldValidate: true })
|
|
|
|
|
}
|
Initial commit: Port Nimara CRM (Layers 0-4)
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>
2026-03-26 11:52:51 +01:00
|
|
|
/>
|
2026-05-05 05:11:26 +02:00
|
|
|
{errors.amount && <p className="text-xs text-destructive">{errors.amount.message}</p>}
|
Initial commit: Port Nimara CRM (Layers 0-4)
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>
2026-03-26 11:52:51 +01:00
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div className="space-y-2">
|
|
|
|
|
<Label htmlFor="currency">Currency</Label>
|
2026-05-09 04:24:46 +02:00
|
|
|
<CurrencySelect
|
|
|
|
|
id="currency"
|
|
|
|
|
value={watch('currency') ?? 'USD'}
|
2026-05-09 18:36:16 +02:00
|
|
|
onValueChange={(v) => setValue('currency', v, { shouldDirty: true })}
|
2026-05-09 04:24:46 +02:00
|
|
|
/>
|
Initial commit: Port Nimara CRM (Layers 0-4)
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>
2026-03-26 11:52:51 +01:00
|
|
|
{errors.currency && (
|
|
|
|
|
<p className="text-xs text-destructive">{errors.currency.message}</p>
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div className="space-y-2">
|
|
|
|
|
<Label htmlFor="establishmentName">Establishment</Label>
|
|
|
|
|
<Input
|
|
|
|
|
id="establishmentName"
|
|
|
|
|
placeholder="Establishment name"
|
|
|
|
|
{...register('establishmentName')}
|
|
|
|
|
/>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div className="space-y-2">
|
|
|
|
|
<Label htmlFor="category">Category</Label>
|
|
|
|
|
<Select
|
2026-03-26 12:29:55 +01:00
|
|
|
onValueChange={(v) => setValue('category', v as CreateExpenseInput['category'])}
|
Initial commit: Port Nimara CRM (Layers 0-4)
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>
2026-03-26 11:52:51 +01:00
|
|
|
defaultValue={expense?.category ?? undefined}
|
|
|
|
|
>
|
|
|
|
|
<SelectTrigger id="category">
|
|
|
|
|
<SelectValue placeholder="Select category" />
|
|
|
|
|
</SelectTrigger>
|
|
|
|
|
<SelectContent>
|
feat(pipeline): 9→7 stage refactor + v1.1 hardening wave
Replaces the legacy 9-stage pipeline with 7 canonical stages
(enquiry → qualified → eoi → reservation → deposit_paid → contract →
nurturing) plus three doc sub-status columns (eoi_doc_status,
reservation_doc_status, contract_doc_status) that track sent/signed
within a single stage instead of branching it.
Schema (migration 0062):
- interests gains assigned_to, deposit_expected_amount/currency,
three doc-status columns, two documenso-id columns, and
date_reservation_signed.
- New tables: qualification_criteria (per-port admin-configurable),
interest_qualifications (per-interest state), payments (deposit /
balance / refund records keyed to interest + client).
- Default qualification criteria seeded for every existing port.
- Dummy-data UPDATEs collapse Sent/Signed pairs and 'completed' into
the new stage + doc-status + outcome shape.
Migration 0063 adds interest_contact_log.voice_transcript and
template_used columns for v1.1-A/B (quick-template buttons + voice
transcription via Web Speech API).
v1.1 phase work bundled here:
- A/B: Quick-template buttons (Call / Visit / Email) + mic toggle on
the contact-log compose dialog (useVoiceTranscription hook).
- C: berth-rules-engine wraps state writes in pg_advisory_xact_lock
with an idempotent re-read; emits rule_evaluated audit traces.
- D: Documenso webhook: reservation/contract sub-status stamping
moved out of the PDF-download try-block so a download failure
no longer swallows the stamp. New integration test coverage.
- E: /admin/qualification-criteria CRUD page + admin component.
- F: default_new_interest_owner exposed in System Settings.
- G: recentActivityCount + active_engagement deal-pulse signal
surfaced as a chip on interests + hot-deals card.
- H: interest_assigned notification on assignedTo change (skips
self-assign, uses a dedupe key).
Plus the supporting components: AssignedToChip, DealPulseChip,
PaymentsSection, QualificationChecklist, MultiEoiChip,
SkipAheadBanner, WonStatusPanel, InterestBerthStatusBanner,
SupplementalInfoRequestButton, UserPicker.
Tests: 1370/1370 vitest pass (added deal-health unit suite +
expanded constants/validators/pipeline-transitions coverage). tsc
clean, eslint clean.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-14 03:39:21 +02:00
|
|
|
{categoryList.map((cat) => (
|
Initial commit: Port Nimara CRM (Layers 0-4)
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>
2026-03-26 11:52:51 +01:00
|
|
|
<SelectItem key={cat} value={cat}>
|
2026-05-13 11:50:07 +02:00
|
|
|
{formatEnum(cat)}
|
Initial commit: Port Nimara CRM (Layers 0-4)
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>
2026-03-26 11:52:51 +01:00
|
|
|
</SelectItem>
|
|
|
|
|
))}
|
|
|
|
|
</SelectContent>
|
|
|
|
|
</Select>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div className="space-y-2">
|
|
|
|
|
<Label htmlFor="paymentMethod">Payment Method</Label>
|
|
|
|
|
<Select
|
2026-05-05 05:11:26 +02:00
|
|
|
onValueChange={(v) =>
|
|
|
|
|
setValue('paymentMethod', v as CreateExpenseInput['paymentMethod'])
|
|
|
|
|
}
|
Initial commit: Port Nimara CRM (Layers 0-4)
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>
2026-03-26 11:52:51 +01:00
|
|
|
defaultValue={expense?.paymentMethod ?? undefined}
|
|
|
|
|
>
|
|
|
|
|
<SelectTrigger id="paymentMethod">
|
|
|
|
|
<SelectValue placeholder="Select method" />
|
|
|
|
|
</SelectTrigger>
|
|
|
|
|
<SelectContent>
|
|
|
|
|
{PAYMENT_METHODS.map((m) => (
|
|
|
|
|
<SelectItem key={m} value={m}>
|
2026-05-13 11:50:07 +02:00
|
|
|
{formatEnum(m)}
|
Initial commit: Port Nimara CRM (Layers 0-4)
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>
2026-03-26 11:52:51 +01:00
|
|
|
</SelectItem>
|
|
|
|
|
))}
|
|
|
|
|
</SelectContent>
|
|
|
|
|
</Select>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div className="space-y-2">
|
|
|
|
|
<Label htmlFor="payer">Payer</Label>
|
feat(pipeline): 9→7 stage refactor + v1.1 hardening wave
Replaces the legacy 9-stage pipeline with 7 canonical stages
(enquiry → qualified → eoi → reservation → deposit_paid → contract →
nurturing) plus three doc sub-status columns (eoi_doc_status,
reservation_doc_status, contract_doc_status) that track sent/signed
within a single stage instead of branching it.
Schema (migration 0062):
- interests gains assigned_to, deposit_expected_amount/currency,
three doc-status columns, two documenso-id columns, and
date_reservation_signed.
- New tables: qualification_criteria (per-port admin-configurable),
interest_qualifications (per-interest state), payments (deposit /
balance / refund records keyed to interest + client).
- Default qualification criteria seeded for every existing port.
- Dummy-data UPDATEs collapse Sent/Signed pairs and 'completed' into
the new stage + doc-status + outcome shape.
Migration 0063 adds interest_contact_log.voice_transcript and
template_used columns for v1.1-A/B (quick-template buttons + voice
transcription via Web Speech API).
v1.1 phase work bundled here:
- A/B: Quick-template buttons (Call / Visit / Email) + mic toggle on
the contact-log compose dialog (useVoiceTranscription hook).
- C: berth-rules-engine wraps state writes in pg_advisory_xact_lock
with an idempotent re-read; emits rule_evaluated audit traces.
- D: Documenso webhook: reservation/contract sub-status stamping
moved out of the PDF-download try-block so a download failure
no longer swallows the stamp. New integration test coverage.
- E: /admin/qualification-criteria CRUD page + admin component.
- F: default_new_interest_owner exposed in System Settings.
- G: recentActivityCount + active_engagement deal-pulse signal
surfaced as a chip on interests + hot-deals card.
- H: interest_assigned notification on assignedTo change (skips
self-assign, uses a dedupe key).
Plus the supporting components: AssignedToChip, DealPulseChip,
PaymentsSection, QualificationChecklist, MultiEoiChip,
SkipAheadBanner, WonStatusPanel, InterestBerthStatusBanner,
SupplementalInfoRequestButton, UserPicker.
Tests: 1370/1370 vitest pass (added deal-health unit suite +
expanded constants/validators/pipeline-transitions coverage). tsc
clean, eslint clean.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-14 03:39:21 +02:00
|
|
|
<UserPicker
|
|
|
|
|
value={(watch('payer') as string | undefined) ?? null}
|
|
|
|
|
onChange={(v) => setValue('payer', v ?? '', { shouldDirty: true })}
|
|
|
|
|
placeholder="Who paid?"
|
|
|
|
|
/>
|
|
|
|
|
<p className="text-xs text-muted-foreground">
|
|
|
|
|
Pick a teammate or choose “Other…” to type any name.
|
|
|
|
|
</p>
|
Initial commit: Port Nimara CRM (Layers 0-4)
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>
2026-03-26 11:52:51 +01:00
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div className="space-y-2">
|
|
|
|
|
<Label htmlFor="paymentStatus">Payment Status</Label>
|
|
|
|
|
<Select
|
2026-03-26 12:06:18 +01:00
|
|
|
onValueChange={(v) => setValue('paymentStatus', v as 'unpaid' | 'paid' | 'partial')}
|
Initial commit: Port Nimara CRM (Layers 0-4)
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>
2026-03-26 11:52:51 +01:00
|
|
|
defaultValue={expense?.paymentStatus ?? 'unpaid'}
|
|
|
|
|
>
|
|
|
|
|
<SelectTrigger id="paymentStatus">
|
|
|
|
|
<SelectValue />
|
|
|
|
|
</SelectTrigger>
|
|
|
|
|
<SelectContent>
|
|
|
|
|
<SelectItem value="unpaid">Unpaid</SelectItem>
|
|
|
|
|
<SelectItem value="paid">Paid</SelectItem>
|
|
|
|
|
<SelectItem value="partial">Partial</SelectItem>
|
|
|
|
|
</SelectContent>
|
|
|
|
|
</Select>
|
|
|
|
|
</div>
|
2026-05-05 13:46:54 +02:00
|
|
|
|
|
|
|
|
<div className="space-y-2">
|
|
|
|
|
<Label htmlFor="tripLabel">Trip / event</Label>
|
2026-05-09 18:36:16 +02:00
|
|
|
<TripLabelCombobox
|
2026-05-05 13:46:54 +02:00
|
|
|
id="tripLabel"
|
2026-05-09 18:36:16 +02:00
|
|
|
value={watch('tripLabel') ?? ''}
|
|
|
|
|
onChange={(label) => setValue('tripLabel', label ?? undefined, { shouldDirty: true })}
|
2026-05-05 13:46:54 +02:00
|
|
|
placeholder="e.g. Palm Beach 2026 (optional)"
|
|
|
|
|
/>
|
|
|
|
|
<p className="text-xs text-muted-foreground">
|
2026-05-09 18:36:16 +02:00
|
|
|
Group expenses by yacht show or business trip. Pick a past trip or type a new one.
|
2026-05-05 13:46:54 +02:00
|
|
|
</p>
|
|
|
|
|
</div>
|
Initial commit: Port Nimara CRM (Layers 0-4)
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>
2026-03-26 11:52:51 +01:00
|
|
|
|
|
|
|
|
<div className="space-y-2">
|
|
|
|
|
<Label htmlFor="description">Description</Label>
|
|
|
|
|
<Textarea
|
|
|
|
|
id="description"
|
|
|
|
|
placeholder="Additional notes..."
|
|
|
|
|
rows={3}
|
|
|
|
|
{...register('description')}
|
|
|
|
|
/>
|
|
|
|
|
</div>
|
|
|
|
|
|
2026-05-05 05:11:26 +02:00
|
|
|
{!isEdit && (
|
|
|
|
|
<div className="space-y-2 rounded-md border p-3">
|
|
|
|
|
<Label className="text-sm font-medium">Receipt</Label>
|
|
|
|
|
{previewUrl ? (
|
|
|
|
|
<div className="relative">
|
|
|
|
|
<img
|
|
|
|
|
src={previewUrl}
|
|
|
|
|
alt="Receipt preview"
|
|
|
|
|
className="max-h-48 rounded border object-contain"
|
|
|
|
|
/>
|
|
|
|
|
<button
|
|
|
|
|
type="button"
|
|
|
|
|
onClick={clearReceipt}
|
|
|
|
|
aria-label="Remove receipt"
|
|
|
|
|
className="absolute top-1 right-1 rounded-full bg-background/90 hover:bg-background border p-1 shadow-sm"
|
|
|
|
|
>
|
|
|
|
|
<X className="h-3 w-3" />
|
|
|
|
|
</button>
|
|
|
|
|
<p className="mt-1 text-xs text-muted-foreground">
|
|
|
|
|
{isUploading
|
|
|
|
|
? 'Uploading...'
|
|
|
|
|
: uploadedReceipt
|
|
|
|
|
? `Uploaded: ${uploadedReceipt.filename}`
|
|
|
|
|
: 'Selecting...'}
|
|
|
|
|
</p>
|
|
|
|
|
</div>
|
|
|
|
|
) : (
|
|
|
|
|
<Button
|
|
|
|
|
type="button"
|
|
|
|
|
variant="outline"
|
|
|
|
|
size="sm"
|
|
|
|
|
className="w-full"
|
|
|
|
|
disabled={noReceipt}
|
|
|
|
|
onClick={() => fileInputRef.current?.click()}
|
|
|
|
|
>
|
fix(audit-wave-10): aria-hidden sweep on decorative Lucide icons (#69)
Mechanical codemod added \`aria-hidden\` to 444 self-closing single-line
Lucide icon JSX elements across 267 .tsx files in:
- shared/, layout/, dashboard/
- admin/ (all sections)
- clients/, berths/, yachts/, companies/, interests/, documents/
- reminders/, reservations/, residential/, expenses/, email/
The regex targeted only the safe pattern \`<IconName className="..." />\`
(no other props, self-closing, capitalized component name). Every match
inspected is a decorative companion to visible text or sits inside a
button whose accessible name comes from \`aria-label\` / sr-only text
— the icon itself should not be announced.
Screen readers no longer double-read the icon + the adjacent label
text (e.g. "Pencil Pencil Edit" → just "Edit"). The existing
@axe-core/playwright smoke test (\`20-accessibility.spec.ts\`) continues
to pass.
Test suite stays at 1315/1315 vitest. typescript clean.
Closes task #69 (aria-hidden sweep) from the AUDIT-2026-05-12 follow-ups
backlog.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-13 12:37:22 +02:00
|
|
|
<Upload className="mr-2 h-4 w-4" aria-hidden />
|
2026-05-05 05:11:26 +02:00
|
|
|
Upload receipt image or PDF
|
|
|
|
|
</Button>
|
|
|
|
|
)}
|
|
|
|
|
<input
|
|
|
|
|
ref={fileInputRef}
|
|
|
|
|
type="file"
|
|
|
|
|
accept="image/*,application/pdf"
|
|
|
|
|
className="hidden"
|
|
|
|
|
onChange={handleFileChange}
|
|
|
|
|
/>
|
|
|
|
|
{uploadError && <p className="text-xs text-destructive">{uploadError}</p>}
|
|
|
|
|
|
|
|
|
|
<div className="flex items-start gap-2 pt-1">
|
|
|
|
|
<Checkbox
|
|
|
|
|
id="noReceipt"
|
|
|
|
|
checked={noReceipt}
|
|
|
|
|
onCheckedChange={(checked) => {
|
|
|
|
|
const next = checked === true;
|
|
|
|
|
setNoReceipt(next);
|
|
|
|
|
if (next) clearReceipt();
|
|
|
|
|
}}
|
|
|
|
|
/>
|
|
|
|
|
<Label htmlFor="noReceipt" className="text-sm font-normal leading-tight">
|
|
|
|
|
I have no receipt for this expense
|
|
|
|
|
</Label>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
{noReceipt && (
|
|
|
|
|
<div className="flex gap-2 rounded-md border border-amber-300 bg-amber-50 p-2 text-xs text-amber-900 dark:border-amber-900 dark:bg-amber-950/40 dark:text-amber-200">
|
fix(audit-wave-10): aria-hidden sweep on decorative Lucide icons (#69)
Mechanical codemod added \`aria-hidden\` to 444 self-closing single-line
Lucide icon JSX elements across 267 .tsx files in:
- shared/, layout/, dashboard/
- admin/ (all sections)
- clients/, berths/, yachts/, companies/, interests/, documents/
- reminders/, reservations/, residential/, expenses/, email/
The regex targeted only the safe pattern \`<IconName className="..." />\`
(no other props, self-closing, capitalized component name). Every match
inspected is a decorative companion to visible text or sits inside a
button whose accessible name comes from \`aria-label\` / sr-only text
— the icon itself should not be announced.
Screen readers no longer double-read the icon + the adjacent label
text (e.g. "Pencil Pencil Edit" → just "Edit"). The existing
@axe-core/playwright smoke test (\`20-accessibility.spec.ts\`) continues
to pass.
Test suite stays at 1315/1315 vitest. typescript clean.
Closes task #69 (aria-hidden sweep) from the AUDIT-2026-05-12 follow-ups
backlog.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-13 12:37:22 +02:00
|
|
|
<AlertTriangle className="h-4 w-4 shrink-0" aria-hidden />
|
2026-05-05 05:11:26 +02:00
|
|
|
<span>
|
|
|
|
|
Expenses without a receipt may not be reimbursed by the parent company. The PDF
|
|
|
|
|
export will flag this expense.
|
|
|
|
|
</span>
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
|
Initial commit: Port Nimara CRM (Layers 0-4)
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>
2026-03-26 11:52:51 +01:00
|
|
|
{mutation.isError && (
|
2026-05-05 05:11:26 +02:00
|
|
|
<p className="text-sm text-destructive">{(mutation.error as Error).message}</p>
|
Initial commit: Port Nimara CRM (Layers 0-4)
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>
2026-03-26 11:52:51 +01:00
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
<SheetFooter className="pt-2">
|
2026-05-05 05:11:26 +02:00
|
|
|
<Button type="button" variant="outline" onClick={() => onOpenChange(false)}>
|
Initial commit: Port Nimara CRM (Layers 0-4)
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>
2026-03-26 11:52:51 +01:00
|
|
|
Cancel
|
|
|
|
|
</Button>
|
2026-05-05 05:11:26 +02:00
|
|
|
<Button
|
|
|
|
|
type="submit"
|
|
|
|
|
disabled={isSubmitting || mutation.isPending || isUploading || !canSubmit}
|
|
|
|
|
>
|
Initial commit: Port Nimara CRM (Layers 0-4)
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>
2026-03-26 11:52:51 +01:00
|
|
|
{(isSubmitting || mutation.isPending) && (
|
fix(audit-wave-10): aria-hidden sweep on decorative Lucide icons (#69)
Mechanical codemod added \`aria-hidden\` to 444 self-closing single-line
Lucide icon JSX elements across 267 .tsx files in:
- shared/, layout/, dashboard/
- admin/ (all sections)
- clients/, berths/, yachts/, companies/, interests/, documents/
- reminders/, reservations/, residential/, expenses/, email/
The regex targeted only the safe pattern \`<IconName className="..." />\`
(no other props, self-closing, capitalized component name). Every match
inspected is a decorative companion to visible text or sits inside a
button whose accessible name comes from \`aria-label\` / sr-only text
— the icon itself should not be announced.
Screen readers no longer double-read the icon + the adjacent label
text (e.g. "Pencil Pencil Edit" → just "Edit"). The existing
@axe-core/playwright smoke test (\`20-accessibility.spec.ts\`) continues
to pass.
Test suite stays at 1315/1315 vitest. typescript clean.
Closes task #69 (aria-hidden sweep) from the AUDIT-2026-05-12 follow-ups
backlog.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-13 12:37:22 +02:00
|
|
|
<Loader2 className="mr-2 h-4 w-4 animate-spin" aria-hidden />
|
Initial commit: Port Nimara CRM (Layers 0-4)
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>
2026-03-26 11:52:51 +01:00
|
|
|
)}
|
fix(audit-wave-9): copy/terminology sweep (copy-auditor)
Address the highest-impact items from the copy-auditor's CRITICAL +
HIGH + MEDIUM bands:
**C2 portal raw-status leak**
- Drop the staff-only `leadCategory` chip from the portal interests
page entirely. Privacy + optics: clients should never see "hot lead"
in their own portal. `eoiStatus` was already wrapped in
`portalSigningLabel`; only the categorical chip remained.
**C3 signing-status label drift**
- Add `src/lib/labels/document-status.ts` as the single source of
truth for the {draft, sent, partially_signed, completed, expired,
cancelled} lifecycle: labels (CRM + portal variants), StatusPill
variant, and the "active / in-flight" set.
- Wire it into interest-eoi-tab, interest-contract-tab,
interest-reservation-tab — they previously redefined identical
STATUS_LABELS / ACTIVE_STATUSES blocks per-file.
**H1 + M3 verbiage codemod**
- `Save Changes` → `Save changes` (sentence case, matches the
surrounding admin/CRM pattern).
- `Saving...` (ASCII three dots) → `Saving…` (Unicode ellipsis).
Matches the project's UTF-8-elsewhere convention and reads
correctly via screen-readers.
**M1 envelope jargon → signing request**
- smart-archive-dialog: "Leave envelope pending" → "Leave signing
request pending"; "Void the signing envelope" → "Cancel the signing
request"; section header updated to match.
- document-detail: "voids the signing envelope" → "cancels the signing
request".
- bulk-archive-wizard: "leave invoices/signing envelopes alone" →
"leave invoices/signing requests alone".
- Documenso admin page intentionally keeps `envelope` (dev/integration
vocabulary).
**M5 Hot Lead casing**
- Normalize `Hot Lead` / `General Interest` / `Specific Qualified` to
sentence case in `constants.ts` LABEL_OVERRIDES and all per-file
lead-category maps so the CRM trend (sentence case) is consistent.
**C1 surface-level rename**
- "Linked prospect (optional)" → "Linked interest (optional)" on the
berth status-change dialog.
- "Deal Documents" tab → "Interest Documents" (URL/route kept as
`/deal-documents` to avoid breaking deep links; rename deferred).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-13 12:12:40 +02:00
|
|
|
{isEdit ? 'Save changes' : 'Create Expense'}
|
Initial commit: Port Nimara CRM (Layers 0-4)
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>
2026-03-26 11:52:51 +01:00
|
|
|
</Button>
|
|
|
|
|
</SheetFooter>
|
|
|
|
|
</form>
|
|
|
|
|
</SheetContent>
|
|
|
|
|
</Sheet>
|
|
|
|
|
);
|
|
|
|
|
}
|