diff --git a/.gitignore b/.gitignore index 0a3ebf8f..56f48cbe 100644 --- a/.gitignore +++ b/.gitignore @@ -116,15 +116,4 @@ frontend/yarn.lock # Uncomment if you're using PDM # pdm.lock # .pdm.toml -ts_2024/abr_2024.timesheet -ts_2024/ago_2024.timesheet -ts_2024/dez_2024.timesheet -ts_2024/fev_2024.timesheet -ts_2024/jan_2024.timesheet -ts_2024/jul_2024.timesheet -ts_2024/jun_2024.timesheet -ts_2024/mai_2024.timesheet -ts_2024/mar_2024.timesheet -ts_2024/nov_2024.timesheet -ts_2024/out_2024.timesheet -ts_2024/set_2024.timesheet +*.timesheet \ No newline at end of file diff --git a/backend/models/src/omni_models/analytics/forecast.py b/backend/models/src/omni_models/analytics/forecast.py index 96442e04..3b09e43b 100644 --- a/backend/models/src/omni_models/analytics/forecast.py +++ b/backend/models/src/omni_models/analytics/forecast.py @@ -286,7 +286,7 @@ def filter_items(items): wah = case_.weekly_approved_hours project_ = None for ti in case_.tracker_info: - if ti.kind == 'consulting': + if ti.kind == 'consulting' and ti.status == "open": project_ = ti break diff --git a/frontend/src/app/financial/revenue-forecast/OnTheTable.tsx b/frontend/src/app/financial/revenue-forecast/OnTheTable.tsx new file mode 100644 index 00000000..4f0e4f9d --- /dev/null +++ b/frontend/src/app/financial/revenue-forecast/OnTheTable.tsx @@ -0,0 +1,289 @@ +import { Table, TableBody, TableRow, TableCell, TableHeader } from "@/components/ui/table"; +import SectionHeader from "@/components/SectionHeader"; +import React from "react"; +import { TableCellComponent } from "./components/TableCell"; +import { formatCurrency } from "./utils"; +import Link from "next/link"; + +interface OnTheTableProps { + title: string; + tableData: any; + tableId: string; + normalized: Record; + useHistorical: Record; + setUseHistorical: (value: React.SetStateAction>) => void; +} + +export function OnTheTable({ + title, + tableData, + tableId, + normalized, + useHistorical, + setUseHistorical, +}: OnTheTableProps) { + const [sortConfig, setSortConfig] = React.useState<{ + key: string; + direction: "asc" | "desc"; + }>({ key: "waste", direction: "desc" }); + + // Filter clients where Expected > Projected + const clientsWithWaste = tableData.clients.filter((client: any) => { + const expected = useHistorical[tableId] + ? client.expectedHistorical + : client.expected; + return expected > client.projected; + }); + + // Sort clients based on current sort configuration + const sortedClients = React.useMemo(() => { + const sorted = [...clientsWithWaste]; + sorted.sort((a, b) => { + let aValue, bValue; + + switch (sortConfig.key) { + case "projected": + aValue = a.projected; + bValue = b.projected; + break; + case "expected": + aValue = useHistorical[tableId] ? a.expectedHistorical : a.expected; + bValue = useHistorical[tableId] ? b.expectedHistorical : b.expected; + break; + case "waste": + aValue = (useHistorical[tableId] ? a.expectedHistorical : a.expected) - a.projected; + bValue = (useHistorical[tableId] ? b.expectedHistorical : b.expected) - b.projected; + break; + default: + aValue = a.name; + bValue = b.name; + } + + if (aValue < bValue) { + return sortConfig.direction === "asc" ? -1 : 1; + } + if (aValue > bValue) { + return sortConfig.direction === "asc" ? 1 : -1; + } + return 0; + }); + return sorted; + }, [clientsWithWaste, sortConfig, useHistorical, tableId]); + + // Calculate total waste + const totalWaste = sortedClients.reduce((acc: number, client: any) => { + const expected = useHistorical[tableId] + ? client.expectedHistorical + : client.expected; + return acc + (expected - client.projected); + }, 0); + + const requestSort = (key: string) => { + setSortConfig((current) => ({ + key, + direction: + current.key === key && current.direction === "desc" ? "asc" : "desc", + })); + }; + + return ( +
+
+ +
+
+ + + + # + requestSort("name")} + > + Client + {sortConfig.key === "name" && (sortConfig.direction === "asc" ? " ↑" : " ↓")} + + requestSort("projected")} + > + Projected + {sortConfig.key === "projected" && (sortConfig.direction === "asc" ? " ↑" : " ↓")} + + +
+ requestSort("expected")} + className="cursor-pointer hover:text-gray-600" + > + Expected {sortConfig.key === "expected" && (sortConfig.direction === "asc" ? "↑" : "↓")} + + +
+
+ requestSort("waste")} + > + Waste + {sortConfig.key === "waste" && (sortConfig.direction === "asc" ? " ↑" : " ↓")} + +
+
+ + {sortedClients.map((client: any, index: number) => { + const expected = useHistorical[tableId] + ? client.expectedHistorical + : client.expected; + const waste = expected - client.projected; + + // Calculate column totals + const projectedTotal = sortedClients.reduce( + (acc: number, c: any) => acc + c.projected, + 0 + ); + const expectedTotal = sortedClients.reduce( + (acc: number, c: any) => acc + (useHistorical[tableId] ? c.expectedHistorical : c.expected), + 0 + ); + const wasteTotal = sortedClients.reduce( + (acc: number, c: any) => { + const cExpected = useHistorical[tableId] ? c.expectedHistorical : c.expected; + return acc + (cExpected - c.projected); + }, + 0 + ); + + return ( + + + {index + 1} + + + + {client.name} + + + + + + + ); + })} + + + Total + acc + client.projected, + 0 + )} + normalizedValue={sortedClients.reduce( + (acc: number, client: any) => acc + client.projected, + 0 + )} + totalValue={sortedClients.reduce( + (acc: number, client: any) => acc + client.projected, + 0 + )} + normalizedTotalValue={sortedClients.reduce( + (acc: number, client: any) => acc + client.projected, + 0 + )} + className="border-x border-gray-200" + normalized={false} + /> + { + const expected = useHistorical[tableId] + ? client.expectedHistorical + : client.expected; + return acc + expected; + }, 0)} + normalizedValue={sortedClients.reduce((acc: number, client: any) => { + const expected = useHistorical[tableId] + ? client.expectedHistorical + : client.expected; + return acc + expected; + }, 0)} + totalValue={sortedClients.reduce((acc: number, client: any) => { + const expected = useHistorical[tableId] + ? client.expectedHistorical + : client.expected; + return acc + expected; + }, 0)} + normalizedTotalValue={sortedClients.reduce((acc: number, client: any) => { + const expected = useHistorical[tableId] + ? client.expectedHistorical + : client.expected; + return acc + expected; + }, 0)} + className="border-x border-gray-200" + normalized={false} + /> + + + +
+
+
+ ); +} \ No newline at end of file diff --git a/frontend/src/app/financial/revenue-forecast/page.tsx b/frontend/src/app/financial/revenue-forecast/page.tsx index e1816783..f36f0551 100644 --- a/frontend/src/app/financial/revenue-forecast/page.tsx +++ b/frontend/src/app/financial/revenue-forecast/page.tsx @@ -17,6 +17,8 @@ import { OtherTable } from "./OtherTable"; import { ConsultingPreTable } from "./ConsultingPreTable"; import { GraphVizDaily } from "./GraphVizDaily"; import OneYearAllocation from "@/app/components/OneYearAllocation"; +import SectionHeader from "@/components/SectionHeader"; +import { OnTheTable } from "./OnTheTable"; export default function RevenueForecastPage() { const [date, setDate] = useState(new Date()); @@ -235,6 +237,16 @@ export default function RevenueForecastPage() { + +