Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Pro rata (starting) #53

Merged
merged 5 commits into from
Nov 29, 2024
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
54 changes: 54 additions & 0 deletions backend/src/api/analytics/schema.graphql
Original file line number Diff line number Diff line change
@@ -331,6 +331,55 @@ type RevenueTracking {
regular: RevenueTrackingRegular!
summaries: RevenueTrackingSummaries!
total: Float!
proRataInfo: RevenueTrackingProRataInfo!
}

type RevenueTrackingProRataInfo {
byKind: [RevenueTrackingProRataByKind!]!
}

type RevenueTrackingProRataByKind {
kind: String!
penalty: Float!
byAccountManager: [RevenueTrackingProRataByAccountManager!]!
}

type RevenueTrackingProRataByAccountManager {
name: String!
penalty: Float!
byClient: [RevenueTrackingProRataByClient!]!
}

type RevenueTrackingProRataByClient {
name: String!
penalty: Float!
bySponsor: [RevenueTrackingProRataBySponsor!]!
}

type RevenueTrackingProRataBySponsor {
name: String!
penalty: Float!
byCase: [RevenueTrackingProRataByCase!]!
}

type RevenueTrackingProRataByCase {
title: String!
penalty: Float!
byProject: [RevenueTrackingProRataByProject!]!
}

type RevenueTrackingProRataByProject {
name: String!
penalty: Float!
partialFee: Float!
byWorker: [RevenueTrackingProRataByWorker!]!
}

type RevenueTrackingProRataByWorker {
name: String!
penalty: Float!
hours: Float!
partialFee: Float!
}

type RevenueTrackingPreContracted {
@@ -394,6 +443,7 @@ type RevenueTrackingByAccountManager {
regular: Float!
preContracted: Float!
total: Float!
partial: Boolean
byClient: [RevenueTrackingByClient!]!
}

@@ -408,6 +458,7 @@ type RevenueTrackingByClient {
regular: Float!
preContracted: Float!
total: Float!
partial: Boolean
bySponsor: [RevenueTrackingBySponsor!]!
}

@@ -422,6 +473,7 @@ type RevenueTrackingBySponsor {
regular: Float!
preContracted: Float!
total: Float!
partial: Boolean
byCase: [RevenueTrackingByCase!]!
}

@@ -436,6 +488,7 @@ type RevenueTrackingByCase {
regular: Float!
preContracted: Float!
total: Float!
partial: Boolean
byProject: [RevenueTrackingByProject!]!
}

@@ -445,4 +498,5 @@ type RevenueTrackingByProject {
rate: Float
hours: Float
fee: Float!
partial: Boolean
}
207 changes: 191 additions & 16 deletions backend/src/models/analytics/revenue_tracking.py

Large diffs are not rendered by default.

3 changes: 0 additions & 3 deletions backend/src/models/semantic/ontology.py
Original file line number Diff line number Diff line change
@@ -107,7 +107,6 @@ def from_wordpress_post(post: Post, ontology) -> 'Client':

return result


class Case(BaseModel):
id: int
title: str
@@ -148,8 +147,6 @@ def from_wordpress_post(
match = re.match(r'^(\d+(?:\.\d+)?)', allocation)
allocation = match.group(1) if match else '0'

print(post.meta.get('valor-pre-contratado', {}))

pre_contracted_value = post.meta.get('valor-pre-contratado', {})
if isinstance(pre_contracted_value, list):
pre_contracted_value = False
1 change: 1 addition & 0 deletions backend/src/models/syntactic/everhour.py
Original file line number Diff line number Diff line change
@@ -50,6 +50,7 @@ class Project(BaseModel):
client_id: Optional[int] = Field(None, alias='client')
budget: Optional[Budget] = None
rate: Optional[Rate] = None
users: Optional[List[int]] = None

@property
def slug(self) -> str:
40 changes: 32 additions & 8 deletions frontend/src/app/components/OmniSidebar.tsx
Original file line number Diff line number Diff line change
@@ -79,17 +79,41 @@ export function OmniSidebar() {
setAboutUsItems(aboutUs);
setAdminItems(admin);

// Set initial active items based on permissions
if (hasFinancialAccess && financial.length > 0) {
setActiveSection("Financial");
setActiveItems(financial);
} else {
setActiveSection("Analytics");
setActiveItems(analytics);
// Determine active section based on current path
let initialSection = "Analytics";
let initialItems = analytics;

if (hasFinancialAccess) {
const isFinancialPath = financial.some(item => pathname.startsWith(item.url));
if (isFinancialPath) {
initialSection = "Financial";
initialItems = financial;
}
}

const isAnalyticsPath = analytics.some(item => pathname.startsWith(item.url));
if (isAnalyticsPath) {
initialSection = "Analytics";
initialItems = analytics;
}

const isAboutUsPath = aboutUs.some(item => pathname.startsWith(item.url));
if (isAboutUsPath) {
initialSection = "About Us";
initialItems = aboutUs;
}

const isAdminPath = admin.some(item => pathname.startsWith(item.url));
if (isAdminPath) {
initialSection = "Administrative";
initialItems = admin;
}

setActiveSection(initialSection);
setActiveItems(initialItems);
}
loadItems();
}, [session?.user?.email, hasFinancialAccess]);
}, [session?.user?.email, hasFinancialAccess, pathname]);

