diff --git a/src/constants/fields.constant.ts b/src/constants/fields.constant.ts
index 749e351..796549d 100644
--- a/src/constants/fields.constant.ts
+++ b/src/constants/fields.constant.ts
@@ -39,15 +39,15 @@ export enum WorkDayType {
export const headersTable: TableHeader[] = [
{ label: "יום", viewMode: "both", rowSpan: 2 },
- { label: "", children: ["מחלה", "חופש"], widths: [48, 48], viewMode: "both" },
+ { label: "", children: ["מחלה", "חופש"], widths: [40, 40], viewMode: "both" },
{
label: "שעות",
children: ["", "כניסה", "יציאה", ""],
- widths: [48, 96, 96, 120],
+ widths: [48, 96, 96, 112],
viewMode: "both",
},
- { label: "סך שעות", rowSpan: 2, viewMode: "both" },
{ label: "סך שעות בפועל", rowSpan: 2, viewMode: "both" },
+ { label: "סך שעות", rowSpan: 2, viewMode: "both" },
{ label: "רגילות", rowSpan: 2, viewMode: "compact" },
{ label: "תוספות", rowSpan: 2, viewMode: "compact" },
{
diff --git a/src/features/work-table/components/DayDetails.tsx b/src/features/work-table/components/DayDetails.tsx
new file mode 100644
index 0000000..7dbea3a
--- /dev/null
+++ b/src/features/work-table/components/DayDetails.tsx
@@ -0,0 +1,249 @@
+import {
+ Box,
+ Paper,
+ Table,
+ TableBody,
+ TableCell,
+ TableHead,
+ TableRow,
+} from "@mui/material";
+import { useTranslation } from "react-i18next";
+
+import { PayBreakdownViewModel } from "@/domain";
+import { formatValue } from "@/utils";
+
+type DayDetailsProps = {
+ breakdown: PayBreakdownViewModel;
+ id: string;
+ showAbsence?: boolean;
+};
+
+type DetailItem = {
+ label: string;
+ value: number;
+};
+
+type DetailSection = {
+ items: DetailItem[];
+ label: string;
+};
+
+type DetailGroupProps = {
+ items?: DetailItem[];
+ sections?: DetailSection[];
+ title?: string;
+};
+
+const DetailGroup = ({ items = [], sections, title }: DetailGroupProps) => {
+ const columns = sections
+ ? sections.flatMap((section) => section.items)
+ : items;
+
+ return (
+
+
+
+ {title && (
+
+
+ {title}
+
+
+ )}
+ {sections && (
+
+ {sections.map((section) => (
+
+ {section.label}
+
+ ))}
+
+ )}
+
+ {columns.map(({ label }) => (
+
+ {label}
+
+ ))}
+
+
+
+
+ {columns.map(({ label, value }) => (
+
+ {formatValue(value)}
+
+ ))}
+
+
+
+
+ );
+};
+
+export const DayDetails = ({
+ breakdown,
+ id,
+ showAbsence = true,
+}: DayDetailsProps) => {
+ const { t } = useTranslation("work-table");
+
+ const groups: DetailGroupProps[] = [
+ {
+ title: t("headers.overtime"),
+ items: [
+ { label: "100%", value: breakdown.regular.hours100.hours },
+ { label: "125%", value: breakdown.regular.hours125.hours },
+ { label: "150%", value: breakdown.regular.hours150.hours },
+ ],
+ },
+ {
+ title: t("headers.shabbat"),
+ items: [
+ { label: "150%", value: breakdown.special.shabbat150.hours },
+ { label: "200%", value: breakdown.special.shabbat200.hours },
+ {
+ label: t("headers.shabbat_credit"),
+ value: breakdown.appliedShabbatCredit.hours,
+ },
+ ],
+ },
+ {
+ title: t("headers.extras"),
+ items: [
+ { label: "20%", value: breakdown.extra.hours20.hours },
+ { label: "50%", value: breakdown.extra.hours50.hours },
+ ],
+ },
+ ...(showAbsence
+ ? [
+ {
+ title: t("headers.absence"),
+ items: [
+ {
+ label: t("headers.sick"),
+ value: breakdown.hours100Sick.hours,
+ },
+ {
+ label: t("headers.vacation"),
+ value: breakdown.hours100Vacation.hours,
+ },
+ ],
+ },
+ ]
+ : []),
+ {
+ sections: [
+ {
+ label: t("headers.meal_allowance"),
+ items: [
+ {
+ label: t("day_details.points"),
+ value: breakdown.perDiemPoints,
+ },
+ ],
+ },
+ {
+ label: t("headers.meal_per_diem"),
+ items: [
+ { label: t("headers.large"), value: breakdown.largePoints },
+ { label: t("headers.small"), value: breakdown.smallPoints },
+ ],
+ },
+ ],
+ },
+ ];
+ const primaryGroups = groups.slice(0, 2);
+ const secondaryGroups = groups.slice(2);
+
+ return (
+
+
+ {primaryGroups.map((group) => (
+ section.label).join("-")
+ }
+ {...group}
+ />
+ ))}
+
+
+ {secondaryGroups.map((group) => (
+ section.label).join("-")
+ }
+ {...group}
+ />
+ ))}
+
+
+ );
+};
diff --git a/src/features/work-table/components/DayRow.tsx b/src/features/work-table/components/DayRow.tsx
index d605590..3c0180b 100644
--- a/src/features/work-table/components/DayRow.tsx
+++ b/src/features/work-table/components/DayRow.tsx
@@ -1,26 +1,25 @@
-import { useCallback, useEffect, useRef } from "react";
+import { useCallback, useEffect, useRef, useState } from "react";
import {
Box,
Checkbox,
Chip,
+ Collapse,
TableCell,
TableRow,
IconButton,
+ Tooltip,
} from "@mui/material";
import { useTranslation } from "react-i18next";
import AddIcon from "@mui/icons-material/Add";
+import KeyboardArrowDownIcon from "@mui/icons-material/KeyboardArrowDown";
+import KeyboardArrowUpIcon from "@mui/icons-material/KeyboardArrowUp";
import { useGlobalState, useWorkDays } from "@/hooks";
-import {
- TableViewMode,
- TimeFieldType,
- WorkDayInfo,
- WorkDayMap,
-} from "@/domain";
+import { TimeFieldType, WorkDayInfo, WorkDayMap } from "@/domain";
import { WorkDayStatus, HolidayKey } from "@/constants";
import {
- ExpandedDayRow,
+ DayDetails,
isSameDayPayMap,
ShiftRow,
useDay,
@@ -36,7 +35,6 @@ type DayRowProps = {
domain: DomainContextType;
workDay: WorkDayInfo;
isLastInWeek?: boolean;
- viewMode: TableViewMode;
shabbatCreditHours: number;
};
@@ -44,7 +42,6 @@ const DayRowComponent = ({
domain,
workDay,
isLastInWeek,
- viewMode,
shabbatCreditHours,
}: DayRowProps) => {
const { dateService } = domain.services;
@@ -55,6 +52,7 @@ const DayRowComponent = ({
const { baseRate, standardHours, year, month, addDay, removeDay } =
useGlobalState();
const { isSpecialFullDay } = useWorkDays();
+ const [detailsOpen, setDetailsOpen] = useState(false);
const {
status,
@@ -109,17 +107,20 @@ const DayRowComponent = ({
const shifts = Object.values(shiftEntries);
const shiftCount = Math.max(shifts.length, 1);
+ const detailsId = `day-details-${workDay.meta.date}`;
+ const expandedBreakdown = dayToPayBreakdownVM(dayPayMap, shabbatCreditHours);
+ const compactBreakdown = dayToCompactPayBreakdownVM(
+ dayPayMap,
+ baseRate,
+ shabbatCreditHours,
+ );
+ const columnCount = baseRate > 0 ? 13 : 12;
return (
<>
{(shifts.length ? shifts : [null]).map((item, index) => (
{index === 0 && (
<>
@@ -175,11 +176,15 @@ const DayRowComponent = ({
rowSpan={shiftCount}
sx={{
textAlign: "center",
- width: 48,
+ width: 40,
+ minWidth: 40,
+ maxWidth: 40,
+ p: 0.25,
verticalAlign: "middle",
}}
>
handleStatusChanged(
@@ -188,7 +193,10 @@ const DayRowComponent = ({
: WorkDayStatus.normal,
)
}
- sx={{ display: specialFullDay ? "none" : "inline-flex" }}
+ sx={{
+ display: specialFullDay ? "none" : "inline-flex",
+ p: 0.5,
+ }}
/>
@@ -197,11 +205,15 @@ const DayRowComponent = ({
sx={{
borderRight: "1px solid black",
textAlign: "center",
- width: 48,
+ width: 40,
+ minWidth: 40,
+ maxWidth: 40,
+ p: 0.25,
verticalAlign: "middle",
}}
>
handleStatusChanged(
@@ -210,7 +222,10 @@ const DayRowComponent = ({
: WorkDayStatus.normal,
)
}
- sx={{ display: specialFullDay ? "none" : "inline-flex" }}
+ sx={{
+ display: specialFullDay ? "none" : "inline-flex",
+ p: 0.5,
+ }}
/>
@@ -270,36 +285,70 @@ const DayRowComponent = ({
>
)}
- {index === 0 &&
- (viewMode === "compact" ? (
+ {index === 0 && (
+ <>
- ) : (
-
- ))}
+ sx={{ minWidth: 48, p: 0.5, verticalAlign: "middle" }}
+ >
+
+ setDetailsOpen((open) => !open)}
+ >
+ {detailsOpen ? (
+
+ ) : (
+
+ )}
+
+
+
+ >
+ )}
))}
+
+
+
+
+
+
+
>
);
};
diff --git a/src/features/work-table/components/ShiftRow.tsx b/src/features/work-table/components/ShiftRow.tsx
index a1a5a08..6dc47b5 100644
--- a/src/features/work-table/components/ShiftRow.tsx
+++ b/src/features/work-table/components/ShiftRow.tsx
@@ -148,8 +148,8 @@ export const ShiftRow = ({
borderRight: "1px solid black",
textAlign: "center",
whiteSpace: "nowrap",
- width: 120,
- maxWidth: 120,
+ width: 112,
+ maxWidth: 112,
p: 0.25,
overflow: "visible",
verticalAlign: "middle",
diff --git a/src/features/work-table/components/WorkTable.tsx b/src/features/work-table/components/WorkTable.tsx
index 26702f3..821253c 100644
--- a/src/features/work-table/components/WorkTable.tsx
+++ b/src/features/work-table/components/WorkTable.tsx
@@ -12,9 +12,7 @@ import {
CardContent,
Alert,
Divider,
- Switch,
- FormControlLabel,
- Tooltip,
+ TableCell,
} from "@mui/material";
import CalendarMonthIcon from "@mui/icons-material/CalendarMonth";
import { useTranslation } from "react-i18next";
@@ -22,35 +20,24 @@ import { useTranslation } from "react-i18next";
import { useGlobalState } from "@/hooks";
import { groupByShabbat } from "@/utils";
import { headersTable } from "@/constants";
-import { analyticsService } from "@/services";
import {
- ExpandedDayRow,
DayRow,
WorkTableHeader,
monthToCompactPayBreakdownVM,
} from "@/features/work-table";
import { DomainContextType } from "@/app";
-import { monthToPayBreakdownVM } from "@/adapters";
-import {
- ShabbatCreditAllocation,
- TableViewMode,
- WorkDayInfo,
-} from "@/domain";
+import { ShabbatCreditAllocation, WorkDayInfo } from "@/domain";
import { CompactDayRow } from "./rows/CompactDayRow";
type WorkTableProps = {
domain: DomainContextType;
workDays: WorkDayInfo[];
- viewMode: TableViewMode;
- onViewModeChange: (mode: TableViewMode) => void;
shabbatCreditAllocation: ShabbatCreditAllocation;
};
export const WorkTable = ({
domain,
workDays,
- viewMode,
- onViewModeChange,
shabbatCreditAllocation,
}: WorkTableProps) => {
const { year, month, baseRate, globalBreakdown } = useGlobalState();
@@ -74,31 +61,6 @@ export const WorkTable = ({
year,
})}
-
- {
- const mode = e.target.checked ? "expanded" : "compact";
- onViewModeChange(mode);
- analyticsService.track({
- name: "view_mode_toggled",
- params: { mode },
- });
- }}
- color="primary"
- />
- }
- label={
-
- {t("table.toggle_view_label")}
-
- }
- labelPlacement="start"
- />
-
{/* Table */}
@@ -132,7 +94,7 @@ export const WorkTable = ({
{groupByWeeks.map((group) => (
@@ -145,7 +107,6 @@ export const WorkTable = ({
key={day.meta.date}
workDay={day}
isLastInWeek={isLastInWeek}
- viewMode={viewMode}
shabbatCreditHours={
shabbatCreditAllocation.appliedHoursByDate[
day.meta.date
@@ -170,27 +131,16 @@ export const WorkTable = ({
},
}}
>
- {viewMode === "compact" ? (
-
- ) : (
-
- )}
+
+
diff --git a/src/features/work-table/components/WorkTableHeader.tsx b/src/features/work-table/components/WorkTableHeader.tsx
index fefc8d8..62697f6 100644
--- a/src/features/work-table/components/WorkTableHeader.tsx
+++ b/src/features/work-table/components/WorkTableHeader.tsx
@@ -109,6 +109,24 @@ export const WorkTableHeader = ({
{t("daily_salary_header")}
)}
+
+ {viewMode === "compact" && (
+
+ {t("headers.details")}
+
+ )}
{/* Row 2: Sub-headers */}
diff --git a/src/features/work-table/components/index.ts b/src/features/work-table/components/index.ts
index dfcd87b..0ce2e43 100644
--- a/src/features/work-table/components/index.ts
+++ b/src/features/work-table/components/index.ts
@@ -1,4 +1,5 @@
export { ExpandedDayRow } from "./rows/ExpandedPayRow";
+export { DayDetails } from "./DayDetails";
export { DayRow } from "./DayRow";
export { WorkTable } from "./WorkTable";
export { WorkTableHeader } from "./WorkTableHeader";
diff --git a/src/features/work-table/components/rows/CompactDayRow.tsx b/src/features/work-table/components/rows/CompactDayRow.tsx
index 4d1c378..f6ac69c 100644
--- a/src/features/work-table/components/rows/CompactDayRow.tsx
+++ b/src/features/work-table/components/rows/CompactDayRow.tsx
@@ -25,13 +25,13 @@ export const CompactDayRow = ({
rowSpan={rowSpan}
sx={{ ...baseCellSx(isFooter), ...rightBorderIfNotFooter(isFooter) }}
>
- {formatValue(breakdown.totalHours)}
+ {formatValue(breakdown.actualHours)}
- {formatValue(breakdown.actualHours)}
+ {formatValue(breakdown.totalHours)}
- {formatValue(breakdown.totalHours)}
+ {formatValue(breakdown.actualHours)}
- {formatValue(breakdown.actualHours)}
+ {formatValue(breakdown.totalHours)}
{formatValue(breakdown.regular.hours100.hours)}
diff --git a/src/i18n/locales/en/work-table.json b/src/i18n/locales/en/work-table.json
index 98bbbde..8afe4f4 100644
--- a/src/i18n/locales/en/work-table.json
+++ b/src/i18n/locales/en/work-table.json
@@ -26,7 +26,8 @@
"small": "Small",
"shabbat_credit": "Shabbat Credit",
"entry": "In",
- "exit": "Out"
+ "exit": "Out",
+ "details": "Details"
},
"months": [
"January",
@@ -71,6 +72,12 @@
"shabbat_credit_summary": "Shabbat credit — earned: {{earned}}, used: {{used}}, unused: {{unused}}.",
"shabbat_credit_unused_note": "Unused Shabbat credit is not included in total hours or salary."
},
+ "day_details": {
+ "show": "Show day details",
+ "hide": "Hide day details",
+ "region_label": "Day pay breakdown",
+ "points": "Points"
+ },
"shift_row": {
"cross_midnight_warning": "A shift crossing midnight was detected. Please check \"cross day\" before saving.",
"tooltip_cross_day_error": "⚠️ Mark as cross-day — end time is before start time",
diff --git a/src/i18n/locales/he/work-table.json b/src/i18n/locales/he/work-table.json
index 472e8ed..7c7e517 100644
--- a/src/i18n/locales/he/work-table.json
+++ b/src/i18n/locales/he/work-table.json
@@ -26,7 +26,8 @@
"small": "קטנה",
"shabbat_credit": "זכות שבת",
"entry": "כניסה",
- "exit": "יציאה"
+ "exit": "יציאה",
+ "details": "פרטים"
},
"months": [
"ינואר",
@@ -71,6 +72,12 @@
"shabbat_credit_summary": "זכות שבת — נצברו: {{earned}}, נוצלו: {{used}}, לא נוצלו: {{unused}}.",
"shabbat_credit_unused_note": "שעות זכות שבת שלא נוצלו אינן נכללות בסך השעות או בחישוב השכר."
},
+ "day_details": {
+ "show": "הצג פרטי יום",
+ "hide": "הסתר פרטי יום",
+ "region_label": "פירוט שכר יומי",
+ "points": "נקודות"
+ },
"shift_row": {
"cross_midnight_warning": "זוהתה משמרת החוצה את חצות. יש לסמן \"חוצה יום\" לפני שמירה.",
"tooltip_cross_day_error": "⚠️ יש לסמן חוצה יום - שעת סיום לפני שעת התחלה",
diff --git a/src/pages/DailyPage.tsx b/src/pages/DailyPage.tsx
index 08d7e57..d446a4a 100644
--- a/src/pages/DailyPage.tsx
+++ b/src/pages/DailyPage.tsx
@@ -20,14 +20,13 @@ import {
Feedback,
} from "@/features";
import {
- useDeviceType,
useFetch,
useGlobalState,
useShabbatCreditAllocation,
useWorkDays,
useAsync,
} from "@/hooks";
-import { ApiResponse, CalendarEventMap, TableViewMode } from "@/domain";
+import { ApiResponse, CalendarEventMap } from "@/domain";
import { buildEventMap } from "@/adapters";
import { DomainContextType } from "@/app";
import { hebcalService, analyticsService } from "@/services";
@@ -38,8 +37,6 @@ const calendarApi = hebcalService();
export const DailyPage = ({ domain }: { domain: DomainContextType }) => {
const { t } = useTranslation("work-table");
const { dateService } = domain.services;
- const { isMobile } = useDeviceType();
-
const { year, month, baseRate, reset } = useGlobalState();
const { workDays, generate } = useWorkDays();
@@ -47,10 +44,6 @@ export const DailyPage = ({ domain }: { domain: DomainContextType }) => {
const [error, setError] = useState(undefined);
- const [viewMode, setViewMode] = useState(
- isMobile ? "compact" : "expanded",
- );
-
const { loading, callEndPoint, cancelEndPoint } = useFetch();
const handleCalendarResult = ({
@@ -173,8 +166,6 @@ export const DailyPage = ({ domain }: { domain: DomainContextType }) => {
diff --git a/src/test/ui/features/DayDetails.test.tsx b/src/test/ui/features/DayDetails.test.tsx
new file mode 100644
index 0000000..66fb726
--- /dev/null
+++ b/src/test/ui/features/DayDetails.test.tsx
@@ -0,0 +1,86 @@
+import { afterAll, beforeAll, describe, expect, it } from "vitest";
+
+import i18n from "@/i18n";
+import { PayBreakdownViewModel } from "@/domain";
+import { DayDetails } from "@/features/work-table/components/DayDetails";
+import { renderWithTheme, screen } from "@/test/ui/utils";
+
+const breakdown: PayBreakdownViewModel = {
+ totalHours: 12,
+ actualHours: 10,
+ regular: {
+ hours100: { hours: 6, percent: 1 },
+ hours125: { hours: 2, percent: 1.25 },
+ hours150: { hours: 1, percent: 1.5 },
+ },
+ extra: {
+ hours20: { hours: 0.5, percent: 0.2 },
+ hours50: { hours: 0.75, percent: 0.5 },
+ },
+ special: {
+ shabbat150: { hours: 1.5, percent: 1.5 },
+ shabbat200: { hours: 0.25, percent: 2 },
+ },
+ hours100Sick: { hours: 3, percent: 1 },
+ hours100Vacation: { hours: 4, percent: 1 },
+ appliedShabbatCredit: { hours: 5, percent: 1 },
+ perDiemPoints: 7,
+ perDiemAmount: 0,
+ largePoints: 8,
+ largeAmount: 0,
+ smallPoints: 9,
+ smallAmount: 0,
+};
+
+describe("DayDetails", () => {
+ beforeAll(async () => {
+ await i18n.changeLanguage("en");
+ });
+
+ afterAll(async () => {
+ await i18n.changeLanguage("he");
+ });
+
+ it("renders the complete day breakdown in an accessible region", () => {
+ renderWithTheme();
+
+ expect(
+ screen.getByRole("region", { name: "Day pay breakdown" }),
+ ).toHaveAttribute("id", "day-details");
+ expect(
+ screen.getByRole("columnheader", { name: "OT" }),
+ ).toBeInTheDocument();
+ expect(
+ screen.getByRole("columnheader", { name: "Shabbat" }),
+ ).toBeInTheDocument();
+ expect(
+ screen.getByRole("columnheader", { name: "Meal Allow." }),
+ ).toBeInTheDocument();
+ expect(
+ screen.getByRole("columnheader", { name: "Per Diem" }),
+ ).toBeInTheDocument();
+ expect(screen.getByText("Shabbat Credit")).toBeInTheDocument();
+ expect(screen.getByText("0.75")).toBeInTheDocument();
+ expect(screen.getByText("9.00")).toBeInTheDocument();
+ });
+
+ it("hides absence details for special full days", () => {
+ renderWithTheme(
+ ,
+ );
+
+ expect(
+ screen.queryByRole("columnheader", { name: "Absence" }),
+ ).not.toBeInTheDocument();
+ expect(
+ screen.getByRole("columnheader", { name: "Meal Allow." }),
+ ).toBeInTheDocument();
+ expect(
+ screen.getByRole("columnheader", { name: "Per Diem" }),
+ ).toBeInTheDocument();
+ });
+});