chore: add pre-commit hooks, tighten ESLint, activate Sentry DSN, publish CI coverage (Phase 1)
- Install husky v9 + lint-staged: pre-commit runs eslint --fix and prettier on staged files - Tighten ESLint base config: no-console→error, ban-ts-comment (ts-ignore banned, ts-expect-error with description allowed), reportUnusedDisableDirectives→error - Migrate web app from deprecated `next lint` to `eslint src/` with flat config and react-hooks plugin - Convert all 5 @ts-ignore to @ts-expect-error with descriptions, remove stale disable comments - Add NEXT_PUBLIC_SENTRY_DSN to docker-compose.prod.yml and .env.example - Add coverage artifact upload step to CI test job - Pre-existing violations (102 warnings) downgraded to warn in web config for Phase 2 cleanup Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -3,7 +3,7 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import Link from "next/link";
|
||||
import dynamic from "next/dynamic";
|
||||
import { EstimateExportFormat } from "@capakraken/shared";
|
||||
import type { EstimateExportFormat } from "@capakraken/shared";
|
||||
import { clsx } from "clsx";
|
||||
import { useSession } from "next-auth/react";
|
||||
import type {
|
||||
@@ -23,52 +23,80 @@ const TabSkeleton = () => (
|
||||
);
|
||||
|
||||
const EstimateWorkspaceDraftEditor = dynamic(
|
||||
() => import("~/components/estimates/EstimateWorkspaceDraftEditor.js").then((mod) => ({ default: mod.EstimateWorkspaceDraftEditor })),
|
||||
() =>
|
||||
import("~/components/estimates/EstimateWorkspaceDraftEditor.js").then((mod) => ({
|
||||
default: mod.EstimateWorkspaceDraftEditor,
|
||||
})),
|
||||
{ loading: TabSkeleton },
|
||||
);
|
||||
|
||||
const WeeklyPhasingView = dynamic(
|
||||
() => import("~/components/estimates/WeeklyPhasingView.js").then((mod) => ({ default: mod.WeeklyPhasingView })),
|
||||
() =>
|
||||
import("~/components/estimates/WeeklyPhasingView.js").then((mod) => ({
|
||||
default: mod.WeeklyPhasingView,
|
||||
})),
|
||||
{ loading: TabSkeleton },
|
||||
);
|
||||
|
||||
const OverviewTab = dynamic(
|
||||
() => import("~/components/estimates/tabs/OverviewTab.js").then((mod) => ({ default: mod.OverviewTab })),
|
||||
() =>
|
||||
import("~/components/estimates/tabs/OverviewTab.js").then((mod) => ({
|
||||
default: mod.OverviewTab,
|
||||
})),
|
||||
{ loading: TabSkeleton },
|
||||
);
|
||||
|
||||
const AssumptionsTab = dynamic(
|
||||
() => import("~/components/estimates/tabs/AssumptionsTab.js").then((mod) => ({ default: mod.AssumptionsTab })),
|
||||
() =>
|
||||
import("~/components/estimates/tabs/AssumptionsTab.js").then((mod) => ({
|
||||
default: mod.AssumptionsTab,
|
||||
})),
|
||||
{ loading: TabSkeleton },
|
||||
);
|
||||
|
||||
const ScopeTab = dynamic(
|
||||
() => import("~/components/estimates/tabs/ScopeTab.js").then((mod) => ({ default: mod.ScopeTab })),
|
||||
() =>
|
||||
import("~/components/estimates/tabs/ScopeTab.js").then((mod) => ({ default: mod.ScopeTab })),
|
||||
{ loading: TabSkeleton },
|
||||
);
|
||||
|
||||
const StaffingTab = dynamic(
|
||||
() => import("~/components/estimates/tabs/StaffingTab.js").then((mod) => ({ default: mod.StaffingTab })),
|
||||
() =>
|
||||
import("~/components/estimates/tabs/StaffingTab.js").then((mod) => ({
|
||||
default: mod.StaffingTab,
|
||||
})),
|
||||
{ loading: TabSkeleton },
|
||||
);
|
||||
|
||||
const FinancialsTab = dynamic(
|
||||
() => import("~/components/estimates/tabs/FinancialsTab.js").then((mod) => ({ default: mod.FinancialsTab })),
|
||||
() =>
|
||||
import("~/components/estimates/tabs/FinancialsTab.js").then((mod) => ({
|
||||
default: mod.FinancialsTab,
|
||||
})),
|
||||
{ loading: TabSkeleton },
|
||||
);
|
||||
|
||||
const VersionsTab = dynamic(
|
||||
() => import("~/components/estimates/tabs/VersionsTab.js").then((mod) => ({ default: mod.VersionsTab })),
|
||||
() =>
|
||||
import("~/components/estimates/tabs/VersionsTab.js").then((mod) => ({
|
||||
default: mod.VersionsTab,
|
||||
})),
|
||||
{ loading: TabSkeleton },
|
||||
);
|
||||
|
||||
const ExportsTab = dynamic(
|
||||
() => import("~/components/estimates/tabs/ExportsTab.js").then((mod) => ({ default: mod.ExportsTab })),
|
||||
() =>
|
||||
import("~/components/estimates/tabs/ExportsTab.js").then((mod) => ({
|
||||
default: mod.ExportsTab,
|
||||
})),
|
||||
{ loading: TabSkeleton },
|
||||
);
|
||||
|
||||
const CommentThread = dynamic(
|
||||
() => import("~/components/comments/CommentThread.js").then((mod) => ({ default: mod.CommentThread })),
|
||||
() =>
|
||||
import("~/components/comments/CommentThread.js").then((mod) => ({
|
||||
default: mod.CommentThread,
|
||||
})),
|
||||
{ loading: TabSkeleton },
|
||||
);
|
||||
|
||||
@@ -137,20 +165,22 @@ export function EstimateWorkspaceClient({ estimateId }: { estimateId: string })
|
||||
const createPlanningHandoffMutation = trpc.estimate.createPlanningHandoff.useMutation();
|
||||
const estimateCommentTarget = { entityType: "estimate" as const, entityId: estimateId };
|
||||
const canLoadCommentCount =
|
||||
canViewCosts
|
||||
&& !isPermissionsLoading
|
||||
&& detailQuery.status === "success"
|
||||
&& detailQuery.data != null;
|
||||
canViewCosts &&
|
||||
!isPermissionsLoading &&
|
||||
detailQuery.status === "success" &&
|
||||
detailQuery.data != null;
|
||||
|
||||
const commentCountQuery = trpc.comment.count.useQuery(
|
||||
estimateCommentTarget,
|
||||
{ enabled: canLoadCommentCount, staleTime: 30_000 },
|
||||
);
|
||||
const commentCountQuery = trpc.comment.count.useQuery(estimateCommentTarget, {
|
||||
enabled: canLoadCommentCount,
|
||||
staleTime: 30_000,
|
||||
});
|
||||
const commentCount = commentCountQuery.data ?? 0;
|
||||
|
||||
const estimate = (detailQuery.data as EstimateWorkspaceView | undefined) ?? null;
|
||||
const hasWorkingVersion = estimate?.versions.some((version) => version.status === "WORKING") ?? false;
|
||||
const editableTab = tab === "overview" || tab === "assumptions" || tab === "scope" || tab === "staffing";
|
||||
const hasWorkingVersion =
|
||||
estimate?.versions.some((version) => version.status === "WORKING") ?? false;
|
||||
const editableTab =
|
||||
tab === "overview" || tab === "assumptions" || tab === "scope" || tab === "staffing";
|
||||
|
||||
useEffect(() => {
|
||||
setIsEditing(false);
|
||||
@@ -258,19 +288,27 @@ export function EstimateWorkspaceClient({ estimateId }: { estimateId: string })
|
||||
<div className="rounded-[28px] border border-gray-200 dark:border-gray-700 bg-gradient-to-br from-white via-white to-brand-50 dark:from-gray-900 dark:via-gray-900 dark:to-gray-900 p-6 shadow-sm dark:shadow-black/20">
|
||||
<div className="flex flex-col gap-5 lg:flex-row lg:items-start lg:justify-between">
|
||||
<div>
|
||||
<p className="text-xs font-semibold uppercase tracking-[0.24em] text-brand-600 dark:text-sky-400">Estimate Workspace <InfoTooltip content="Central workspace for inspecting and editing an estimate's scope, staffing, financials, and version history." /></p>
|
||||
<p className="text-xs font-semibold uppercase tracking-[0.24em] text-brand-600 dark:text-sky-400">
|
||||
Estimate Workspace{" "}
|
||||
<InfoTooltip content="Central workspace for inspecting and editing an estimate's scope, staffing, financials, and version history." />
|
||||
</p>
|
||||
<h1 className="mt-2 text-3xl font-semibold text-gray-900 dark:text-gray-50">
|
||||
{estimate?.name ?? "Loading estimate"}
|
||||
</h1>
|
||||
<p className="mt-2 max-w-3xl text-sm text-gray-600 dark:text-gray-300">
|
||||
Use the tabs below to inspect the connected estimate structure, version context, and staffing breakdown without relying on spreadsheet tabs.
|
||||
Use the tabs below to inspect the connected estimate structure, version context, and
|
||||
staffing breakdown without relying on spreadsheet tabs.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{estimate && (
|
||||
<div className="flex flex-col gap-3 lg:items-end">
|
||||
<div className="grid gap-2 text-sm text-gray-500 dark:text-gray-400 lg:text-right">
|
||||
<span>{estimate.project ? `${estimate.project.shortCode} - ${estimate.project.name}` : "Standalone estimate"}</span>
|
||||
<span>
|
||||
{estimate.project
|
||||
? `${estimate.project.shortCode} - ${estimate.project.name}`
|
||||
: "Standalone estimate"}
|
||||
</span>
|
||||
<span>Updated {formatDateLong(estimate.updatedAt)}</span>
|
||||
</div>
|
||||
{canEdit && hasWorkingVersion && (
|
||||
@@ -282,7 +320,11 @@ export function EstimateWorkspaceClient({ estimateId }: { estimateId: string })
|
||||
}}
|
||||
className="rounded-2xl border border-brand-200 dark:border-sky-700 bg-white dark:bg-gray-800 px-4 py-2 text-sm font-semibold text-brand-700 dark:text-sky-300 transition hover:border-brand-300 dark:hover:border-sky-600 hover:bg-brand-50 dark:hover:bg-gray-700"
|
||||
>
|
||||
{isEditing ? "Close editor" : editableTab ? "Edit working draft" : "Draft editor available in editable tabs"}
|
||||
{isEditing
|
||||
? "Close editor"
|
||||
: editableTab
|
||||
? "Edit working draft"
|
||||
: "Draft editor available in editable tabs"}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
@@ -293,7 +335,9 @@ export function EstimateWorkspaceClient({ estimateId }: { estimateId: string })
|
||||
{isPermissionsLoading ? (
|
||||
<EmptyState>Loading estimate workspace...</EmptyState>
|
||||
) : !canViewCosts ? (
|
||||
<EmptyState>Your role can access the estimate list, but not the detailed financial workspace.</EmptyState>
|
||||
<EmptyState>
|
||||
Your role can access the estimate list, but not the detailed financial workspace.
|
||||
</EmptyState>
|
||||
) : detailQuery.isLoading ? (
|
||||
<EmptyState>Loading estimate workspace...</EmptyState>
|
||||
) : detailQuery.error ? (
|
||||
|
||||
Reference in New Issue
Block a user