feat(uat-batch-12): password-reveal env messaging + berth Latest-stage sortable
- registry-driven-form password-reveal eye toggle: when the value is resolved from env / default fallback (not port / global override), the toggle is now disabled with a tooltip explaining "Value comes from the environment. Configure in admin to enable reveal." Stops the silent-no-op confusion that read as a broken toggle. - Berth list: 'Latest deal stage' column dropped enableSorting:false. Service-side adds a stageSort correlated subquery that ranks each berth by the highest active interest's pipelineStage (enquiry=1 → contract=7); NULLS LAST regardless of direction so empty rows always land at the bottom. tsc clean. 1419/1419 vitest pass. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -436,7 +436,27 @@ function SettingField({
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
disabled={reveal.isPending}
|
||||
disabled={
|
||||
reveal.isPending ||
|
||||
// Disable when the value is resolved from env/default and the
|
||||
// rep hasn't typed anything yet — there's no in-app cleartext
|
||||
// path for those, and silently no-op'ing was indistinguishable
|
||||
// from a broken toggle.
|
||||
(!showSecret &&
|
||||
resolved?.isSet === true &&
|
||||
(resolved?.source === 'env' || resolved?.source === 'default') &&
|
||||
!(typeof draft === 'string' && draft.length > 0))
|
||||
}
|
||||
title={
|
||||
!showSecret &&
|
||||
resolved?.isSet === true &&
|
||||
(resolved?.source === 'env' || resolved?.source === 'default') &&
|
||||
!(typeof draft === 'string' && draft.length > 0)
|
||||
? 'Value comes from the environment. Configure in admin to enable reveal.'
|
||||
: showSecret
|
||||
? 'Hide value'
|
||||
: 'Reveal value'
|
||||
}
|
||||
onClick={() => {
|
||||
if (showSecret) {
|
||||
// Hide. If this draft came from the server reveal, drop it so
|
||||
|
||||
@@ -273,7 +273,7 @@ export const berthColumns: ColumnDef<BerthRow, unknown>[] = [
|
||||
{
|
||||
id: 'latestInterestStage',
|
||||
header: 'Latest deal stage',
|
||||
enableSorting: false,
|
||||
enableSorting: true,
|
||||
cell: ({ row }) => {
|
||||
const s = row.original.latestInterestStage;
|
||||
if (!s) return <span className="text-muted-foreground">-</span>;
|
||||
|
||||
@@ -94,6 +94,11 @@ export async function listBerths(portId: string, query: ListBerthsQuery) {
|
||||
case 'activeInterestCount':
|
||||
// Sorted via correlated subquery in customOrderBy below.
|
||||
return null;
|
||||
case 'latestInterestStage':
|
||||
// Sorted via correlated subquery in customOrderBy below — the
|
||||
// column doesn't exist on berths; it's the highest-ranked
|
||||
// active interest's pipeline stage per berth.
|
||||
return null;
|
||||
default:
|
||||
// No sort requested → natural mooring order is the friendliest
|
||||
// default for the berth grid (groups by pontoon letter).
|
||||
@@ -119,6 +124,36 @@ export async function listBerths(portId: string, query: ListBerthsQuery) {
|
||||
]
|
||||
: null;
|
||||
|
||||
// Sort by highest active pipeline stage per berth. Berths with no
|
||||
// active interest get NULL; we land them at the bottom regardless of
|
||||
// direction by paired ORDER BY rank + NULLS LAST.
|
||||
const stageDirection = query.order === 'asc' ? 'ASC' : 'DESC';
|
||||
const stageSort =
|
||||
query.sort === 'latestInterestStage'
|
||||
? [
|
||||
sql`(
|
||||
SELECT MAX(
|
||||
CASE i.pipeline_stage
|
||||
WHEN 'enquiry' THEN 1
|
||||
WHEN 'qualified' THEN 2
|
||||
WHEN 'nurturing' THEN 3
|
||||
WHEN 'eoi' THEN 4
|
||||
WHEN 'reservation' THEN 5
|
||||
WHEN 'deposit_paid' THEN 6
|
||||
WHEN 'contract' THEN 7
|
||||
ELSE 0
|
||||
END
|
||||
)
|
||||
FROM ${interestBerths} ib
|
||||
INNER JOIN ${interests} i ON i.id = ib.interest_id
|
||||
WHERE ib.berth_id = ${berths.id}
|
||||
AND i.port_id = ${portId}
|
||||
AND i.archived_at IS NULL
|
||||
AND i.outcome IS NULL
|
||||
) ${sql.raw(stageDirection)} NULLS LAST`,
|
||||
]
|
||||
: null;
|
||||
|
||||
const result = await buildListQuery({
|
||||
table: berths,
|
||||
portIdColumn: berths.portId,
|
||||
@@ -127,7 +162,7 @@ export async function listBerths(portId: string, query: ListBerthsQuery) {
|
||||
updatedAtColumn: berths.updatedAt,
|
||||
filters,
|
||||
sort: sortColumn ? { column: sortColumn, direction: query.order } : undefined,
|
||||
customOrderBy: demandSort ?? (sortColumn ? undefined : NATURAL_MOORING_SORT),
|
||||
customOrderBy: stageSort ?? demandSort ?? (sortColumn ? undefined : NATURAL_MOORING_SORT),
|
||||
page: query.page,
|
||||
pageSize: query.limit,
|
||||
searchColumns: [berths.mooringNumber, berths.area],
|
||||
|
||||
Reference in New Issue
Block a user