Skip to content

Commit

Permalink
Merge pull request #102 from ElemarJR/forecast_for_consultants
Browse files Browse the repository at this point in the history
Forecast for consultants
  • Loading branch information
ElemarJR authored Jan 14, 2025
2 parents 4267b30 + 67c7477 commit 61f834f
Show file tree
Hide file tree
Showing 5 changed files with 488 additions and 17 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
import { Table, TableBody, TableRow, TableCell, TableHeader, TableHead } from "@/components/ui/table";
import SectionHeader from "@/components/SectionHeader";
import React from "react";

interface Client {
name: string;
hoursNeeded: number;
}

interface AllocationOpportunitiesTableProps {
clients: Client[];
sortConfig: { key: string; direction: "asc" | "desc" };
onRequestSort: (key: string) => void;
}

export function AllocationOpportunitiesTable({
clients,
sortConfig,
onRequestSort,
}: AllocationOpportunitiesTableProps) {
const sortedClients = React.useMemo(() => {
if (!sortConfig.key) return clients;

return [...clients].sort((a, b) => {
if (sortConfig.key === "name") {
return sortConfig.direction === "asc"
? a.name.localeCompare(b.name)
: b.name.localeCompare(a.name);
}

// For hoursNeeded
const aValue = a.hoursNeeded;
const bValue = b.hoursNeeded;

if (aValue < bValue) {
return sortConfig.direction === "asc" ? -1 : 1;
}
if (aValue > bValue) {
return sortConfig.direction === "asc" ? 1 : -1;
}
return 0;
});
}, [clients, sortConfig]);

const renderSortHeader = (key: string, label: string, className: string = "") => (
<TableHead
onClick={() => onRequestSort(key)}
className={`text-right cursor-pointer hover:bg-gray-100 ${className}`}
>
{label}
{sortConfig.key === key && (sortConfig.direction === "asc" ? " ↑" : " ↓")}
</TableHead>
);

return (
<div className="mt-8">
<SectionHeader title="Allocation Opportunities" subtitle="Clients needing additional hours" />
<div className="px-2">
<Table>
<TableHeader className="bg-gray-50">
<TableRow>
<TableHead className="w-[50px] text-center">#</TableHead>
<TableHead className="border-r border-gray-400">Client</TableHead>
{renderSortHeader("hoursNeeded", "Hours Needed", "w-[120px] border-r border-gray-400")}
</TableRow>
</TableHeader>
<TableBody>
{sortedClients.map((client, index) => (
<TableRow key={client.name} className="h-[57px]">
<TableCell className="text-right pr-4 text-gray-500 text-[10px]">
{index + 1}
</TableCell>
<TableCell className="border-r border-gray-400">
<div className="flex items-center gap-2">
<span>{client.name}</span>
</div>
</TableCell>
<TableCell className="text-right border-r border-gray-400">
{!isNaN(client.hoursNeeded) ? `${client.hoursNeeded.toFixed(1)}h` : '-'}
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</div>
</div>
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

import { useParams } from "next/navigation";
import { useQuery } from "@apollo/client";
import { GET_CONSULTANT, Consultant } from "./queries";
import { GET_CONSULTANT, Consultant, QueryResponse } from "./queries";
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
import { useState } from "react";
import SectionHeader from "@/components/SectionHeader";
Expand All @@ -15,6 +15,7 @@ import Link from "next/link";
import { Heading } from "@/components/catalyst/heading";
import { Card } from "@/components/ui/card";
import OneYearAllocation from "@/app/components/OneYearAllocation";
import { AllocationOpportunitiesTable } from "./AllocationOpportunitiesTable";

interface ClientSummary {
client: string;
Expand Down Expand Up @@ -149,6 +150,12 @@ const SummarySection = ({
);
};

interface ClientGap {
name: string;
gap: number;
hoursNeeded: number;
}

export default function ConsultantPage() {
const params = useParams();
const slug = params.slug as string;
Expand All @@ -157,6 +164,12 @@ export default function ConsultantPage() {
new Date(new Date().getFullYear(), new Date().getMonth() - 1, 1)
);

// Add sorting state
const [sortConfig, setSortConfig] = useState<{ key: string; direction: "asc" | "desc" }>({
key: "hoursNeeded",
direction: "desc"
});

// Previous month states
const [selectedDayPrev, setSelectedDayPrev] = useState<number | null>(null);
const [selectedRowPrev, setSelectedRowPrev] = useState<number | null>(null);
Expand Down Expand Up @@ -211,9 +224,7 @@ export default function ConsultantPage() {
const currentMonthDataset = getVisibleDates(selectedDateCurr);
const previousMonthDataset = getVisibleDates(selectedDatePrev);

const { data, loading, error } = useQuery<{
consultantOrEngineer: Consultant;
}>(GET_CONSULTANT, {
const { data, loading, error } = useQuery<QueryResponse>(GET_CONSULTANT, {
variables: {
slug,
dataset1: previousMonthDataset,
Expand Down Expand Up @@ -366,6 +377,29 @@ export default function ConsultantPage() {
return [...createSummaries("client"), ...createSummaries("sponsor")];
};

const handleRequestSort = (key: string) => {
setSortConfig((prevConfig) => ({
key,
direction:
prevConfig.key === key && prevConfig.direction === "asc"
? "desc"
: "asc",
}));
};

const clientsWithGap = data?.forecast?.byKind?.consulting?.byClient
?.filter(client => client.projected < client.expected)
?.map(client => {
const hourlyRate = client.inAnalysis / client.inAnalysisConsultingHours;
const gap = client.expected - client.projected;
const hoursNeeded = gap / hourlyRate;

return {
name: client.name,
hoursNeeded,
};
}) || [];

return (
<div className="w-full p-2">
<div className="bg-white p-6 mb-8">
Expand Down Expand Up @@ -555,6 +589,14 @@ export default function ConsultantPage() {
</div>
</div>
</div>

{clientsWithGap.length > 0 && (
<AllocationOpportunitiesTable
clients={clientsWithGap}
sortConfig={sortConfig}
onRequestSort={handleRequestSort}
/>
)}
</div>
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,21 @@ export const GET_CONSULTANT = gql`
}
}
}
forecast {
byKind {
consulting {
byClient {
name
slug
inAnalysisConsultingHours
inAnalysis
projected
expected
}
}
}
}
}
`;

Expand Down Expand Up @@ -168,3 +183,21 @@ export interface Consultant {
}>;
};
}

export interface QueryResponse {
consultantOrEngineer: Consultant;
forecast: {
byKind: {
consulting: {
byClient: Array<{
name: string;
slug: string;
inAnalysisConsultingHours: number;
inAnalysis: number;
projected: number;
expected: number;
}>;
};
};
};
}
Loading

0 comments on commit 61f834f

Please sign in to comment.