diff --git a/src/app/reports/slas/page.tsx b/src/app/reports/slas/page.tsx index 29d089a..2eb2a65 100644 --- a/src/app/reports/slas/page.tsx +++ b/src/app/reports/slas/page.tsx @@ -8,6 +8,7 @@ import { Avatar, AvatarFallback } from "@/components/ui/avatar" import { Checkbox } from "@/components/ui/checkbox" import { Header } from "@/components/layout/header" import { Sidebar } from "@/components/layout/sidebar" +import { ChevronUp, ChevronDown, ChevronsUpDown } from "lucide-react" import { usePaymentTransfers, formatAmount } from "@/hooks/usePayments" import { useSLAs } from "@/hooks/useSLAs" import { useProjectSelection } from "@/contexts/project-context" @@ -15,23 +16,50 @@ import { useProjectSelection } from "@/contexts/project-context" interface ReportData { id: string period: string + periodRaw: number entity: string entityInitial: string entityColor: string amount: string + amountRaw: number status: "paid-out" | "pending" | "processing" } interface TicketData { id: string date: string + dateRaw: string entity: string entityInitial: string entityColor: string amount: string + amountRaw: number status: "completed" | "pending" | "processing" } +type MonthlySortField = "period" | "entity" | "amount" | "status" +type TicketsSortField = "date" | "entity" | "amount" | "status" +type SortDirection = "asc" | "desc" + +function ReportsSortIcon({ + field, + sortField, + sortDirection, +}: { + field: T + sortField: T | null + sortDirection: SortDirection +}) { + if (sortField !== field) { + return + } + return sortDirection === "asc" ? ( + + ) : ( + + ) +} + // Helper function to format date const formatDate = (dateString: string) => { const date = new Date(dateString) @@ -85,6 +113,10 @@ export default function ReportsSLAsPage() { const [selectedFilter, setSelectedFilter] = useState("all") const [selectedRows, setSelectedRows] = useState([]) const [selectedTicketRows, setSelectedTicketRows] = useState([]) + const [monthlySortField, setMonthlySortField] = useState(null) + const [monthlySortDirection, setMonthlySortDirection] = useState("asc") + const [ticketsSortField, setTicketsSortField] = useState(null) + const [ticketsSortDirection, setTicketsSortDirection] = useState("asc") const { selectedProjectId } = useProjectSelection() const projectId = selectedProjectId ?? undefined @@ -128,10 +160,12 @@ export default function ReportsSLAsPage() { return { id: key, period: data.month, + periodRaw: new Date(data.month).getTime(), entity: (data.sla as any)?.name || "Unknown", entityInitial: initial, entityColor: color, amount: formatAmount(data.total, "usd"), + amountRaw: data.total, status: "paid-out" as const, } }) @@ -148,21 +182,117 @@ export default function ReportsSLAsPage() { return { id: transfer.id, date: formatDate(transfer.completed_at!), + dateRaw: transfer.completed_at!, entity: (transfer.sla as any)?.name || "Unknown", entityInitial: initial, entityColor: color, amount: formatAmount(transfer.amount_smallest_unit, transfer.currency), + amountRaw: transfer.amount_smallest_unit, status: transfer.status === "completed" ? "completed" : "pending", } }) }, [transfersData, slasData]) + const sortedReports = useMemo(() => { + if (!monthlySortField) return reports + const sorted = [...reports] + sorted.sort((a, b) => { + let aValue: string | number + let bValue: string | number + switch (monthlySortField) { + case "period": + aValue = a.periodRaw + bValue = b.periodRaw + break + case "entity": + aValue = a.entity.toLowerCase() + bValue = b.entity.toLowerCase() + break + case "amount": + aValue = a.amountRaw + bValue = b.amountRaw + break + case "status": + aValue = a.status.toLowerCase() + bValue = b.status.toLowerCase() + break + default: + return 0 + } + if (aValue < bValue) return monthlySortDirection === "asc" ? -1 : 1 + if (aValue > bValue) return monthlySortDirection === "asc" ? 1 : -1 + return 0 + }) + return sorted + }, [reports, monthlySortField, monthlySortDirection]) + + const sortedTickets = useMemo(() => { + if (!ticketsSortField) return tickets + const sorted = [...tickets] + sorted.sort((a, b) => { + let aValue: string | number + let bValue: string | number + switch (ticketsSortField) { + case "date": + aValue = new Date(a.dateRaw).getTime() + bValue = new Date(b.dateRaw).getTime() + break + case "entity": + aValue = a.entity.toLowerCase() + bValue = b.entity.toLowerCase() + break + case "amount": + aValue = a.amountRaw + bValue = b.amountRaw + break + case "status": + aValue = a.status.toLowerCase() + bValue = b.status.toLowerCase() + break + default: + return 0 + } + if (aValue < bValue) return ticketsSortDirection === "asc" ? -1 : 1 + if (aValue > bValue) return ticketsSortDirection === "asc" ? 1 : -1 + return 0 + }) + return sorted + }, [tickets, ticketsSortField, ticketsSortDirection]) + + const handleMonthlySort = (field: MonthlySortField) => { + if (monthlySortField === field) { + if (monthlySortDirection === "asc") { + setMonthlySortDirection("desc") + } else { + setMonthlySortField(null) + setMonthlySortDirection("asc") + } + } else { + setMonthlySortField(field) + setMonthlySortDirection("asc") + } + } + + const handleTicketsSort = (field: TicketsSortField) => { + if (ticketsSortField === field) { + if (ticketsSortDirection === "asc") { + setTicketsSortDirection("desc") + } else { + setTicketsSortField(null) + setTicketsSortDirection("asc") + } + } else { + setTicketsSortField(field) + setTicketsSortDirection("asc") + } + } + const handleRowSelect = (id: string) => { setSelectedRows((prev) => (prev.includes(id) ? prev.filter((rowId) => rowId !== id) : [...prev, id])) } const handleSelectAll = () => { - setSelectedRows(selectedRows.length === reports.length ? [] : reports.map((report) => report.id)) + setSelectedRows(selectedRows.length === sortedReports.length ? [] : sortedReports.map((report) => report.id)) } const handleTicketRowSelect = (id: string) => { @@ -170,7 +300,7 @@ export default function ReportsSLAsPage() { } const handleTicketSelectAll = () => { - setSelectedTicketRows(selectedTicketRows.length === tickets.length ? [] : tickets.map((ticket) => ticket.id)) + setSelectedTicketRows(selectedTicketRows.length === sortedTickets.length ? [] : sortedTickets.map((ticket) => ticket.id)) } return ( @@ -214,100 +344,108 @@ export default function ReportsSLAsPage() {
{/* Filters */} -
- {activeTab === "tickets" ? ( - <> - - - - - ) : ( - <> - - - - +
+ {activeTab === "tickets" && ( + )} + +
{/* Monthly Reports Table */} {activeTab === "monthly" && (
{/* Table Header */} -
+
- 0} - onCheckedChange={handleSelectAll} + 0} + onChange={handleSelectAll} />
- Period +
- Entity +
- Amount +
- Status +
@@ -317,10 +455,10 @@ export default function ReportsSLAsPage() {
{isLoading ? (
Loading reports...
- ) : reports.length === 0 ? ( + ) : sortedReports.length === 0 ? (
No reports found
) : ( - reports.map((report) => ( + sortedReports.map((report) => (
@@ -355,14 +493,14 @@ export default function ReportsSLAsPage() { @@ -377,25 +515,55 @@ export default function ReportsSLAsPage() { {activeTab === "tickets" && (
{/* Table Header */} -
+
- 0} - onCheckedChange={handleTicketSelectAll} + 0} + onChange={handleTicketSelectAll} />
- Date +
- Entity +
- Amount +
- Status +
@@ -405,10 +573,10 @@ export default function ReportsSLAsPage() {
{isLoading ? (
Loading tickets...
- ) : tickets.length === 0 ? ( + ) : sortedTickets.length === 0 ? (
No tickets found
) : ( - tickets.map((ticket) => ( + sortedTickets.map((ticket) => (
@@ -455,14 +623,14 @@ export default function ReportsSLAsPage() { diff --git a/src/app/reports/support/page.tsx b/src/app/reports/support/page.tsx index 346bbda..c49c4e9 100644 --- a/src/app/reports/support/page.tsx +++ b/src/app/reports/support/page.tsx @@ -6,13 +6,36 @@ import { Header } from "@/components/layout/header" import { Sidebar } from "@/components/layout/sidebar" import { Button } from "@/components/ui/button" import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select" -import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table" import { Checkbox } from "@/components/ui/checkbox" import { Badge } from "@/components/ui/badge" +import { ChevronUp, ChevronDown, ChevronsUpDown } from "lucide-react" import { getStatusBadgeClass } from "@/lib/status-colors" import { usePaymentTransfers, formatAmount } from "@/hooks/usePayments" import { useProjectSelection } from "@/contexts/project-context" +type MonthlySortField = "period" | "description" | "amount" | "status" +type TicketsSortField = "ticketId" | "date" | "helper" | "amount" | "status" +type SortDirection = "asc" | "desc" + +function ReportsSortIcon({ + field, + sortField, + sortDirection, +}: { + field: T + sortField: T | null + sortDirection: SortDirection +}) { + if (sortField !== field) { + return + } + return sortDirection === "asc" ? ( + + ) : ( + + ) +} + const HELPER_COLORS = ["#f4bccc", "#d0f6bc", "#f6e6bc", "#bcedf6", "#cbbcf6", "#82c95f"] function formatDate(dateString: string) { @@ -56,6 +79,10 @@ export default function ReportsSupportPage() { const [selectedFilter, setSelectedFilter] = useState<"all" | "current">("all") const [selectedRows, setSelectedRows] = useState([]) const [selectedTicketRows, setSelectedTicketRows] = useState([]) + const [monthlySortField, setMonthlySortField] = useState(null) + const [monthlySortDirection, setMonthlySortDirection] = useState("asc") + const [ticketsSortField, setTicketsSortField] = useState(null) + const [ticketsSortDirection, setTicketsSortDirection] = useState("asc") const { selectedProjectId } = useProjectSelection() const projectId = selectedProjectId ?? undefined @@ -97,11 +124,13 @@ export default function ReportsSupportPage() { .map(([key, data]) => ({ id: key, period: data.month, + periodRaw: new Date(data.month).getTime(), description: "Monthly report", amount: formatAmount(data.total, "usd"), + amountRaw: data.total, status: "Paid out" as const, })) - .sort((a, b) => new Date(b.period).getTime() - new Date(a.period).getTime()) + .sort((a, b) => b.periodRaw - a.periodRaw) }, [transfersData]) // Tickets: individual payment transfers with ticket + helper info @@ -121,6 +150,7 @@ export default function ReportsSupportPage() { helperInitial: initial, helperColor: color, amount: formatAmount(transfer.amount_smallest_unit, transfer.currency), + amountRaw: transfer.amount_smallest_unit, status: transfer.status === "completed" ? "Completed" : transfer.status === "failed" ? "Failed" : "Pending", statusType: transfer.status, } @@ -137,16 +167,110 @@ export default function ReportsSupportPage() { // Filter monthly reports by selected month const filteredMonthlyReports = useMemo(() => { - if (!selectedMonth) return monthlyReports - return monthlyReports.filter((r) => r.period === selectedMonth) - }, [monthlyReports, selectedMonth]) + const list = !selectedMonth ? monthlyReports : monthlyReports.filter((r) => r.period === selectedMonth) + if (!monthlySortField) return list + const sorted = [...list] + sorted.sort((a, b) => { + let aValue: string | number + let bValue: string | number + switch (monthlySortField) { + case "period": + aValue = a.periodRaw + bValue = b.periodRaw + break + case "description": + aValue = a.description.toLowerCase() + bValue = b.description.toLowerCase() + break + case "amount": + aValue = a.amountRaw + bValue = b.amountRaw + break + case "status": + aValue = a.status.toLowerCase() + bValue = b.status.toLowerCase() + break + default: + return 0 + } + if (aValue < bValue) return monthlySortDirection === "asc" ? -1 : 1 + if (aValue > bValue) return monthlySortDirection === "asc" ? 1 : -1 + return 0 + }) + return sorted + }, [monthlyReports, selectedMonth, monthlySortField, monthlySortDirection]) + + const sortedTickets = useMemo(() => { + if (!ticketsSortField) return ticketsData + const sorted = [...ticketsData] + sorted.sort((a, b) => { + let aValue: string | number + let bValue: string | number + switch (ticketsSortField) { + case "ticketId": + aValue = (a.ticketId ?? "").toLowerCase() + bValue = (b.ticketId ?? "").toLowerCase() + break + case "date": + aValue = new Date(a.dateRaw).getTime() + bValue = new Date(b.dateRaw).getTime() + break + case "helper": + aValue = a.helper.toLowerCase() + bValue = b.helper.toLowerCase() + break + case "amount": + aValue = a.amountRaw + bValue = b.amountRaw + break + case "status": + aValue = a.status.toLowerCase() + bValue = b.status.toLowerCase() + break + default: + return 0 + } + if (aValue < bValue) return ticketsSortDirection === "asc" ? -1 : 1 + if (aValue > bValue) return ticketsSortDirection === "asc" ? 1 : -1 + return 0 + }) + return sorted + }, [ticketsData, ticketsSortField, ticketsSortDirection]) + + const handleMonthlySort = (field: MonthlySortField) => { + if (monthlySortField === field) { + if (monthlySortDirection === "asc") { + setMonthlySortDirection("desc") + } else { + setMonthlySortField(null) + setMonthlySortDirection("asc") + } + } else { + setMonthlySortField(field) + setMonthlySortDirection("asc") + } + } + + const handleTicketsSort = (field: TicketsSortField) => { + if (ticketsSortField === field) { + if (ticketsSortDirection === "asc") { + setTicketsSortDirection("desc") + } else { + setTicketsSortField(null) + setTicketsSortDirection("asc") + } + } else { + setTicketsSortField(field) + setTicketsSortDirection("asc") + } + } const handleRowSelect = (id: string) => { setSelectedRows((prev) => (prev.includes(id) ? prev.filter((r) => r !== id) : [...prev, id])) } const handleSelectAll = () => { - const ids = activeTab === "monthly" ? filteredMonthlyReports.map((r) => r.id) : ticketsData.map((t) => t.id) + const ids = activeTab === "monthly" ? filteredMonthlyReports.map((r) => r.id) : sortedTickets.map((t) => t.id) setSelectedRows((prev) => (prev.length === ids.length ? [] : ids)) } @@ -156,15 +280,15 @@ export default function ReportsSupportPage() { const handleTicketSelectAll = () => { setSelectedTicketRows((prev) => - prev.length === ticketsData.length ? [] : ticketsData.map((t) => t.id) + prev.length === sortedTickets.length ? [] : sortedTickets.map((t) => t.id) ) } - const displayReports = activeTab === "monthly" ? filteredMonthlyReports : ticketsData + const displayReports = activeTab === "monthly" ? filteredMonthlyReports : sortedTickets const allSelected = activeTab === "monthly" ? selectedRows.length === filteredMonthlyReports.length && filteredMonthlyReports.length > 0 - : selectedTicketRows.length === ticketsData.length && ticketsData.length > 0 + : selectedTicketRows.length === sortedTickets.length && sortedTickets.length > 0 return (
@@ -252,131 +376,230 @@ export default function ReportsSupportPage() {
-
- - - - - + {/* Table Header */} +
+ {activeTab === "monthly" ? ( +
+
+ - - {activeTab === "monthly" ? ( - <> - Period - Description - - ) : ( - <> - Ticket ID - Date - Helper - - )} - Amount - Status - - - - - {isLoading ? ( - - - Loading... - - - ) : activeTab === "monthly" ? ( - filteredMonthlyReports.length === 0 ? ( - - - No reports found - - - ) : ( - filteredMonthlyReports.map((report) => ( - - +
+
+ +
+
+ +
+
+ +
+
+ +
+
+
+ ) : ( +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+
+ )} +
+ + {/* Table Body */} +
+ {isLoading ? ( +
Loading...
+ ) : activeTab === "monthly" ? ( + filteredMonthlyReports.length === 0 ? ( +
No reports found
+ ) : ( + filteredMonthlyReports.map((report) => ( +
+
+
handleRowSelect(report.id)} /> - - {report.period} - {report.description} - {report.amount} - - +
+
+ {report.period} +
+
+ {report.description} +
+
+ {report.amount} +
+
+ {report.status} - - -
- - -
-
- - )) - ) - ) : ticketsData.length === 0 ? ( - - - No tickets found - - - ) : ( - ticketsData.map((ticket) => ( - - +
+
+ + +
+
+
+ )) + ) + ) : sortedTickets.length === 0 ? ( +
No tickets found
+ ) : ( + sortedTickets.map((ticket) => ( +
+
+
handleTicketRowSelect(ticket.id)} /> - - +
+
{ticket.ticketId ? ( {getShortTicketId(ticket.ticketId)} ) : ( - "—" + )} - - {ticket.date} - -
-
- {ticket.helperInitial} -
- {ticket.helper} +
+
+ {ticket.date} +
+
+
+ {ticket.helperInitial}
- - {ticket.amount} - + {ticket.helper} +
+
+ {ticket.amount} +
+
{ticket.statusType === "pending" ? ( - + {ticket.status} ) : ticket.statusType === "failed" ? ( - + {ticket.status} ) : ( - + )} - - -
- {ticket.ticketId ? ( - - ) : ( - - )} -
+
+ {ticket.ticketId ? ( + -
- - - )) - )} - -
+ ) : ( + + )} + +
+
+
+ )) + )} +