refactor: consolidate duplicated code across web and API packages
- Extract shared render helpers (vacation blocks, range overlay, overbooking blink) into renderHelpers.tsx - Centralize status badge styles and vacation color maps into status-styles.ts - Extract dragMath.ts utility from useTimelineDrag for reuse - Split useInvalidatePlanningViews into useInvalidateTimeline (4 queries) + useInvalidatePlanningViews (8 queries) - Adopt findUniqueOrThrow() and Prisma select constants across API routers - Add shared fmtEur() helper for API-side money formatting - Wrap TimelineResourcePanel and TimelineProjectPanel with React.memo - Fix pre-existing TS2589 deep type errors in TeamCalendar and VacationModal - 38 files changed, reducing ~400 lines of duplicated code Co-Authored-By: claude-flow <ruv@ruv.net>
This commit is contained in:
@@ -1,11 +1,12 @@
|
||||
"use client";
|
||||
|
||||
import { useRef, useState, useMemo, useCallback } from "react";
|
||||
import { useRef, useState, useMemo } from "react";
|
||||
import { AllocationStatus } from "@planarchy/shared";
|
||||
import { useFocusTrap } from "~/hooks/useFocusTrap.js";
|
||||
import { formatDateMedium } from "~/lib/format.js";
|
||||
import { formatCents, formatDateMedium } from "~/lib/format.js";
|
||||
import { getPlanningEntryMutationId } from "~/lib/planningEntryIds.js";
|
||||
import { trpc } from "~/lib/trpc/client.js";
|
||||
import { useInvalidatePlanningViews } from "~/hooks/useInvalidatePlanningViews.js";
|
||||
import { InfoTooltip } from "~/components/ui/InfoTooltip.js";
|
||||
|
||||
interface OpenDemandAllocation {
|
||||
@@ -69,15 +70,7 @@ export function FillOpenDemandModal({ allocation, onClose, onSuccess }: FillOpen
|
||||
searchTimerRef.current = setTimeout(() => setDebouncedSearch(value), 200);
|
||||
}
|
||||
|
||||
const utils = trpc.useUtils();
|
||||
const invalidatePlanningViews = useCallback(async () => {
|
||||
await utils.allocation.list.invalidate();
|
||||
await utils.allocation.listView.invalidate();
|
||||
await utils.timeline.getEntries.invalidate();
|
||||
await utils.timeline.getEntriesView.invalidate();
|
||||
await utils.timeline.getProjectContext.invalidate();
|
||||
await utils.timeline.getBudgetStatus.invalidate();
|
||||
}, [utils]);
|
||||
const invalidatePlanningViews = useInvalidatePlanningViews();
|
||||
|
||||
const { data: resources } = trpc.resource.list.useQuery(
|
||||
{ isActive: true, search: debouncedSearch || undefined, limit: 50 },
|
||||
@@ -211,7 +204,7 @@ export function FillOpenDemandModal({ allocation, onClose, onSuccess }: FillOpen
|
||||
</div>
|
||||
<div className="text-xs text-gray-500 dark:text-gray-400">
|
||||
{allocation.hoursPerDay}h/day · {totalDemandHours.toLocaleString()}h total
|
||||
{allocation.budgetCents && allocation.budgetCents > 0 ? ` · Budget: ${(allocation.budgetCents / 100).toLocaleString("de-DE")} EUR` : ""}
|
||||
{allocation.budgetCents && allocation.budgetCents > 0 ? ` · Budget: ${formatCents(allocation.budgetCents)} EUR` : ""}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -414,7 +407,7 @@ export function FillOpenDemandModal({ allocation, onClose, onSuccess }: FillOpen
|
||||
</td>
|
||||
<td className="px-3 py-2 text-right text-gray-600 dark:text-gray-300">{r.hoursPerDay}h</td>
|
||||
<td className="px-3 py-2 text-right text-gray-600 dark:text-gray-300">{Math.round(r.availableHours)}h</td>
|
||||
<td className="px-3 py-2 text-right text-gray-600 dark:text-gray-300">{(r.estimatedCostCents / 100).toLocaleString("de-DE")} EUR</td>
|
||||
<td className="px-3 py-2 text-right text-gray-600 dark:text-gray-300">{formatCents(r.estimatedCostCents)} EUR</td>
|
||||
<td className="px-3 py-2 text-right">
|
||||
<span className={`font-medium ${r.coveragePercent >= 100 ? "text-green-600" : r.coveragePercent >= 50 ? "text-amber-600" : "text-red-600"}`}>
|
||||
{r.coveragePercent}%
|
||||
@@ -431,7 +424,7 @@ export function FillOpenDemandModal({ allocation, onClose, onSuccess }: FillOpen
|
||||
{Math.round(consumedHours)}h / {totalDemandHours}h
|
||||
</td>
|
||||
<td className="px-3 py-2 text-right text-xs font-semibold text-gray-700 dark:text-gray-300">
|
||||
{(planned.reduce((s, r) => s + r.estimatedCostCents, 0) / 100).toLocaleString("de-DE")} EUR
|
||||
{formatCents(planned.reduce((s, r) => s + r.estimatedCostCents, 0))} EUR
|
||||
</td>
|
||||
<td className="px-3 py-2 text-right text-xs font-semibold text-gray-700 dark:text-gray-300">
|
||||
{totalDemandHours > 0 ? Math.round((consumedHours / totalDemandHours) * 100) : 0}%
|
||||
@@ -441,7 +434,7 @@ export function FillOpenDemandModal({ allocation, onClose, onSuccess }: FillOpen
|
||||
<tr>
|
||||
<td colSpan={3} className="px-3 py-1.5 text-right text-xs text-gray-500 dark:text-gray-400">Role Budget:</td>
|
||||
<td className="px-3 py-1.5 text-right text-xs font-semibold text-gray-700 dark:text-gray-300">
|
||||
{(allocation.budgetCents / 100).toLocaleString("de-DE")} EUR
|
||||
{formatCents(allocation.budgetCents)} EUR
|
||||
</td>
|
||||
<td className="px-3 py-1.5 text-right text-xs">
|
||||
{(() => {
|
||||
@@ -449,7 +442,7 @@ export function FillOpenDemandModal({ allocation, onClose, onSuccess }: FillOpen
|
||||
const remain = allocation.budgetCents! - totalCost;
|
||||
return (
|
||||
<span className={remain < 0 ? "text-red-600 font-medium" : "text-green-600"}>
|
||||
{remain < 0 ? `${(Math.abs(remain) / 100).toLocaleString("de-DE")} over` : `${(remain / 100).toLocaleString("de-DE")} left`}
|
||||
{remain < 0 ? `${formatCents(Math.abs(remain))} over` : `${formatCents(remain)} left`}
|
||||
</span>
|
||||
);
|
||||
})()}
|
||||
|
||||
Reference in New Issue
Block a user