Skip to content

Commit

Permalink
Merge pull request #50 from ElemarJR/improving_revenue_business_logic
Browse files Browse the repository at this point in the history
Improving revenue business logic
  • Loading branch information
ElemarJR authored Nov 27, 2024
2 parents e276519 + 489fc6a commit 90fd3e7
Show file tree
Hide file tree
Showing 18 changed files with 760 additions and 1,303 deletions.
10 changes: 6 additions & 4 deletions backend/src/api/datasets/timesheets.py
Original file line number Diff line number Diff line change
Expand Up @@ -124,13 +124,15 @@ def summarize_by_group(df: pd.DataFrame, group_column: str, name_key: str = "nam

if group_column == 'CaseTitle' and 'workersByTrackingProject' in map:
wdf = df[df['CaseTitle'] == group_value]
workersByTrackingProject = wdf.groupby('ProjectId')['WorkerName'].apply(lambda x: x.unique().tolist()).reset_index()
summary['workers_by_tracking_project'] = [
workersByTrackingProject = wdf.groupby('ProjectId')['WorkerName'].agg(list).reset_index()
project_workers = [
{
'project_id': row['ProjectId'],
'workers': row['WorkerName']
} for _, row in workersByTrackingProject.iterrows()
'workers': sorted(set(row['WorkerName']))
}
for _, row in workersByTrackingProject.iterrows()
]
summary['workers_by_tracking_project'] = project_workers

if group_column == 'CaseTitle' and 'byWorker' in map:
summary['by_worker'] = summarize_by_worker(group_df, map['byWorker'])
Expand Down
33 changes: 15 additions & 18 deletions backend/src/api/inconsistencies.py
Original file line number Diff line number Diff line change
Expand Up @@ -107,7 +107,7 @@ def resolve_inconsistencies(_, info) -> list[Inconsistency]:
# Build mapping of everhour project IDs to cases
everhour_id_to_cases = {}
for case in cases:
if case.is_active and case.everhour_projects_ids:
if case.everhour_projects_ids:
for everhour_id in case.everhour_projects_ids:
if everhour_id not in everhour_id_to_cases:
everhour_id_to_cases[everhour_id] = []
Expand All @@ -121,23 +121,20 @@ def resolve_inconsistencies(_, info) -> list[Inconsistency]:
}

if duplicate_everhour_ids:
details = []
for everhour_id, cases_list in duplicate_everhour_ids.items():
case_names = [case.title for case in cases_list]
details.append(f"Everhour project ID {everhour_id} is used in cases: {', '.join(case_names)}")

result.append(Inconsistency(
'Duplicate Everhour Project IDs',
f'{len(duplicate_everhour_ids)} Everhour project ID(s) are used in multiple cases:\n' + '\n'.join(details)
))

# for case in cases:
# if case.is_active and not case.pre_contracted_value:
# for tp in case.tracker_info:
# if tp.billing and tp.billing.type == 'fixed_fee':
# result.append(Inconsistency(
# 'Case not marked as "pre-contracted" has a fixed fee tracking project',
# f'Case "{case.title}" has no pre-contracted value set, but has a fixed billing type tracking project.'
# ))
cases_titles = [case.title for case in cases_list]
result.append(Inconsistency(
f'{everhour_id} referenced in multiple cases',
f'Cases:\n' + '; '.join(cases_titles)
))

for case in cases:
if case.is_active and (not case.start_of_contract or not case.end_of_contract):
for project in case.tracker_info:
if project.billing and project.billing.type == 'fixed_fee' and project.budget and project.budget.period == 'general':
result.append(Inconsistency(
'Missing contract dates for fixed fee project',
f'The case "{case.title}" contains a fixed fee project "{project.name}" but is missing contract start/end dates. These dates are required to properly distribute the fixed fee across months.'
))

return result
64 changes: 55 additions & 9 deletions backend/src/models/analytics/revenue_tracking.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,10 @@

from datetime import date, datetime
from models.helpers.slug import slugify
from models.domain.cases import Case
import pandas as pd
from dataclasses import dataclass

