= ({
+ data = [],
+ onDateClick,
+ renderDayContent,
+ locale = enGB,
+ daysLabels = defaultDaysLabels,
+ noDataText = "No Data",
+ classNames = {}
+}) => {
+ const [currentMonth, setCurrentMonth] = useState(new Date());
+ const calendarDates = useMemo(() => generateFullCalendar(currentMonth), [currentMonth]);
+ const groupedData = useMemo(
+ () => new Map(data.map((plan) => [format(new Date(plan.date), "yyyy-MM-dd"), plan])),
+ [data]
+ );
+
+ const handlePreviousMonth = useCallback(() => setCurrentMonth((prev) => addMonths(prev, -1)), []);
+ const handleNextMonth = useCallback(() => setCurrentMonth((prev) => addMonths(prev, 1)), []);
+
+ return (
+
+ {/* Header */}
+
+
+
+ {format(currentMonth, "MMMM yyyy", { locale: locale })}
+
+
+
+
+ {/* Grid */}
+
+ {daysLabels.map((day) => (
+
{day}
+ ))}
+
+
+
+ {calendarDates.map((date) => {
+ const formattedDate = format(date, "yyyy-MM-dd");
+ const plan = groupedData.get(formattedDate);
+ return (
+
onDateClick?.(date)}
+ onKeyDown={(e) => {
+ if (e.key === 'Enter' || e.key === ' ') {
+ e.preventDefault();
+ onDateClick?.(date);
+ }
+ }}
+ >
+
+
+ {format(date, "dd MMM yyyy")}
+
+
+ Total{" : "}
+ {plan && }
+
+
+ {renderDayContent ? (
+ renderDayContent(date, plan)
+ ) : plan ? (
+
+ {plan.tasks.map((task) => (
+
+ {task.task?.title}
+
+ ))}
+
+ ) : (
+
+ {noDataText}
+
+ )}
+
+ );
+ })}
+
+
+ );
+};
+
+export default MonthlyTimesheetCalendar;
diff --git a/apps/web/app/[locale]/timesheet/[memberId]/components/TimesheetView.tsx b/apps/web/app/[locale]/timesheet/[memberId]/components/TimesheetView.tsx
index ee72a1f79..18606f719 100644
--- a/apps/web/app/[locale]/timesheet/[memberId]/components/TimesheetView.tsx
+++ b/apps/web/app/[locale]/timesheet/[memberId]/components/TimesheetView.tsx
@@ -18,7 +18,7 @@ export function TimesheetView({ data, loading }: { data?: GroupedTimesheet[]; lo
if (data.length === 0) {
return (
-
+
{t('pages.timesheet.NO_ENTRIES_FOUND')}
);
diff --git a/apps/web/app/[locale]/timesheet/[memberId]/components/WeeklyTimesheetCalendar.tsx b/apps/web/app/[locale]/timesheet/[memberId]/components/WeeklyTimesheetCalendar.tsx
new file mode 100644
index 000000000..db2805f0b
--- /dev/null
+++ b/apps/web/app/[locale]/timesheet/[memberId]/components/WeeklyTimesheetCalendar.tsx
@@ -0,0 +1,150 @@
+import React, { useMemo, useState, useCallback } from "react";
+import { format, addDays, startOfWeek, endOfWeek, eachDayOfInterval, Locale } from "date-fns";
+import { enGB } from "date-fns/locale";
+import { cn } from "@/lib/utils";
+import { GroupedTimesheet } from "@/app/hooks/features/useTimesheet";
+import { TotalDurationByDate } from "@/lib/features";
+import { formatDate } from "@/app/helpers";
+
+type WeeklyCalendarProps = {
+ data?: GroupedTimesheet[];
+ onDateClick?: (date: Date) => void;
+ renderDayContent?: (date: Date, plan?: GroupedTimesheet) => React.ReactNode;
+ locale?: Locale;
+ daysLabels?: string[];
+ noDataText?: string;
+ classNames?: {
+ container?: string;
+ header?: string;
+ grid?: string;
+ day?: string;
+ noData?: string;
+ };
+};
+
+const defaultDaysLabels = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"];
+
+const generateWeek = (currentDate: Date) => {
+ const weekStart = startOfWeek(currentDate, { weekStartsOn: 0 });
+ const weekEnd = endOfWeek(currentDate, { weekStartsOn: 0 });
+ return eachDayOfInterval({ start: weekStart, end: weekEnd });
+};
+
+const WeeklyTimesheetCalendar: React.FC
= ({
+ data = [],
+ onDateClick,
+ renderDayContent,
+ locale = enGB,
+ daysLabels = defaultDaysLabels,
+ noDataText = "No Data",
+ classNames = {},
+}) => {
+ const [currentDate, setCurrentDate] = useState(new Date());
+
+ // Calculate the current week based on `currentDate`
+ const weekDates = useMemo(() => generateWeek(currentDate), [currentDate]);
+
+ // Map data to the respective dates
+ const groupedData = useMemo(
+ () => new Map(data.map((plan) => [format(new Date(plan.date), "yyyy-MM-dd"), plan])),
+ [data]
+ );
+
+ // Handlers for navigation
+ const handlePreviousWeek = useCallback(() => setCurrentDate((prev) => addDays(prev, -7)), []);
+ const handleNextWeek = useCallback(() => setCurrentDate((prev) => addDays(prev, 7)), []);
+
+ return (
+
+
+
+
+ {`Week of ${format(weekDates[0], "MMM d", { locale })} - ${format(
+ weekDates[6],
+ "MMM d, yyyy",
+ { locale }
+ )}`}
+
+
+
+
+
+ {daysLabels.map((day) => (
+
{day}
+ ))}
+
+
+
+ {weekDates.map((date) => {
+ const formattedDate = format(date, "yyyy-MM-dd");
+ const plan = groupedData.get(formattedDate);
+
+ return (
+
onDateClick?.(date)}
+ onKeyDown={(e) => {
+ if (e.key === "Enter" || e.key === " ") {
+ e.preventDefault();
+ onDateClick?.(date);
+ }
+ }}
+ >
+
+
+ {format(date, "dd MMM yyyy")}
+
+
+ Total{" : "}
+ {plan && (
+
+ )}
+
+
+ {renderDayContent ? (
+ renderDayContent(date, plan)
+ ) : plan ? (
+
+ {plan.tasks.map((task) => (
+
+ {task.task?.title}
+
+ ))}
+
+ ) : (
+
+ {noDataText}
+
+ )}
+
+ );
+ })}
+
+
+ );
+};
+
+export default WeeklyTimesheetCalendar;
diff --git a/apps/web/app/[locale]/timesheet/[memberId]/page.tsx b/apps/web/app/[locale]/timesheet/[memberId]/page.tsx
index 3eecb7d5f..ade889e9b 100644
--- a/apps/web/app/[locale]/timesheet/[memberId]/page.tsx
+++ b/apps/web/app/[locale]/timesheet/[memberId]/page.tsx
@@ -39,6 +39,10 @@ const TimeSheet = React.memo(function TimeSheetPage({ params }: { params: { memb
const { user } = useAuthenticateUser();
const [search, setSearch] = useState('');
const [filterStatus, setFilterStatus] = useLocalStorageState('timesheet-filter-status', 'All Tasks');
+ const [timesheetNavigator, setTimesheetNavigator] = useLocalStorageState(
+ 'timesheet-viewMode',
+ 'ListView'
+ );
const [dateRange, setDateRange] = React.useState<{ from: Date | null; to: Date | null }>({
from: startOfDay(new Date()),
@@ -46,7 +50,8 @@ const TimeSheet = React.memo(function TimeSheetPage({ params }: { params: { memb
});
const { timesheet, statusTimesheet, loadingTimesheet } = useTimesheet({
startDate: dateRange.from ?? '',
- endDate: dateRange.to ?? ''
+ endDate: dateRange.to ?? '',
+ timesheetViewMode: timesheetNavigator
});
@@ -83,10 +88,7 @@ const TimeSheet = React.memo(function TimeSheetPage({ params }: { params: { memb
const username = user?.name || user?.firstName || user?.lastName || user?.username;
- const [timesheetNavigator, setTimesheetNavigator] = useLocalStorageState(
- 'timesheet-viewMode',
- 'ListView'
- );
+
const fullWidth = useAtomValue(fullWidthState);
const { isTrackingEnabled, activeTeam } = useOrganizationTeams();
diff --git a/apps/web/app/hooks/features/useTimesheet.ts b/apps/web/app/hooks/features/useTimesheet.ts
index 91e320c53..9add0836d 100644
--- a/apps/web/app/hooks/features/useTimesheet.ts
+++ b/apps/web/app/hooks/features/useTimesheet.ts
@@ -11,6 +11,7 @@ import { useTimelogFilterOptions } from './useTimelogFilterOptions';
interface TimesheetParams {
startDate?: Date | string;
endDate?: Date | string;
+ timesheetViewMode?: 'ListView' | 'CalendarView'
}
export interface GroupedTimesheet {
@@ -90,6 +91,7 @@ const groupByMonth = createGroupingFunction(date =>
export function useTimesheet({
startDate,
endDate,
+ timesheetViewMode
}: TimesheetParams) {
const { user } = useAuthenticateUser();
const [timesheet, setTimesheet] = useAtom(timesheetRapportState);
@@ -262,14 +264,17 @@ export function useTimesheet({
const timesheetElementGroup = useMemo(() => {
- if (timesheetGroupByDays === 'Daily') {
- return groupByDate(timesheet);
- }
- if (timesheetGroupByDays === 'Weekly') {
- return groupByWeek(timesheet);
+ if (timesheetViewMode === 'ListView') {
+ if (timesheetGroupByDays === 'Daily') {
+ return groupByDate(timesheet);
+ }
+ if (timesheetGroupByDays === 'Weekly') {
+ return groupByWeek(timesheet);
+ }
+ return groupByMonth(timesheet);
}
- return groupByMonth(timesheet);
- }, [timesheetGroupByDays, timesheet]);
+ return groupByDate(timesheet);
+ }, [timesheetGroupByDays, timesheetViewMode, timesheet]);
useEffect(() => {
diff --git a/apps/web/lib/features/integrations/calendar/table-time-sheet.tsx b/apps/web/lib/features/integrations/calendar/table-time-sheet.tsx
index 2ee67b72e..117d90d43 100644
--- a/apps/web/lib/features/integrations/calendar/table-time-sheet.tsx
+++ b/apps/web/lib/features/integrations/calendar/table-time-sheet.tsx
@@ -56,7 +56,8 @@ import {
StatusType,
EmployeeAvatar,
getTimesheetButtons,
- statusTable
+ statusTable,
+ ProjectLogo
} from '@/app/[locale]/timesheet/[memberId]/components';
import { useTranslations } from 'next-intl';
import { formatDate } from '@/app/helpers';
@@ -342,7 +343,10 @@ export function DataTableTimeSheet({ data }: { data?: GroupedTimesheet[] }) {
taskNumberClassName="text-sm"
/>
- {task.project && task.project.name}
+
+ {task.project?.imageUrl &&
}
+
{task.project?.name}
+
{
const targetDateISO = new Date(createdAt).toISOString();
+
const filteredLogs = timesheetLog.filter(
(item) => formatDate(item.timesheet.createdAt) === formatDate(targetDateISO));
const totalDurationInSeconds = filteredLogs.reduce(
diff --git a/apps/web/locales/ar.json b/apps/web/locales/ar.json
index cec448f14..c94dd66a5 100644
--- a/apps/web/locales/ar.json
+++ b/apps/web/locales/ar.json
@@ -240,6 +240,15 @@
"SINGULAR": "ورقة الحضور",
"PLURAL": "ورقات الحضور"
},
+ "DAYS": {
+ "sun": "الأحد",
+ "mon": "الإثنين",
+ "tue": "الثلاثاء",
+ "wed": "الأربعاء",
+ "thu": "الخميس",
+ "fri": "الجمعة",
+ "sat": "السبت"
+ },
"COPY_ISSUE_LINK": "نسخ رابط المشكلة",
"MAKE_A_COPY": "إنشاء نسخة",
"ASSIGNEE": "المسند إليه",
diff --git a/apps/web/locales/bg.json b/apps/web/locales/bg.json
index ce2a85668..19cc6cc07 100644
--- a/apps/web/locales/bg.json
+++ b/apps/web/locales/bg.json
@@ -258,6 +258,15 @@
"SINGULAR": "Работен лист",
"PLURAL": "Работни листове"
},
+ "DAYS": {
+ "sun": "Нед",
+ "mon": "Пон",
+ "tue": "Вто",
+ "wed": "Сря",
+ "thu": "Чет",
+ "fri": "Пет",
+ "sat": "Съб"
+ },
"COPY_ISSUE_LINK": "Копирай връзката на проблема",
"MAKE_A_COPY": "Направете копие",
"ASSIGNEE": "Назначен",
diff --git a/apps/web/locales/de.json b/apps/web/locales/de.json
index 38a13ed56..fa9e07c55 100644
--- a/apps/web/locales/de.json
+++ b/apps/web/locales/de.json
@@ -258,6 +258,15 @@
"SINGULAR": "Stundenzettel",
"PLURAL": "Stundenzettel"
},
+ "DAYS": {
+ "sun": "So",
+ "mon": "Mo",
+ "tue": "Di",
+ "wed": "Mi",
+ "thu": "Do",
+ "fri": "Fr",
+ "sat": "Sa"
+ },
"COPY_ISSUE_LINK": "Problemlink kopieren",
"MAKE_A_COPY": "Eine Kopie erstellen",
"ASSIGNEE": "Zessionar",
diff --git a/apps/web/locales/en.json b/apps/web/locales/en.json
index 180cfd86b..978ff6969 100644
--- a/apps/web/locales/en.json
+++ b/apps/web/locales/en.json
@@ -258,6 +258,15 @@
"SINGULAR": "Timesheet",
"PLURAL": "Timesheets"
},
+ "DAYS": {
+ "sun": "Sun",
+ "mon": "Mon",
+ "tue": "Tue",
+ "wed": "Wed",
+ "thu": "Thu",
+ "fri": "Fri",
+ "sat": "Sat"
+ },
"COPY_ISSUE_LINK": "Copy issue link",
"MAKE_A_COPY": "Make a copy",
"ASSIGNEE": "Assignee",
diff --git a/apps/web/locales/es.json b/apps/web/locales/es.json
index 78196ca87..5a1b3a1c0 100644
--- a/apps/web/locales/es.json
+++ b/apps/web/locales/es.json
@@ -258,6 +258,15 @@
"SINGULAR": "Hoja de horas",
"PLURAL": "Hojas de horas"
},
+ "DAYS": {
+ "sun": "Dom",
+ "mon": "Lun",
+ "tue": "Mar",
+ "wed": "Mié",
+ "thu": "Jue",
+ "fri": "Vie",
+ "sat": "Sáb"
+ },
"COPY_ISSUE_LINK": "Copiar enlace del problema",
"MAKE_A_COPY": "Hacer una copia",
"ASSIGNEE": "Cesionario",
diff --git a/apps/web/locales/fr.json b/apps/web/locales/fr.json
index f603ed553..88e51f215 100644
--- a/apps/web/locales/fr.json
+++ b/apps/web/locales/fr.json
@@ -258,6 +258,15 @@
"SINGULAR": "Feuille de temps",
"PLURAL": "Feuilles de temps"
},
+ "DAYS": {
+ "sun": "Dim",
+ "mon": "Lun",
+ "tue": "Mar",
+ "wed": "Mer",
+ "thu": "Jeu",
+ "fri": "Ven",
+ "sat": "Sam"
+ },
"COPY_ISSUE_LINK": "Copier le lien du problème",
"MAKE_A_COPY": "Faire une copie",
"ASSIGNEE": "Cessionnaire",
diff --git a/apps/web/locales/he.json b/apps/web/locales/he.json
index a1221c365..0177769f8 100644
--- a/apps/web/locales/he.json
+++ b/apps/web/locales/he.json
@@ -258,6 +258,15 @@
"SINGULAR": "דוח שעות",
"PLURAL": "דוחות שעות"
},
+ "DAYS": {
+ "sun": "יום א׳",
+ "mon": "יום ב׳",
+ "tue": "יום ג׳",
+ "wed": "יום ד׳",
+ "thu": "יום ה׳",
+ "fri": "יום ו׳",
+ "sat": "שבת"
+ },
"COPY_ISSUE_LINK": "העתק קישור לבעיה",
"MAKE_A_COPY": "בצע עותק",
"ASSIGNEE": "נמען",
diff --git a/apps/web/locales/it.json b/apps/web/locales/it.json
index e7d7ab32c..b8e80cd98 100644
--- a/apps/web/locales/it.json
+++ b/apps/web/locales/it.json
@@ -241,6 +241,15 @@
"SINGULAR": "Scheda attività",
"PLURAL": "Schede attività"
},
+ "DAYS": {
+ "sun": "Dom",
+ "mon": "Lun",
+ "tue": "Mar",
+ "wed": "Mer",
+ "thu": "Gio",
+ "fri": "Ven",
+ "sat": "Sab"
+ },
"CALENDAR": "Calendario",
"SELECT": "Seleziona",
"SAVE_CHANGES": "Salva modifiche",
diff --git a/apps/web/locales/nl.json b/apps/web/locales/nl.json
index 5bc5d834c..8768d926f 100644
--- a/apps/web/locales/nl.json
+++ b/apps/web/locales/nl.json
@@ -241,6 +241,15 @@
"SINGULAR": "Urenstaat",
"PLURAL": "Urenstaten"
},
+ "DAYS": {
+ "sun": "Zon",
+ "mon": "Maa",
+ "tue": "Din",
+ "wed": "Woe",
+ "thu": "Don",
+ "fri": "Vri",
+ "sat": "Zat"
+ },
"CALENDAR": "Kalender",
"SELECT": "Selecteren",
"SAVE_CHANGES": "Wijzigingen opslaan",
diff --git a/apps/web/locales/pl.json b/apps/web/locales/pl.json
index ce7d15b24..4b98a4d8b 100644
--- a/apps/web/locales/pl.json
+++ b/apps/web/locales/pl.json
@@ -241,6 +241,15 @@
"SINGULAR": "Karta pracy",
"PLURAL": "Karty pracy"
},
+ "DAYS": {
+ "sun": "Nie",
+ "mon": "Pon",
+ "tue": "Wto",
+ "wed": "Śro",
+ "thu": "Czw",
+ "fri": "Pią",
+ "sat": "Sob"
+ },
"CALENDAR": "Kalendarz",
"SELECT": "Wybierz",
"SAVE_CHANGES": "Zapisz zmiany",
diff --git a/apps/web/locales/pt.json b/apps/web/locales/pt.json
index bacf7d021..a587193be 100644
--- a/apps/web/locales/pt.json
+++ b/apps/web/locales/pt.json
@@ -242,6 +242,15 @@
"SINGULAR": "Folha de ponto",
"PLURAL": "Folhas de ponto"
},
+ "DAYS": {
+ "sun": "Dom",
+ "mon": "Seg",
+ "tue": "Ter",
+ "wed": "Qua",
+ "thu": "Qui",
+ "fri": "Sex",
+ "sat": "Sáb"
+ },
"CALENDAR": "Calendário",
"SELECT": "Selecionar",
"SAVE_CHANGES": "Salvar alterações",
diff --git a/apps/web/locales/ru.json b/apps/web/locales/ru.json
index 1819cc5f6..0e7dab1af 100644
--- a/apps/web/locales/ru.json
+++ b/apps/web/locales/ru.json
@@ -241,6 +241,15 @@
"SINGULAR": "Табель",
"PLURAL": "Табели"
},
+ "DAYS": {
+ "sun": "Вс",
+ "mon": "Пн",
+ "tue": "Вт",
+ "wed": "Ср",
+ "thu": "Чт",
+ "fri": "Пт",
+ "sat": "Сб"
+ },
"CALENDAR": "Календарь",
"SELECT": "Выбрать",
"SAVE_CHANGES": "Сохранить изменения",
diff --git a/apps/web/locales/zh.json b/apps/web/locales/zh.json
index 2b4f4b63a..f53aeb649 100644
--- a/apps/web/locales/zh.json
+++ b/apps/web/locales/zh.json
@@ -241,6 +241,15 @@
"SINGULAR": "时间表",
"PLURAL": "时间表"
},
+ "DAYS": {
+ "sun": "日",
+ "mon": "一",
+ "tue": "二",
+ "wed": "三",
+ "thu": "四",
+ "fri": "五",
+ "sat": "六"
+ },
"CALENDAR": "日历",
"SELECT": "Select",
"SAVE_CHANGES": "保存更改",