feat(yachts): list + owner-scoped list + autocomplete

Adds `listYachts`, `listYachtsForOwner`, and `autocomplete` to the
yacht service so UIs can page/filter yachts per port, look up all
yachts tied to a given client/company, and power search-as-you-type.

`listYachts` delegates to the shared port-scoped `buildListQuery`,
supporting search over name/hullNumber/registration plus ownerType,
ownerId and status filters; `autocomplete` caps at 10 results and is
tenant-scoped; `listYachtsForOwner` returns all yachts whose current
owner matches, newest first. Extends `makeYacht` factory to accept
flat `name`, `status`, `hullNumber`, `registration` overrides.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Matt Ciaccio
2026-04-24 00:03:36 +02:00
parent 8a5cd1ef0e
commit 7c408cf975
3 changed files with 283 additions and 4 deletions

View File

@@ -1,17 +1,20 @@
import { and, eq, sql } from 'drizzle-orm';
import { and, eq, ilike, or, sql } from 'drizzle-orm';
import { db } from '@/lib/db';
import { yachts, yachtOwnershipHistory, clients } from '@/lib/db/schema';
import type { Yacht } from '@/lib/db/schema/yachts';
import { companies } from '@/lib/db/schema/companies';
import { createAuditLog } from '@/lib/audit';
import { NotFoundError, ValidationError } from '@/lib/errors';
import { emitToRoom } from '@/lib/socket/server';
import { diffEntity } from '@/lib/entity-diff';
import { buildListQuery } from '@/lib/db/query-builder';
import { withTransaction } from '@/lib/db/utils';
import type { z } from 'zod';
import type {
createYachtSchema,
UpdateYachtInput,
TransferOwnershipInput,
ListYachtsInput,
} from '@/lib/validators/yachts';
type CreateYachtInput = z.input<typeof createYachtSchema>;
@@ -263,3 +266,74 @@ export async function transferOwnership(
return updated!;
});
}
// ─── List ─────────────────────────────────────────────────────────────────────
export async function listYachts(portId: string, query: ListYachtsInput) {
const { page, limit, sort, order, search, includeArchived, ownerType, ownerId, status } = query;
const filters = [];
if (ownerType) filters.push(eq(yachts.currentOwnerType, ownerType));
if (ownerId) filters.push(eq(yachts.currentOwnerId, ownerId));
if (status) filters.push(eq(yachts.status, status));
let sortColumn: typeof yachts.name | typeof yachts.createdAt | typeof yachts.updatedAt =
yachts.updatedAt;
if (sort === 'name') sortColumn = yachts.name;
else if (sort === 'createdAt') sortColumn = yachts.createdAt;
const result = await buildListQuery<Yacht>({
table: yachts,
portIdColumn: yachts.portId,
portId,
idColumn: yachts.id,
updatedAtColumn: yachts.updatedAt,
searchColumns: [yachts.name, yachts.hullNumber, yachts.registration],
searchTerm: search,
filters,
sort: sort ? { column: sortColumn, direction: order } : undefined,
page,
pageSize: limit,
includeArchived,
archivedAtColumn: yachts.archivedAt,
});
return result;
}
// ─── List for owner ───────────────────────────────────────────────────────────
export async function listYachtsForOwner(
portId: string,
ownerType: 'client' | 'company',
ownerId: string,
) {
return await db.query.yachts.findMany({
where: and(
eq(yachts.portId, portId),
eq(yachts.currentOwnerType, ownerType),
eq(yachts.currentOwnerId, ownerId),
),
orderBy: (t, { desc }) => [desc(t.updatedAt)],
});
}
// ─── Autocomplete ─────────────────────────────────────────────────────────────
export async function autocomplete(portId: string, q: string) {
const pattern = `%${q}%`;
return await db
.select()
.from(yachts)
.where(
and(
eq(yachts.portId, portId),
or(
ilike(yachts.name, pattern),
ilike(yachts.hullNumber, pattern),
ilike(yachts.registration, pattern),
),
),
)
.limit(10);
}