mirror of
https://github.com/marcogll/TaxHacker_s23.git
synced 2026-01-13 13:25:18 +00:00
feat: pagination + hide fields in settings
This commit is contained in:
@@ -43,7 +43,12 @@ export function ExportTransactionsDialog({
|
||||
const handleSubmit = () => {
|
||||
router.push(
|
||||
`/export/transactions?${new URLSearchParams({
|
||||
...exportFilters,
|
||||
search: exportFilters?.search || "",
|
||||
dateFrom: exportFilters?.dateFrom || "",
|
||||
dateTo: exportFilters?.dateTo || "",
|
||||
ordering: exportFilters?.ordering || "",
|
||||
categoryCode: exportFilters?.categoryCode || "",
|
||||
projectCode: exportFilters?.projectCode || "",
|
||||
fields: exportFields.join(","),
|
||||
includeAttachments: includeAttachments.toString(),
|
||||
}).toString()}`
|
||||
|
||||
@@ -41,12 +41,12 @@ export function FilePreview({ file }: { file: File }) {
|
||||
<p className="text-sm overflow-ellipsis">
|
||||
<strong>Type:</strong> {file.mimetype}
|
||||
</p>
|
||||
{/* <p className="text-sm overflow-ellipsis">
|
||||
<strong>Uploaded:</strong> {format(file.createdAt, "MMM d, yyyy")}
|
||||
</p> */}
|
||||
<p className="text-sm">
|
||||
<strong>Size:</strong> {fileSize < 1 ? (fileSize * 1024).toFixed(2) + " KB" : fileSize.toFixed(2) + " MB"}
|
||||
</p>
|
||||
<p className="text-xs overflow-ellipsis">
|
||||
<strong>Path:</strong> {file.path}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
|
||||
@@ -10,11 +10,27 @@ export const FormSelectCategory = ({
|
||||
categories,
|
||||
emptyValue,
|
||||
placeholder,
|
||||
hideIfEmpty = false,
|
||||
...props
|
||||
}: { title: string; categories: Category[]; emptyValue?: string; placeholder?: string } & SelectProps) => {
|
||||
}: {
|
||||
title: string
|
||||
categories: Category[]
|
||||
emptyValue?: string
|
||||
placeholder?: string
|
||||
hideIfEmpty?: boolean
|
||||
} & SelectProps) => {
|
||||
const items = useMemo(
|
||||
() => categories.map((category) => ({ code: category.code, name: category.name, color: category.color })),
|
||||
[categories]
|
||||
)
|
||||
return <FormSelect title={title} items={items} emptyValue={emptyValue} placeholder={placeholder} {...props} />
|
||||
return (
|
||||
<FormSelect
|
||||
title={title}
|
||||
items={items}
|
||||
emptyValue={emptyValue}
|
||||
placeholder={placeholder}
|
||||
hideIfEmpty={hideIfEmpty}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -8,11 +8,27 @@ export const FormSelectCurrency = ({
|
||||
currencies,
|
||||
emptyValue,
|
||||
placeholder,
|
||||
hideIfEmpty = false,
|
||||
...props
|
||||
}: { title: string; currencies: Currency[]; emptyValue?: string; placeholder?: string } & SelectProps) => {
|
||||
}: {
|
||||
title: string
|
||||
currencies: Currency[]
|
||||
emptyValue?: string
|
||||
placeholder?: string
|
||||
hideIfEmpty?: boolean
|
||||
} & SelectProps) => {
|
||||
const items = useMemo(
|
||||
() => currencies.map((currency) => ({ code: currency.code, name: `${currency.code} - ${currency.name}` })),
|
||||
[currencies]
|
||||
)
|
||||
return <FormSelect title={title} items={items} emptyValue={emptyValue} placeholder={placeholder} {...props} />
|
||||
return (
|
||||
<FormSelect
|
||||
title={title}
|
||||
items={items}
|
||||
emptyValue={emptyValue}
|
||||
placeholder={placeholder}
|
||||
hideIfEmpty={hideIfEmpty}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -7,14 +7,22 @@ export const FormSelectProject = ({
|
||||
projects,
|
||||
emptyValue,
|
||||
placeholder,
|
||||
hideIfEmpty = false,
|
||||
...props
|
||||
}: { title: string; projects: Project[]; emptyValue?: string; placeholder?: string } & SelectProps) => {
|
||||
}: {
|
||||
title: string
|
||||
projects: Project[]
|
||||
emptyValue?: string
|
||||
placeholder?: string
|
||||
hideIfEmpty?: boolean
|
||||
} & SelectProps) => {
|
||||
return (
|
||||
<FormSelect
|
||||
title={title}
|
||||
items={projects.map((project) => ({ code: project.code, name: project.name, color: project.color }))}
|
||||
emptyValue={emptyValue}
|
||||
placeholder={placeholder}
|
||||
hideIfEmpty={hideIfEmpty}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
|
||||
@@ -5,8 +5,9 @@ export const FormSelectType = ({
|
||||
title,
|
||||
emptyValue,
|
||||
placeholder,
|
||||
hideIfEmpty = false,
|
||||
...props
|
||||
}: { title: string; emptyValue?: string; placeholder?: string } & SelectProps) => {
|
||||
}: { title: string; emptyValue?: string; placeholder?: string; hideIfEmpty?: boolean } & SelectProps) => {
|
||||
const items = [
|
||||
{ code: "expense", name: "Expense" },
|
||||
{ code: "income", name: "Income" },
|
||||
@@ -14,5 +15,14 @@ export const FormSelectType = ({
|
||||
{ code: "other", name: "Other" },
|
||||
]
|
||||
|
||||
return <FormSelect title={title} items={items} emptyValue={emptyValue} placeholder={placeholder} {...props} />
|
||||
return (
|
||||
<FormSelect
|
||||
title={title}
|
||||
items={items}
|
||||
emptyValue={emptyValue}
|
||||
placeholder={placeholder}
|
||||
hideIfEmpty={hideIfEmpty}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -53,13 +53,19 @@ export const FormSelect = ({
|
||||
items,
|
||||
emptyValue,
|
||||
placeholder,
|
||||
hideIfEmpty = false,
|
||||
...props
|
||||
}: {
|
||||
title: string
|
||||
items: Array<{ code: string; name: string; color?: string }>
|
||||
emptyValue?: string
|
||||
placeholder?: string
|
||||
hideIfEmpty?: boolean
|
||||
} & SelectProps) => {
|
||||
if (hideIfEmpty && (!props.defaultValue || props.defaultValue.toString().trim() === "") && !props.value) {
|
||||
return null
|
||||
}
|
||||
|
||||
return (
|
||||
<span className="flex flex-col gap-1">
|
||||
<span className="text-sm font-medium">{title}</span>
|
||||
|
||||
@@ -3,35 +3,146 @@
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { Input } from "@/components/ui/input"
|
||||
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table"
|
||||
import { CircleCheck, Edit, Trash2 } from "lucide-react"
|
||||
import { Check, Edit, Trash2 } from "lucide-react"
|
||||
import { useOptimistic, useState } from "react"
|
||||
|
||||
interface CrudColumn<T> {
|
||||
key: keyof T
|
||||
label: string
|
||||
type?: "text" | "number" | "checkbox" | "select"
|
||||
options?: string[]
|
||||
defaultValue?: string | boolean
|
||||
editable?: boolean
|
||||
}
|
||||
|
||||
interface CrudProps<T> {
|
||||
items: T[]
|
||||
columns: {
|
||||
key: keyof T
|
||||
label: string
|
||||
type?: "text" | "number" | "checkbox"
|
||||
defaultValue?: string
|
||||
editable?: boolean
|
||||
}[]
|
||||
onDelete: (id: string) => Promise<void>
|
||||
onAdd: (data: Partial<T>) => Promise<void>
|
||||
onEdit?: (id: string, data: Partial<T>) => Promise<void>
|
||||
columns: CrudColumn<T>[]
|
||||
onDelete: (id: string) => Promise<{ success: boolean; error?: string }>
|
||||
onAdd: (data: Partial<T>) => Promise<{ success: boolean; error?: string }>
|
||||
onEdit?: (id: string, data: Partial<T>) => Promise<{ success: boolean; error?: string }>
|
||||
}
|
||||
|
||||
export function CrudTable<T extends { [key: string]: any }>({ items, columns, onDelete, onAdd, onEdit }: CrudProps<T>) {
|
||||
const [isAdding, setIsAdding] = useState(false)
|
||||
const [editingId, setEditingId] = useState<string | null>(null)
|
||||
const [newItem, setNewItem] = useState<Partial<T>>({})
|
||||
const [editingItem, setEditingItem] = useState<Partial<T>>({})
|
||||
const [newItem, setNewItem] = useState<Partial<T>>(itemDefaults(columns))
|
||||
const [editingItem, setEditingItem] = useState<Partial<T>>(itemDefaults(columns))
|
||||
const [optimisticItems, addOptimisticItem] = useOptimistic(items, (state, newItem: T) => [...state, newItem])
|
||||
|
||||
const FormCell = (item: T, column: CrudColumn<T>) => {
|
||||
if (column.type === "checkbox") {
|
||||
return item[column.key] ? <Check /> : ""
|
||||
}
|
||||
return item[column.key]
|
||||
}
|
||||
|
||||
const EditFormCell = (item: T, column: CrudColumn<T>) => {
|
||||
if (column.type === "checkbox") {
|
||||
return (
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={editingItem[column.key]}
|
||||
onChange={(e) =>
|
||||
setEditingItem({
|
||||
...editingItem,
|
||||
[column.key]: e.target.checked,
|
||||
})
|
||||
}
|
||||
/>
|
||||
)
|
||||
} else if (column.type === "select") {
|
||||
return (
|
||||
<select
|
||||
value={editingItem[column.key]}
|
||||
className="p-2 rounded-md border bg-transparent"
|
||||
onChange={(e) =>
|
||||
setEditingItem({
|
||||
...editingItem,
|
||||
[column.key]: e.target.value,
|
||||
})
|
||||
}
|
||||
>
|
||||
{column.options?.map((option) => (
|
||||
<option key={option} value={option}>
|
||||
{option}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<Input
|
||||
type="text"
|
||||
value={editingItem[column.key] || ""}
|
||||
onChange={(e) =>
|
||||
setEditingItem({
|
||||
...editingItem,
|
||||
[column.key]: e.target.value,
|
||||
})
|
||||
}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
const AddFormCell = (column: CrudColumn<T>) => {
|
||||
if (column.type === "checkbox") {
|
||||
return (
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={Boolean(newItem[column.key] || column.defaultValue)}
|
||||
onChange={(e) =>
|
||||
setNewItem({
|
||||
...newItem,
|
||||
[column.key]: e.target.checked,
|
||||
})
|
||||
}
|
||||
/>
|
||||
)
|
||||
} else if (column.type === "select") {
|
||||
return (
|
||||
<select
|
||||
value={String(newItem[column.key] || column.defaultValue || "")}
|
||||
className="p-2 rounded-md border bg-transparent"
|
||||
onChange={(e) =>
|
||||
setNewItem({
|
||||
...newItem,
|
||||
[column.key]: e.target.value,
|
||||
})
|
||||
}
|
||||
>
|
||||
{column.options?.map((option) => (
|
||||
<option key={option} value={option}>
|
||||
{option}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
)
|
||||
}
|
||||
return (
|
||||
<Input
|
||||
type={column.type || "text"}
|
||||
value={String(newItem[column.key] || column.defaultValue || "")}
|
||||
onChange={(e) =>
|
||||
setNewItem({
|
||||
...newItem,
|
||||
[column.key]: e.target.value,
|
||||
})
|
||||
}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
const handleAdd = async () => {
|
||||
try {
|
||||
await onAdd(newItem)
|
||||
setIsAdding(false)
|
||||
setNewItem({})
|
||||
const result = await onAdd(newItem)
|
||||
if (result.success) {
|
||||
setIsAdding(false)
|
||||
setNewItem(itemDefaults(columns))
|
||||
} else {
|
||||
alert(result.error)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Failed to add item:", error)
|
||||
}
|
||||
@@ -40,9 +151,13 @@ export function CrudTable<T extends { [key: string]: any }>({ items, columns, on
|
||||
const handleEdit = async (id: string) => {
|
||||
if (!onEdit) return
|
||||
try {
|
||||
await onEdit(id, editingItem)
|
||||
setEditingId(null)
|
||||
setEditingItem({})
|
||||
const result = await onEdit(id, editingItem)
|
||||
if (result.success) {
|
||||
setEditingId(null)
|
||||
setEditingItem({})
|
||||
} else {
|
||||
alert(result.error)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Failed to edit item:", error)
|
||||
}
|
||||
@@ -55,7 +170,10 @@ export function CrudTable<T extends { [key: string]: any }>({ items, columns, on
|
||||
|
||||
const handleDelete = async (id: string) => {
|
||||
try {
|
||||
await onDelete(id)
|
||||
const result = await onDelete(id)
|
||||
if (!result.success) {
|
||||
alert(result.error)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Failed to delete item:", error)
|
||||
}
|
||||
@@ -77,26 +195,9 @@ export function CrudTable<T extends { [key: string]: any }>({ items, columns, on
|
||||
<TableRow key={index}>
|
||||
{columns.map((column) => (
|
||||
<TableCell key={String(column.key)} className="first:font-semibold">
|
||||
{editingId === (item.code || item.id) && column.editable ? (
|
||||
<Input
|
||||
type={column.type || "text"}
|
||||
value={editingItem[column.key] || ""}
|
||||
onChange={(e) =>
|
||||
setEditingItem({
|
||||
...editingItem,
|
||||
[column.key]: column.type === "checkbox" ? e.target.checked : e.target.value,
|
||||
})
|
||||
}
|
||||
/>
|
||||
) : column.type === "checkbox" ? (
|
||||
item[column.key] ? (
|
||||
<CircleCheck />
|
||||
) : (
|
||||
""
|
||||
)
|
||||
) : (
|
||||
item[column.key]
|
||||
)}
|
||||
{editingId === (item.code || item.id) && column.editable
|
||||
? EditFormCell(item, column)
|
||||
: FormCell(item, column)}
|
||||
</TableCell>
|
||||
))}
|
||||
<TableCell>
|
||||
@@ -113,7 +214,14 @@ export function CrudTable<T extends { [key: string]: any }>({ items, columns, on
|
||||
) : (
|
||||
<>
|
||||
{onEdit && (
|
||||
<Button variant="ghost" size="icon" onClick={() => startEditing(item)}>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
onClick={() => {
|
||||
startEditing(item)
|
||||
setIsAdding(false)
|
||||
}}
|
||||
>
|
||||
<Edit />
|
||||
</Button>
|
||||
)}
|
||||
@@ -132,18 +240,7 @@ export function CrudTable<T extends { [key: string]: any }>({ items, columns, on
|
||||
<TableRow>
|
||||
{columns.map((column) => (
|
||||
<TableCell key={String(column.key)} className="first:font-semibold">
|
||||
{column.editable && (
|
||||
<Input
|
||||
type={column.type || "text"}
|
||||
value={newItem[column.key] || column.defaultValue || ""}
|
||||
onChange={(e) =>
|
||||
setNewItem({
|
||||
...newItem,
|
||||
[column.key]: column.type === "checkbox" ? e.target.checked : e.target.value,
|
||||
})
|
||||
}
|
||||
/>
|
||||
)}
|
||||
{column.editable && AddFormCell(column)}
|
||||
</TableCell>
|
||||
))}
|
||||
<TableCell>
|
||||
@@ -160,7 +257,23 @@ export function CrudTable<T extends { [key: string]: any }>({ items, columns, on
|
||||
)}
|
||||
</TableBody>
|
||||
</Table>
|
||||
{!isAdding && <Button onClick={() => setIsAdding(true)}>Add New</Button>}
|
||||
{!isAdding && (
|
||||
<Button
|
||||
onClick={() => {
|
||||
setIsAdding(true)
|
||||
setEditingId(null)
|
||||
}}
|
||||
>
|
||||
Add New
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function itemDefaults<T>(columns: CrudColumn<T>[]) {
|
||||
return columns.reduce((acc, column) => {
|
||||
acc[column.key] = column.defaultValue as T[keyof T]
|
||||
return acc
|
||||
}, {} as Partial<T>)
|
||||
}
|
||||
|
||||
@@ -27,7 +27,7 @@ export default function LLMSettingsForm({ settings, fields }: { settings: Record
|
||||
</small>
|
||||
|
||||
<FormTextarea
|
||||
title="Prompt for Analyze Transaction"
|
||||
title="Prompt for File Analysis Form"
|
||||
name="prompt_analyse_new_file"
|
||||
defaultValue={settings.prompt_analyse_new_file}
|
||||
className="h-96"
|
||||
|
||||
114
components/transactions/pagination.tsx
Normal file
114
components/transactions/pagination.tsx
Normal file
@@ -0,0 +1,114 @@
|
||||
"use client"
|
||||
|
||||
import {
|
||||
PaginationContent,
|
||||
PaginationEllipsis,
|
||||
PaginationItem,
|
||||
PaginationLink,
|
||||
Pagination as PaginationRoot,
|
||||
} from "@/components/ui/pagination"
|
||||
import { useRouter, useSearchParams } from "next/navigation"
|
||||
|
||||
const MAX_VISIBLE_PAGES = 5
|
||||
|
||||
export function Pagination({ totalItems, itemsPerPage = 1000 }: { totalItems: number; itemsPerPage: number }) {
|
||||
const router = useRouter()
|
||||
const searchParams = useSearchParams()
|
||||
|
||||
const totalPages = Math.ceil(totalItems / itemsPerPage)
|
||||
const currentPage = parseInt(searchParams.get("page") || "1")
|
||||
|
||||
const onPageChange = (page: number) => {
|
||||
const params = new URLSearchParams(searchParams.toString())
|
||||
params.set("page", page.toString())
|
||||
router.push(`?${params.toString()}`)
|
||||
}
|
||||
|
||||
const getPageNumbers = () => {
|
||||
const pageNumbers = []
|
||||
|
||||
// Show all page numbers if total pages is small
|
||||
if (totalPages <= MAX_VISIBLE_PAGES) {
|
||||
for (let i = 1; i <= totalPages; i++) {
|
||||
pageNumbers.push(i)
|
||||
}
|
||||
} else {
|
||||
// Always include the first page
|
||||
pageNumbers.push(1)
|
||||
|
||||
// Calculate the range around the current page
|
||||
let startPage = Math.max(2, currentPage - 1)
|
||||
let endPage = Math.min(totalPages - 1, currentPage + 1)
|
||||
|
||||
// Adjust if we're near the start
|
||||
if (currentPage <= 3) {
|
||||
endPage = Math.min(totalPages - 1, 4)
|
||||
}
|
||||
|
||||
// Adjust if we're near the end
|
||||
if (currentPage >= totalPages - 2) {
|
||||
startPage = Math.max(2, totalPages - 3)
|
||||
}
|
||||
|
||||
// Add ellipsis after first page if needed
|
||||
if (startPage > 2) {
|
||||
pageNumbers.push("ellipsis-start")
|
||||
}
|
||||
|
||||
// Add middle page numbers
|
||||
for (let i = startPage; i <= endPage; i++) {
|
||||
pageNumbers.push(i)
|
||||
}
|
||||
|
||||
// Add ellipsis before last page if needed
|
||||
if (endPage < totalPages - 1) {
|
||||
pageNumbers.push("ellipsis-end")
|
||||
}
|
||||
|
||||
// Always include the last page
|
||||
pageNumbers.push(totalPages)
|
||||
}
|
||||
|
||||
return pageNumbers
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex justify-center w-full mt-4">
|
||||
<PaginationRoot>
|
||||
<PaginationContent>
|
||||
{/* <PaginationItem>
|
||||
<PaginationPrevious
|
||||
onClick={() => currentPage > 1 && onPageChange(currentPage - 1)}
|
||||
className={currentPage <= 1 ? "pointer-events-none opacity-50" : "cursor-pointer"}
|
||||
/>
|
||||
</PaginationItem> */}
|
||||
|
||||
{getPageNumbers().map((pageNumber, index) =>
|
||||
pageNumber === "ellipsis-start" || pageNumber === "ellipsis-end" ? (
|
||||
<PaginationItem key={`ellipsis-${index}`}>
|
||||
<PaginationEllipsis />
|
||||
</PaginationItem>
|
||||
) : (
|
||||
<PaginationItem key={pageNumber}>
|
||||
<PaginationLink
|
||||
isActive={currentPage === pageNumber}
|
||||
onClick={() => onPageChange(pageNumber as number)}
|
||||
className="cursor-pointer"
|
||||
>
|
||||
{pageNumber}
|
||||
</PaginationLink>
|
||||
</PaginationItem>
|
||||
)
|
||||
)}
|
||||
|
||||
{/* <PaginationItem>
|
||||
<PaginationNext
|
||||
onClick={() => currentPage < totalPages && onPageChange(currentPage + 1)}
|
||||
className={currentPage >= totalPages ? "pointer-events-none opacity-50" : "cursor-pointer"}
|
||||
/>
|
||||
</PaginationItem> */}
|
||||
</PaginationContent>
|
||||
</PaginationRoot>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -1,8 +1,8 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import * as CheckboxPrimitive from "@radix-ui/react-checkbox"
|
||||
import { Check } from "lucide-react"
|
||||
import * as React from "react"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
@@ -13,14 +13,12 @@ const Checkbox = React.forwardRef<
|
||||
<CheckboxPrimitive.Root
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"peer h-4 w-4 shrink-0 rounded-sm border border-primary shadow focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground",
|
||||
"flex peer size-4 justify-center items-center shrink-0 rounded-sm border border-primary ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<CheckboxPrimitive.Indicator
|
||||
className={cn("flex items-center justify-center text-current")}
|
||||
>
|
||||
<CheckboxPrimitive.Indicator className={cn("flex items-center justify-center text-current")}>
|
||||
<Check className="h-4 w-4" />
|
||||
</CheckboxPrimitive.Indicator>
|
||||
</CheckboxPrimitive.Root>
|
||||
|
||||
117
components/ui/pagination.tsx
Normal file
117
components/ui/pagination.tsx
Normal file
@@ -0,0 +1,117 @@
|
||||
import * as React from "react"
|
||||
import { ChevronLeft, ChevronRight, MoreHorizontal } from "lucide-react"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
import { ButtonProps, buttonVariants } from "@/components/ui/button"
|
||||
|
||||
const Pagination = ({ className, ...props }: React.ComponentProps<"nav">) => (
|
||||
<nav
|
||||
role="navigation"
|
||||
aria-label="pagination"
|
||||
className={cn("mx-auto flex w-full justify-center", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
Pagination.displayName = "Pagination"
|
||||
|
||||
const PaginationContent = React.forwardRef<
|
||||
HTMLUListElement,
|
||||
React.ComponentProps<"ul">
|
||||
>(({ className, ...props }, ref) => (
|
||||
<ul
|
||||
ref={ref}
|
||||
className={cn("flex flex-row items-center gap-1", className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
PaginationContent.displayName = "PaginationContent"
|
||||
|
||||
const PaginationItem = React.forwardRef<
|
||||
HTMLLIElement,
|
||||
React.ComponentProps<"li">
|
||||
>(({ className, ...props }, ref) => (
|
||||
<li ref={ref} className={cn("", className)} {...props} />
|
||||
))
|
||||
PaginationItem.displayName = "PaginationItem"
|
||||
|
||||
type PaginationLinkProps = {
|
||||
isActive?: boolean
|
||||
} & Pick<ButtonProps, "size"> &
|
||||
React.ComponentProps<"a">
|
||||
|
||||
const PaginationLink = ({
|
||||
className,
|
||||
isActive,
|
||||
size = "icon",
|
||||
...props
|
||||
}: PaginationLinkProps) => (
|
||||
<a
|
||||
aria-current={isActive ? "page" : undefined}
|
||||
className={cn(
|
||||
buttonVariants({
|
||||
variant: isActive ? "outline" : "ghost",
|
||||
size,
|
||||
}),
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
PaginationLink.displayName = "PaginationLink"
|
||||
|
||||
const PaginationPrevious = ({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof PaginationLink>) => (
|
||||
<PaginationLink
|
||||
aria-label="Go to previous page"
|
||||
size="default"
|
||||
className={cn("gap-1 pl-2.5", className)}
|
||||
{...props}
|
||||
>
|
||||
<ChevronLeft className="h-4 w-4" />
|
||||
<span>Previous</span>
|
||||
</PaginationLink>
|
||||
)
|
||||
PaginationPrevious.displayName = "PaginationPrevious"
|
||||
|
||||
const PaginationNext = ({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof PaginationLink>) => (
|
||||
<PaginationLink
|
||||
aria-label="Go to next page"
|
||||
size="default"
|
||||
className={cn("gap-1 pr-2.5", className)}
|
||||
{...props}
|
||||
>
|
||||
<span>Next</span>
|
||||
<ChevronRight className="h-4 w-4" />
|
||||
</PaginationLink>
|
||||
)
|
||||
PaginationNext.displayName = "PaginationNext"
|
||||
|
||||
const PaginationEllipsis = ({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<"span">) => (
|
||||
<span
|
||||
aria-hidden
|
||||
className={cn("flex h-9 w-9 items-center justify-center", className)}
|
||||
{...props}
|
||||
>
|
||||
<MoreHorizontal className="h-4 w-4" />
|
||||
<span className="sr-only">More pages</span>
|
||||
</span>
|
||||
)
|
||||
PaginationEllipsis.displayName = "PaginationEllipsis"
|
||||
|
||||
export {
|
||||
Pagination,
|
||||
PaginationContent,
|
||||
PaginationLink,
|
||||
PaginationItem,
|
||||
PaginationPrevious,
|
||||
PaginationNext,
|
||||
PaginationEllipsis,
|
||||
}
|
||||
@@ -38,6 +38,14 @@ export default function AnalyzeForm({
|
||||
const [isSaving, setIsSaving] = useState(false)
|
||||
const [saveError, setSaveError] = useState("")
|
||||
|
||||
const fieldsMap = useMemo(
|
||||
() =>
|
||||
fields.reduce((acc, field) => {
|
||||
acc[field.code] = field
|
||||
return acc
|
||||
}, {} as Record<string, Field>),
|
||||
[fields]
|
||||
)
|
||||
const extraFields = useMemo(() => fields.filter((field) => field.isExtra), [fields])
|
||||
const initialFormState = useMemo(
|
||||
() => ({
|
||||
@@ -151,6 +159,7 @@ export default function AnalyzeForm({
|
||||
name="merchant"
|
||||
value={formData.merchant}
|
||||
onChange={(e) => setFormData((prev) => ({ ...prev, merchant: e.target.value }))}
|
||||
hideIfEmpty={!fieldsMap["merchant"]?.isVisibleInAnalysis}
|
||||
/>
|
||||
|
||||
<FormInput
|
||||
@@ -158,7 +167,7 @@ export default function AnalyzeForm({
|
||||
name="description"
|
||||
value={formData.description}
|
||||
onChange={(e) => setFormData((prev) => ({ ...prev, description: e.target.value }))}
|
||||
hideIfEmpty={true}
|
||||
hideIfEmpty={!fieldsMap["description"]?.isVisibleInAnalysis}
|
||||
/>
|
||||
|
||||
<div className="flex flex-wrap gap-4">
|
||||
@@ -182,6 +191,7 @@ export default function AnalyzeForm({
|
||||
name="currencyCode"
|
||||
value={formData.currencyCode}
|
||||
onValueChange={(value) => setFormData((prev) => ({ ...prev, currencyCode: value }))}
|
||||
hideIfEmpty={!fieldsMap["currencyCode"]?.isVisibleInAnalysis}
|
||||
/>
|
||||
|
||||
<FormSelectType
|
||||
@@ -189,6 +199,7 @@ export default function AnalyzeForm({
|
||||
name="type"
|
||||
value={formData.type}
|
||||
onValueChange={(value) => setFormData((prev) => ({ ...prev, type: value }))}
|
||||
hideIfEmpty={!fieldsMap["type"]?.isVisibleInAnalysis}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -212,7 +223,7 @@ export default function AnalyzeForm({
|
||||
name="issuedAt"
|
||||
value={formData.issuedAt}
|
||||
onChange={(e) => setFormData((prev) => ({ ...prev, issuedAt: e.target.value }))}
|
||||
hideIfEmpty={true}
|
||||
hideIfEmpty={!fieldsMap["issuedAt"]?.isVisibleInAnalysis}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -224,9 +235,10 @@ export default function AnalyzeForm({
|
||||
value={formData.categoryCode}
|
||||
onValueChange={(value) => setFormData((prev) => ({ ...prev, categoryCode: value }))}
|
||||
placeholder="Select Category"
|
||||
hideIfEmpty={!fieldsMap["categoryCode"]?.isVisibleInAnalysis}
|
||||
/>
|
||||
|
||||
{projects.length >= 0 && (
|
||||
{projects.length > 0 && (
|
||||
<FormSelectProject
|
||||
title="Project"
|
||||
projects={projects}
|
||||
@@ -234,6 +246,7 @@ export default function AnalyzeForm({
|
||||
value={formData.projectCode}
|
||||
onValueChange={(value) => setFormData((prev) => ({ ...prev, projectCode: value }))}
|
||||
placeholder="Select Project"
|
||||
hideIfEmpty={!fieldsMap["projectCode"]?.isVisibleInAnalysis}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
@@ -243,7 +256,7 @@ export default function AnalyzeForm({
|
||||
name="note"
|
||||
value={formData.note}
|
||||
onChange={(e) => setFormData((prev) => ({ ...prev, note: e.target.value }))}
|
||||
hideIfEmpty={true}
|
||||
hideIfEmpty={!fieldsMap["note"]?.isVisibleInAnalysis}
|
||||
/>
|
||||
|
||||
{extraFields.map((field) => (
|
||||
@@ -254,7 +267,7 @@ export default function AnalyzeForm({
|
||||
name={field.code}
|
||||
value={formData[field.code as keyof typeof formData]}
|
||||
onChange={(e) => setFormData((prev) => ({ ...prev, [field.code]: e.target.value }))}
|
||||
hideIfEmpty={true}
|
||||
hideIfEmpty={!field.isVisibleInAnalysis}
|
||||
/>
|
||||
))}
|
||||
|
||||
@@ -264,7 +277,7 @@ export default function AnalyzeForm({
|
||||
name="text"
|
||||
value={formData.text}
|
||||
onChange={(e) => setFormData((prev) => ({ ...prev, text: e.target.value }))}
|
||||
hideIfEmpty={true}
|
||||
hideIfEmpty={!fieldsMap["text"]?.isVisibleInAnalysis}
|
||||
/>
|
||||
</div>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user