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 { DraggableTableRow } from "~/components/ui/DraggableTableRow.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";
@@ -182,6 +183,7 @@ export function ProjectsClient() {
const [modalOpen, setModalOpen] = useState(false);
const [wizardOpen, setWizardOpen] = useState(false);
const [editingProject, setEditingProject] = useState<Project | null>(null);
const [successToast, setSuccessToast] = useState<string | null>(null);
const [openStatusProjectId, setOpenStatusProjectId] = useState<string | null>(null);
const [batchStatusPicker, setBatchStatusPicker] = useState(false);
const [confirmBatchStatus, setConfirmBatchStatus] = useState<{ ids: string[]; status: string } | null>(null);
@@ -749,10 +751,34 @@ export function ProjectsClient() {
/>
{/* 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 */}
<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>
);
}
@@ -32,7 +32,7 @@ export default async function ProjectDetailPage({ params }: ProjectDetailPagePro
}
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 unfilledSeats = activeDemands.reduce((sum, demand) => sum + demand.unfilledHeadcount, 0);
@@ -95,9 +95,10 @@ function projectToForm(project: Project): FormState {
interface ProjectModalProps {
project?: Project | null;
onClose: () => void;
onSuccess?: (name: string) => void;
}
export function ProjectModal({ project, onClose }: ProjectModalProps) {
export function ProjectModal({ project, onClose, onSuccess }: ProjectModalProps) {
const isEdit = !!project;
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()
const createMutation = trpc.project.create.useMutation({
onSuccess: async () => {
await utils.project.list.invalidate();
await utils.project.listWithCosts.invalidate();
onSuccess?.(form.name.trim());
onClose();
},
onError: (err) => {
@@ -126,7 +128,8 @@ export function ProjectModal({ project, onClose }: ProjectModalProps) {
const updateMutation = trpc.project.update.useMutation({
onSuccess: async () => {
await utils.project.list.invalidate();
await utils.project.listWithCosts.invalidate();
onSuccess?.(form.name.trim());
onClose();
},
onError: (err) => {
@@ -1041,9 +1041,10 @@ function Step5({ state, onChange, onSubmit, isSubmitting, submitError }: Step5Pr
interface ProjectWizardProps {
open: boolean;
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 [step, setStep] = useState(0);
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.getEntriesView.invalidate();
setShowConfetti(true);
setShowSuccessToast(true);
setTimeout(() => {
setShowConfetti(false);
onSuccess?.(project.shortCode, project.name);
handleClose();
}, 1200);
} catch (err) {