fix(ux): resolve tickets #59 #66 #67 — project feedback and demand summary

#66: Project detail "Open Demands" summary incorrectly counted COMPLETED
demands as open. Fix: add `status !== "COMPLETED"` to the activeDemands
filter in /projects/[id]/page.tsx.

#59/#67: Project creation and edit had two bugs:
1. Both invalidated `project.list` but the page queries `project.listWithCosts`
   — the list never refreshed after a save.
2. Success toasts were either absent (ProjectModal) or mounted inside the
   wizard component that unmounts before the toast finishes.
Fix: correct invalidation key to listWithCosts; add optional onSuccess prop
to both ProjectWizard and ProjectModal; ProjectsClient wires onSuccess to a
persistent SuccessToast rendered outside the modals.

Co-Authored-By: claude-flow <ruv@ruv.net>
This commit is contained in:
2026-04-03 16:05:29 +02:00
parent b7bb6d05af
commit 2da29c8191
4 changed files with 39 additions and 8 deletions
@@ -28,6 +28,7 @@ import { useRowOrder } from "~/hooks/useRowOrder.js";
import { SortableColumnHeader } from "~/components/ui/SortableColumnHeader.js"; import { SortableColumnHeader } from "~/components/ui/SortableColumnHeader.js";
import { DraggableTableRow } from "~/components/ui/DraggableTableRow.js"; import { DraggableTableRow } from "~/components/ui/DraggableTableRow.js";
import { ShoringBadge } from "~/components/projects/ShoringIndicator.js"; import { ShoringBadge } from "~/components/projects/ShoringIndicator.js";
import { SuccessToast } from "~/components/ui/SuccessToast.js";
import { PROJECT_STATUS_BADGE as STATUS_COLORS, ORDER_TYPE_BADGE as ORDER_TYPE_COLORS } from "~/lib/status-styles.js"; import { PROJECT_STATUS_BADGE as STATUS_COLORS, ORDER_TYPE_BADGE as ORDER_TYPE_COLORS } from "~/lib/status-styles.js";
@@ -182,6 +183,7 @@ export function ProjectsClient() {
const [modalOpen, setModalOpen] = useState(false); const [modalOpen, setModalOpen] = useState(false);
const [wizardOpen, setWizardOpen] = useState(false); const [wizardOpen, setWizardOpen] = useState(false);
const [editingProject, setEditingProject] = useState<Project | null>(null); const [editingProject, setEditingProject] = useState<Project | null>(null);
const [successToast, setSuccessToast] = useState<string | null>(null);
const [openStatusProjectId, setOpenStatusProjectId] = useState<string | null>(null); const [openStatusProjectId, setOpenStatusProjectId] = useState<string | null>(null);
const [batchStatusPicker, setBatchStatusPicker] = useState(false); const [batchStatusPicker, setBatchStatusPicker] = useState(false);
const [confirmBatchStatus, setConfirmBatchStatus] = useState<{ ids: string[]; status: string } | null>(null); const [confirmBatchStatus, setConfirmBatchStatus] = useState<{ ids: string[]; status: string } | null>(null);
@@ -749,10 +751,34 @@ export function ProjectsClient() {
/> />
{/* Modal */} {/* Modal */}
{modalOpen && <ProjectModal project={editingProject} onClose={closeModal} />} {modalOpen && (
<ProjectModal
project={editingProject}
onClose={closeModal}
onSuccess={(name) =>
setSuccessToast(
editingProject
? `Project "${name}" updated successfully.`
: `Project "${name}" created successfully.`,
)
}
/>
)}
{/* Wizard */} {/* Wizard */}
<ProjectWizard open={wizardOpen} onClose={() => setWizardOpen(false)} /> <ProjectWizard
open={wizardOpen}
onClose={() => setWizardOpen(false)}
onSuccess={(shortCode, name) =>
setSuccessToast(`Project "${shortCode}${name}" created successfully.`)
}
/>
<SuccessToast
show={successToast !== null}
message={successToast ?? ""}
onDone={() => setSuccessToast(null)}
/>
</div> </div>
); );
} }
@@ -32,7 +32,7 @@ export default async function ProjectDetailPage({ params }: ProjectDetailPagePro
} }
const activeAssignments = project.assignments.filter((assignment) => assignment.status !== "CANCELLED"); const activeAssignments = project.assignments.filter((assignment) => assignment.status !== "CANCELLED");
const activeDemands = project.demands.filter((demand) => demand.status !== "CANCELLED"); const activeDemands = project.demands.filter((demand) => demand.status !== "CANCELLED" && demand.status !== "COMPLETED");
const requestedSeats = activeDemands.reduce((sum, demand) => sum + demand.requestedHeadcount, 0); const requestedSeats = activeDemands.reduce((sum, demand) => sum + demand.requestedHeadcount, 0);
const unfilledSeats = activeDemands.reduce((sum, demand) => sum + demand.unfilledHeadcount, 0); const unfilledSeats = activeDemands.reduce((sum, demand) => sum + demand.unfilledHeadcount, 0);
@@ -95,9 +95,10 @@ function projectToForm(project: Project): FormState {
interface ProjectModalProps { interface ProjectModalProps {
project?: Project | null; project?: Project | null;
onClose: () => void; onClose: () => void;
onSuccess?: (name: string) => void;
} }
export function ProjectModal({ project, onClose }: ProjectModalProps) { export function ProjectModal({ project, onClose, onSuccess }: ProjectModalProps) {
const isEdit = !!project; const isEdit = !!project;
const utils = trpc.useUtils(); const utils = trpc.useUtils();
@@ -116,7 +117,8 @@ export function ProjectModal({ project, onClose }: ProjectModalProps) {
// @ts-ignore TS2589: tRPC infers union type too deeply for CreateProjectSchema with .refine() // @ts-ignore TS2589: tRPC infers union type too deeply for CreateProjectSchema with .refine()
const createMutation = trpc.project.create.useMutation({ const createMutation = trpc.project.create.useMutation({
onSuccess: async () => { onSuccess: async () => {
await utils.project.list.invalidate(); await utils.project.listWithCosts.invalidate();
onSuccess?.(form.name.trim());
onClose(); onClose();
}, },
onError: (err) => { onError: (err) => {
@@ -126,7 +128,8 @@ export function ProjectModal({ project, onClose }: ProjectModalProps) {
const updateMutation = trpc.project.update.useMutation({ const updateMutation = trpc.project.update.useMutation({
onSuccess: async () => { onSuccess: async () => {
await utils.project.list.invalidate(); await utils.project.listWithCosts.invalidate();
onSuccess?.(form.name.trim());
onClose(); onClose();
}, },
onError: (err) => { onError: (err) => {
@@ -1041,9 +1041,10 @@ function Step5({ state, onChange, onSubmit, isSubmitting, submitError }: Step5Pr
interface ProjectWizardProps { interface ProjectWizardProps {
open: boolean; open: boolean;
onClose: () => void; onClose: () => void;
onSuccess?: (shortCode: string, name: string) => void;
} }
export function ProjectWizard({ open, onClose }: ProjectWizardProps) { export function ProjectWizard({ open, onClose, onSuccess }: ProjectWizardProps) {
const utils = trpc.useUtils(); const utils = trpc.useUtils();
const [step, setStep] = useState(0); const [step, setStep] = useState(0);
const [state, setState] = useState<WizardState>(makeDefaultState); const [state, setState] = useState<WizardState>(makeDefaultState);
@@ -1164,13 +1165,14 @@ export function ProjectWizard({ open, onClose }: ProjectWizardProps) {
} }
} }
await utils.project.list.invalidate(); await utils.project.listWithCosts.invalidate();
await utils.timeline.getEntries.invalidate(); await utils.timeline.getEntries.invalidate();
await utils.timeline.getEntriesView.invalidate(); await utils.timeline.getEntriesView.invalidate();
setShowConfetti(true); setShowConfetti(true);
setShowSuccessToast(true); setShowSuccessToast(true);
setTimeout(() => { setTimeout(() => {
setShowConfetti(false); setShowConfetti(false);
onSuccess?.(project.shortCode, project.name);
handleClose(); handleClose();
}, 1200); }, 1200);
} catch (err) { } catch (err) {