From 617ccd2517748804689ccd3a284bd33018f4f851 Mon Sep 17 00:00:00 2001 From: Elemar Rodrigues Severo Junior Date: Mon, 2 Dec 2024 07:23:27 +0100 Subject: [PATCH 01/10] Add appointments summary to GraphQL schema and resolver Extend the GraphQL schema to include an `appointments` field with a new `AppointmentSummary` type, providing detailed appointment information. Implement a corresponding function in the timesheets resolver to process and return appointment data when requested. This update improves data granularity and supports expanded query capabilities. --- backend/src/api/datasets/schema.graphql | 38 +++++++++++++++++++++++++ backend/src/api/datasets/timesheets.py | 30 +++++++++++++++++++ 2 files changed, 68 insertions(+) diff --git a/backend/src/api/datasets/schema.graphql b/backend/src/api/datasets/schema.graphql index fd015cca..0b0b0f28 100644 --- a/backend/src/api/datasets/schema.graphql +++ b/backend/src/api/datasets/schema.graphql @@ -136,9 +136,47 @@ type TimesheetSummary implements ISummary { byWeek: [WeekSummary!]! byOffer: [NamedSummary!]! + appointments: [AppointmentSummary!]! + filterableFields: [FilterableField]! } +type AppointmentSummary { + createdAt: Date! + date: Date! + + dayOfWeek: String! + month: String! + year: String! + yearMonth: String! + + isSquad: Boolean! + isEximiaco: Boolean! + + week: String! + + timeInHs: Float! + + kind: String! + createdAtWeek: String! + + correctness: String! + isLte: Boolean! + + workerName: String! + workerSlug: String! + + caseTitle: String! + + sponsor: String! + clientName: String! + + accountManagerName: String! + accountManagerSlug: String! + + productsOrServices: String! +} + type KindSummary { internal: OneKindSummary consulting: OneKindSummary diff --git a/backend/src/api/datasets/timesheets.py b/backend/src/api/datasets/timesheets.py index 527187fe..a01d6fcb 100644 --- a/backend/src/api/datasets/timesheets.py +++ b/backend/src/api/datasets/timesheets.py @@ -7,6 +7,7 @@ from graphql import GraphQLResolveInfo from api.utils.fields import build_fields_map, get_requested_fields_from, get_selections_from_info +from models.helpers.slug import slugify import globals @@ -281,6 +282,35 @@ def compute_timesheet(map, slug: str=None, kind: str="ALL", filters = None): if 'byOffer' in requested_fields: result['by_offer'] = summarize_by_offer(df, map['byOffer']) + + if 'appointments' in requested_fields: + def summarize_appointments(df, fields): + appointments = [] + + fields_slugs = {} + for field in fields: + # Convert camelCase to snake_case + parts = [] + current_part = field[0].lower() + for c in field[1:]: + if c.isupper(): + parts.append(current_part) + current_part = c.lower() + else: + current_part += c + parts.append(current_part) + field_slug = '_'.join(parts) + fields_slugs[field] = field_slug + + for _, row in df.iterrows(): + appointment = {} + for field in fields: + field_slug = fields_slugs[field] + appointment[field_slug] = row[field] + appointments.append(appointment) + return appointments + + result['appointments'] = summarize_appointments(df, globals.omni_datasets.timesheets.get_all_fields()) return result From 74a979f6af2b99d7a74f965c230d5ec6f5c4a8c5 Mon Sep 17 00:00:00 2001 From: Elemar Rodrigues Severo Junior Date: Tue, 3 Dec 2024 10:16:50 -0300 Subject: [PATCH 02/10] Update schema and dataset models for appointment details Replace `AppointmentSummary` with `Appointment` in the GraphQL schema to include the `comment` field for richer appointment data. Enhance the dataset model by adding logic to process appointment types and include a `Comment` field for additional context. --- backend/src/api/datasets/schema.graphql | 5 +++-- backend/src/models/datasets/timesheet_dataset.py | 3 +++ 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/backend/src/api/datasets/schema.graphql b/backend/src/api/datasets/schema.graphql index 0b0b0f28..c3b19540 100644 --- a/backend/src/api/datasets/schema.graphql +++ b/backend/src/api/datasets/schema.graphql @@ -136,14 +136,15 @@ type TimesheetSummary implements ISummary { byWeek: [WeekSummary!]! byOffer: [NamedSummary!]! - appointments: [AppointmentSummary!]! + appointments: [Appointment!]! filterableFields: [FilterableField]! } -type AppointmentSummary { +type Appointment { createdAt: Date! date: Date! + comment: String dayOfWeek: String! month: String! diff --git a/backend/src/models/datasets/timesheet_dataset.py b/backend/src/models/datasets/timesheet_dataset.py index dd6c762d..5bb58b02 100644 --- a/backend/src/models/datasets/timesheet_dataset.py +++ b/backend/src/models/datasets/timesheet_dataset.py @@ -102,6 +102,8 @@ def enrich_row(row): row['case_title'] = case.title row['sponsor'] = case.sponsor row['case'] = f"{case.title}" + + if row['kind'] == 'consulting' and case # Obter produtos ou serviços associados products_or_services = [get_offer_name(offer) for offer in case.offers_ids] @@ -143,6 +145,7 @@ def get_all_fields(self): 'DayOfWeek', 'Month', 'Year', + 'Comment', 'YearMonth', 'UserId', 'Time', From 1f68f920435086613c9af6321b6d7a105c718c81 Mon Sep 17 00:00:00 2001 From: Elemar Rodrigues Severo Junior Date: Tue, 3 Dec 2024 11:01:03 -0300 Subject: [PATCH 03/10] Improve stat type selection and formatting in calendar Introduced calculation of available stat types dynamically, picking the first available type by default. Adjusted the formatting of hours display to one decimal place for more precision. Simplified the rendering of tab triggers to reflect only available stat types, enhancing UI responsiveness. --- .../src/app/components/AllocationCalendar.tsx | 81 ++++++++++--------- 1 file changed, 43 insertions(+), 38 deletions(-) diff --git a/frontend/src/app/components/AllocationCalendar.tsx b/frontend/src/app/components/AllocationCalendar.tsx index 6ad29709..7ebd6ee3 100644 --- a/frontend/src/app/components/AllocationCalendar.tsx +++ b/frontend/src/app/components/AllocationCalendar.tsx @@ -91,7 +91,7 @@ const WeekTotalCell = ({ {statHours > 0 && ( <> {rowPercentage && {rowPercentage}%} - {statHours}h + {statHours.toFixed(1)}h {columnPercentage && {columnPercentage}%} )} @@ -173,7 +173,32 @@ export function AllocationCalendar({ setIsAllSelected, timesheet }: AllocationCalendarProps) { - const [selectedStatType, setSelectedStatType] = useState('consulting'); + // Calculate total hours for each type + const totalHours = { + consulting: 0, + handsOn: 0, + squad: 0, + internal: 0 + }; + + timesheet.byDate.forEach((day: { + totalConsultingHours?: number; + totalHandsOnHours?: number; + totalSquadHours?: number; + totalInternalHours?: number; + }) => { + totalHours.consulting += day.totalConsultingHours || 0; + totalHours.handsOn += day.totalHandsOnHours || 0; + totalHours.squad += day.totalSquadHours || 0; + totalHours.internal += day.totalInternalHours || 0; + }); + + // Get available stat types (those with hours > 0) + const availableStatTypes = Object.entries(totalHours) + .filter(([_, hours]) => hours > 0) + .map(([type]) => type as StatType); + + const [selectedStatType, setSelectedStatType] = useState(availableStatTypes[0] || 'consulting'); const getHoursForDate = (date: Date) => { const formattedDate = `${date.getFullYear()}-${(date.getMonth() + 1).toString().padStart(2, '0')}-${date.getDate().toString().padStart(2, '0')}`; @@ -250,26 +275,6 @@ export function AllocationCalendar({ "July", "August", "September", "October", "November", "December" ]; - // Calculate total hours for each type - const totalHours = { - consulting: 0, - handsOn: 0, - squad: 0, - internal: 0 - }; - - timesheet.byDate.forEach((day: { - totalConsultingHours?: number; - totalHandsOnHours?: number; - totalSquadHours?: number; - totalInternalHours?: number; - }) => { - totalHours.consulting += day.totalConsultingHours || 0; - totalHours.handsOn += day.totalHandsOnHours || 0; - totalHours.squad += day.totalSquadHours || 0; - totalHours.internal += day.totalInternalHours || 0; - }); - return (
@@ -290,22 +295,22 @@ export function AllocationCalendar({
- - - {totalHours.consulting > 0 && ( - handleStatTypeChange('consulting')} className="flex-1">Consulting - )} - {totalHours.handsOn > 0 && ( - handleStatTypeChange('handsOn')} className="flex-1">Hands-on - )} - {totalHours.squad > 0 && ( - handleStatTypeChange('squad')} className="flex-1">Squad - )} - {totalHours.internal > 0 && ( - handleStatTypeChange('internal')} className="flex-1">Internal - )} - - + {availableStatTypes.length > 0 && ( + + + {availableStatTypes.map(type => ( + handleStatTypeChange(type)} + className="flex-1" + > + {type === 'handsOn' ? 'Hands-on' : type.charAt(0).toUpperCase() + type.slice(1)} + + ))} + + + )}
{/* Header row with day names */} From 2bd1f994d4e8ff47d085c83e84f5a9ada88cabe8 Mon Sep 17 00:00:00 2001 From: Elemar Rodrigues Severo Junior Date: Tue, 3 Dec 2024 11:01:19 -0300 Subject: [PATCH 04/10] Add side-by-side calendar analysis for client datasets Introduce functionality to display and compare two different month's timesheet data in a side-by-side layout using AllocationCalendar components. This change allows users to select and view data for both current and previous months for detailed client analysis. Additionally, update GraphQL query to retrieve timesheet data for both datasets, enhancing the client's data visualization capabilities. --- .../src/models/datasets/timesheet_dataset.py | 2 - .../src/app/about-us/clients/[slug]/page.tsx | 159 ++++++++++++------ .../app/about-us/clients/[slug]/queries.ts | 25 ++- 3 files changed, 127 insertions(+), 59 deletions(-) diff --git a/backend/src/models/datasets/timesheet_dataset.py b/backend/src/models/datasets/timesheet_dataset.py index 5bb58b02..744e7032 100644 --- a/backend/src/models/datasets/timesheet_dataset.py +++ b/backend/src/models/datasets/timesheet_dataset.py @@ -102,8 +102,6 @@ def enrich_row(row): row['case_title'] = case.title row['sponsor'] = case.sponsor row['case'] = f"{case.title}" - - if row['kind'] == 'consulting' and case # Obter produtos ou serviços associados products_or_services = [get_offer_name(offer) for offer in case.offers_ids] diff --git a/frontend/src/app/about-us/clients/[slug]/page.tsx b/frontend/src/app/about-us/clients/[slug]/page.tsx index 349f0bf6..cf3fef7e 100644 --- a/frontend/src/app/about-us/clients/[slug]/page.tsx +++ b/frontend/src/app/about-us/clients/[slug]/page.tsx @@ -8,6 +8,8 @@ import { ClientHeader } from "./ClientHeader"; import { Divider } from "@/components/catalyst/divider"; import { CasesGallery } from "../../cases/CasesGallery"; import { AllocationSection } from "./AllocationSection"; +import { AllocationCalendar } from "@/app/components/AllocationCalendar"; +import SectionHeader from "@/components/SectionHeader"; export default function ClientPage() { const { slug } = useParams(); @@ -18,12 +20,72 @@ export default function ClientPage() { ); const [selectedStat, setSelectedStat] = useState("total"); + // Calendar states for current month + const [selectedDateCurr, setSelectedDateCurr] = useState(new Date()); + const [selectedDayCurr, setSelectedDayCurr] = useState(null); + const [selectedRowCurr, setSelectedRowCurr] = useState(null); + const [selectedColumnCurr, setSelectedColumnCurr] = useState(null); + const [isAllSelectedCurr, setIsAllSelectedCurr] = useState(false); + + // Calendar states for previous month + const [selectedDatePrev, setSelectedDatePrev] = useState( + new Date(new Date().getFullYear(), new Date().getMonth() - 1, 1) + ); + const [selectedDayPrev, setSelectedDayPrev] = useState(null); + const [selectedRowPrev, setSelectedRowPrev] = useState(null); + const [selectedColumnPrev, setSelectedColumnPrev] = useState(null); + const [isAllSelectedPrev, setIsAllSelectedPrev] = useState(false); + + // Calculate visible dates for both datasets + const getVisibleDates = (date: Date) => { + const currentMonth = date.getMonth(); + const currentYear = date.getFullYear(); + + const firstDayOfMonth = new Date(currentYear, currentMonth, 1); + const lastDayOfMonth = new Date(currentYear, currentMonth + 1, 0); + + const startingDayOfWeek = firstDayOfMonth.getDay(); + const firstVisibleDate = new Date( + currentYear, + currentMonth, + -startingDayOfWeek + 1 + ); + + const daysInMonth = lastDayOfMonth.getDate(); + const totalDays = startingDayOfWeek + daysInMonth; + const weeksNeeded = Math.ceil(totalDays / 7); + const remainingDays = weeksNeeded * 7 - (startingDayOfWeek + daysInMonth); + + const lastVisibleDate = new Date( + currentYear, + currentMonth + 1, + remainingDays + ); + + const formatDate = (date: Date) => { + return `${date.getDate().toString().padStart(2, "0")}-${( + date.getMonth() + 1 + ) + .toString() + .padStart(2, "0")}-${date.getFullYear()}`; + }; + + return `${formatDate(firstVisibleDate)}-${formatDate(lastVisibleDate)}`; + }; + + const currentMonthDataset = getVisibleDates(selectedDateCurr); + const previousMonthDataset = getVisibleDates(selectedDatePrev); + const { data: clientData, loading: clientLoading, error: clientError, } = useQuery(GET_CLIENT_BY_SLUG, { - variables: { slug }, + variables: { + slug, + dataset1: previousMonthDataset, + dataset2: currentMonthDataset + }, }); const { @@ -54,64 +116,52 @@ export default function ClientPage() { setSelectedStat(statName === selectedStat ? "total" : statName); }; - 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 getStatusColor = ( - status: string - ): "zinc" | "rose" | "amber" | "lime" => { - switch (status) { - case "Critical": - return "rose"; - case "Requires attention": - return "amber"; - case "All right": - return "lime"; - default: - return "zinc"; - } - }; - - const getDaysSinceUpdate = (updateDate: string | null) => { - if (!updateDate) return null; - const update = new Date(updateDate); - const today = new Date(); - const diffTime = today.getTime() - update.getTime(); - const diffDays = Math.ceil(diffTime / (1000 * 60 * 60 * 24)); - return diffDays; - }; - if (clientLoading) return