INTERNAL_KIND = "Internal"
PROJECT_KINDS = ["consulting", "handsOn", "squad"]
NA_VALUE = "N/A"
Expand Down Expand Up @@ -51,7 +53,7 @@ def _compute_revenue_tracking_base(date_of_interest: date, process_project):
if case.find_client_name(globals.omni_models.clients) == client_name and case.sponsor == sponsor_name:
by_project = []
for project in case.tracker_info:
project_data = process_project(project, df)
project_data = process_project(case, project, df)
if project_data:
by_project.append(project_data)

Expand Down Expand Up @@ -123,7 +125,7 @@ def _compute_revenue_tracking_base(date_of_interest: date, process_project):
}

def compute_regular_revenue_tracking(date_of_interest: date):
def process_project(project, timesheet_df):
def process_project(_, project, timesheet_df):
if project.rate and project.rate.rate:
project_df = timesheet_df[timesheet_df["ProjectId"] == project.id]
if len(project_df) > 0:
Expand All @@ -140,14 +142,58 @@ def process_project(project, timesheet_df):
return _compute_revenue_tracking_base(date_of_interest, process_project)

def compute_pre_contracted_revenue_tracking(date_of_interest: date):
def process_project(project, _):
def process_project(case: Case, project, timesheet_df: pd.DataFrame):
if project.billing and project.billing.fee and project.billing.fee != 0:
return {
"kind": project.kind,
"name": project.name,
"fee": project.billing.fee / 100,
"fixed": True
}
if project.budget and project.budget.period == 'general':

if not case.start_of_contract:
print(f'--> {project.name} has no start or end of contract')

d = timesheet_df[timesheet_df["Date"].notna()]["Date"].iloc[0]
m = d.month
y = d.year

start = case.start_of_contract.replace(day=1)
end = case.end_of_contract
if end is None:
end = start

in_contract = start.year <= y <= end.year

if in_contract and y == start.year:
in_contract = m >= start.month

if in_contract and y == end.year:
in_contract = m <= end.month

if not in_contract:
return None

if start.year == end.year:
number_of_months = end.month - start.month + 1
else:
months_on_start_year = 12 - start.month + 1
months_on_end_year = end.month
if end.year - start.year > 1:
number_of_months = months_on_start_year + months_on_end_year + (end.year - start.year - 1) * 12
else:
number_of_months = months_on_start_year + months_on_end_year

fee = project.billing.fee / 100 / number_of_months

return {
"kind": project.kind,
"name": project.name,
"fee": fee,
"fixed": True
}
else:
return {
"kind": project.kind,
"name": project.name,
"fee": project.billing.fee / 100,
"fixed": True
}
return None

return _compute_revenue_tracking_base(date_of_interest, process_project)
Expand Down
1 change: 1 addition & 0 deletions frontend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
"@radix-ui/react-alert-dialog": "^1.1.2",
"@radix-ui/react-avatar": "^1.1.1",
"@radix-ui/react-checkbox": "^1.1.2",
"@radix-ui/react-collapsible": "^1.1.1",
"@radix-ui/react-dialog": "^1.1.2",
"@radix-ui/react-dropdown-menu": "^2.1.2",
"@radix-ui/react-hover-card": "^1.1.2",
Expand Down
173 changes: 173 additions & 0 deletions frontend/src/app/about-us/clients/[slug]/AllocationSection.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,173 @@
"use client";

import React from "react";
import { Stat } from "@/app/components/analytics/stat";
import DatasetSelector from "@/app/analytics/datasets/DatasetSelector";
import TopWorkers from "@/app/components/panels/TopWorkers";
import TopSponsors from "@/app/components/panels/TopSponsors";
import { CasesTable } from "./CasesTable";
import { Divider } from "@/components/catalyst/divider";
import SectionHeader from "@/components/SectionHeader";
import { CasesGallery } from "../../cases/CasesGallery";

