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
141 changes: 120 additions & 21 deletions src/components/BulkActionBar.tsx
Original file line number Diff line number Diff line change
@@ -1,19 +1,26 @@
"use client";

import { useState } from "react";
import { splitClient } from "@/lib/stellar";
import type { Invoice } from "@stellar-split/sdk";
import {
archiveInvoices,
unarchiveInvoices,
isInvoiceArchived,
getUndoTimeout,
} from "@/lib/archiveInvoices";
import {
exportInvoicesBulk,
printInvoicesBulk,
type BulkExportFormat,
} from "@/lib/bulkInvoiceOps";

interface Props {
selectedCount: number;
selectedInvoices: Invoice[];
onCancel: () => void;
onBulkCancel: () => Promise<void>;
onBulkExport: () => void;
/** @deprecated Use the built-in export; kept for backward-compat. */
onBulkExport?: () => void;
onSelectAll: () => void;
onDeselectAll: () => void;
totalCount: number;
Expand All @@ -23,7 +30,10 @@ interface Props {

/**
* BulkActionBar — toolbar for bulk operations on selected invoices.
* Supports: cancel, export, archive, unarchive operations.
*
* Supports: cancel, export (CSV / JSON), print, archive, unarchive.
*
* Issue #810: extended with print and multi-format export.
*/
export default function BulkActionBar({
selectedCount,
Expand All @@ -43,6 +53,7 @@ export default function BulkActionBar({
isArchive: boolean;
} | null>(null);
const [showUndoToast, setShowUndoToast] = useState(false);
const [showExportMenu, setShowExportMenu] = useState(false);

const pendingCount = selectedInvoices.filter(
(inv) => inv.status === "Pending",
Expand All @@ -52,6 +63,8 @@ export default function BulkActionBar({
isInvoiceArchived(inv.id),
).length;

// ── Archive ────────────────────────────────────────────────────────────────

const handleArchive = async () => {
const idsToArchive = selectedInvoices
.filter((inv) => !isInvoiceArchived(inv.id))
Expand All @@ -64,11 +77,7 @@ export default function BulkActionBar({
archiveInvoices(idsToArchive);
setUndoAction({ ids: idsToArchive, isArchive: true });
setShowUndoToast(true);

setTimeout(() => {
setShowUndoToast(false);
}, getUndoTimeout());

setTimeout(() => setShowUndoToast(false), getUndoTimeout());
onArchiveChange?.();
} finally {
setArchiveLoading(false);
Expand All @@ -87,11 +96,7 @@ export default function BulkActionBar({
unarchiveInvoices(idsToUnarchive);
setUndoAction({ ids: idsToUnarchive, isArchive: false });
setShowUndoToast(true);

setTimeout(() => {
setShowUndoToast(false);
}, getUndoTimeout());

setTimeout(() => setShowUndoToast(false), getUndoTimeout());
onArchiveChange?.();
} finally {
setArchiveLoading(false);
Expand All @@ -100,20 +105,43 @@ export default function BulkActionBar({

const handleUndo = () => {
if (!undoAction) return;

if (undoAction.isArchive) {
unarchiveInvoices(undoAction.ids);
} else {
archiveInvoices(undoAction.ids);
}

setShowUndoToast(false);
onArchiveChange?.();
};

// ── Export ─────────────────────────────────────────────────────────────────

const handleExport = (format: BulkExportFormat) => {
setShowExportMenu(false);
if (selectedInvoices.length === 0) return;
exportInvoicesBulk(selectedInvoices, format);
};

// Fall back to legacy prop if provided and no built-in export needed
const handleLegacyExport = () => {
if (onBulkExport) {
onBulkExport();
} else {
handleExport("csv");
}
};

// ── Print ──────────────────────────────────────────────────────────────────

const handlePrint = () => {
if (selectedInvoices.length === 0) return;
printInvoicesBulk(selectedInvoices, { showRecipients: true });
};

return (
<>
<div className="sticky bottom-0 left-0 right-0 bg-gray-900 border-t border-gray-800 p-4 flex flex-wrap items-center justify-between gap-3 z-30">
{/* Left: selection info */}
<div className="flex items-center gap-2 flex-wrap">
<span className="text-sm font-semibold text-gray-300">
{selectedCount} selected
Expand All @@ -132,7 +160,9 @@ export default function BulkActionBar({
</button>
</div>

{/* Right: actions */}
<div className="flex items-center gap-2 flex-wrap">
{/* Cancel pending invoices */}
{pendingCount > 0 && (
<button
type="button"
Expand All @@ -143,35 +173,102 @@ export default function BulkActionBar({
</button>
)}

{/* Archive */}
{!archivedOnly && archivedCount < selectedCount && (
<button
type="button"
onClick={handleArchive}
disabled={archiveLoading}
className="text-xs px-3 py-1.5 rounded-lg bg-amber-700 hover:bg-amber-600 disabled:bg-gray-600 disabled:cursor-not-allowed text-white font-semibold transition-colors"
>
{archiveLoading ? "Archiving..." : `Archive (${selectedCount - archivedCount})`}
{archiveLoading
? "Archiving…"
: `Archive (${selectedCount - archivedCount})`}
</button>
)}

{/* Unarchive */}
{archivedCount > 0 && (
<button
type="button"
onClick={handleUnarchive}
disabled={archiveLoading}
className="text-xs px-3 py-1.5 rounded-lg bg-blue-700 hover:bg-blue-600 disabled:bg-gray-600 disabled:cursor-not-allowed text-white font-semibold transition-colors"
>
{archiveLoading ? "Unarchiving..." : `Unarchive (${archivedCount})`}
{archiveLoading
? "Unarchiving…"
: `Unarchive (${archivedCount})`}
</button>
)}

{/* Print */}
<button
type="button"
onClick={onBulkExport}
onClick={handlePrint}
className="text-xs px-3 py-1.5 rounded-lg bg-gray-700 hover:bg-gray-600 text-gray-200 font-semibold transition-colors"
title="Print selected invoices"
>
Export CSV
🖨 Print
</button>

{/* Export with format dropdown */}
<div className="relative">
<button
type="button"
onClick={() => setShowExportMenu((v) => !v)}
className="text-xs px-3 py-1.5 rounded-lg bg-gray-700 hover:bg-gray-600 text-gray-200 font-semibold transition-colors flex items-center gap-1"
aria-haspopup="true"
aria-expanded={showExportMenu}
>
Export
<svg
className="w-3 h-3 ml-0.5"
viewBox="0 0 20 20"
fill="currentColor"
aria-hidden="true"
>
<path
fillRule="evenodd"
d="M5.293 7.293a1 1 0 011.414 0L10 10.586l3.293-3.293a1 1 0 111.414 1.414l-4 4a1 1 0 01-1.414 0l-4-4a1 1 0 010-1.414z"
clipRule="evenodd"
/>
</svg>
</button>

{showExportMenu && (
<>
{/* Backdrop to close menu */}
<div
className="fixed inset-0 z-30"
aria-hidden="true"
onClick={() => setShowExportMenu(false)}
/>
<div
className="absolute right-0 bottom-full mb-1 w-36 bg-gray-800 border border-gray-700 rounded-lg shadow-xl z-40 overflow-hidden"
role="menu"
>
<button
type="button"
role="menuitem"
onClick={() => handleExport("csv")}
className="w-full text-left text-xs px-3 py-2 text-gray-200 hover:bg-gray-700 transition-colors"
>
Export as CSV
</button>
<button
type="button"
role="menuitem"
onClick={() => handleExport("json")}
className="w-full text-left text-xs px-3 py-2 text-gray-200 hover:bg-gray-700 transition-colors"
>
Export as JSON
</button>
</div>
</>
)}
</div>

{/* Done */}
<button
type="button"
onClick={onCancel}
Expand All @@ -184,9 +281,11 @@ export default function BulkActionBar({

{/* Undo toast */}
{showUndoToast && undoAction && (
<div className="fixed bottom-4 right-4 bg-gray-800 border border-gray-700 rounded-lg p-3 shadow-lg z-40 flex items-center gap-3">
<div className="fixed bottom-20 right-4 bg-gray-800 border border-gray-700 rounded-lg p-3 shadow-lg z-40 flex items-center gap-3">
<span className="text-sm text-gray-200">
{undoAction.isArchive ? "Invoices archived" : "Invoices unarchived"}
{undoAction.isArchive
? "Invoices archived"
: "Invoices unarchived"}
</span>
<button
type="button"
Expand Down
Loading