Loading client data...

; if (clientError) return

Error loading client: {clientError.message}

; + if (!clientData?.client) return

No client data found

; - 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; - } - }) || []; + const { timesheet1, timesheet2 } = clientData.client; return (
- + + + + +
+
+
+ +
+
+ +
+
+
- - -
); } diff --git a/frontend/src/app/about-us/clients/[slug]/queries.ts b/frontend/src/app/about-us/clients/[slug]/queries.ts index cb63585f..9cf0a94a 100644 --- a/frontend/src/app/about-us/clients/[slug]/queries.ts +++ b/frontend/src/app/about-us/clients/[slug]/queries.ts @@ -1,11 +1,33 @@ import { gql } from "@apollo/client"; export const GET_CLIENT_BY_SLUG = gql` - query GetClientBySlug($slug: String!) { + query GetClientBySlug($slug: String!, $dataset1: String!, $dataset2: String!) { client(slug: $slug) { name logoUrl isStrategic + + timesheet1: timesheet(slug: $dataset1) { + byDate { + date + totalHours + totalConsultingHours + totalHandsOnHours + totalSquadHours + totalInternalHours + } + } + + timesheet2: timesheet(slug: $dataset2) { + byDate { + date + totalHours + totalConsultingHours + totalHandsOnHours + totalSquadHours + totalInternalHours + } + } } } `; @@ -42,6 +64,7 @@ export const GET_CLIENT_TIMESHEET = gql` } } } + timesheet(slug: $datasetSlug, filters: [{ field: "ClientName", selectedValues: [$clientName] }]) { uniqueClients uniqueCases From 0e517243a7a0208a19187c9b9ff33a7bb735ad95 Mon Sep 17 00:00:00 2001 From: Elemar Rodrigues Severo Junior Date: Wed, 4 Dec 2024 11:41:10 -0300 Subject: [PATCH 05/10] Enhance timesheet with client summary feature Added functionality to display a summary of client appointments in the consultant's page for both previous and current timesheets. The update allows users to filter summaries based on a selected statistic type, providing detailed information like total hours and appointment details per client. This enhancement improves the user interface with an organized display of client data and allows more informative insights into time allocation. --- .../consultants-and-engineers/[slug]/page.tsx | 193 +++++++++++++++++- .../[slug]/queries.ts | 28 +++ .../src/app/components/AllocationCalendar.tsx | 8 +- 3 files changed, 220 insertions(+), 9 deletions(-) diff --git a/frontend/src/app/about-us/consultants-and-engineers/[slug]/page.tsx b/frontend/src/app/about-us/consultants-and-engineers/[slug]/page.tsx index ce18eceb..f8a14e30 100644 --- a/frontend/src/app/about-us/consultants-and-engineers/[slug]/page.tsx +++ b/frontend/src/app/about-us/consultants-and-engineers/[slug]/page.tsx @@ -7,6 +7,69 @@ import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"; import { useState } from "react"; import SectionHeader from "@/components/SectionHeader"; import { AllocationCalendar } from "@/app/components/AllocationCalendar"; +import { Sheet, SheetContent, SheetHeader, SheetTitle, SheetTrigger } from "@/components/ui/sheet"; +import { StatType } from "@/app/constants/colors"; +import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table"; + +interface ClientSummary { + client: string; + hours: number; + appointments: any[]; +} + +const ClientSummarySection = ({ + summaries, + selectedStatType +}: { + summaries: ClientSummary[] | null; + selectedStatType: StatType; +}) => { + if (!summaries) return null; + + return ( +
+

Client Summary:

+ {summaries.map((summary) => ( +
+ {summary.client} +
+ {summary.hours.toFixed(1)}h + + + Details + + + + {summary.client} - {selectedStatType.charAt(0).toUpperCase() + selectedStatType.slice(1)} Hours + +
+ + + + Date + Hours + Comment + + + + {summary.appointments.map((apt, idx) => ( + + {apt.date} + {apt.timeInHs}h + {apt.comment} + + ))} + +
+
+
+
+
+
+ ))} +
+ ); +}; export default function ConsultantPage() { const params = useParams(); @@ -19,18 +82,16 @@ export default function ConsultantPage() { // Previous month states const [selectedDayPrev, setSelectedDayPrev] = useState(null); const [selectedRowPrev, setSelectedRowPrev] = useState(null); - const [selectedColumnPrev, setSelectedColumnPrev] = useState( - null - ); + const [selectedColumnPrev, setSelectedColumnPrev] = useState(null); const [isAllSelectedPrev, setIsAllSelectedPrev] = useState(false); + const [selectedStatTypePrev, setSelectedStatTypePrev] = useState('consulting'); // Current month states const [selectedDayCurr, setSelectedDayCurr] = useState(null); const [selectedRowCurr, setSelectedRowCurr] = useState(null); - const [selectedColumnCurr, setSelectedColumnCurr] = useState( - null - ); + const [selectedColumnCurr, setSelectedColumnCurr] = useState(null); const [isAllSelectedCurr, setIsAllSelectedCurr] = useState(false); + const [selectedStatTypeCurr, setSelectedStatTypeCurr] = useState('consulting'); // Calculate visible dates for both datasets const getVisibleDates = (date: Date) => { @@ -89,6 +150,98 @@ export default function ConsultantPage() { const { name, position, photoUrl, timesheet1, timesheet2 } = data.consultantOrEngineer; + const getSelectedClientSummary = (timesheet: any, selectedDay: number | null, selectedRow: number | null, selectedColumn: number | null, isAllSelected: boolean, selectedDate: Date, selectedStatType: StatType) => { + if (!selectedDay && !selectedRow && !selectedColumn && !isAllSelected) return null; + + const clientHours: { [key: string]: { total: number, consulting: number, handsOn: number, squad: number, internal: number } } = {}; + const clientAppointments: { [key: string]: any[] } = {}; + + timesheet.appointments.forEach((appointment: { + date: string; + clientName: string; + timeInHs: number; + comment: string; + kind: string; + }) => { + const appointmentDate = new Date(appointment.date); + const dayOfMonth = appointmentDate.getUTCDate(); + const dayOfWeek = appointmentDate.getUTCDay(); + const appointmentMonth = appointmentDate.getUTCMonth(); + + const firstDayOfMonth = new Date(appointmentDate.getUTCFullYear(), appointmentDate.getUTCMonth(), 1); + const firstDayOffset = firstDayOfMonth.getUTCDay(); + + const weekIndex = Math.floor((dayOfMonth + firstDayOffset - 1) / 7); + + const shouldInclude = (isAllSelected || + (selectedDay !== null && dayOfMonth === selectedDay) || + (selectedRow !== null && weekIndex === selectedRow) || + (selectedColumn !== null && dayOfWeek === selectedColumn)) && + appointmentMonth === selectedDate.getMonth(); + + if (shouldInclude) { + const clientName = appointment.clientName; + if (!clientHours[clientName]) { + clientHours[clientName] = { + total: 0, + consulting: 0, + handsOn: 0, + squad: 0, + internal: 0 + }; + } + + const dayData = timesheet.byDate.find((d: any) => + new Date(d.date).getUTCDate() === dayOfMonth && + new Date(d.date).getUTCMonth() === appointmentMonth + ); + + if (dayData) { + clientHours[clientName].total += appointment.timeInHs; + + // Map appointment kind to hours type + switch(appointment.kind.toLowerCase()) { + case 'consulting': + clientHours[clientName].consulting += appointment.timeInHs; + break; + case 'handson': + clientHours[clientName].handsOn += appointment.timeInHs; + break; + case 'squad': + clientHours[clientName].squad += appointment.timeInHs; + break; + case 'internal': + clientHours[clientName].internal += appointment.timeInHs; + break; + } + + if (!clientAppointments[clientName]) { + clientAppointments[clientName] = []; + } + + const days = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat']; + clientAppointments[clientName].push({ + ...appointment, + date: `${days[appointmentDate.getUTCDay()]} ${appointmentDate.getUTCDate()}` + }); + } + } + }); + + // Filter based on selected stat type + return Object.entries(clientHours) + .map(([client, hours]) => ({ + client, + hours: hours[selectedStatType], + appointments: clientAppointments[client].filter(apt => + apt.kind.toLowerCase() === selectedStatType || + (selectedStatType === 'handsOn' && apt.kind.toLowerCase() === 'handson') + ) + })) + .filter(summary => summary.hours > 0) + .sort((a, b) => b.hours - a.hours); + }; + return (
@@ -119,6 +272,20 @@ export default function ConsultantPage() { isAllSelected={isAllSelectedPrev} setIsAllSelected={setIsAllSelectedPrev} timesheet={timesheet1} + selectedStatType={selectedStatTypePrev} + setSelectedStatType={setSelectedStatTypePrev} + /> +
@@ -134,6 +301,20 @@ export default function ConsultantPage() { isAllSelected={isAllSelectedCurr} setIsAllSelected={setIsAllSelectedCurr} timesheet={timesheet2} + selectedStatType={selectedStatTypeCurr} + setSelectedStatType={setSelectedStatTypeCurr} + /> +
diff --git a/frontend/src/app/about-us/consultants-and-engineers/[slug]/queries.ts b/frontend/src/app/about-us/consultants-and-engineers/[slug]/queries.ts index 7ea85053..bacabe52 100644 --- a/frontend/src/app/about-us/consultants-and-engineers/[slug]/queries.ts +++ b/frontend/src/app/about-us/consultants-and-engineers/[slug]/queries.ts @@ -8,6 +8,13 @@ export const GET_CONSULTANT = gql` position timesheet1: timesheet(slug: $dataset1) { + appointments { + kind + date + clientName + comment + timeInHs + } byDate { date totalHours @@ -19,6 +26,13 @@ export const GET_CONSULTANT = gql` } timesheet2: timesheet(slug: $dataset2) { + appointments { + kind + date + clientName + comment + timeInHs + } byDate { date totalHours @@ -38,6 +52,13 @@ export interface Consultant { position: string; timesheet1: { + appointments: Array<{ + date: string; + clientName: string; + comment: string; + timeInHs: number; + kind: string; + }>; byDate: Array<{ date: string; totalHours: number; @@ -49,6 +70,13 @@ export interface Consultant { }; timesheet2: { + appointments: Array<{ + date: string; + clientName: string; + comment: string; + timeInHs: number; + kind: string; + }>; byDate: Array<{ date: string; totalHours: number; diff --git a/frontend/src/app/components/AllocationCalendar.tsx b/frontend/src/app/components/AllocationCalendar.tsx index 7ebd6ee3..8ad473e0 100644 --- a/frontend/src/app/components/AllocationCalendar.tsx +++ b/frontend/src/app/components/AllocationCalendar.tsx @@ -158,6 +158,8 @@ interface AllocationCalendarProps { isAllSelected: boolean; setIsAllSelected: (selected: boolean) => void; timesheet: any; + selectedStatType: StatType; + setSelectedStatType: (type: StatType) => void; } export function AllocationCalendar({ @@ -171,7 +173,9 @@ export function AllocationCalendar({ setSelectedColumn, isAllSelected, setIsAllSelected, - timesheet + timesheet, + selectedStatType, + setSelectedStatType }: AllocationCalendarProps) { // Calculate total hours for each type const totalHours = { @@ -198,8 +202,6 @@ export function AllocationCalendar({ .filter(([_, hours]) => hours > 0) .map(([type]) => type as StatType); - const [selectedStatType, setSelectedStatType] = useState(availableStatTypes[0] || 'consulting'); - const getHoursForDate = (date: Date) => { const formattedDate = `${date.getFullYear()}-${(date.getMonth() + 1).toString().padStart(2, '0')}-${date.getDate().toString().padStart(2, '0')}`; const dayData = timesheet.byDate.find((d: { date: string }) => d.date === formattedDate); From f06e7a4795d9bac3a1a349263eae3cb9df936e2c Mon Sep 17 00:00:00 2001 From: Elemar Rodrigues Severo Junior Date: Wed, 4 Dec 2024 16:05:47 -0300 Subject: [PATCH 06/10] Ensure selectedStatType defaults when unavailable If the current selectedStatType is not among the available options, the system now defaults to the first available type. This change prevents potential errors or undefined behaviors when interacting with the AllocationCalendar component. --- frontend/src/app/components/AllocationCalendar.tsx | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/frontend/src/app/components/AllocationCalendar.tsx b/frontend/src/app/components/AllocationCalendar.tsx index 8ad473e0..1d7f2f6b 100644 --- a/frontend/src/app/components/AllocationCalendar.tsx +++ b/frontend/src/app/components/AllocationCalendar.tsx @@ -202,6 +202,11 @@ export function AllocationCalendar({ .filter(([_, hours]) => hours > 0) .map(([type]) => type as StatType); + // If selectedStatType is not available, use first available type + if (!availableStatTypes.includes(selectedStatType) && availableStatTypes.length > 0) { + setSelectedStatType(availableStatTypes[0]); + } + const getHoursForDate = (date: Date) => { const formattedDate = `${date.getFullYear()}-${(date.getMonth() + 1).toString().padStart(2, '0')}-${date.getDate().toString().padStart(2, '0')}`; const dayData = timesheet.byDate.find((d: { date: string }) => d.date === formattedDate); From 80585340455793a528a055c87e281c7f18cd38d8 Mon Sep 17 00:00:00 2001 From: Elemar Rodrigues Severo Junior Date: Wed, 4 Dec 2024 16:06:24 -0300 Subject: [PATCH 07/10] Add worker summary functionality to client pages Introduce a WorkerSummarySection component that displays detailed summaries of workers' hours and appointments, categorized by type. Update queries to include appointment data and enhance state management for better time tracking across current and previous months. --- .../src/app/about-us/clients/[slug]/page.tsx | 199 +++++++++++++++++- .../app/about-us/clients/[slug]/queries.ts | 16 ++ 2 files changed, 207 insertions(+), 8 deletions(-) diff --git a/frontend/src/app/about-us/clients/[slug]/page.tsx b/frontend/src/app/about-us/clients/[slug]/page.tsx index cf3fef7e..c59d38b2 100644 --- a/frontend/src/app/about-us/clients/[slug]/page.tsx +++ b/frontend/src/app/about-us/clients/[slug]/page.tsx @@ -10,6 +10,69 @@ import { CasesGallery } from "../../cases/CasesGallery"; import { AllocationSection } from "./AllocationSection"; import { AllocationCalendar } from "@/app/components/AllocationCalendar"; import SectionHeader from "@/components/SectionHeader"; +import { Sheet, SheetContent, SheetHeader, SheetTitle, SheetTrigger } from "@/components/ui/sheet"; +import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table"; +import { StatType } from "@/app/constants/colors"; + +interface WorkerSummary { + worker: string; + hours: number; + appointments: any[]; +} + +const WorkerSummarySection = ({ + summaries, + selectedStatType +}: { + summaries: WorkerSummary[] | null; + selectedStatType: StatType; +}) => { + if (!summaries) return null; + + return ( +
+

Worker Summary:

+ {summaries.map((summary) => ( +
+ {summary.worker} +
+ {summary.hours.toFixed(1)}h + + + Details + + + + {summary.worker} - {selectedStatType.charAt(0).toUpperCase() + selectedStatType.slice(1)} Hours + +
+ + + + Date + Hours + Comment + + + + {summary.appointments.map((apt, idx) => ( + + {apt.date} + {apt.timeInHs}h + {apt.comment} + + ))} + +
+
+
+
+
+
+ ))} +
+ ); +}; export default function ClientPage() { const { slug } = useParams(); @@ -20,14 +83,7 @@ export default function ClientPage() { ); const [selectedStat, setSelectedStat] = useState("total"); - // Calendar states for current month - const [selectedDateCurr, setSelectedDateCurr] = useState(new Date()); - const [selectedDayCurr, setSelectedDayCurr] = useState(null); - const [selectedRowCurr, setSelectedRowCurr] = useState(null); - const [selectedColumnCurr, setSelectedColumnCurr] = useState(null); - const [isAllSelectedCurr, setIsAllSelectedCurr] = useState(false); - - // Calendar states for previous month + // Previous month states const [selectedDatePrev, setSelectedDatePrev] = useState( new Date(new Date().getFullYear(), new Date().getMonth() - 1, 1) ); @@ -35,6 +91,15 @@ export default function ClientPage() { const [selectedRowPrev, setSelectedRowPrev] = useState(null); const [selectedColumnPrev, setSelectedColumnPrev] = useState(null); const [isAllSelectedPrev, setIsAllSelectedPrev] = useState(false); + const [selectedStatTypePrev, setSelectedStatTypePrev] = useState('consulting'); + + // Current month states + const [selectedDateCurr, setSelectedDateCurr] = useState(new Date()); + const [selectedDayCurr, setSelectedDayCurr] = useState(null); + const [selectedRowCurr, setSelectedRowCurr] = useState(null); + const [selectedColumnCurr, setSelectedColumnCurr] = useState(null); + const [isAllSelectedCurr, setIsAllSelectedCurr] = useState(false); + const [selectedStatTypeCurr, setSelectedStatTypeCurr] = useState('consulting'); // Calculate visible dates for both datasets const getVisibleDates = (date: Date) => { @@ -116,6 +181,96 @@ export default function ClientPage() { setSelectedStat(statName === selectedStat ? "total" : statName); }; + const getSelectedWorkerSummary = (timesheet: any, selectedDay: number | null, selectedRow: number | null, selectedColumn: number | null, isAllSelected: boolean, selectedDate: Date, selectedStatType: StatType) => { + if (!selectedDay && !selectedRow && !selectedColumn && !isAllSelected) return null; + + const workerHours: { [key: string]: { total: number, consulting: number, handsOn: number, squad: number, internal: number } } = {}; + const workerAppointments: { [key: string]: any[] } = {}; + + timesheet.appointments.forEach((appointment: { + date: string; + workerName: string; + timeInHs: number; + comment: string; + kind: string; + }) => { + const appointmentDate = new Date(appointment.date); + const dayOfMonth = appointmentDate.getUTCDate(); + const dayOfWeek = appointmentDate.getUTCDay(); + const appointmentMonth = appointmentDate.getUTCMonth(); + + const firstDayOfMonth = new Date(appointmentDate.getUTCFullYear(), appointmentDate.getUTCMonth(), 1); + const firstDayOffset = firstDayOfMonth.getUTCDay(); + + const weekIndex = Math.floor((dayOfMonth + firstDayOffset - 1) / 7); + + const shouldInclude = (isAllSelected || + (selectedDay !== null && dayOfMonth === selectedDay) || + (selectedRow !== null && weekIndex === selectedRow) || + (selectedColumn !== null && dayOfWeek === selectedColumn)) && + appointmentMonth === selectedDate.getMonth(); + + if (shouldInclude) { + const workerName = appointment.workerName; + if (!workerHours[workerName]) { + workerHours[workerName] = { + total: 0, + consulting: 0, + handsOn: 0, + squad: 0, + internal: 0 + }; + } + + const dayData = timesheet.byDate.find((d: any) => + new Date(d.date).getUTCDate() === dayOfMonth && + new Date(d.date).getUTCMonth() === appointmentMonth + ); + + if (dayData) { + workerHours[workerName].total += appointment.timeInHs; + + switch(appointment.kind.toLowerCase()) { + case 'consulting': + workerHours[workerName].consulting += appointment.timeInHs; + break; + case 'handson': + workerHours[workerName].handsOn += appointment.timeInHs; + break; + case 'squad': + workerHours[workerName].squad += appointment.timeInHs; + break; + case 'internal': + workerHours[workerName].internal += appointment.timeInHs; + break; + } + + if (!workerAppointments[workerName]) { + workerAppointments[workerName] = []; + } + + const days = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat']; + workerAppointments[workerName].push({ + ...appointment, + date: `${days[appointmentDate.getUTCDay()]} ${appointmentDate.getUTCDate()}` + }); + } + } + }); + + return Object.entries(workerHours) + .map(([worker, hours]) => ({ + worker, + hours: hours[selectedStatType], + appointments: workerAppointments[worker].filter(apt => + apt.kind.toLowerCase() === selectedStatType || + (selectedStatType === 'handsOn' && apt.kind.toLowerCase() === 'handson') + ) + })) + .filter(summary => summary.hours > 0) + .sort((a, b) => a.worker.localeCompare(b.worker)); // Changed this line to sort alphabetically by worker name + }; + if (clientLoading) return

Loading client data...

; if (clientError) return

Error loading client: {clientError.message}

; if (!clientData?.client) return

No client data found

; @@ -143,6 +298,20 @@ export default function ClientPage() { isAllSelected={isAllSelectedPrev} setIsAllSelected={setIsAllSelectedPrev} timesheet={timesheet1} + selectedStatType={selectedStatTypePrev} + setSelectedStatType={setSelectedStatTypePrev} + /> +
@@ -158,6 +327,20 @@ export default function ClientPage() { isAllSelected={isAllSelectedCurr} setIsAllSelected={setIsAllSelectedCurr} timesheet={timesheet2} + selectedStatType={selectedStatTypeCurr} + setSelectedStatType={setSelectedStatTypeCurr} + /> +
diff --git a/frontend/src/app/about-us/clients/[slug]/queries.ts b/frontend/src/app/about-us/clients/[slug]/queries.ts index 9cf0a94a..22fda365 100644 --- a/frontend/src/app/about-us/clients/[slug]/queries.ts +++ b/frontend/src/app/about-us/clients/[slug]/queries.ts @@ -8,6 +8,14 @@ export const GET_CLIENT_BY_SLUG = gql` isStrategic timesheet1: timesheet(slug: $dataset1) { + appointments { + kind + date + workerName + clientName + comment + timeInHs + } byDate { date totalHours @@ -19,6 +27,14 @@ export const GET_CLIENT_BY_SLUG = gql` } timesheet2: timesheet(slug: $dataset2) { + appointments { + kind + date + workerName + clientName + comment + timeInHs + } byDate { date totalHours From fee0367785647c42472e2eba441e606a25e96726 Mon Sep 17 00:00:00 2001 From: Elemar Rodrigues Severo Junior Date: Wed, 4 Dec 2024 16:07:08 -0300 Subject: [PATCH 08/10] Add workerName to queries and sort clients alphabetically Include workerName field in appointments query to provide more detailed information. Update client sorting logic to arrange clients alphabetically by their name instead of by hours, improving the readability of the data. --- .../app/about-us/consultants-and-engineers/[slug]/page.tsx | 4 ++-- .../app/about-us/consultants-and-engineers/[slug]/queries.ts | 1 + 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/frontend/src/app/about-us/consultants-and-engineers/[slug]/page.tsx b/frontend/src/app/about-us/consultants-and-engineers/[slug]/page.tsx index f8a14e30..6d35a241 100644 --- a/frontend/src/app/about-us/consultants-and-engineers/[slug]/page.tsx +++ b/frontend/src/app/about-us/consultants-and-engineers/[slug]/page.tsx @@ -228,7 +228,7 @@ export default function ConsultantPage() { } }); - // Filter based on selected stat type + // Filter based on selected stat type and sort alphabetically by client name return Object.entries(clientHours) .map(([client, hours]) => ({ client, @@ -239,7 +239,7 @@ export default function ConsultantPage() { ) })) .filter(summary => summary.hours > 0) - .sort((a, b) => b.hours - a.hours); + .sort((a, b) => a.client.localeCompare(b.client)); }; return ( diff --git a/frontend/src/app/about-us/consultants-and-engineers/[slug]/queries.ts b/frontend/src/app/about-us/consultants-and-engineers/[slug]/queries.ts index bacabe52..97b810d5 100644 --- a/frontend/src/app/about-us/consultants-and-engineers/[slug]/queries.ts +++ b/frontend/src/app/about-us/consultants-and-engineers/[slug]/queries.ts @@ -11,6 +11,7 @@ export const GET_CONSULTANT = gql` appointments { kind date + workerName clientName comment timeInHs From b7bca43ee067fb514b4212d2ac91b452d1905916 Mon Sep 17 00:00:00 2001 From: Elemar Rodrigues Severo Junior Date: Wed, 4 Dec 2024 16:39:45 -0300 Subject: [PATCH 09/10] Add comparative timesheets and client summary analysis Introduce side-by-side timesheet comparison for current and previous months using `AllocationCalendar`. Added `ClientSummarySection` to display client-specific details with enhanced data visualization. --- .../about-us/account-managers/[slug]/page.tsx | 296 +++++++++++++++++- .../account-managers/[slug]/queries.ts | 147 ++++++++- 2 files changed, 428 insertions(+), 15 deletions(-) diff --git a/frontend/src/app/about-us/account-managers/[slug]/page.tsx b/frontend/src/app/about-us/account-managers/[slug]/page.tsx index 21d50814..416700e9 100644 --- a/frontend/src/app/about-us/account-managers/[slug]/page.tsx +++ b/frontend/src/app/about-us/account-managers/[slug]/page.tsx @@ -8,29 +8,244 @@ import { useState } from "react"; import { TimesheetSummary } from "./TimesheetSummary"; import { CasesSummary } from "./CasesSummary"; import { ActiveDealsSummary } from "./ActiveDealsSummary"; -import { Summaries } from "@/app/financial/revenue-tracking/components/Summaries"; -import { RevenueTrackingQuery } from "@/app/financial/revenue-tracking/types"; +import { AllocationCalendar } from "@/app/components/AllocationCalendar"; +import SectionHeader from "@/components/SectionHeader"; +import { Sheet, SheetContent, SheetHeader, SheetTitle, SheetTrigger } from "@/components/ui/sheet"; +import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table"; +import { StatType } from "@/app/constants/colors"; + +interface ClientSummary { + client: string; + hours: number; + appointments: any[]; +} + +const ClientSummarySection = ({ + summaries, + selectedStatType +}: { + summaries: ClientSummary[] | null; + selectedStatType: StatType; +}) => { + if (!summaries) return null; + + return ( +
+

Client Summary:

+ {summaries.map((summary) => ( +
+ {summary.client} +
+ {summary.hours.toFixed(1)}h + + + Details + + + + {summary.client} - {selectedStatType.charAt(0).toUpperCase() + selectedStatType.slice(1)} Hours + +
+ + + + Date + Worker + Hours + Comment + + + + {summary.appointments.map((apt, idx) => ( + + {apt.date} + {apt.workerName} + {apt.timeInHs}h + {apt.comment} + + ))} + +
+
+
+
+
+
+ ))} +
+ ); +}; export default function AccountManagerPage() { const params = useParams(); const slug = params.slug as string; const [selectedDataset, setSelectedDataset] = useState("timesheet-last-six-weeks"); - const { data, loading, error } = useQuery<{ accountManager: AccountManager, revenueTracking: any }>( + // Previous month states + const [selectedDatePrev, setSelectedDatePrev] = useState( + new Date(new Date().getFullYear(), new Date().getMonth() - 1, 1) + ); + const [selectedDayPrev, setSelectedDayPrev] = useState(null); + const [selectedRowPrev, setSelectedRowPrev] = useState(null); + const [selectedColumnPrev, setSelectedColumnPrev] = useState(null); + const [isAllSelectedPrev, setIsAllSelectedPrev] = useState(false); + const [selectedStatTypePrev, setSelectedStatTypePrev] = useState('consulting'); + + // Current month states + const [selectedDateCurr, setSelectedDateCurr] = useState(new Date()); + const [selectedDayCurr, setSelectedDayCurr] = useState(null); + const [selectedRowCurr, setSelectedRowCurr] = useState(null); + const [selectedColumnCurr, setSelectedColumnCurr] = useState(null); + const [isAllSelectedCurr, setIsAllSelectedCurr] = useState(false); + const [selectedStatTypeCurr, setSelectedStatTypeCurr] = useState('consulting'); + + const getVisibleDates = (date: Date) => { + const currentMonth = date.getMonth(); + const currentYear = date.getFullYear(); + + const firstDayOfMonth = new Date(currentYear, currentMonth, 1); + const lastDayOfMonth = new Date(currentYear, currentMonth + 1, 0); + + const startingDayOfWeek = firstDayOfMonth.getDay(); + const firstVisibleDate = new Date( + currentYear, + currentMonth, + -startingDayOfWeek + 1 + ); + + const daysInMonth = lastDayOfMonth.getDate(); + const totalDays = startingDayOfWeek + daysInMonth; + const weeksNeeded = Math.ceil(totalDays / 7); + const remainingDays = weeksNeeded * 7 - (startingDayOfWeek + daysInMonth); + + const lastVisibleDate = new Date( + currentYear, + currentMonth + 1, + remainingDays + ); + + const formatDate = (date: Date) => { + return `${date.getDate().toString().padStart(2, "0")}-${( + date.getMonth() + 1 + ) + .toString() + .padStart(2, "0")}-${date.getFullYear()}`; + }; + + return `${formatDate(firstVisibleDate)}-${formatDate(lastVisibleDate)}`; + }; + + const currentMonthDataset = getVisibleDates(selectedDateCurr); + const previousMonthDataset = getVisibleDates(selectedDatePrev); + + const { data, loading, error } = useQuery<{ accountManager: AccountManager }>( GET_ACCOUNT_MANAGER, { variables: { slug, - dataset: selectedDataset.replace('timesheet-', '') + dataset: selectedDataset.replace('timesheet-', ''), + dataset1: previousMonthDataset, + dataset2: currentMonthDataset } } ); + const getSelectedClientSummary = (timesheet: any, selectedDay: number | null, selectedRow: number | null, selectedColumn: number | null, isAllSelected: boolean, selectedDate: Date, selectedStatType: StatType) => { + if (!selectedDay && !selectedRow && !selectedColumn && !isAllSelected) return null; + + const clientHours: { [key: string]: { total: number, consulting: number, handsOn: number, squad: number, internal: number } } = {}; + const clientAppointments: { [key: string]: any[] } = {}; + + timesheet.appointments.forEach((appointment: { + date: string; + clientName: string; + workerName: string; + timeInHs: number; + comment: string; + kind: string; + }) => { + const appointmentDate = new Date(appointment.date); + const dayOfMonth = appointmentDate.getUTCDate(); + const dayOfWeek = appointmentDate.getUTCDay(); + const appointmentMonth = appointmentDate.getUTCMonth(); + + const firstDayOfMonth = new Date(appointmentDate.getUTCFullYear(), appointmentDate.getUTCMonth(), 1); + const firstDayOffset = firstDayOfMonth.getUTCDay(); + + const weekIndex = Math.floor((dayOfMonth + firstDayOffset - 1) / 7); + + const shouldInclude = (isAllSelected || + (selectedDay !== null && dayOfMonth === selectedDay) || + (selectedRow !== null && weekIndex === selectedRow) || + (selectedColumn !== null && dayOfWeek === selectedColumn)) && + appointmentMonth === selectedDate.getMonth(); + + if (shouldInclude) { + const clientName = appointment.clientName; + if (!clientHours[clientName]) { + clientHours[clientName] = { + total: 0, + consulting: 0, + handsOn: 0, + squad: 0, + internal: 0 + }; + } + + const dayData = timesheet.byDate.find((d: any) => + new Date(d.date).getUTCDate() === dayOfMonth && + new Date(d.date).getUTCMonth() === appointmentMonth + ); + + if (dayData) { + clientHours[clientName].total += appointment.timeInHs; + + switch(appointment.kind.toLowerCase()) { + case 'consulting': + clientHours[clientName].consulting += appointment.timeInHs; + break; + case 'handson': + clientHours[clientName].handsOn += appointment.timeInHs; + break; + case 'squad': + clientHours[clientName].squad += appointment.timeInHs; + break; + case 'internal': + clientHours[clientName].internal += appointment.timeInHs; + break; + } + + if (!clientAppointments[clientName]) { + clientAppointments[clientName] = []; + } + + const days = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat']; + clientAppointments[clientName].push({ + ...appointment, + date: `${days[appointmentDate.getUTCDay()]} ${appointmentDate.getUTCDate()}` + }); + } + } + }); + + return Object.entries(clientHours) + .map(([client, hours]) => ({ + client, + hours: hours[selectedStatType], + appointments: clientAppointments[client].filter(apt => + apt.kind.toLowerCase() === selectedStatType || + (selectedStatType === 'handsOn' && apt.kind.toLowerCase() === 'handson') + ) + })) + .filter(summary => summary.hours > 0) + .sort((a, b) => b.hours - a.hours); + }; + if (loading) return
Loading...
; if (error) return
Error loading data
; if (!data?.accountManager) return
Manager not found
; - const { name, position, photoUrl } = data.accountManager; + const { name, position, photoUrl, timesheet1, timesheet2, cases, activeDeals } = data.accountManager; return (
@@ -45,8 +260,70 @@ export default function AccountManagerPage() {
- - {/**/} + + +
+
+
+ + +
+
+ + +
+
+
- - - + + ); } diff --git a/frontend/src/app/about-us/account-managers/[slug]/queries.ts b/frontend/src/app/about-us/account-managers/[slug]/queries.ts index 831a6302..c8e4d281 100644 --- a/frontend/src/app/about-us/account-managers/[slug]/queries.ts +++ b/frontend/src/app/about-us/account-managers/[slug]/queries.ts @@ -2,7 +2,7 @@ import { RevenueTrackingQuery } from "@/app/financial/revenue-tracking/types"; import { gql } from "@apollo/client"; export const GET_ACCOUNT_MANAGER = gql` - query GetAccountManager($slug: String!, $dataset: String!) { + query GetAccountManager($slug: String!, $dataset: String!, $dataset1: String!, $dataset2: String!) { accountManager(slug: $slug) { photoUrl name @@ -34,6 +34,45 @@ export const GET_ACCOUNT_MANAGER = gql` client { name } isStale } + + timesheet1: timesheet(slug: $dataset1) { + appointments { + kind + date + workerName + clientName + comment + timeInHs + } + byDate { + date + totalHours + totalConsultingHours + totalHandsOnHours + totalSquadHours + totalInternalHours + } + } + + timesheet2: timesheet(slug: $dataset2) { + appointments { + kind + date + workerName + clientName + comment + timeInHs + } + byDate { + date + totalHours + totalConsultingHours + totalHandsOnHours + totalSquadHours + totalInternalHours + } + } + timesheet(slug: $dataset) { byKind { consulting { @@ -152,7 +191,9 @@ export interface AccountManager { activeDeals: Array<{ title: string; clientOrProspectName: string; - client: { id: number }; + client: { + id: string; + }; stageName: string; stageOrderNr: number; daysSinceLastUpdate: number; @@ -166,14 +207,54 @@ export interface AccountManager { hasDescription: boolean; lastUpdate: { status: string; - date: Date; + date: string; author: string; observations: string; }; - client: { name: string; accountManager: { name: string } }; + client: { + name: string; + }; isStale: boolean; }>; + timesheet1: { + appointments: Array<{ + kind: string; + date: string; + workerName: string; + clientName: string; + comment: string; + timeInHs: number; + }>; + byDate: Array<{ + date: string; + totalHours: number; + totalConsultingHours: number; + totalHandsOnHours: number; + totalSquadHours: number; + totalInternalHours: number; + }>; + }; + + timesheet2: { + appointments: Array<{ + kind: string; + date: string; + workerName: string; + clientName: string; + comment: string; + timeInHs: number; + }>; + byDate: Array<{ + date: string; + totalHours: number; + totalConsultingHours: number; + totalHandsOnHours: number; + totalSquadHours: number; + totalInternalHours: number; + }>; + }; + timesheet: { byKind: { consulting: { @@ -208,7 +289,12 @@ export interface AccountManager { byCase: Array<{ title: string; caseDetails: { - client: { name: string, accountManager: { name: string } }; + client: { + name: string; + accountManager: { + name: string; + }; + }; sponsor: string; }; byWorker: Array<{ @@ -220,4 +306,55 @@ export interface AccountManager { }>; }>; }; + + revenueTracking: { + year: number; + month: number; + summaries: { + byMode: { + regular: number; + preContracted: number; + total: number; + }; + byKind: Array<{ + name: string; + regular: number; + preContracted: number; + total: number; + }>; + byAccountManager: Array<{ + name: string; + slug: string; + regular: number; + preContracted: number; + total: number; + consultingFee: number; + consultingPreFee: number; + handsOnFee: number; + squadFee: number; + }>; + byClient: Array<{ + name: string; + slug: string; + regular: number; + preContracted: number; + total: number; + consultingFee: number; + consultingPreFee: number; + handsOnFee: number; + squadFee: number; + }>; + bySponsor: Array<{ + name: string; + slug: string; + regular: number; + preContracted: number; + total: number; + consultingFee: number; + consultingPreFee: number; + handsOnFee: number; + squadFee: number; + }>; + }; + }; } From 410ab3494dde6935eb74a54136588ac237f92e3f Mon Sep 17 00:00:00 2001 From: Elemar Rodrigues Severo Junior Date: Wed, 4 Dec 2024 16:44:15 -0300 Subject: [PATCH 10/10] Refactor component formatting for improved readability Reformatted and indented the code consistently across the component for better readability and maintenance. The changes also include improving the structure of function parameters and jsx elements, making it easier to follow. No functional changes were made to the logic or behavior of the component. --- .../about-us/account-managers/[slug]/page.tsx | 282 +++++++++++------- 1 file changed, 179 insertions(+), 103 deletions(-) diff --git a/frontend/src/app/about-us/account-managers/[slug]/page.tsx b/frontend/src/app/about-us/account-managers/[slug]/page.tsx index 416700e9..5eb5a4f1 100644 --- a/frontend/src/app/about-us/account-managers/[slug]/page.tsx +++ b/frontend/src/app/about-us/account-managers/[slug]/page.tsx @@ -10,8 +10,21 @@ import { CasesSummary } from "./CasesSummary"; import { ActiveDealsSummary } from "./ActiveDealsSummary"; import { AllocationCalendar } from "@/app/components/AllocationCalendar"; import SectionHeader from "@/components/SectionHeader"; -import { Sheet, SheetContent, SheetHeader, SheetTitle, SheetTrigger } from "@/components/ui/sheet"; -import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table"; +import { + Sheet, + SheetContent, + SheetHeader, + SheetTitle, + SheetTrigger, +} from "@/components/ui/sheet"; +import { + Table, + TableBody, + TableCell, + TableHead, + TableHeader, + TableRow, +} from "@/components/ui/table"; import { StatType } from "@/app/constants/colors"; interface ClientSummary { @@ -20,9 +33,9 @@ interface ClientSummary { appointments: any[]; } -const ClientSummarySection = ({ - summaries, - selectedStatType +const ClientSummarySection = ({ + summaries, + selectedStatType, }: { summaries: ClientSummary[] | null; selectedStatType: StatType; @@ -33,7 +46,10 @@ const ClientSummarySection = ({

Client Summary:

{summaries.map((summary) => ( -
+
{summary.client}
{summary.hours.toFixed(1)}h @@ -43,7 +59,12 @@ const ClientSummarySection = ({ - {summary.client} - {selectedStatType.charAt(0).toUpperCase() + selectedStatType.slice(1)} Hours + + {summary.client} -{" "} + {selectedStatType.charAt(0).toUpperCase() + + selectedStatType.slice(1)}{" "} + Hours +
@@ -58,10 +79,18 @@ const ClientSummarySection = ({ {summary.appointments.map((apt, idx) => ( - {apt.date} - {apt.workerName} - {apt.timeInHs}h - {apt.comment} + + {apt.date} + + + {apt.workerName} + + + {apt.timeInHs}h + + + {apt.comment} + ))} @@ -79,7 +108,9 @@ const ClientSummarySection = ({ export default function AccountManagerPage() { const params = useParams(); const slug = params.slug as string; - const [selectedDataset, setSelectedDataset] = useState("timesheet-last-six-weeks"); + const [selectedDataset, setSelectedDataset] = useState( + "timesheet-last-six-weeks" + ); // Previous month states const [selectedDatePrev, setSelectedDatePrev] = useState( @@ -87,17 +118,23 @@ export default function AccountManagerPage() { ); const [selectedDayPrev, setSelectedDayPrev] = useState(null); const [selectedRowPrev, setSelectedRowPrev] = useState(null); - const [selectedColumnPrev, setSelectedColumnPrev] = useState(null); + const [selectedColumnPrev, setSelectedColumnPrev] = useState( + null + ); const [isAllSelectedPrev, setIsAllSelectedPrev] = useState(false); - const [selectedStatTypePrev, setSelectedStatTypePrev] = useState('consulting'); + const [selectedStatTypePrev, setSelectedStatTypePrev] = + useState("consulting"); // Current month states const [selectedDateCurr, setSelectedDateCurr] = useState(new Date()); const [selectedDayCurr, setSelectedDayCurr] = useState(null); const [selectedRowCurr, setSelectedRowCurr] = useState(null); - const [selectedColumnCurr, setSelectedColumnCurr] = useState(null); + const [selectedColumnCurr, setSelectedColumnCurr] = useState( + null + ); const [isAllSelectedCurr, setIsAllSelectedCurr] = useState(false); - const [selectedStatTypeCurr, setSelectedStatTypeCurr] = useState('consulting'); + const [selectedStatTypeCurr, setSelectedStatTypeCurr] = + useState("consulting"); const getVisibleDates = (date: Date) => { const currentMonth = date.getMonth(); @@ -141,103 +178,132 @@ export default function AccountManagerPage() { const { data, loading, error } = useQuery<{ accountManager: AccountManager }>( GET_ACCOUNT_MANAGER, { - variables: { + variables: { slug, - dataset: selectedDataset.replace('timesheet-', ''), + dataset: selectedDataset.replace("timesheet-", ""), dataset1: previousMonthDataset, - dataset2: currentMonthDataset - } + dataset2: currentMonthDataset, + }, } ); - const getSelectedClientSummary = (timesheet: any, selectedDay: number | null, selectedRow: number | null, selectedColumn: number | null, isAllSelected: boolean, selectedDate: Date, selectedStatType: StatType) => { - if (!selectedDay && !selectedRow && !selectedColumn && !isAllSelected) return null; - - const clientHours: { [key: string]: { total: number, consulting: number, handsOn: number, squad: number, internal: number } } = {}; + const getSelectedClientSummary = ( + timesheet: any, + selectedDay: number | null, + selectedRow: number | null, + selectedColumn: number | null, + isAllSelected: boolean, + selectedDate: Date, + selectedStatType: StatType + ) => { + if (!selectedDay && !selectedRow && !selectedColumn && !isAllSelected) + return null; + + const clientHours: { + [key: string]: { + total: number; + consulting: number; + handsOn: number; + squad: number; + internal: number; + }; + } = {}; const clientAppointments: { [key: string]: any[] } = {}; - - timesheet.appointments.forEach((appointment: { - date: string; - clientName: string; - workerName: string; - timeInHs: number; - comment: string; - kind: string; - }) => { - const appointmentDate = new Date(appointment.date); - const dayOfMonth = appointmentDate.getUTCDate(); - const dayOfWeek = appointmentDate.getUTCDay(); - const appointmentMonth = appointmentDate.getUTCMonth(); - - const firstDayOfMonth = new Date(appointmentDate.getUTCFullYear(), appointmentDate.getUTCMonth(), 1); - const firstDayOffset = firstDayOfMonth.getUTCDay(); - - const weekIndex = Math.floor((dayOfMonth + firstDayOffset - 1) / 7); - - const shouldInclude = (isAllSelected || - (selectedDay !== null && dayOfMonth === selectedDay) || - (selectedRow !== null && weekIndex === selectedRow) || - (selectedColumn !== null && dayOfWeek === selectedColumn)) && - appointmentMonth === selectedDate.getMonth(); - - if (shouldInclude) { - const clientName = appointment.clientName; - if (!clientHours[clientName]) { - clientHours[clientName] = { - total: 0, - consulting: 0, - handsOn: 0, - squad: 0, - internal: 0 - }; - } - const dayData = timesheet.byDate.find((d: any) => - new Date(d.date).getUTCDate() === dayOfMonth && - new Date(d.date).getUTCMonth() === appointmentMonth + timesheet.appointments.forEach( + (appointment: { + date: string; + clientName: string; + workerName: string; + timeInHs: number; + comment: string; + kind: string; + }) => { + const appointmentDate = new Date(appointment.date); + const dayOfMonth = appointmentDate.getUTCDate(); + const dayOfWeek = appointmentDate.getUTCDay(); + const appointmentMonth = appointmentDate.getUTCMonth(); + + const firstDayOfMonth = new Date( + appointmentDate.getUTCFullYear(), + appointmentDate.getUTCMonth(), + 1 ); - - if (dayData) { - clientHours[clientName].total += appointment.timeInHs; - - switch(appointment.kind.toLowerCase()) { - case 'consulting': - clientHours[clientName].consulting += appointment.timeInHs; - break; - case 'handson': - clientHours[clientName].handsOn += appointment.timeInHs; - break; - case 'squad': - clientHours[clientName].squad += appointment.timeInHs; - break; - case 'internal': - clientHours[clientName].internal += appointment.timeInHs; - break; + const firstDayOffset = firstDayOfMonth.getUTCDay(); + + const weekIndex = Math.floor((dayOfMonth + firstDayOffset - 1) / 7); + + const shouldInclude = + (isAllSelected || + (selectedDay !== null && dayOfMonth === selectedDay) || + (selectedRow !== null && weekIndex === selectedRow) || + (selectedColumn !== null && dayOfWeek === selectedColumn)) && + appointmentMonth === selectedDate.getMonth(); + + if (shouldInclude) { + const clientName = appointment.clientName; + if (!clientHours[clientName]) { + clientHours[clientName] = { + total: 0, + consulting: 0, + handsOn: 0, + squad: 0, + internal: 0, + }; } - if (!clientAppointments[clientName]) { - clientAppointments[clientName] = []; + const dayData = timesheet.byDate.find( + (d: any) => + new Date(d.date).getUTCDate() === dayOfMonth && + new Date(d.date).getUTCMonth() === appointmentMonth + ); + + if (dayData) { + clientHours[clientName].total += appointment.timeInHs; + + switch (appointment.kind.toLowerCase()) { + case "consulting": + clientHours[clientName].consulting += appointment.timeInHs; + break; + case "handson": + clientHours[clientName].handsOn += appointment.timeInHs; + break; + case "squad": + clientHours[clientName].squad += appointment.timeInHs; + break; + case "internal": + clientHours[clientName].internal += appointment.timeInHs; + break; + } + + if (!clientAppointments[clientName]) { + clientAppointments[clientName] = []; + } + + const days = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"]; + clientAppointments[clientName].push({ + ...appointment, + date: `${ + days[appointmentDate.getUTCDay()] + } ${appointmentDate.getUTCDate()}`, + }); } - - const days = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat']; - clientAppointments[clientName].push({ - ...appointment, - date: `${days[appointmentDate.getUTCDay()]} ${appointmentDate.getUTCDate()}` - }); } } - }); + ); return Object.entries(clientHours) .map(([client, hours]) => ({ client, hours: hours[selectedStatType], - appointments: clientAppointments[client].filter(apt => - apt.kind.toLowerCase() === selectedStatType || - (selectedStatType === 'handsOn' && apt.kind.toLowerCase() === 'handson') - ) + appointments: clientAppointments[client].filter( + (apt) => + apt.kind.toLowerCase() === selectedStatType || + (selectedStatType === "handsOn" && + apt.kind.toLowerCase() === "handson") + ), })) - .filter(summary => summary.hours > 0) + .filter((summary) => summary.hours > 0) .sort((a, b) => b.hours - a.hours); }; @@ -245,7 +311,15 @@ export default function AccountManagerPage() { if (error) return
Error loading data
; if (!data?.accountManager) return
Manager not found
; - const { name, position, photoUrl, timesheet1, timesheet2, cases, activeDeals } = data.accountManager; + const { + name, + position, + photoUrl, + timesheet1, + timesheet2, + cases, + activeDeals, + } = data.accountManager; return (
@@ -261,7 +335,7 @@ export default function AccountManagerPage() { - +
@@ -325,14 +399,16 @@ export default function AccountManagerPage() {
- +
+ - - + + +
); }