interface AllocationSectionProps {
selectedDataset: string;
onDatasetSelect: (value: string) => void;
timesheetData: any;
timesheetLoading: boolean;
timesheetError: any;
selectedStat: string;
handleStatClick: (statName: string) => void;
}

export function AllocationSection({
selectedDataset,
onDatasetSelect,
timesheetData,
timesheetLoading,
timesheetError,
selectedStat,
handleStatClick,
}: AllocationSectionProps) {
const getStatClassName = (statName: string) => {
return `cursor-pointer transition-all duration-300 ${
selectedStat === statName
? "ring-2 ring-black shadow-lg scale-105"
: "hover:scale-102"
}`;
};

const timesheet = timesheetData?.timesheet;

const filteredCases =
timesheet?.byCase?.filter((caseData: any) => {
switch (selectedStat) {
case "consulting":
return caseData.totalConsultingHours > 0;
case "handsOn":
return caseData.totalHandsOnHours > 0;
case "squad":
return caseData.totalSquadHours > 0;
case "internal":
return caseData.totalInternalHours > 0;
case "total":
return caseData.totalHours > 0;
default:
return true;
}
}) || [];

return (
<>
<SectionHeader title="ALLOCATION" subtitle="" />
<div className="px-2">
<div className="mb-4">
<DatasetSelector
selectedDataset={selectedDataset}
onDatasetSelect={onDatasetSelect}
/>
</div>

{timesheetLoading ? (
<p>Loading timesheet data...</p>
) : timesheetError ? (
<p>Error loading timesheet: {timesheetError.message}</p>
) : (
<>
<div className="grid sm:grid-cols-2 lg:grid-cols-5 gap-4 mb-4">
<div
className={`${getStatClassName("total")} transform`}
onClick={() => handleStatClick("total")}
>
<Stat
title="Total Hours"
value={timesheet?.totalHours?.toString() || "0"}
/>
</div>
<div
className={`${getStatClassName("consulting")} transform`}
onClick={() => handleStatClick("consulting")}
>
<Stat
title="Consulting Hours"
value={
timesheet?.byKind?.consulting?.totalHours?.toString() || "0"
}
color="#F59E0B"
total={timesheet?.totalHours}
/>
</div>
<div
className={`${getStatClassName("handsOn")} transform`}
onClick={() => handleStatClick("handsOn")}
>
<Stat
title="Hands-On Hours"
value={
timesheet?.byKind?.handsOn?.totalHours?.toString() || "0"
}
color="#8B5CF6"
total={timesheet?.totalHours}
/>
</div>
<div
className={`${getStatClassName("squad")} transform`}
onClick={() => handleStatClick("squad")}
>
<Stat
title="Squad Hours"
value={
timesheet?.byKind?.squad?.totalHours?.toString() || "0"
}
color="#3B82F6"
total={timesheet?.totalHours}
/>
</div>
<div
className={`${getStatClassName("internal")} transform`}
onClick={() => handleStatClick("internal")}
>
<Stat
title="Internal Hours"
value={
timesheet?.byKind?.internal?.totalHours?.toString() || "0"
}
color="#10B981"
total={timesheet?.totalHours}
/>
</div>
</div>

<div className="mt-6">
<CasesTable filteredCases={filteredCases} />
</div>

<div className="grid grid-cols-1 md:grid-cols-2 gap-8 mt-8 mb-8">
<TopWorkers
workerData={timesheet?.byWorker || []}
selectedStat={selectedStat}
totalHours={timesheet?.totalHours || 0}
/>
<TopSponsors
sponsorData={timesheet?.bySponsor || []}
selectedStat={selectedStat}
totalHours={timesheet?.totalHours || 0}
/>
</div>

<SectionHeader
title="Cases"
subtitle={filteredCases.length + " active"}
/>
<div className="px-2">
<CasesGallery
filteredCases={filteredCases}
timesheetData={timesheet}
/>
</div>
</>
)}
</div>
</>
);
}
Loading

0 comments on commit 90fd3e7

Please sign in to comment.