Skip to content

Commit

Permalink
Merge pull request #72 from ElemarJR/forecast_daily_and_historical
Browse files Browse the repository at this point in the history
Forecast daily and historical
  • Loading branch information
ElemarJR authored Dec 19, 2024
2 parents d99a73a + a2f4b58 commit 5bad038
Show file tree
Hide file tree
Showing 7 changed files with 241 additions and 7 deletions.
5 changes: 5 additions & 0 deletions backend/api/src/analytics/schema.graphql
Original file line number Diff line number Diff line change
Expand Up @@ -584,6 +584,7 @@ type ForecastConsultingTotals {
sameDayThreeMonthsAgo: Float!
projected: Float!
expected: Float!
expectedHistorical: Float!
}

type ForecastBySponsorEntry {
Expand Down Expand Up @@ -639,6 +640,7 @@ type ForecastConsultingByClientEntry {
sameDayTwoMonthsAgo: Float!
sameDayThreeMonthsAgo: Float!
expected: Float!
expectedHistorical: Float!
}

type ForecastConsultingBySponsorEntry {
Expand All @@ -654,6 +656,7 @@ type ForecastConsultingBySponsorEntry {
sameDayThreeMonthsAgo: Float!
projected: Float!
expected: Float!
expectedHistorical: Float!
}

type ForecastConsultingByCaseEntry {
Expand All @@ -670,6 +673,7 @@ type ForecastConsultingByCaseEntry {
sameDayThreeMonthsAgo: Float!
projected: Float!
expected: Float!
expectedHistorical: Float!
}

type ForecastConsultingByProjectEntry {
Expand All @@ -685,4 +689,5 @@ type ForecastConsultingByProjectEntry {
sameDayThreeMonthsAgo: Float!
projected: Float!
expected: Float!
expectedHistorical: Float!
}
15 changes: 14 additions & 1 deletion backend/models/src/omni_models/analytics/forecast.py
Original file line number Diff line number Diff line change
Expand Up @@ -250,8 +250,20 @@ def adjust_entity(entity):
entity['same_day_one_month_ago'] = entity.get('same_day_one_month_ago', 0)
entity['same_day_two_months_ago'] = entity.get('same_day_two_months_ago', 0)
entity['same_day_three_months_ago'] = entity.get('same_day_three_months_ago', 0)

entity['projected'] = (entity['in_analysis'] / number_of_working_days_in_analysis_partial) * number_of_working_days_in_analysis

previous_value = entity.get('one_month_ago', 0)
two_months_ago_value = entity.get('two_months_ago', 0)
three_months_ago_value = entity.get('three_months_ago', 0)

if previous_value == 0 and two_months_ago_value == 0 and three_months_ago_value == 0:
entity['expected_historical'] = entity['projected'] if entity['projected'] else 0
elif two_months_ago_value == 0 and three_months_ago_value == 0:
entity['expected_historical'] = previous_value
elif three_months_ago_value == 0:
entity['expected_historical'] = previous_value * 0.8 + two_months_ago_value * 0.2
else:
entity['expected_historical'] = previous_value * 0.6 + two_months_ago_value * 0.25 + three_months_ago_value * 0.15

for client in by_client:
adjust_entity(client)
Expand All @@ -278,6 +290,7 @@ def adjust_entity(entity):
totals['same_day_three_months_ago'] = sum(client.get('same_day_three_months_ago', 0) for client in by_client)
totals['projected'] = sum(client.get('projected', 0) for client in by_client)
totals['expected'] = sum(client.get('expected', 0) for client in by_client)
totals['expected_historical'] = sum(client.get('expected_historical', 0) for client in by_client)

return {
'slug': slug,
Expand Down
1 change: 1 addition & 0 deletions frontend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
"@radix-ui/react-separator": "^1.1.0",
"@radix-ui/react-slot": "^1.1.0",
"@radix-ui/react-tabs": "^1.1.1",
"@radix-ui/react-toggle": "^1.1.1",
"@radix-ui/react-tooltip": "^1.1.3",
"@react-spring/web": "^9.7.5",
"class-variance-authority": "^0.7.0",
Expand Down
144 changes: 138 additions & 6 deletions frontend/src/app/financial/revenue-forecast/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import SectionHeader from "@/components/SectionHeader";
import { NavBar } from "@/app/components/NavBar";
import { FilterFieldsSelect } from "../../components/FilterFieldsSelect";
import { RevenueProgression } from "./RevenueProgression";
import { Toggle } from "@/components/ui/toggle";

const formatCurrency = (value: number) => {
return new Intl.NumberFormat("en-US", {
Expand Down Expand Up @@ -72,6 +73,12 @@ export default function RevenueForecastPage() {
handsOn: [],
squad: []
});
const [useHistorical, setUseHistorical] = useState<Record<string, boolean>>({
consulting: false,
consultingPre: false,
handsOn: false,
squad: false
});

useEffect(() => {
const today = new Date();
Expand Down Expand Up @@ -209,6 +216,45 @@ export default function RevenueForecastPage() {
);
};

const renderPerWorkingDayCell = (value: number, previousValue: number | null, className: string = "", projected?: number, expected?: number) => {
const isProjectedLessThanExpected = projected !== undefined && expected !== undefined && projected < expected;
const bgColor = isProjectedLessThanExpected ? "bg-red-100" : "";

const valuePerDay = value;
const previousValuePerDay = previousValue;

let percentageChange = 0;
let indicator = "";
let indicatorColor = "";

if (previousValuePerDay && previousValuePerDay > 0) {
percentageChange = ((valuePerDay - previousValuePerDay) / previousValuePerDay) * 100;

if (percentageChange > 0) {
indicator = "↑";
indicatorColor = "text-green-600";
} else if (percentageChange < 0) {
indicator = "↓";
indicatorColor = "text-red-600";
}
}

return (
<TableCell
className={`text-right ${className} ${
value === 0 ? "text-gray-300" : ""
} relative ${bgColor}`}
>
{formatCurrency(value)}
{previousValue !== null && percentageChange !== 0 && (
<span className={`absolute bottom-0 right-1 text-[10px] ${indicatorColor}`}>
{indicator} {Math.abs(percentageChange).toFixed(1)}%
</span>
)}
</TableCell>
);
};

const renderSortHeader = (key: string, label: string, workingDays: number | null = null, className: string = "") => (
<TableHead
onClick={() => requestSort(key, tableId)}
Expand All @@ -230,6 +276,7 @@ export default function RevenueForecastPage() {
const renderRow = (item: any, depth: number = 0) => {
const baseClasses = depth === 1 ? "bg-gray-50" : depth === 2 ? "bg-gray-100" : depth === 3 ? "bg-gray-150" : "";
const paddingLeft = depth * 4;
const expectedValue = useHistorical[tableId] ? item.expectedHistorical : item.expected;

return (
<TableRow key={item.name || item.title} className={`h-[57px] ${baseClasses} ${depth === 0 ? 'border-b-[1px]' : ''}`}>
Expand Down Expand Up @@ -265,8 +312,8 @@ export default function RevenueForecastPage() {
{renderCell(item.sameDayOneMonthAgo, total.sameDayOneMonthAgo, "border-x border-gray-200 text-[12px]")}
{renderCell(item.oneMonthAgo, total.oneMonthAgo, "border-r border-gray-400 text-[12px]")}
{renderCell(item.realized, total.realized, "border-x border-gray-200")}
{renderCell(item.projected, total.projected, "border-x border-gray-200", item.projected, item.expected)}
{renderCell(item.expected, total.expected, "border-r border-gray-400", item.projected, item.expected)}
{renderCell(item.projected, total.projected, "border-x border-gray-200", item.projected, expectedValue)}
{renderCell(expectedValue, useHistorical[tableId] ? total.expectedHistorical : total.expected, "border-r border-gray-400", item.projected, expectedValue)}
</TableRow>
);
};
Expand All @@ -275,7 +322,7 @@ export default function RevenueForecastPage() {
<div id={tableId} className="mt-8 scroll-mt-[68px] sm:scroll-mt-[68px]">
<SectionHeader
title={title}
subtitle={`${formatCurrency(total.realized)} / ${formatCurrency(total.expected)}`}
subtitle={`${formatCurrency(total.realized)} / ${formatCurrency(useHistorical[tableId] ? total.expectedHistorical : total.expected)}`}
/>
<div className="px-2">
<Table>
Expand All @@ -297,7 +344,34 @@ export default function RevenueForecastPage() {
{renderSortHeader("oneMonthAgo", "Full Month", null, "w-[95px] border-r border-gray-400")}
{renderSortHeader("realized", "Realized", workingDays.inAnalysisPartial, "w-[120px] border-x border-gray-200")}
{renderSortHeader("projected", "Projected", null, "w-[120px] border-x border-gray-200")}
{renderSortHeader("expected", "Expected", null, "w-[120px] border-r border-gray-400")}
<TableHead className="w-[120px] border-r border-gray-400">
<div className="flex flex-col items-end">
<span
onClick={() => requestSort(useHistorical[tableId] ? "expectedHistorical" : "expected", tableId)}
className="cursor-pointer hover:text-gray-600"
>
Expected {sortConfig.key === (useHistorical[tableId] ? "expectedHistorical" : "expected") && (sortConfig.direction === "asc" ? "↑" : "↓")}
</span>
<button
onClick={() => {
setUseHistorical(prev => ({
...prev,
[tableId]: !prev[tableId]
}));
}}
className={`
text-[10px] mt-0.5
${useHistorical[tableId]
? 'text-blue-600'
: 'text-gray-400 hover:text-gray-600'
}
transition-colors cursor-pointer
`}
>
historical
</button>
</div>
</TableHead>
</TableRow>
</TableHeader>
<TableBody>
Expand Down Expand Up @@ -339,8 +413,61 @@ export default function RevenueForecastPage() {
{renderCell(total.sameDayOneMonthAgo, total.sameDayOneMonthAgo, "border-x border-gray-200 text-[12px]")}
{renderCell(total.oneMonthAgo, total.oneMonthAgo, "border-r border-gray-400 text-[12px]")}
{renderCell(total.realized, total.realized, "border-x border-gray-200")}
{renderCell(total.projected, total.projected, "border-x border-gray-200", total.projected, total.expected)}
{renderCell(total.expected, total.expected, "border-r border-gray-400", total.projected, total.expected)}
{renderCell(total.projected, total.projected, "border-x border-gray-200", total.projected, useHistorical[tableId] ? total.expectedHistorical : total.expected)}
{renderCell(useHistorical[tableId] ? total.expectedHistorical : total.expected, useHistorical[tableId] ? total.expectedHistorical : total.expected, "border-r border-gray-400", total.projected, useHistorical[tableId] ? total.expectedHistorical : total.expected)}
</TableRow>
<TableRow className="font-bold text-gray-500 h-[57px]">
<TableCell></TableCell>
<TableCell className="border-r border-gray-400">Per Working Day</TableCell>
{renderPerWorkingDayCell(
total.sameDayThreeMonthsAgo / workingDays.sameDayThreeMonthsAgo,
null,
"border-x border-gray-200 text-[12px]"
)}
{renderPerWorkingDayCell(
total.threeMonthsAgo / workingDays.threeMonthsAgo,
null,
"border-r border-gray-400 text-[12px]"
)}
{renderPerWorkingDayCell(
total.sameDayTwoMonthsAgo / workingDays.sameDayTwoMonthsAgo,
total.sameDayThreeMonthsAgo / workingDays.sameDayThreeMonthsAgo,
"border-x border-gray-200 text-[12px]"
)}
{renderPerWorkingDayCell(
total.twoMonthsAgo / workingDays.twoMonthsAgo,
total.threeMonthsAgo / workingDays.threeMonthsAgo,
"border-r border-gray-400 text-[12px]"
)}
{renderPerWorkingDayCell(
total.sameDayOneMonthAgo / workingDays.sameDayOneMonthAgo,
total.sameDayTwoMonthsAgo / workingDays.sameDayTwoMonthsAgo,
"border-x border-gray-200 text-[12px]"
)}
{renderPerWorkingDayCell(
total.oneMonthAgo / workingDays.oneMonthAgo,
total.twoMonthsAgo / workingDays.twoMonthsAgo,
"border-r border-gray-400 text-[12px]"
)}
{renderPerWorkingDayCell(
total.realized / workingDays.inAnalysisPartial,
total.sameDayOneMonthAgo / workingDays.sameDayOneMonthAgo,
"border-x border-gray-200"
)}
{renderPerWorkingDayCell(
total.projected / workingDays.inAnalysis,
total.oneMonthAgo / workingDays.oneMonthAgo,
"border-x border-gray-200",
total.projected / workingDays.inAnalysis,
(useHistorical[tableId] ? total.expectedHistorical : total.expected) / workingDays.inAnalysis
)}
{renderPerWorkingDayCell(
(useHistorical[tableId] ? total.expectedHistorical : total.expected) / workingDays.inAnalysis,
total.oneMonthAgo / workingDays.oneMonthAgo,
"border-r border-gray-400",
total.projected / workingDays.inAnalysis,
(useHistorical[tableId] ? total.expectedHistorical : total.expected) / workingDays.inAnalysis
)}
</TableRow>
</TableBody>
</Table>
Expand Down Expand Up @@ -711,6 +838,7 @@ export default function RevenueForecastPage() {
realized: client.inAnalysis,
projected: client.projected,
expected: client.expected,
expectedHistorical: client.expectedHistorical,
})),
sponsors: data.forecast.byKind.consulting.bySponsor.map((sponsor: any) => ({
name: sponsor.name,
Expand All @@ -725,6 +853,7 @@ export default function RevenueForecastPage() {
realized: sponsor.inAnalysis,
projected: sponsor.projected,
expected: sponsor.expected,
expectedHistorical: sponsor.expectedHistorical,
})),
cases: data.forecast.byKind.consulting.byCase.map((caseItem: any) => ({
title: caseItem.title,
Expand All @@ -740,6 +869,7 @@ export default function RevenueForecastPage() {
realized: caseItem.inAnalysis,
projected: caseItem.projected,
expected: caseItem.expected,
expectedHistorical: caseItem.expectedHistorical,
})),
projects: data.forecast.byKind.consulting.byProject.map((project: any) => ({
name: project.name,
Expand All @@ -754,6 +884,7 @@ export default function RevenueForecastPage() {
realized: project.inAnalysis,
projected: project.projected,
expected: project.expected,
expectedHistorical: project.expectedHistorical,
})),
totals: {
sameDayThreeMonthsAgo:
Expand All @@ -768,6 +899,7 @@ export default function RevenueForecastPage() {
realized: data.forecast.byKind.consulting.totals.inAnalysis,
projected: data.forecast.byKind.consulting.totals.projected,
expected: data.forecast.byKind.consulting.totals.expected,
expectedHistorical: data.forecast.byKind.consulting.totals.expectedHistorical,
},
},
consultingPre: {
Expand Down
5 changes: 5 additions & 0 deletions frontend/src/app/financial/revenue-forecast/query.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ export const REVENUE_FORECAST_QUERY = gql`
inAnalysis
projected
expected
expectedHistorical
oneMonthAgo
sameDayOneMonthAgo
twoMonthsAgo
Expand All @@ -54,6 +55,7 @@ export const REVENUE_FORECAST_QUERY = gql`
inAnalysis
projected
expected
expectedHistorical
oneMonthAgo
sameDayOneMonthAgo
twoMonthsAgo
Expand All @@ -68,6 +70,7 @@ export const REVENUE_FORECAST_QUERY = gql`
inAnalysis
projected
expected
expectedHistorical
oneMonthAgo
sameDayOneMonthAgo
twoMonthsAgo
Expand All @@ -83,6 +86,7 @@ export const REVENUE_FORECAST_QUERY = gql`
inAnalysis
projected
expected
expectedHistorical
oneMonthAgo
sameDayOneMonthAgo
twoMonthsAgo
Expand All @@ -97,6 +101,7 @@ export const REVENUE_FORECAST_QUERY = gql`
inAnalysis
projected
expected
expectedHistorical
oneMonthAgo
sameDayOneMonthAgo
twoMonthsAgo
Expand Down
45 changes: 45 additions & 0 deletions frontend/src/components/ui/toggle.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
"use client"

import * as React from "react"
import * as TogglePrimitive from "@radix-ui/react-toggle"
import { cva, type VariantProps } from "class-variance-authority"

import { cn } from "@/lib/utils"

const toggleVariants = cva(
"inline-flex items-center justify-center gap-2 rounded-md text-sm font-medium transition-colors hover:bg-muted hover:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 data-[state=on]:bg-accent data-[state=on]:text-accent-foreground [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0",
{
variants: {
variant: {
default: "bg-transparent",
outline:
"border border-input bg-transparent shadow-sm hover:bg-accent hover:text-accent-foreground",
},
size: {
default: "h-9 px-2 min-w-9",
sm: "h-8 px-1.5 min-w-8",
lg: "h-10 px-2.5 min-w-10",
},
},
defaultVariants: {
variant: "default",
size: "default",
},
}
)

const Toggle = React.forwardRef<
React.ElementRef<typeof TogglePrimitive.Root>,
React.ComponentPropsWithoutRef<typeof TogglePrimitive.Root> &
VariantProps<typeof toggleVariants>
>(({ className, variant, size, ...props }, ref) => (
<TogglePrimitive.Root
ref={ref}
className={cn(toggleVariants({ variant, size, className }))}
{...props}
/>
))

Toggle.displayName = TogglePrimitive.Root.displayName

export { Toggle, toggleVariants }
Loading

0 comments on commit 5bad038

Please sign in to comment.