mirror of
https://github.com/marcogll/TaxHacker_s23.git
synced 2026-01-13 21:35:19 +00:00
feat: bugfixes, spedup, bulk actions,
This commit is contained in:
77
components/transactions/bulk-actions.tsx
Normal file
77
components/transactions/bulk-actions.tsx
Normal file
@@ -0,0 +1,77 @@
|
||||
"use client"
|
||||
|
||||
import { bulkDeleteTransactionsAction } from "@/app/transactions/actions"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from "@/components/ui/dropdown-menu"
|
||||
import { ChevronUp, Trash2 } from "lucide-react"
|
||||
import { useState } from "react"
|
||||
|
||||
const bulkActions = [
|
||||
{
|
||||
id: "delete",
|
||||
label: "Bulk Delete",
|
||||
icon: Trash2,
|
||||
variant: "destructive" as const,
|
||||
action: bulkDeleteTransactionsAction,
|
||||
confirmMessage:
|
||||
"Are you sure you want to delete these transactions and all their files? This action cannot be undone.",
|
||||
},
|
||||
]
|
||||
|
||||
interface BulkActionsMenuProps {
|
||||
selectedIds: string[]
|
||||
onActionComplete?: () => void
|
||||
}
|
||||
|
||||
export function BulkActionsMenu({ selectedIds, onActionComplete }: BulkActionsMenuProps) {
|
||||
const [isLoading, setIsLoading] = useState(false)
|
||||
|
||||
const handleAction = async (actionId: string) => {
|
||||
const action = bulkActions.find((a) => a.id === actionId)
|
||||
if (!action) return
|
||||
|
||||
if (action.confirmMessage) {
|
||||
if (!confirm(action.confirmMessage)) return
|
||||
}
|
||||
|
||||
try {
|
||||
setIsLoading(true)
|
||||
const result = await action.action(selectedIds)
|
||||
if (!result.success) {
|
||||
throw new Error(result.error)
|
||||
}
|
||||
onActionComplete?.()
|
||||
} catch (error) {
|
||||
console.error(`Failed to execute bulk action ${actionId}:`, error)
|
||||
alert(`Failed to execute action: ${error}`)
|
||||
} finally {
|
||||
setIsLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="fixed bottom-4 right-4 z-50">
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button className="min-w-48" disabled={isLoading}>
|
||||
{selectedIds.length} transactions
|
||||
<ChevronUp className="h-4 w-4" />
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end">
|
||||
{bulkActions.map((action) => (
|
||||
<DropdownMenuItem
|
||||
key={action.id}
|
||||
onClick={() => handleAction(action.id)}
|
||||
className="gap-2"
|
||||
disabled={isLoading}
|
||||
>
|
||||
<action.icon className="h-4 w-4" />
|
||||
{action.label}
|
||||
</DropdownMenuItem>
|
||||
))}
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -55,18 +55,13 @@ export default function TransactionEditForm({
|
||||
const handleDelete = async () => {
|
||||
startTransition(async () => {
|
||||
await deleteAction(transaction.id)
|
||||
router.back()
|
||||
})
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (deleteState?.success) {
|
||||
router.push("/transactions")
|
||||
}
|
||||
}, [deleteState, router])
|
||||
|
||||
useEffect(() => {
|
||||
if (saveState?.success) {
|
||||
router.push("/transactions")
|
||||
router.back()
|
||||
}
|
||||
}, [saveState, router])
|
||||
|
||||
@@ -152,7 +147,7 @@ export default function TransactionEditForm({
|
||||
))}
|
||||
|
||||
<div className="flex justify-end space-x-4 pt-6">
|
||||
<Button type="button" onClick={handleDelete} variant="outline" disabled={isDeleting}>
|
||||
<Button type="button" onClick={handleDelete} variant="destructive" disabled={isDeleting}>
|
||||
{isDeleting ? "⏳ Deleting..." : "Delete Transaction"}
|
||||
</Button>
|
||||
|
||||
|
||||
@@ -4,22 +4,11 @@ import { DateRangePicker } from "@/components/forms/date-range-picker"
|
||||
import { Input } from "@/components/ui/input"
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"
|
||||
import { TransactionFilters } from "@/data/transactions"
|
||||
import { useTransactionFilters } from "@/hooks/use-transaction-filters"
|
||||
import { Category, Project } from "@prisma/client"
|
||||
import { format } from "date-fns"
|
||||
import { useRouter, useSearchParams } from "next/navigation"
|
||||
import { useEffect, useState } from "react"
|
||||
|
||||
export function TransactionSearchAndFilters({ categories, projects }: { categories: Category[]; projects: Project[] }) {
|
||||
const searchParams = useSearchParams()
|
||||
const router = useRouter()
|
||||
|
||||
const [filters, setFilters] = useState<TransactionFilters>({
|
||||
search: searchParams.get("search") || "",
|
||||
dateFrom: searchParams.get("dateFrom") || "",
|
||||
dateTo: searchParams.get("dateTo") || "",
|
||||
categoryCode: searchParams.get("categoryCode") || "",
|
||||
projectCode: searchParams.get("projectCode") || "",
|
||||
})
|
||||
const [filters, setFilters] = useTransactionFilters()
|
||||
|
||||
const handleFilterChange = (name: keyof TransactionFilters, value: any) => {
|
||||
setFilters((prev) => ({
|
||||
@@ -28,45 +17,6 @@ export function TransactionSearchAndFilters({ categories, projects }: { categori
|
||||
}))
|
||||
}
|
||||
|
||||
const applyFilters = () => {
|
||||
const params = new URLSearchParams(searchParams.toString())
|
||||
if (filters.search) {
|
||||
params.set("search", filters.search)
|
||||
} else {
|
||||
params.delete("search")
|
||||
}
|
||||
|
||||
if (filters.dateFrom) {
|
||||
params.set("dateFrom", format(new Date(filters.dateFrom), "yyyy-MM-dd"))
|
||||
} else {
|
||||
params.delete("dateFrom")
|
||||
}
|
||||
|
||||
if (filters.dateTo) {
|
||||
params.set("dateTo", format(new Date(filters.dateTo), "yyyy-MM-dd"))
|
||||
} else {
|
||||
params.delete("dateTo")
|
||||
}
|
||||
|
||||
if (filters.categoryCode && filters.categoryCode !== "-") {
|
||||
params.set("categoryCode", filters.categoryCode)
|
||||
} else {
|
||||
params.delete("categoryCode")
|
||||
}
|
||||
|
||||
if (filters.projectCode && filters.projectCode !== "-") {
|
||||
params.set("projectCode", filters.projectCode)
|
||||
} else {
|
||||
params.delete("projectCode")
|
||||
}
|
||||
|
||||
router.push(`/transactions?${params.toString()}`)
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
applyFilters()
|
||||
}, [filters])
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-4">
|
||||
<div className="flex flex-wrap gap-4">
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
"use client"
|
||||
|
||||
import { BulkActionsMenu } from "@/components/transactions/bulk-actions"
|
||||
import { Badge } from "@/components/ui/badge"
|
||||
import { Checkbox } from "@/components/ui/checkbox"
|
||||
import { Table, TableBody, TableCell, TableFooter, TableHead, TableHeader, TableRow } from "@/components/ui/table"
|
||||
@@ -236,6 +237,9 @@ export function TransactionList({ transactions }: { transactions: Transaction[]
|
||||
</TableRow>
|
||||
</TableFooter>
|
||||
</Table>
|
||||
{selectedIds.length > 0 && (
|
||||
<BulkActionsMenu selectedIds={selectedIds} onActionComplete={() => setSelectedIds([])} />
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
"use client"
|
||||
|
||||
import { deleteTransactionFileAction, uploadTransactionFileAction } from "@/app/transactions/actions"
|
||||
import { deleteTransactionFileAction, uploadTransactionFilesAction } from "@/app/transactions/actions"
|
||||
import { FilePreview } from "@/components/files/preview"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { Card } from "@/components/ui/card"
|
||||
import { FILE_ACCEPTED_MIMETYPES } from "@/lib/files"
|
||||
import { File, Transaction } from "@prisma/client"
|
||||
import { Loader2, Upload } from "lucide-react"
|
||||
import { Loader2, Upload, X } from "lucide-react"
|
||||
import { useState } from "react"
|
||||
|
||||
export default function TransactionFiles({ transaction, files }: { transaction: Transaction; files: File[] }) {
|
||||
@@ -21,8 +21,10 @@ export default function TransactionFiles({ transaction, files }: { transaction:
|
||||
if (e.target.files && e.target.files.length > 0) {
|
||||
const formData = new FormData()
|
||||
formData.append("transactionId", transaction.id)
|
||||
formData.append("file", e.target.files[0])
|
||||
await uploadTransactionFileAction(formData)
|
||||
for (let i = 0; i < e.target.files.length; i++) {
|
||||
formData.append("files", e.target.files[i])
|
||||
}
|
||||
await uploadTransactionFilesAction(formData)
|
||||
setIsUploading(false)
|
||||
}
|
||||
}
|
||||
@@ -30,19 +32,24 @@ export default function TransactionFiles({ transaction, files }: { transaction:
|
||||
return (
|
||||
<>
|
||||
{files.map((file) => (
|
||||
<Card key={file.id} className="p-4">
|
||||
<FilePreview file={file} />
|
||||
|
||||
<Button type="button" onClick={() => handleDeleteFile(file.id)} variant="destructive" className="w-full">
|
||||
Delete File
|
||||
<Card key={file.id} className="p-4 relative">
|
||||
<Button
|
||||
type="button"
|
||||
onClick={() => handleDeleteFile(file.id)}
|
||||
variant="destructive"
|
||||
size="icon"
|
||||
className="absolute -right-2 -top-2 rounded-full w-6 h-6 z-10"
|
||||
>
|
||||
<X className="h-4 w-4" />
|
||||
</Button>
|
||||
<FilePreview file={file} />
|
||||
</Card>
|
||||
))}
|
||||
|
||||
<Card className="relative h-32 p-4">
|
||||
<Card className="relative min-h-32 p-4">
|
||||
<input type="hidden" name="transactionId" value={transaction.id} />
|
||||
<label
|
||||
className="h-full w-full flex flex-col items-center justify-center p-4 border-2 border-dashed border-gray-300 rounded-lg cursor-pointer hover:border-primary transition-colors"
|
||||
className="h-full w-full flex flex-col gap-2 items-center justify-center p-4 border-2 border-dashed border-gray-300 rounded-lg cursor-pointer hover:border-primary transition-colors"
|
||||
onDragEnter={(e) => {
|
||||
e.currentTarget.classList.add("border-primary")
|
||||
}}
|
||||
@@ -56,9 +63,11 @@ export default function TransactionFiles({ transaction, files }: { transaction:
|
||||
<>
|
||||
<Upload className="w-8 h-8 text-gray-400" />
|
||||
<p className="text-sm text-gray-500">Add more files to this invoice</p>
|
||||
<p className="text-xs text-gray-500">(or just drop them on this page)</p>
|
||||
</>
|
||||
)}
|
||||
<input
|
||||
multiple
|
||||
type="file"
|
||||
name="file"
|
||||
className="absolute inset-0 top-0 left-0 w-full h-full opacity-0"
|
||||
|
||||
Reference in New Issue
Block a user