return (
<Sidebar
300 changes: 300 additions & 0 deletions frontend/src/app/financial/pro-rata/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,300 @@
"use client";

import { useQuery } from "@apollo/client";
import { format } from "date-fns";
import { useState, useEffect } from "react";
import { DatePicker } from "@/components/DatePicker";
import { PRO_RATA_QUERY } from "./query";
import { ChevronDownIcon, ChevronRightIcon } from "lucide-react";
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from "@/components/ui/table";
import SectionHeader from "@/components/SectionHeader";

const formatNumber = (value: number) => {
return new Intl.NumberFormat("en-US", {
minimumFractionDigits: 2,
maximumFractionDigits: 2,
}).format(value);
};

const formatPercent = (value: number, total: number) => {
return new Intl.NumberFormat("en-US", {
style: "percent",
minimumFractionDigits: 1,
maximumFractionDigits: 1,
}).format(value / total);
};

export default function ProRataPage() {
const [date, setDate] = useState<Date>(new Date(new Date().getFullYear(), new Date().getMonth(), 0));
const [expanded, setExpanded] = useState<{ [key: string]: boolean }>({});

useEffect(() => {
const today = new Date();
const lastDayOfPreviousMonth = new Date(today.getFullYear(), today.getMonth(), 0);
setDate(lastDayOfPreviousMonth);
}, []);

const { loading, error, data } = useQuery(PRO_RATA_QUERY, {
variables: { dateOfInterest: format(date, "yyyy-MM-dd") },
});

if (loading) return <div>Loading...</div>;
if (error) return <div>Error: {error.message}</div>;

const { proRataInfo } = data.revenueTracking;

const toggleExpand = (id: string) => {
setExpanded((prev) => ({
...prev,
[id]: !prev[id],
}));
};

const renderExpandIcon = (id: string) => {
return expanded[id] ? (
<ChevronDownIcon className="w-4 h-4 inline mr-2" />
) : (
<ChevronRightIcon className="w-4 h-4 inline mr-2" />
);
};

return (
<div className="flex flex-col gap-6">
<div className="flex items-center">
<DatePicker date={date} onSelectedDateChange={setDate} />
<div className="flex-grow h-px bg-gray-200 ml-2"></div>
</div>

<div className="px-2">
{proRataInfo.byKind.map((kind: any) => {
const kindTotal = {
penalty: kind.penalty,
};

return (
<div key={kind.kind} className="mb-8">
<SectionHeader
title={kind.kind === "handsOn" ? "hands-on" : kind.kind}
subtitle=""
/>
<div className="px-2">
<Table>
<TableHeader>
<TableRow>
<TableHead>Name</TableHead>
<TableHead className="text-right w-[120px]">
Hours
</TableHead>
<TableHead className="text-right w-[120px]">
Partial Fee
</TableHead>
<TableHead className="text-right w-[120px]">
Penalty
</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{kind.byAccountManager.map((manager: any) => (
<>
<TableRow
key={manager.name}
className="font-bold bg-gray-50"
>
<TableCell className="font-medium">
{manager.name}
</TableCell>
<TableCell className="text-right">-</TableCell>
<TableCell className="text-right">-</TableCell>
<TableCell className="text-right">
{formatNumber(manager.penalty)}
</TableCell>
</TableRow>

{manager.byClient.map((client: any) => (
<>
<TableRow
key={`${manager.name}-${client.name}`}
className="hover:bg-gray-50 cursor-pointer"
onClick={() =>
toggleExpand(
`client-${manager.name}-${client.name}`
)
}
>
<TableCell className="pl-8">
{renderExpandIcon(
`client-${manager.name}-${client.name}`
)}
{client.name}
</TableCell>
<TableCell className="text-right">-</TableCell>
<TableCell className="text-right">-</TableCell>
<TableCell className="text-right">
{formatNumber(client.penalty)}
</TableCell>
</TableRow>

{expanded[
`client-${manager.name}-${client.name}`
] &&
client.bySponsor.map((sponsor: any) => (
<>
<TableRow
key={`${manager.name}-${client.name}-${sponsor.name}`}
className="hover:bg-gray-50 cursor-pointer bg-gray-50"
onClick={() =>
toggleExpand(
`sponsor-${manager.name}-${client.name}-${sponsor.name}`
)
}
>
<TableCell className="pl-12">
{renderExpandIcon(
`sponsor-${manager.name}-${client.name}-${sponsor.name}`
)}
{sponsor.name}
</TableCell>
<TableCell className="text-right">
-
</TableCell>
<TableCell className="text-right">
-
</TableCell>
<TableCell className="text-right">
{formatNumber(sponsor.penalty)}
</TableCell>
</TableRow>

{expanded[
`sponsor-${manager.name}-${client.name}-${sponsor.name}`
] &&
sponsor.byCase.map((case_: any) => (
<>
<TableRow
key={`${manager.name}-${client.name}-${sponsor.name}-${case_.title}`}
className="hover:bg-gray-50 cursor-pointer bg-gray-100"
onClick={() =>
toggleExpand(
`case-${manager.name}-${client.name}-${sponsor.name}-${case_.title}`
)
}
>
<TableCell className="pl-16">
{renderExpandIcon(
`case-${manager.name}-${client.name}-${sponsor.name}-${case_.title}`
)}
{case_.title}
</TableCell>
<TableCell className="text-right">
-
</TableCell>
<TableCell className="text-right">
-
</TableCell>
<TableCell className="text-right">
{formatNumber(case_.penalty)}
</TableCell>
</TableRow>

{expanded[
`case-${manager.name}-${client.name}-${sponsor.name}-${case_.title}`
] &&
case_.byProject.map(
(project: any) => (
<>
<TableRow
key={`${manager.name}-${client.name}-${sponsor.name}-${case_.title}-${project.name}`}
className="hover:bg-gray-50 cursor-pointer bg-gray-150"
onClick={() =>
toggleExpand(
`project-${manager.name}-${client.name}-${sponsor.name}-${case_.title}-${project.name}`
)
}
>
<TableCell className="pl-20">
{renderExpandIcon(
`project-${manager.name}-${client.name}-${sponsor.name}-${case_.title}-${project.name}`
)}
{project.name}
</TableCell>
<TableCell className="text-right">
-
</TableCell>
<TableCell className="text-right">
{formatNumber(
project.partialFee
)}
</TableCell>
<TableCell className="text-right">
{formatNumber(
project.penalty
)}
</TableCell>
</TableRow>

{expanded[
`project-${manager.name}-${client.name}-${sponsor.name}-${case_.title}-${project.name}`
] &&
project.byWorker.map(
(worker: any) => (
<TableRow
key={`${manager.name}-${client.name}-${sponsor.name}-${case_.title}-${project.name}-${worker.name}`}
className="bg-gray-200"
>
<TableCell className="pl-24">
{worker.name}
</TableCell>
<TableCell className="text-right">
{formatNumber(
worker.hours
)}
</TableCell>
<TableCell className="text-right">
{formatNumber(
worker.partialFee
)}
</TableCell>
<TableCell className="text-right">
{formatNumber(
worker.penalty
)}
</TableCell>
</TableRow>
)
)}
</>
)
)}
</>
))}
</>
))}
</>
))}
</>
))}
<TableRow className="font-bold border-t-2 border-gray-300">
<TableCell>Total</TableCell>
<TableCell className="text-right">-</TableCell>
<TableCell className="text-right">-</TableCell>
<TableCell className="text-right">
{formatNumber(kindTotal.penalty)}
</TableCell>
</TableRow>
</TableBody>
</Table>
</div>
</div>
);
})}
</div>
</div>
);
}
44 changes: 44 additions & 0 deletions frontend/src/app/financial/pro-rata/query.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import { gql } from "@apollo/client";

export const PRO_RATA_QUERY = gql`
query ProRata($dateOfInterest: Date!) {
revenueTracking(dateOfInterest: $dateOfInterest) {
month
year
day
proRataInfo {
byKind {
kind
penalty
byAccountManager {
name
penalty
byClient {
name
penalty
bySponsor {
name
penalty
byCase {
title
penalty
byProject {
name
partialFee
penalty
byWorker {
name
hours
partialFee
penalty
}
}
}
}
}
}
}
}
}
}
`;
8 changes: 7 additions & 1 deletion frontend/src/app/navigation.tsx
Original file line number Diff line number Diff line change
@@ -14,6 +14,7 @@ import {
ChartLineIcon,
DollarSignIcon,
TrendingUpIcon,
PercentIcon,
} from "lucide-react";

import { getFlag } from './flags';
@@ -30,7 +31,12 @@ export function getFinancialSidebarItems(userEmail?: string | null) {
title: "Revenue Forecast",
url: "/financial/revenue-forecast",
icon: TrendingUpIcon,
}
},
{
title: "Pro-Rata",
url: "/financial/pro-rata",
icon: PercentIcon,
}
] : []),
];
}