Files
CapaKraken/apps/web/src/components/ui/ProjectCombobox.tsx
T
Hartmut ac845d72b7 refactor: deduplicate modals, notifications, confirms, comboboxes, proficiency
Modal Overlay (Finding 1 — 6 admin files):
- Migrated CountriesClient, ManagementLevelsClient, OrgUnitsClient,
  CalculationRulesClient, UtilizationCategoriesClient, RoleModal
  from inline fixed-overlay to AnimatedModal component
- Gains: animated transitions, backdrop blur, escape key for free

Notification Helper (Finding 9 — 9 API files, 14 call sites):
- New createNotification() + createNotificationsForUsers() in
  packages/api/src/lib/create-notification.ts
- Handles exactOptionalPropertyTypes spread + SSE emit internally
- Simplified: budget-alerts, estimate-reminders, auto-staffing,
  vacation-conflicts, chargeability-alerts, comment, vacation, notification

ConfirmDialog (Finding 3 — 11 files):
- Replaced all window.confirm() calls with ConfirmDialog component
- Files: CommentThread, EffortRules, ExperienceMultipliers,
  ManagementLevels, CalculationRules, Countries, RateCards,
  ApplyEffortRules, ApplyExperienceMultipliers, NotificationCenter,
  ReminderModal

EntityCombobox (Finding 4 — 3 files):
- New generic EntityCombobox<T> with customization hooks
- ResourceCombobox + ProjectCombobox rewritten as thin wrappers
- All consumers unchanged (backwards-compatible props)

Proficiency Constants (Finding 2 — 2 files):
- SkillsAnalytics + SkillMarketplace now import from skills/shared.tsx
- Deleted ~70 LOC of local duplicate definitions

Regression: 283 engine + 37 staffing tests pass. TypeScript clean.
AI Assistant: all 87 tools verified accessible.

Co-Authored-By: claude-flow <ruv@ruv.net>
2026-03-22 21:50:39 +01:00

65 lines
1.7 KiB
TypeScript

"use client";
import { useCallback } from "react";
import { trpc } from "~/lib/trpc/client.js";
import type { ProjectStatus } from "@planarchy/shared";
import { EntityCombobox } from "./EntityCombobox.js";
type ProjectItem = { id: string; shortCode: string; name: string };
interface ProjectComboboxProps {
value: string | null;
onChange: (id: string | null) => void;
placeholder?: string;
disabled?: boolean;
status?: ProjectStatus;
className?: string;
}
export function ProjectCombobox({
status,
...props
}: ProjectComboboxProps) {
const useSearchQuery = (search: string, enabled: boolean) => {
const { data } = trpc.project.list.useQuery(
{ search: search || undefined, limit: 15, ...(status ? { status } : {}) },
{ enabled, staleTime: 30_000 },
);
return { data: (data?.projects ?? []) as ProjectItem[] };
};
const useSelectedQuery = (_id: string | null, enabled: boolean) => {
const { data } = trpc.project.list.useQuery(
{ limit: 500 },
{ enabled, staleTime: 60_000 },
);
return { data: (data?.projects ?? []) as ProjectItem[] };
};
const getLabel = useCallback(
(p: ProjectItem) => `${p.shortCode} \u2014 ${p.name}`,
[],
);
const renderItem = useCallback(
(p: ProjectItem) => (
<>
<span className="font-medium text-xs text-gray-400 dark:text-gray-500 mr-1.5">{p.shortCode}</span>
<span>{p.name}</span>
</>
),
[],
);
return (
<EntityCombobox<ProjectItem>
{...props}
placeholder={props.placeholder ?? "Search project\u2026"}
useSearchQuery={useSearchQuery}
useSelectedQuery={useSelectedQuery}
getLabel={getLabel}
renderItem={renderItem}
/>
);
}