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, }); }