|
1 | 1 | "use client"; |
2 | 2 |
|
3 | 3 | import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query"; |
4 | | -import { useEffect, useState } from "react"; |
| 4 | +import { useCallback, useEffect, useRef, useState } from "react"; |
5 | 5 | import { useTranslations } from "next-intl"; |
6 | 6 | import { authClient } from "@/lib/auth-client"; |
7 | 7 | import { ApplicationTable } from "./application-table"; |
@@ -154,11 +154,33 @@ export function Dashboard({ user, shareUrl, initialStatus, initialSource, initia |
154 | 154 | archiveMutation.mutate({ id, archive }); |
155 | 155 | } |
156 | 156 |
|
| 157 | + const bulkArchiveMutation = useMutation({ |
| 158 | + mutationFn: async (ids: string[]) => { |
| 159 | + await Promise.all(ids.map((id) => archiveApplication(id, true))); |
| 160 | + }, |
| 161 | + onSettled: () => { |
| 162 | + queryClient.invalidateQueries({ queryKey: ["applications"] }); |
| 163 | + }, |
| 164 | + }); |
| 165 | + |
157 | 166 | // Filter by archive status |
158 | 167 | const activeApplications = applications.filter((a) => !a.archivedAt); |
159 | 168 | const archivedApplications = applications.filter((a) => !!a.archivedAt); |
160 | 169 | const visibleApplications = showArchived ? archivedApplications : activeApplications; |
161 | 170 |
|
| 171 | + function handleBulkArchive(days: number) { |
| 172 | + const cutoff = new Date(); |
| 173 | + cutoff.setDate(cutoff.getDate() - days); |
| 174 | + const old = activeApplications.filter((a) => { |
| 175 | + const d = a.appliedAt ? new Date(a.appliedAt) : new Date(a.createdAt); |
| 176 | + return d < cutoff; |
| 177 | + }); |
| 178 | + if (old.length === 0) return; |
| 179 | + if (confirm(ta("archive_old_confirm", { count: old.length, days }))) { |
| 180 | + bulkArchiveMutation.mutate(old.map((a) => a.id)); |
| 181 | + } |
| 182 | + } |
| 183 | + |
162 | 184 | const stats = { |
163 | 185 | total: activeApplications.length, |
164 | 186 | inbound: activeApplications.filter((a) => a.status === "inbound").length, |
@@ -433,6 +455,8 @@ export function Dashboard({ user, shareUrl, initialStatus, initialSource, initia |
433 | 455 | )} |
434 | 456 | </button> |
435 | 457 |
|
| 458 | + {!showArchived && <ArchiveOldDropdown applications={activeApplications} onArchive={handleBulkArchive} isPending={bulkArchiveMutation.isPending} />} |
| 459 | + |
436 | 460 | <button |
437 | 461 | onClick={() => exportToCsv(visibleApplications)} |
438 | 462 | title={ta("export_csv")} |
@@ -484,6 +508,90 @@ export function Dashboard({ user, shareUrl, initialStatus, initialSource, initia |
484 | 508 | ); |
485 | 509 | } |
486 | 510 |
|
| 511 | +const ARCHIVE_THRESHOLDS = [30, 60, 90, 180] as const; |
| 512 | + |
| 513 | +function ArchiveOldDropdown({ |
| 514 | + applications, |
| 515 | + onArchive, |
| 516 | + isPending, |
| 517 | +}: { |
| 518 | + applications: Application[]; |
| 519 | + onArchive: (days: number) => void; |
| 520 | + isPending: boolean; |
| 521 | +}) { |
| 522 | + const ta = useTranslations("actions"); |
| 523 | + const [open, setOpen] = useState(false); |
| 524 | + const ref = useRef<HTMLDivElement>(null); |
| 525 | + |
| 526 | + const countForDays = useCallback( |
| 527 | + (days: number) => { |
| 528 | + const cutoff = new Date(); |
| 529 | + cutoff.setDate(cutoff.getDate() - days); |
| 530 | + return applications.filter((a) => { |
| 531 | + const d = a.appliedAt ? new Date(a.appliedAt) : new Date(a.createdAt); |
| 532 | + return d < cutoff; |
| 533 | + }).length; |
| 534 | + }, |
| 535 | + [applications] |
| 536 | + ); |
| 537 | + |
| 538 | + useEffect(() => { |
| 539 | + function handleClickOutside(e: MouseEvent) { |
| 540 | + if (ref.current && !ref.current.contains(e.target as Node)) { |
| 541 | + setOpen(false); |
| 542 | + } |
| 543 | + } |
| 544 | + if (open) { |
| 545 | + document.addEventListener("mousedown", handleClickOutside); |
| 546 | + return () => document.removeEventListener("mousedown", handleClickOutside); |
| 547 | + } |
| 548 | + }, [open]); |
| 549 | + |
| 550 | + return ( |
| 551 | + <div ref={ref} className="relative w-full sm:w-auto"> |
| 552 | + <button |
| 553 | + onClick={() => setOpen((v) => !v)} |
| 554 | + disabled={isPending} |
| 555 | + className={`flex w-full items-center justify-center gap-1.5 rounded-lg border px-3 py-2 text-sm font-medium transition-colors sm:w-auto ${ |
| 556 | + open |
| 557 | + ? "border-amber-300 dark:border-amber-600 bg-amber-50 dark:bg-amber-950/30 text-amber-700 dark:text-amber-300" |
| 558 | + : "border-gray-200 dark:border-gray-600 text-gray-600 dark:text-gray-300 hover:bg-gray-50 dark:hover:bg-gray-700" |
| 559 | + } ${isPending ? "opacity-50 cursor-wait" : ""}`} |
| 560 | + > |
| 561 | + {isPending ? ta("archive_old_archiving") : ta("archive_old")} |
| 562 | + <span className="text-xs">{open ? "▲" : "▼"}</span> |
| 563 | + </button> |
| 564 | + {open && ( |
| 565 | + <div className="absolute right-0 z-20 mt-1 w-56 rounded-lg border border-gray-200 bg-white shadow-lg dark:border-gray-600 dark:bg-gray-800"> |
| 566 | + {ARCHIVE_THRESHOLDS.map((days) => { |
| 567 | + const count = countForDays(days); |
| 568 | + return ( |
| 569 | + <button |
| 570 | + key={days} |
| 571 | + onClick={() => { |
| 572 | + setOpen(false); |
| 573 | + onArchive(days); |
| 574 | + }} |
| 575 | + disabled={count === 0} |
| 576 | + className="flex w-full items-center justify-between px-4 py-2.5 text-sm text-left transition-colors hover:bg-gray-50 dark:hover:bg-gray-700 disabled:opacity-40 disabled:cursor-not-allowed first:rounded-t-lg last:rounded-b-lg" |
| 577 | + > |
| 578 | + <span className="text-gray-700 dark:text-gray-200"> |
| 579 | + {ta("archive_old_option", { days })} |
| 580 | + </span> |
| 581 | + {count > 0 && ( |
| 582 | + <span className="inline-flex h-5 min-w-[1.25rem] items-center justify-center rounded-full bg-amber-100 px-1.5 text-xs font-bold text-amber-700 dark:bg-amber-900/50 dark:text-amber-300"> |
| 583 | + {count} |
| 584 | + </span> |
| 585 | + )} |
| 586 | + </button> |
| 587 | + ); |
| 588 | + })} |
| 589 | + </div> |
| 590 | + )} |
| 591 | + </div> |
| 592 | + ); |
| 593 | +} |
| 594 | + |
487 | 595 | function StatCard({ |
488 | 596 | label, |
489 | 597 | value, |
|
0 commit comments