Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 14 additions & 2 deletions app/(main)/(auth)/applications/page.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
import type { Metadata } from 'next';

import { getMyApplications } from '@/prisma/data/applications';
import {
getApplicationCompletion,
getMyApplications,
} from '@/prisma/data/applications';

import { getCurrentUser } from '@/lib/auth/server';

Expand All @@ -13,14 +16,23 @@ export default async function MyApplicationsPage() {
const user = await getCurrentUser();
const applications = await getMyApplications(user.id);
const now = new Date();
const completion = await getApplicationCompletion(
applications
.filter((a) => a.status === 'draft')
.map((a) => ({ id: a.id, positionId: a.positionId, userId: user.id })),
);

return (
<div className="flex w-full flex-col gap-6">
<PageHeader
title="My Applications"
description="Track your drafts and submitted applications."
/>
<MyApplicationsTable applications={applications} now={now} />
<MyApplicationsTable
applications={applications}
now={now}
completion={completion}
/>
</div>
);
}
1 change: 1 addition & 0 deletions app/(main)/(auth)/manage/applications/[id]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@ export default async function ApplicationDetailPage({
applicantName={applicantName}
applicantEmail={application.user.email}
history={history}
isAdmin={user.isAdmin}
/>
}
/>
Expand Down
16 changes: 16 additions & 0 deletions components/features/application-status-actions.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import { isError } from '@/lib/utils';
import { ApplicationStatusDialog } from '@/components/features/application-status-dialog';
import { ApplicationStatusMenu } from '@/components/features/application-status-menu';
import { useApplicationStatusMove } from '@/components/features/use-application-status-move';
import { useForceWithdrawApplication } from '@/components/features/use-force-withdraw-application';
import { Button } from '@/components/ui/button';
import { ConfirmDialog } from '@/components/ui/confirm-dialog';
import {
Expand All @@ -28,6 +29,7 @@ interface ApplicationStatusActionsProps {
currentStatus: $Enums.ApplicationStatus;
applicantName?: string;
applicantEmail?: string;
isAdmin: boolean;
}

// Table row `⋯` menu only — the detail page's header actions live in
Expand All @@ -37,6 +39,7 @@ export function ApplicationStatusActions({
currentStatus,
applicantName,
applicantEmail,
isAdmin,
}: ApplicationStatusActionsProps) {
const displayName = applicantName ?? 'this application';
const { isPending, selectTarget, confirmDialogProps } =
Expand All @@ -46,6 +49,13 @@ export function ApplicationStatusActions({
applicantEmail,
currentStatus,
});
const forceWithdraw = useForceWithdrawApplication({
applicationId,
applicantName,
currentStatus,
});
const canForceWithdraw =
isAdmin && !isNonReviewableApplicationStatus(currentStatus);

const [dialogOpen, setDialogOpen] = useState(false);
const [history, setHistory] = useState<ApplicationStatusHistoryEntry[]>([]);
Expand Down Expand Up @@ -105,10 +115,15 @@ export function ApplicationStatusActions({
isPending={isPending}
onSelect={selectTarget}
onSeeMore={openDialog}
canForceWithdraw={canForceWithdraw}
onForceWithdraw={forceWithdraw.openConfirm}
/>
</DropdownMenuContent>
</DropdownMenu>
<ConfirmDialog {...confirmDialogProps} />
{canForceWithdraw && (
<ConfirmDialog {...forceWithdraw.confirmDialogProps} />
)}
<ApplicationStatusDialog
applicationId={applicationId}
applicantName={displayName}
Expand All @@ -117,6 +132,7 @@ export function ApplicationStatusActions({
history={history}
isHistoryLoading={isHistoryLoading}
historyFailed={historyFailed}
isAdmin={isAdmin}
open={dialogOpen}
onOpenChange={setDialogOpen}
/>
Expand Down
41 changes: 39 additions & 2 deletions components/features/application-status-dialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import {
} from '@/lib/utils';

import { useApplicationStatusMove } from '@/components/features/use-application-status-move';
import { useForceWithdrawApplication } from '@/components/features/use-force-withdraw-application';
import { Button } from '@/components/ui/button';
import { ConfirmDialog } from '@/components/ui/confirm-dialog';
import {
Expand Down Expand Up @@ -47,6 +48,7 @@ interface ApplicationStatusDialogProps {
// its pre-fetched history and leaves these unset.
isHistoryLoading?: boolean;
historyFailed?: boolean;
isAdmin: boolean;
open: boolean;
onOpenChange: (open: boolean) => void;
}
Expand All @@ -59,6 +61,7 @@ export function ApplicationStatusDialog({
history,
isHistoryLoading = false,
historyFailed = false,
isAdmin,
open,
onOpenChange,
}: ApplicationStatusDialogProps) {
Expand All @@ -71,8 +74,15 @@ export function ApplicationStatusDialog({
applicantEmail,
currentStatus,
});
const forceWithdraw = useForceWithdrawApplication({
applicationId,
applicantName,
currentStatus,
});

const canOverride = !isNonReviewableApplicationStatus(currentStatus);
const canForceWithdraw =
isAdmin && !isNonReviewableApplicationStatus(currentStatus);
const selectingDecision =
selectedStatus === 'accepted' || selectedStatus === 'rejected';

Expand Down Expand Up @@ -110,7 +120,7 @@ export function ApplicationStatusDialog({
onValueChange={(v) =>
setSelectedStatus(v as $Enums.ApplicationStatus)
}
disabled={move.isPending}
disabled={move.isPending || forceWithdraw.isPending}
>
<SelectTrigger
id="status-dialog-select"
Expand All @@ -131,7 +141,11 @@ export function ApplicationStatusDialog({
</Select>
<Button
size="sm"
disabled={!selectedStatus || move.isPending}
disabled={
!selectedStatus ||
move.isPending ||
forceWithdraw.isPending
}
onClick={handleApply}
>
{move.isPending &&
Expand All @@ -156,6 +170,26 @@ export function ApplicationStatusDialog({
</div>
)}

{canForceWithdraw && (
<div className="flex flex-col gap-2 border-t pt-4">
<h3 className="text-sm font-medium">Force withdraw</h3>
<p className="text-muted-foreground text-xs">
Return this application to {applicantName} without telling
them. No email is sent.
</p>
<Button
variant="destructive"
size="sm"
className="w-fit"
disabled={move.isPending || forceWithdraw.isPending}
onClick={forceWithdraw.openConfirm}
>
<ACTION_ICONS.forceWithdraw />
Force withdraw
</Button>
</div>
)}

<div className="flex flex-col gap-2">
<h3 className="text-sm font-medium">History</h3>
{isHistoryLoading ? (
Expand Down Expand Up @@ -204,6 +238,9 @@ export function ApplicationStatusDialog({
</DialogContent>
</Dialog>
<ConfirmDialog {...move.confirmDialogProps} />
{canForceWithdraw && (
<ConfirmDialog {...forceWithdraw.confirmDialogProps} />
)}
</>
);
}
21 changes: 21 additions & 0 deletions components/features/application-status-header-actions.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import type { ApplicationStatusHistoryEntry } from '@/lib/types';
import { ApplicationStatusDialog } from '@/components/features/application-status-dialog';
import { ApplicationStatusMenu } from '@/components/features/application-status-menu';
import { useApplicationStatusMove } from '@/components/features/use-application-status-move';
import { useForceWithdrawApplication } from '@/components/features/use-force-withdraw-application';
import { Button } from '@/components/ui/button';
import { ConfirmDialog } from '@/components/ui/confirm-dialog';
import {
Expand All @@ -32,6 +33,7 @@ interface ApplicationStatusHeaderActionsProps {
applicantName: string;
applicantEmail: string;
history: ApplicationStatusHistoryEntry[];
isAdmin: boolean;
}

// Unresolved gets a split button; everything else gets a standalone caret — same dropdown either way.
Expand All @@ -41,6 +43,7 @@ export function ApplicationStatusHeaderActions({
applicantName,
applicantEmail,
history,
isAdmin,
}: ApplicationStatusHeaderActionsProps) {
const [dialogOpen, setDialogOpen] = useState(false);
const move = useApplicationStatusMove({
Expand All @@ -49,6 +52,13 @@ export function ApplicationStatusHeaderActions({
applicantEmail,
currentStatus,
});
const forceWithdraw = useForceWithdrawApplication({
applicationId,
applicantName,
currentStatus,
});
const canForceWithdraw =
isAdmin && !isNonReviewableApplicationStatus(currentStatus);

const dialog = (
<ApplicationStatusDialog
Expand All @@ -57,6 +67,7 @@ export function ApplicationStatusHeaderActions({
applicantEmail={applicantEmail}
currentStatus={currentStatus}
history={history}
isAdmin={isAdmin}
open={dialogOpen}
onOpenChange={setDialogOpen}
/>
Expand Down Expand Up @@ -99,10 +110,15 @@ export function ApplicationStatusHeaderActions({
isPending={move.isPending}
onSelect={move.selectTarget}
onSeeMore={() => setDialogOpen(true)}
canForceWithdraw={canForceWithdraw}
onForceWithdraw={forceWithdraw.openConfirm}
/>
</DropdownMenuContent>
</DropdownMenu>
{confirmDialog}
{canForceWithdraw && (
<ConfirmDialog {...forceWithdraw.confirmDialogProps} />
)}
{dialog}
</>
);
Expand Down Expand Up @@ -145,12 +161,17 @@ export function ApplicationStatusHeaderActions({
isPending={move.isPending}
onSelect={move.selectTarget}
onSeeMore={() => setDialogOpen(true)}
canForceWithdraw={canForceWithdraw}
onForceWithdraw={forceWithdraw.openConfirm}
/>
</DropdownMenuContent>
</DropdownMenu>
</div>

{confirmDialog}
{canForceWithdraw && (
<ConfirmDialog {...forceWithdraw.confirmDialogProps} />
)}
{dialog}
</>
);
Expand Down
16 changes: 16 additions & 0 deletions components/features/application-status-menu.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ interface ApplicationStatusMenuProps {
isPending?: boolean;
onSelect: (target: $Enums.ApplicationStatus) => void;
onSeeMore: () => void;
canForceWithdraw?: boolean;
onForceWithdraw?: () => void;
}

// One menu shape for both surfaces — the only prop that differs is hoistNext.
Expand All @@ -29,6 +31,8 @@ export function ApplicationStatusMenu({
isPending = false,
onSelect,
onSeeMore,
canForceWithdraw = false,
onForceWithdraw,
}: ApplicationStatusMenuProps) {
const { next, decisions } = getApplicationStatusMenu(status);
const showNext = !hoistNext && next !== null;
Expand Down Expand Up @@ -61,6 +65,18 @@ export function ApplicationStatusMenu({
))}
{(showNext || decisions.length > 0) && <DropdownMenuSeparator />}
<DropdownMenuItem onSelect={onSeeMore}>See more</DropdownMenuItem>
{canForceWithdraw && (
<>
<DropdownMenuSeparator />
<DropdownMenuItem
variant="destructive"
disabled={isPending}
onSelect={onForceWithdraw}
>
Force withdraw
</DropdownMenuItem>
</>
)}
</>
);
}
Loading
Loading