From 315e0cd62c3569cccdc2df58478259cb88ad4e01 Mon Sep 17 00:00:00 2001 From: akash2017sky Date: Wed, 29 Apr 2026 15:34:49 +0100 Subject: [PATCH] fix: Use BC resources directly for teammate timesheet view (#189) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Teammate selector was fetching BC employees and trying to map each back to a resource via getResourceByEmail, which derives a BC User ID from the email's local-part. That mapping fails whenever the employee record has no email or its prefix doesn't match the resource's timeSheetOwnerUserId convention, leaving the timesheet silently empty. Switch the dropdown to fetch resources (where useTimeSheet=true) and fetch the timesheet directly via resource.number — same pattern the Team page already uses. Co-Authored-By: Claude Opus 4.7 (1M context) --- src/components/dashboard/Dashboard.tsx | 2 +- src/components/timesheet/TeammateSelector.tsx | 52 ++++++++++--------- src/components/timesheet/WeeklyTimesheet.tsx | 14 +++-- src/hooks/useTeammateStore.ts | 18 ++++--- src/hooks/useTimeEntriesStore.ts | 6 +-- src/services/bc/timeEntryService.ts | 23 ++++---- 6 files changed, 62 insertions(+), 53 deletions(-) diff --git a/src/components/dashboard/Dashboard.tsx b/src/components/dashboard/Dashboard.tsx index 5d52f5e..535e3bc 100644 --- a/src/components/dashboard/Dashboard.tsx +++ b/src/components/dashboard/Dashboard.tsx @@ -25,7 +25,7 @@ export function Dashboard() {

Timesheet

{isViewingTeammate - ? `Viewing ${selectedTeammate.displayName}'s timesheet` + ? `Viewing ${selectedTeammate.name || selectedTeammate.displayName || selectedTeammate.number}'s timesheet` : 'Track your time and sync to Business Central'}

diff --git a/src/components/timesheet/TeammateSelector.tsx b/src/components/timesheet/TeammateSelector.tsx index a88549f..97d68e7 100644 --- a/src/components/timesheet/TeammateSelector.tsx +++ b/src/components/timesheet/TeammateSelector.tsx @@ -11,7 +11,18 @@ import { import { useTeammateStore, useCompanyStore } from '@/hooks'; import { useAuth } from '@/services/auth'; import { cn } from '@/utils'; -import type { BCEmployee } from '@/types'; +import { bcClient } from '@/services/bc'; +import type { BCResource } from '@/types'; + +// Resources expose `name` (and sometimes `displayName`); pick the best label. +function teammateLabel(t: BCResource): string { + return t.name || t.displayName || t.number; +} + +function teammateInitial(t: BCResource): string { + const label = teammateLabel(t); + return label?.[0] || '?'; +} export function TeammateSelector() { const [isOpen, setIsOpen] = useState(false); @@ -52,31 +63,32 @@ export function TeammateSelector() { return () => document.removeEventListener('keydown', handleKeyDown); }, [isOpen]); - const handleSelect = (teammate: BCEmployee | null) => { + const handleSelect = (teammate: BCResource | null) => { selectTeammate(teammate); setIsOpen(false); setSearchQuery(''); }; - // Filter teammates by search query, excluding current user + // Filter teammates by search query const filteredTeammates = teammates.filter((teammate) => { - // Filter by search if (searchQuery) { const query = searchQuery.toLowerCase(); - const matchesName = teammate.displayName.toLowerCase().includes(query); - const matchesEmail = teammate.email?.toLowerCase().includes(query); - if (!matchesName && !matchesEmail) return false; + const matchesName = teammateLabel(teammate).toLowerCase().includes(query); + const matchesUserId = teammate.timeSheetOwnerUserId?.toLowerCase().includes(query); + const matchesNumber = teammate.number.toLowerCase().includes(query); + if (!matchesName && !matchesUserId && !matchesNumber) return false; } return true; }); - // Separate current user from other teammates - const currentUserEmail = account?.username?.toLowerCase(); + // Identify the current user's resource by deriving the BC User ID from their UPN + // (matches the convention used in bcClient.deriveBCUserId). + const currentUserBCId = account?.username ? bcClient.deriveBCUserId(account.username) : undefined; const currentUserTeammate = filteredTeammates.find( - (t) => t.email?.toLowerCase() === currentUserEmail + (t) => t.timeSheetOwnerUserId?.toUpperCase() === currentUserBCId ); const otherTeammates = filteredTeammates.filter( - (t) => t.email?.toLowerCase() !== currentUserEmail + (t) => t.timeSheetOwnerUserId?.toUpperCase() !== currentUserBCId ); // Don't show if no teammates available (only self) @@ -84,7 +96,7 @@ export function TeammateSelector() { return null; } - const displayName = selectedTeammate ? selectedTeammate.displayName : 'My Timesheet'; + const displayName = selectedTeammate ? teammateLabel(selectedTeammate) : 'My Timesheet'; return (
@@ -160,7 +172,7 @@ export function TeammateSelector() { My Timesheet {currentUserTeammate && ( - ({currentUserTeammate.displayName}) + ({teammateLabel(currentUserTeammate)}) )}
@@ -198,19 +210,11 @@ export function TeammateSelector() { aria-selected={isSelected} >
- {teammate.givenName?.[0] || - teammate.surname?.[0] || - teammate.displayName?.[0] || - '?'} - {teammate.givenName?.[0] && teammate.surname?.[0] - ? teammate.surname[0] - : ''} + {teammateInitial(teammate)}
-
{teammate.displayName}
- {teammate.jobTitle && ( -
{teammate.jobTitle}
- )} +
{teammateLabel(teammate)}
+
{teammate.number}
{isSelected && ( diff --git a/src/components/timesheet/WeeklyTimesheet.tsx b/src/components/timesheet/WeeklyTimesheet.tsx index 74dc103..a56c800 100644 --- a/src/components/timesheet/WeeklyTimesheet.tsx +++ b/src/components/timesheet/WeeklyTimesheet.tsx @@ -445,12 +445,12 @@ Thank you!`)}`}
Viewing{' '} - {selectedTeammate.displayName} + + {selectedTeammate.name || selectedTeammate.displayName || selectedTeammate.number} + 's timesheet - {selectedTeammate.jobTitle && ( - ({selectedTeammate.jobTitle}) - )} + ({selectedTeammate.number})
Read-only @@ -623,7 +623,11 @@ Thank you!`)}`} {isViewingTeammate ? ( <>

- No time entries found for {selectedTeammate.displayName} this week. + No time entries found for{' '} + {selectedTeammate.name || + selectedTeammate.displayName || + selectedTeammate.number}{' '} + this week.

Time entries will appear here once added to their timesheet. diff --git a/src/hooks/useTeammateStore.ts b/src/hooks/useTeammateStore.ts index 9472edc..a127443 100644 --- a/src/hooks/useTeammateStore.ts +++ b/src/hooks/useTeammateStore.ts @@ -1,15 +1,15 @@ import { create } from 'zustand'; -import type { BCEmployee } from '@/types'; +import type { BCResource } from '@/types'; import { bcClient } from '@/services/bc/bcClient'; interface TeammateStore { - teammates: BCEmployee[]; - selectedTeammate: BCEmployee | null; + teammates: BCResource[]; + selectedTeammate: BCResource | null; isLoading: boolean; error: string | null; fetchTeammates: () => Promise; - selectTeammate: (teammate: BCEmployee | null) => void; + selectTeammate: (teammate: BCResource | null) => void; clearSelection: () => void; isViewingTeammate: () => boolean; } @@ -23,15 +23,19 @@ export const useTeammateStore = create((set, get) => ({ fetchTeammates: async () => { set({ isLoading: true, error: null }); try { - const employees = await bcClient.getEmployees("status eq 'Active'"); - set({ teammates: employees, isLoading: false }); + // Resources are the entities that own timesheets, so look them up directly + // (employees -> resources mapping is unreliable in BC). + const resources = await bcClient.getResources(); + // Only show resources that actually use timesheets — others have nothing to display. + const withTimesheet = resources.filter((r) => r.useTimeSheet); + set({ teammates: withTimesheet, isLoading: false }); } catch (error) { const message = error instanceof Error ? error.message : 'Failed to fetch teammates'; set({ error: message, isLoading: false, teammates: [] }); } }, - selectTeammate: (teammate: BCEmployee | null) => { + selectTeammate: (teammate: BCResource | null) => { set({ selectedTeammate: teammate }); }, diff --git a/src/hooks/useTimeEntriesStore.ts b/src/hooks/useTimeEntriesStore.ts index daf16d5..bb0dff8 100644 --- a/src/hooks/useTimeEntriesStore.ts +++ b/src/hooks/useTimeEntriesStore.ts @@ -1,5 +1,5 @@ import { create } from 'zustand'; -import type { TimeEntry, WeekData, BCEmployee, BCTimeSheet, TimesheetDisplayStatus } from '@/types'; +import type { TimeEntry, WeekData, BCResource, BCTimeSheet, TimesheetDisplayStatus } from '@/types'; import { timeEntryService, NoResourceError, @@ -26,7 +26,7 @@ interface TimeEntriesStore { // Entry operations fetchWeekEntries: (userId: string, weekStart?: Date) => Promise; - fetchTeammateEntries: (teammate: BCEmployee, weekStart?: Date) => Promise; + fetchTeammateEntries: (teammate: BCResource, weekStart?: Date) => Promise; addEntry: ( entry: Omit< TimeEntry, @@ -136,7 +136,7 @@ export const useTimeEntriesStore = create((set, get) => ({ } }, - fetchTeammateEntries: async (teammate: BCEmployee, weekStart?: Date) => { + fetchTeammateEntries: async (teammate: BCResource, weekStart?: Date) => { const week = weekStart || get().currentWeekStart; set({ isLoading: true, error: null, currentWeekStart: week }); diff --git a/src/services/bc/timeEntryService.ts b/src/services/bc/timeEntryService.ts index 480ddaa..f52e26d 100644 --- a/src/services/bc/timeEntryService.ts +++ b/src/services/bc/timeEntryService.ts @@ -5,7 +5,7 @@ import type { BCTimeSheet, BCTimeSheetLine, BCTimeSheetDetail, - BCEmployee, + BCResource, } from '@/types'; import { format, startOfWeek } from 'date-fns'; @@ -578,17 +578,12 @@ export const timeEntryService = { /** * Get entries for a teammate from Business Central. + * The teammate is a BC Resource — we already have its number, so we can fetch + * the timesheet directly without an email→resource lookup. */ - async getTeammateEntries(weekStart: Date, teammate: BCEmployee): Promise { + async getTeammateEntries(weekStart: Date, teammate: BCResource): Promise { try { - // Get resource by employee email - const resource = teammate.email ? await bcClient.getResourceByEmail(teammate.email) : null; - - if (!resource) { - return []; - } - - const timesheet = await this.getTimesheet(resource.number, weekStart); + const timesheet = await this.getTimesheet(teammate.number, weekStart); const [lines, details] = await Promise.all([ bcClient.getTimeSheetLines(timesheet.number), bcClient.getAllTimeSheetDetails(timesheet.number), @@ -596,13 +591,15 @@ export const timeEntryService = { return bcDataToTimeEntries(lines, details, timesheet, teammate.id); } catch (error) { - // Log error for debugging but don't expose to user - // This can fail for various reasons: no timesheet, no resource, network issues + // Common case: teammate has no timesheet for this week — return empty. + if (error instanceof NoTimesheetError) { + return []; + } if (process.env.NODE_ENV === 'development') { console.error('Failed to get teammate entries:', { weekStart, teammateId: teammate.id, - teammateEmail: teammate.email, + teammateNumber: teammate.number, error, }); }