#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:
@@ -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) {
|
||||||
|
|||||||
Reference in New Issue
Block a user