Repository: Kuama-IT/kuama-control-center
Pull request #27: refactor: dipendentin in cloud report
Author: login: kuamanet
Created: 2025-03-30T09:41:52Z | Merged: 2025-03-30T19:49:55Z

feat: employee national security number, phone number and iban
feat: import absence reasons from dipendenti in cloud
feat: import closure days from dipendenti in cloud
feat: k closure days

<!-- This is an auto-generated comment: release notes by coderabbit.ai -->
## Summary by CodeRabbit

- **New Features**
  - Employee profiles now include detailed contact and financial information.
  - Reports display enhanced absence analytics with additional reasons and closure details.
  - New synchronization options enable more accurate updating of absence and closure data.
  - New utility functions facilitate report generation and data handling.
  - New asynchronous functions for retrieving absence reasons and closures from the database.

- **Enhancements**
  - Report layouts have been revamped into modular components for a clearer presentation.
  - Improved error handling ensures smoother data retrieval during report generation.
  - Enhanced API client functionality provides more detailed employee data and additional endpoints.

- **Chores**
  - Updated date management dependency for enhanced functionality.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->


Diff:
diff --git a/__tests__/modules/dipendenti-in-cloud/dipendenti-in-cloud-api-client.test.ts b/__tests__/modules/dipendenti-in-cloud/dipendenti-in-cloud-api-client.test.ts
index 9a1bb1c..e056818 100644
--- a/__tests__/modules/dipendenti-in-cloud/dipendenti-in-cloud-api-client.test.ts
+++ b/__tests__/modules/dipendenti-in-cloud/dipendenti-in-cloud-api-client.test.ts
@@ -1,15 +1,13 @@
 import { describe, expect, test } from "vitest";
-import { PubblicaWebApi } from "@/modules/pubblica-web/pubblica-web-api-client";
-import { serverEnv } from "@/env/server-env";
-import { dipendentiInCloudApiClient } from "@/modules/dipendenti-in-cloud/dipendenti-in-cloud-api-client";
-import { parseSalary } from "@/modules/dipendenti-in-cloud/dipendenti-in-cloud-utils";
 
 // N.B. This was written just to build the client, it is performing real calls and presumes a working env is present
+
 describe("dipendenti-in-cloud-api", () => {
   // just to make vitest happy and keep this suite of tests for reference
   test("fake test", () => {
     expect(1).toBe(1);
   });
+
   // test("get employees", async () => {
   //   const employees = await dipendentiInCloudApiClient.getEmployees();
   //
@@ -54,6 +52,17 @@ describe("dipendenti-in-cloud-api", () => {
   //   }
   // });
 
+  // test("get absence reasons", async () => {
+  //   const reasons = await dipendentiInCloudApiClient.getAbsenceReasons();
+  //   expect(reasons.length).greaterThan(1);
+  // });
+  //
+  // test("get closures", async () => {
+  //   const closures = await dipendentiInCloudApiClient.getClosures();
+  //   expect(closures.length).greaterThan(1);
+  //   expect(closures.find((it) => it.day === 1 && it.month === 1)).toBeDefined();
+  // });
+
   // test("it can send payslip from PubblicaWeb to DipendentiInCloud", async () => {
   //   const pubblicaWebClient = new PubblicaWebApi(
   //     serverEnv.pubblicaWebUsername,
diff --git a/drizzle/schema.ts b/drizzle/schema.ts
index e1075ad..4ad35cc 100644
--- a/drizzle/schema.ts
+++ b/drizzle/schema.ts
@@ -122,6 +122,9 @@ export const kEmployees = pgTable("k_employees", {
   avatarUrl: text(),
   dipendentiInCloudId: varchar({ length: 256 }).unique(),
   hiredOn: date(),
+  nationalInsuranceNumber: varchar({ length: 256 }),
+  phoneNumber: varchar({ length: 256 }),
+  iban: varchar({ length: 256 }),
 });
 
 // Each month our employment consultant sends us a pdf file with the estimated costs for each employee
@@ -133,12 +136,28 @@ export const kEmployeeEstimatedCosts = pgTable("k_employee_estimated_costs", {
   date: date().notNull(),
 });
 
+// Used to generate the legend of the report for our payrolls consultant
+export const kAbsenceReasons = pgTable("k_absence_reasons", {
+  id: serial().primaryKey(),
+  code: varchar({ length: 256 }).notNull(),
+  name: varchar({ length: 256 }).notNull(),
+});
+
+export const kClosures = pgTable("k_closures", {
+  id: serial().primaryKey(),
+  day: integer().notNull(),
+  month: integer().notNull(),
+  year: integer(),
+  description: varchar({ length: 256 }),
+});
+
 // Help to understand who is working this week
 export const kAbsenceDays = pgTable("k_absence_days", {
   id: serial().primaryKey(),
   date: date(),
   employeeId: serial().references(() => kEmployees.id),
   description: text(),
+  reasonCode: varchar({ length: 256 }),
   duration: interval({ fields: "minute" }),
   pending: boolean(),
   timeStart: varchar(),
diff --git a/package.json b/package.json
index 3e52abb..6c00b03 100644
--- a/package.json
+++ b/package.json
@@ -19,6 +19,7 @@
     "@fattureincloud/fattureincloud-ts-sdk": "^2.1.1",
     "@gitbeaker/rest": "^42.1.0",
     "@hookform/resolvers": "^3.10.0",
+    "@js-joda/core": "^5.6.4",
     "@octokit/core": "^6.1.3",
     "@octokit/plugin-rest-endpoint-methods": "^13.3.0",
     "@radix-ui/react-alert-dialog": "^1.1.6",
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 78f8f69..7278575 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -21,6 +21,9 @@ importers:
       '@hookform/resolvers':
         specifier: ^3.10.0
         version: 3.10.0(react-hook-form@7.54.2(react@19.0.0))
+      '@js-joda/core':
+        specifier: ^5.6.4
+        version: 5.6.4
       '@octokit/core':
         specifier: ^6.1.3
         version: 6.1.3
@@ -1073,6 +1076,9 @@ packages:
   '@jridgewell/trace-mapping@0.3.25':
     resolution: {integrity: sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==}
 
+  '@js-joda/core@5.6.4':
+    resolution: {integrity: sha512-ChdLDTYMEoYoiKZMT90wZMEdGvZ2/QZMnhvjvEqeO5oLoxUfSiLzfe6Lhf3g88+MhZ+utbAu7PAxX1sZkLo5pA==}
+
   '@next/env@15.2.4':
     resolution: {integrity: sha512-+SFtMgoiYP3WoSswuNmxJOCwi06TdWE733D+WPjpXIe4LXGULwEaofiiAy6kbS0+XjM5xF5n3lKuBwN2SnqD9g==}
 
@@ -4186,6 +4192,8 @@ snapshots:
       '@jridgewell/resolve-uri': 3.1.2
       '@jridgewell/sourcemap-codec': 1.5.0
 
+  '@js-joda/core@5.6.4': {}
+
   '@next/env@15.2.4': {}
 
   '@next/swc-darwin-arm64@15.2.4':
diff --git a/src/app/reports/dipendenti-in-cloud/page.tsx b/src/app/reports/dipendenti-in-cloud/page.tsx
index f68b09a..d947735 100644
--- a/src/app/reports/dipendenti-in-cloud/page.tsx
+++ b/src/app/reports/dipendenti-in-cloud/page.tsx
@@ -2,7 +2,7 @@ import DipendentiInCloudReport from "@/modules/dipendenti-in-cloud/components/di
 import { kEmployeesServer } from "@/modules/k-employees/k-employee-server";
 import { isFailure } from "@/utils/server-action-utils";
 import { kAbsenceDaysServer } from "@/modules/k-absence-days/k-absence-days-server";
-import { endOfMonth, startOfMonth } from "date-fns";
+import { endOfMonth, parse, startOfMonth } from "date-fns";
 
 export default async function Page() {
   const employees = await kEmployeesServer.listAll();
@@ -12,7 +12,9 @@ export default async function Page() {
   }
 
   // TODO from and to should be parameters
-  const date = new Date();
+  // TODO add token to the request
+  // TODO date it's fixed to march until we get acceptance from the payroll office
+  const date = parse("10-03-2025", "dd-MM-yyyy", new Date());
   const from = startOfMonth(date);
   const to = endOfMonth(date);
 
@@ -25,9 +27,19 @@ export default async function Page() {
     return <pre>Could not load absences</pre>;
   }
 
+  const absenceReasons = await kAbsenceDaysServer.listReasons();
+  if (isFailure(absenceReasons)) {
+    return <pre>Could not load absence reasons</pre>;
+  }
+
+  const closures = await kAbsenceDaysServer.closures();
+  if (isFailure(closures)) {
+    return <pre>Could not load closures</pre>;
+  }
+
   const uniqueReasons: string[] = [];
   for (const absence of absences) {
-    const reason = absence.k_absence_days?.description;
+    const reason = absence.k_absence_days?.reasonCode;
 
     if (reason && !uniqueReasons.includes(reason)) {
       uniqueReasons.push(reason);
@@ -40,7 +52,9 @@ export default async function Page() {
       to={to}
       absences={absences}
       employees={employees}
-      uniqueReasons={uniqueReasons}
+      uniqueAbsenceReasons={uniqueReasons}
+      absenceReasons={absenceReasons}
+      closures={closures}
     />
   );
 }
diff --git a/src/modules/dipendenti-in-cloud/actions/dipendenti-in-cloud-import-absence-reasons-and-closures.ts b/src/modules/dipendenti-in-cloud/actions/dipendenti-in-cloud-import-absence-reasons-and-closures.ts
new file mode 100644
index 0000000..60b8352
--- /dev/null
+++ b/src/modules/dipendenti-in-cloud/actions/dipendenti-in-cloud-import-absence-reasons-and-closures.ts
@@ -0,0 +1,34 @@
+"use server";
+import { handleServerErrors } from "@/utils/server-action-utils";
+import { kAbsenceReasons, kClosures } from "@/drizzle/schema";
+import { db } from "@/drizzle/drizzle-db";
+import { dipendentiInCloudApiClient } from "@/modules/dipendenti-in-cloud/dipendenti-in-cloud-api-client";
+
+const dipendentiInCloudImportAbsenceReasonsAndClosures = async () => {
+  const absenceReasons = await dipendentiInCloudApiClient.getAbsenceReasons();
+  const closures = await dipendentiInCloudApiClient.getClosures();
+  await db.transaction(async (tx) => {
+    await tx.delete(kAbsenceReasons);
+    await tx.delete(kClosures);
+
+    await tx.insert(kAbsenceReasons).values(absenceReasons);
+    await tx.insert(kClosures).values(
+      closures.map(({ day, month, year, disabled_reason }) => ({
+        day,
+        month,
+        year,
+        description: disabled_reason,
+      })),
+    );
+  });
+
+  return {
+    message: `Imported ${absenceReasons.length} absence reasons and ${closures.length} closures`,
+  };
+};
+
+const handled = handleServerErrors(
+  dipendentiInCloudImportAbsenceReasonsAndClosures,
+);
+
+export default handled;
diff --git a/src/modules/dipendenti-in-cloud/actions/dic-import-action.ts b/src/modules/dipendenti-in-cloud/actions/dipendenti-in-cloud-import-employees-action.ts
similarity index 87%
rename from src/modules/dipendenti-in-cloud/actions/dic-import-action.ts
rename to src/modules/dipendenti-in-cloud/actions/dipendenti-in-cloud-import-employees-action.ts
index f72843b..20db610 100644
--- a/src/modules/dipendenti-in-cloud/actions/dic-import-action.ts
+++ b/src/modules/dipendenti-in-cloud/actions/dipendenti-in-cloud-import-employees-action.ts
@@ -26,6 +26,9 @@ const handled = handleServerErrors(async () => {
           fullName: employee.full_name,
           hiredOn: employee.current_contract?.valid_from,
           email: employee.email,
+          iban: employee.iban,
+          nationalInsuranceNumber: employee.tax_code,
+          phoneNumber: employee.phone_number,
         });
         continue;
       }
@@ -38,6 +41,9 @@ const handled = handleServerErrors(async () => {
           surname: employee.last_name,
           fullName: employee.full_name,
           hiredOn: employee.current_contract?.valid_from,
+          iban: employee.iban,
+          nationalInsuranceNumber: employee.tax_code,
+          phoneNumber: employee.phone_number,
         })
         .where(eq(lower(kEmployees.email), employee.email.toLowerCase()));
     }
diff --git a/src/modules/dipendenti-in-cloud/actions/sync-dipendenti-in-cloud-timesheet-action.ts b/src/modules/dipendenti-in-cloud/actions/sync-dipendenti-in-cloud-timesheet-action.ts
index 763c620..66479a9 100644
--- a/src/modules/dipendenti-in-cloud/actions/sync-dipendenti-in-cloud-timesheet-action.ts
+++ b/src/modules/dipendenti-in-cloud/actions/sync-dipendenti-in-cloud-timesheet-action.ts
@@ -70,6 +70,7 @@ const syncTimesheet = async ({ from, to }: { from: Date; to: Date }) => {
                     date,
                     employeeId,
                     description: reason.name,
+                    reasonCode: reason.code,
                     duration: `${shift.duration ?? shift.duration_pending} minute`,
                     pending: shift.duration_pending !== null,
                     timeStart: shift.time_start,
@@ -80,6 +81,7 @@ const syncTimesheet = async ({ from, to }: { from: Date; to: Date }) => {
                     date,
                     employeeId,
                     description: reason.name,
+                    reasonCode: reason.code,
                     duration: `${duration ?? duration_pending} minute`,
                     pending: duration_pending !== null,
                     timeStart: "08:00:00",
diff --git a/src/modules/dipendenti-in-cloud/components/dipendenti-in-cloud-report.tsx b/src/modules/dipendenti-in-cloud/components/dipendenti-in-cloud-report.tsx
index 71fd932..fb7b547 100644
--- a/src/modules/dipendenti-in-cloud/components/dipendenti-in-cloud-report.tsx
+++ b/src/modules/dipendenti-in-cloud/components/dipendenti-in-cloud-report.tsx
@@ -1,299 +1,213 @@
 import {
-  eachDayOfInterval,
-  getDay,
-  format,
-  add,
-  subHours,
-  subMinutes,
-  startOfDay,
-} from "date-fns";
-import {
-  Table,
-  TableHeader,
-  TableBody,
-  TableRow,
-  TableHead,
-  TableCell,
-  TableCaption,
-} from "@/components/ui/table";
-import { it } from "date-fns/locale";
-import { KAbsenceDaysList } from "@/modules/k-absence-days/actions/k-absence-days-list";
-import { KEmployeesListAllActionResult } from "@/modules/k-employees/actions/k-employee-list-all-action";
-import parsePostgresInterval from "postgres-interval";
-
-export type DipendentiInCloudReportProps = {
-  from: Date;
-  to: Date;
-  absences: KAbsenceDaysList;
-  uniqueReasons: string[]; // todo rename to uniqueAbsenceReasons
-  employees: KEmployeesListAllActionResult;
-};
+  DipendentiInCloudReportProps,
+  getDataForReport,
+  getItalianDayLetter,
+  getReportTitle,
+  MonthDayItem,
+  ReportRow,
+  UIDay,
+} from "../dipendenti-in-cloud-report-utils";
+import { format } from "date-fns";
 
 /**
- * Returns the letter corresponding to the day of the week in Italian:
- * 0 (Sunday)    => D
- * 1 (Monday)    => L
- * 2 (Tuesday)   => M
- * 3 (Wednesday) => M
- * 4 (Thursday)  => G
- * 5 (Friday)    => V
- * 6 (Saturday)  => S
+ * DipendentiInCloudReport
+ * A component to build a monthly report of worked hours/not worked hours
+ * to our payroll office.
+ * @constructor
  */
-function getItalianDayLetter(date: Date): string {
-  const dayIndex = getDay(date); // 0=Sunday, 1=Monday, ...
-  switch (dayIndex) {
-    case 0:
-      return "D";
-    case 1:
-      return "L";
-    case 2:
-      return "M";
-    case 3:
-      return "M";
-    case 4:
-      return "G";
-    case 5:
-      return "V";
-    case 6:
-      return "S";
-    default:
-      return "";
-  }
-}
-
-const isSaturdayOrSunday = (date: Date): boolean => {
-  const day = getDay(date);
-  return day === 0 || day === 6; // 0 = Sunday, 6 = Saturday
-};
-
-export default function DipendentiInCloudReport({
-  from,
-  to,
-  employees,
-  absences,
-  uniqueReasons,
-}: DipendentiInCloudReportProps) {
-  // Calculate days in the received period
-  const daysInMonth = eachDayOfInterval({
-    start: from,
-    end: to,
-  });
-  const maxWorkableHoursPerDay = 8;
-  const maxWorkableHoursPerPeriod = daysInMonth.reduce((total, day) => {
-    if (isSaturdayOrSunday(day)) {
-      return total;
-    }
-
-    return total + maxWorkableHoursPerDay;
-  }, 0);
-
-  // Format month and year label
-  const monthYearLabel = format(from, "MMMM yyyy", { locale: it });
-  const monthYearLabelCapitalized =
-    monthYearLabel.charAt(0).toUpperCase() + monthYearLabel.slice(1);
+export default function DipendentiInCloudReport(
+  props: DipendentiInCloudReportProps,
+) {
+  const { rows, month } = getDataForReport(props);
 
+  const employeeCellClass = "w-[200px] min-w-[200px]";
+  const dayCellClass = "w-[40px] max-w-[40px]";
   return (
     <div className="m-16 space-y-4 text-sm min-w-max">
-      <div className="text-end">
-        Mese: <strong>{monthYearLabelCapitalized}</strong>
-      </div>
+      <ReportTitle date={props.from} />
 
       <div className="border-black border text-black flex flex-col">
-        <div className="uppercase font-bold py-2 border-black  flex justify-center">
-          Ore lavorate
-        </div>
-
-        <div className="uppercase border-t border-black flex">
-          {/* employee name */}
-          <div className="w-[100px] min-w-[100px] py-2 border-r border-black"></div>
+        <ReportPreHeader />
+
+        <ReportHeader
+          month={month}
+          dayCellClass={dayCellClass}
+          employeeCellClass={employeeCellClass}
+        />
+
+        <ReportBody
+          rows={rows}
+          dayCellClass={dayCellClass}
+          employeeCellClass={employeeCellClass}
+        />
+      </div>
+      <ReportLegend reasons={props.absenceReasons} />
+    </div>
+  );
+}
 
-          {/* absence reason */}
-          <div className="w-[40px] min-w-[40px] py-2 border-r border-black"></div>
+const ReportTitle = ({ date }: { date: Date }) => {
+  return (
+    <div className="text-end">
+      Mese: <strong>{getReportTitle(date)}</strong>
+    </div>
+  );
+};
 
-          {/* days */}
-          {daysInMonth.map((day, index) => {
-            return (
+const ReportPreHeader = () => (
+  <div className="uppercase font-bold py-2 border-black  flex justify-center">
+    Ore lavorate
+  </div>
+);
+
+const ReportHeader = ({
+  month,
+  employeeCellClass,
+  dayCellClass,
+}: {
+  month: MonthDayItem[];
+  dayCellClass: string;
+  employeeCellClass: string;
+}) => (
+  <div className="uppercase border-t border-black flex">
+    {/* employee name */}
+    <div className={`${employeeCellClass} py-2 border-r border-black`}></div>
+
+    {/* absence reason */}
+    <div className={`${dayCellClass} py-2 border-r border-black`}></div>
+
+    {/* days */}
+    {month.map((day, index) => {
+      return (
+        <div
+          key={`report-header-${index}`}
+          className={`flex flex-col items-center ${dayCellClass} border-r border-black text-xs`}
+        >
+          <span>{getItalianDayLetter(day.date)}</span>
+          <span>{format(day.date, "d")}</span>
+        </div>
+      );
+    })}
+    <div className={"flex items-center justify-center flex-1"}>Totali</div>
+  </div>
+);
+
+const ReportBody = ({
+  rows,
+  employeeCellClass,
+  dayCellClass,
+}: {
+  rows: ReportRow[];
+  employeeCellClass: string;
+  dayCellClass: string;
+}) => {
+  return rows.map((item, index) => {
+    return (
+      <div className="uppercase border-t border-black flex" key={index}>
+        <div
+          className={`${employeeCellClass} py-2 border-r border-black flex items-center justify-center text-center`}
+        >
+          <div className="flex flex-col gap-1">
+            {item.employeeName}
+            <pre className="font-[10px]">
+              {item.employeeNationalInsuranceNumber}
+            </pre>
+          </div>
+        </div>
+        <div className="flex flex-col flex-1">
+          {item.reasons.map(({ code, days }, reasonIndex) => (
+            <div
+              className={`flex ${reasonIndex > 0 ? "border-t border-black" : ""}`}
+              key={`${index}-${reasonIndex}-${code}`}
+            >
               <div
-                key={index}
-                className={`flex flex-col items-center min-w-[40px] w-[40px] border-r border-black text-xs`}
+                className={`${dayCellClass} py-2 border-r border-black flex items-center justify-center`}
               >
-                <span>{getItalianDayLetter(day)}</span>
-                <span>{format(day, "d")}</span>
-              </div>
-            );
-          })}
-          <div className="text-end w-[100px]">Totali</div>
-        </div>
-
-        {employees.map((employee) => {
-          let monthlyTotal = maxWorkableHoursPerPeriod;
-          return (
-            <div className="uppercase border-t border-black flex">
-              {/* employee name */}
-              <div className="min-w-[100px] w-[100px] py-2 border-r border-black flex items-center justify-center text-center">
-                {employee.name} {employee.surname}
+                {code}
               </div>
-              <div className="flex flex-col flex-1">
-                {uniqueReasons.map((reason, index) => {
-                  let reasonText = reason;
-                  if (reason.toLowerCase() === "rol") {
-                    reasonText = "ROL";
-                  }
 
-                  if (reason.toLowerCase() === "permessi ex festività") {
-                    reasonText = "PEF";
-                  }
-
-                  if (reason.toLowerCase() === "permessi") {
-                    reasonText = "P";
-                  }
-
-                  if (reason.toLowerCase() === "donazione sangue") {
-                    reasonText = "DS";
-                  }
-
-                  return (
-                    <div
-                      className={`flex ${index > 0 ? "border-t border-black" : ""}`}
-                    >
-                      <div className="min-w-[40px] w-[40px] py-2 border-r border-black flex items-center justify-center">
-                        {reasonText}
-                      </div>
-                      {/* days */}
-                      {daysInMonth.map((day, index) => {
-                        if (isSaturdayOrSunday(day)) {
-                          const dayOfWeek = getDay(day);
-                          const extraClasses =
-                            dayOfWeek === 0
-                              ? "bg-gray-300"
-                              : dayOfWeek === 6
-                                ? "bg-gray-200"
-                                : "";
-                          return (
-                            <div
-                              key={index}
-                              className={`${extraClasses} min-w-[60p] w-[40px] border-r border-black`}
-                            ></div>
-                          );
-                        }
-
-                        const employeeAbsences = absences.filter(
-                          ({ k_employees, k_absence_days }) => {
-                            return (
-                              k_employees?.id === employee.id &&
-                              k_absence_days.date ===
-                                format(day, "yyyy-MM-dd") &&
-                              k_absence_days.description === reason
-                            );
-                          },
-                        );
-
-                        const hoursAndMinutes = employeeAbsences.reduce(
-                          (total, { k_absence_days: absence }) => {
-                            const { hours, minutes } = parsePostgresInterval(
-                              absence?.duration ?? "",
-                            );
+              {days.map((day, dayIndex) => (
+                <ReportDayCell
+                  day={day}
+                  dayCellClass={dayCellClass}
+                  key={`r-${index}-${reasonIndex}-${dayIndex}-${day.formattedDate}`}
+                />
+              ))}
+            </div>
+          ))}
+
+          <div className={`flex border-t border-black`}>
+            <div className={`${dayCellClass} py-2 border-r border-black`} />
+            {item.totals.map((day, itemTotalIndex) => (
+              <ReportDayCell
+                day={day}
+                dayCellClass={dayCellClass}
+                key={`t-${index}-${itemTotalIndex}-${day.formattedDate}`}
+              />
+            ))}
+          </div>
+        </div>
 
-                            return {
-                              hours: total.hours + hours,
-                              minutes: total.minutes + minutes,
-                            };
-                          },
-                          { hours: 0, minutes: 0 },
-                        );
-                        const { hours, minutes } = parsePostgresInterval(
-                          `${hoursAndMinutes.hours}:${hoursAndMinutes.minutes}:00`,
-                        );
-                        monthlyTotal -= hours;
-                        // minutes to hours
-                        const extraHours = Math.floor(minutes / 60);
-                        monthlyTotal -= extraHours;
-                        return (
-                          <div
-                            key={index}
-                            className={`flex flex-col justify-center items-center min-w-[40px] w-[40px] border-r border-black text-xs`}
-                          >
-                            {(hours > 0 || minutes > 0) && (
-                              <>
-                                {hours > 0 && `${hours}h`}{" "}
-                                {minutes > 0 && `${minutes}m`}
-                              </>
-                            )}
-                          </div>
-                        );
-                      })}
-                    </div>
-                  );
-                })}
+        <div className="flex-1 text-end items-center flex justify-center">
+          {item.monthlyTotalLabel}
+        </div>
+      </div>
+    );
+  });
+};
 
-                <div className={`flex border-t border-black`}>
-                  <div className="min-w-[40px] w-[40px] py-2 border-r border-black"></div>
-                  {daysInMonth.map((day, index) => {
-                    const employeeAbsences = absences.filter(
-                      ({ k_employees, k_absence_days }) => {
-                        return (
-                          k_employees?.id === employee.id &&
-                          k_absence_days.date === format(day, "yyyy-MM-dd")
-                        );
-                      },
-                    );
-                    const { hours, minutes } = employeeAbsences.reduce(
-                      (total, { k_absence_days: absence }) => {
-                        const { hours, minutes } = parsePostgresInterval(
-                          absence?.duration ?? "",
-                        );
+const ReportDayCell = ({
+  day,
+  dayCellClass,
+}: {
+  day: UIDay;
+  dayCellClass: string;
+}) => {
+  if (day.isClosureOrWeekend) {
+    return <NotWorkingDayCell day={day} dayCellClass={dayCellClass} />;
+  }
 
-                        return {
-                          hours: total.hours + hours,
-                          minutes: total.minutes + minutes,
-                        };
-                      },
-                      { hours: 0, minutes: 0 },
-                    );
-                    const date = startOfDay(new Date());
-                    const maxDuration = { hours: maxWorkableHoursPerDay };
-                    const baseDate = add(date, maxDuration);
-                    const baseDateMinusHours = subHours(baseDate, hours);
-                    const baseDateMinusMinutes = subMinutes(
-                      baseDateMinusHours,
-                      minutes,
-                    );
+  return (
+    <div
+      className={`flex flex-col justify-center items-center ${dayCellClass} border-r border-black text-xs py-2`}
+    >
+      {day.label}
+    </div>
+  );
+};
 
-                    if (isSaturdayOrSunday(day)) {
-                      const dayOfWeek = getDay(day);
-                      const extraClasses =
-                        dayOfWeek === 0
-                          ? "bg-gray-300"
-                          : dayOfWeek === 6
-                            ? "bg-gray-200"
-                            : "";
-                      return (
-                        <div
-                          key={index}
-                          className={`${extraClasses} min-w-[40px] w-[40px] border-r border-black`}
-                        ></div>
-                      );
-                    }
+const NotWorkingDayCell = ({
+  day,
+  dayCellClass,
+}: {
+  day: MonthDayItem;
+  dayCellClass: string;
+}) => {
+  const extraClasses = day.isSunday
+    ? "bg-gray-300"
+    : day.isSaturday
+      ? "bg-gray-200"
+      : "bg-gray-100";
+  return (
+    <div className={`${extraClasses} ${dayCellClass} border-r border-black`} />
+  );
+};
 
-                    return (
-                      <div
-                        key={index}
-                        className={`min-w-[40px] w-[40px] border-r border-black text-xs flex items-center justify-center py-2`}
-                      >
-                        {format(baseDateMinusMinutes, "HH:mm")}
-                      </div>
-                    );
-                  })}
-                </div>
-              </div>
+const ReportLegend = ({
+  reasons,
+}: {
+  reasons: { code: string; name: string }[];
+}) => {
+  return (
+    <div className="flex flex-col text-sm">
+      <div className="uppercase font-bold py-2">Legenda</div>
 
-              <div className="flex-1 text-end w-[100px] items-center flex justify-center">
-                {monthlyTotal}
-              </div>
-            </div>
-          );
-        })}
-      </div>
+      {reasons.map((reason) => (
+        <div key={reason.code} className="flex gap-1 items-center text-xs">
+          <span className="w-12 font-bold">{reason.code}</span>
+          <span>{reason.name}</span>
+        </div>
+      ))}
     </div>
   );
-}
+};
diff --git a/src/modules/dipendenti-in-cloud/components/sync-dipendenti-in-cloud.tsx b/src/modules/dipendenti-in-cloud/components/sync-dipendenti-in-cloud.tsx
index 2b86498..19f60ec 100644
--- a/src/modules/dipendenti-in-cloud/components/sync-dipendenti-in-cloud.tsx
+++ b/src/modules/dipendenti-in-cloud/components/sync-dipendenti-in-cloud.tsx
@@ -4,26 +4,17 @@ import { endOfMonth, format, startOfMonth } from "date-fns";
 import { FormEvent, useState, useTransition } from "react";
 import { DateRange } from "react-day-picker";
 import syncDipendentiInCloudTimesheet from "@/modules/dipendenti-in-cloud/actions/sync-dipendenti-in-cloud-timesheet-action";
+import syncDipendentiInCloudEmployees from "@/modules/dipendenti-in-cloud/actions/dipendenti-in-cloud-import-employees-action";
+import syncDipendentiInCloudAbsenceReasonsAndClosures from "@/modules/dipendenti-in-cloud/actions/dipendenti-in-cloud-import-absence-reasons-and-closures";
 import { Calendar } from "@/components/ui/calendar";
 import { Button } from "@/components/ui/button";
 import { FaArrowRight, FaCalendar, FaSync } from "react-icons/fa";
 import { Separator } from "@/components/ui/separator";
-import syncDipendentiInCloudEmployees from "@/modules/dipendenti-in-cloud/actions/dic-import-action";
 import { isFailure } from "@/utils/server-action-utils";
 import { notifyError, notifySuccess } from "@/modules/ui/components/notify";
 import { cn } from "@/lib/utils";
 
 export default function SyncDipendentiInCloud() {
-  const today = new Date();
-  const firstDateRange = startOfMonth(today);
-  const lastDateRange = endOfMonth(today);
-  const initialRange = {
-    from: firstDateRange,
-    to: lastDateRange,
-  };
-  const [range, setRange] = useState<DateRange | undefined>(initialRange);
-  const [isPending, startTransition] = useTransition();
-
   return (
     <div className="border rounded-lg p-4">
       <Title>Dipendenti in cloud</Title>
@@ -32,6 +23,8 @@ export default function SyncDipendentiInCloud() {
         <SyncTimesheetForm />
         <Separator />
         <SyncEmployeeData />
+        <Separator />
+        <SyncAbsenceReasonsAndClosures />
       </div>
     </div>
   );
@@ -45,7 +38,9 @@ const SyncEmployeeData = () => {
     startTransition(async () => {
       const res = await syncDipendentiInCloudEmployees();
       if (isFailure(res)) {
-        notifyError("Error while syncing dipendenti in cloud general info");
+        notifyError(
+          "Error while syncing dipendenti in cloud employees general info",
+        );
         return;
       }
 
@@ -63,6 +58,32 @@ const SyncEmployeeData = () => {
   );
 };
 
+const SyncAbsenceReasonsAndClosures = () => {
+  const [isPending, startTransition] = useTransition();
+
+  const onSubmit = (ev: FormEvent) => {
+    ev.preventDefault();
+    startTransition(async () => {
+      const res = await syncDipendentiInCloudAbsenceReasonsAndClosures();
+      if (isFailure(res)) {
+        notifyError("Error while syncing dipendenti in cloud general info");
+        return;
+      }
+
+      notifySuccess(res.message);
+    });
+  };
+
+  return (
+    <form onSubmit={onSubmit}>
+      <Button disabled={isPending} size="lg">
+        <FaSync className={cn({ "animate-spin": isPending })} />
+        Sync general information
+      </Button>
+    </form>
+  );
+};
+
 const SyncTimesheetForm = () => {
   const today = new Date();
   const firstDateRange = startOfMonth(today);
diff --git a/src/modules/dipendenti-in-cloud/dipendenti-in-cloud-api-client.ts b/src/modules/dipendenti-in-cloud/dipendenti-in-cloud-api-client.ts
index 4909908..8b50f7c 100644
--- a/src/modules/dipendenti-in-cloud/dipendenti-in-cloud-api-client.ts
+++ b/src/modules/dipendenti-in-cloud/dipendenti-in-cloud-api-client.ts
@@ -1,5 +1,9 @@
 import { serverEnv } from "@/env/server-env";
 import {
+  absenceReasonsResponseSchema,
+  closuresResponseSchema,
+  DipendentiInCloudEmployeeDetail,
+  dipendentiInCloudEmployeeDetailResponseSchema,
   dipendentiInCloudEmployeesSchema,
   dipendentiInCloudPayrollsSchema,
   dipendentiInCloudTimesheetResponseSchema,
@@ -35,7 +39,18 @@ export class DipendentiInCloudApi {
 
     const parsed = dipendentiInCloudEmployeesSchema.parse(jsonResponse);
 
-    return parsed.data;
+    const employees: DipendentiInCloudEmployeeDetail[] = [];
+    for (const employee of parsed.data) {
+      const endpoint = `${this.endpoint}employees/${employee.id}`;
+
+      const rawResponse = await fetch(endpoint, this.authenticationHeaders);
+      const jsonResponse = await rawResponse.json();
+
+      const employeeDetail =
+        dipendentiInCloudEmployeeDetailResponseSchema.parse(jsonResponse).data;
+      employees.push(employeeDetail);
+    }
+    return employees;
   }
 
   async getMonthlyTimesheet(from: Date, to: Date, employees: { id: string }[]) {
@@ -148,6 +163,33 @@ export class DipendentiInCloudApi {
     const res = await fetch(salary.url);
     return await res.arrayBuffer();
   }
+
+  async getAbsenceReasons() {
+    const endpoint = `${this.endpoint}reasons`;
+    const res = await fetch(endpoint, this.authenticationHeaders);
+    const jsonResponse = await res.json();
+
+    return absenceReasonsResponseSchema
+      .parse(jsonResponse)
+      .data.list.filter((it) => it.active === 1);
+  }
+
+  async getClosures() {
+    const endpoint = `${this.endpoint}closures?`;
+    const params = new URLSearchParams({
+      per_page: "50",
+      page: "1",
+    });
+
+    const rawResponse = await fetch(
+      endpoint + params,
+      this.authenticationHeaders,
+    );
+
+    const jsonResponse = await rawResponse.json();
+
+    return closuresResponseSchema.parse(jsonResponse).data;
+  }
 }
 
 export const dipendentiInCloudApiClient = new DipendentiInCloudApi(
diff --git a/src/modules/dipendenti-in-cloud/dipendenti-in-cloud-report-utils.ts b/src/modules/dipendenti-in-cloud/dipendenti-in-cloud-report-utils.ts
new file mode 100644
index 0000000..fea9f88
--- /dev/null
+++ b/src/modules/dipendenti-in-cloud/dipendenti-in-cloud-report-utils.ts
@@ -0,0 +1,280 @@
+import { eachDayOfInterval, format, getDay } from "date-fns";
+import { it } from "date-fns/locale";
+import { KAbsenceDaysList } from "@/modules/k-absence-days/actions/k-absence-days-list";
+import { KEmployeesListAllActionResult } from "@/modules/k-employees/actions/k-employee-list-all-action";
+import parsePostgresInterval from "postgres-interval";
+import { KAbsenceReasonList } from "@/modules/k-absence-days/actions/k-absence-reasons-list";
+import { KClosuresList } from "@/modules/k-absence-days/actions/k-closures-list";
+import { ChronoUnit, Duration } from "@js-joda/core";
+
+export type DipendentiInCloudReportProps = {
+  from: Date;
+  to: Date;
+  absences: KAbsenceDaysList;
+  uniqueAbsenceReasons: string[];
+  employees: KEmployeesListAllActionResult;
+  absenceReasons: KAbsenceReasonList;
+  closures: KClosuresList;
+};
+
+/**
+ * Returns the letter corresponding to the day of the week in Italian:
+ * 0 (Sunday)    => D
+ * 1 (Monday)    => L
+ * 2 (Tuesday)   => M
+ * 3 (Wednesday) => M
+ * 4 (Thursday)  => G
+ * 5 (Friday)    => V
+ * 6 (Saturday)  => S
+ */
+export function getItalianDayLetter(date: Date): string {
+  const dayIndex = getDay(date); // 0=Sunday, 1=Monday, ...
+  switch (dayIndex) {
+    case 0:
+      return "D";
+    case 1:
+      return "L";
+    case 2:
+      return "M";
+    case 3:
+      return "M";
+    case 4:
+      return "G";
+    case 5:
+      return "V";
+    case 6:
+      return "S";
+    default:
+      return "";
+  }
+}
+
+export type MonthDayItem = {
+  isSaturday: boolean;
+  isSunday: boolean;
+  isClosure: boolean;
+  isWeekend: boolean;
+  isClosureOrWeekend: boolean;
+  date: Date;
+  formattedDate: string;
+};
+
+const toMonthDayItem = (date: Date, closures: KClosuresList) => {
+  const day = getDay(date);
+  const isSaturday = day === 6;
+  const isSunday = day === 0;
+  const isClosure = closures.some((closure) => {
+    return closure.date === date;
+  });
+
+  return {
+    isSaturday,
+    isSunday,
+    isClosure,
+    isWeekend: isSaturday || isSunday,
+    isClosureOrWeekend: isSaturday || isSunday || isClosure,
+    date,
+    formattedDate: format(date, "yyyy-MM-dd"),
+  };
+};
+
+const maxWorkableHoursPerDay = 8;
+
+export const getReportTitle = (date: Date) => {
+  // Format month and year label
+  const monthYearLabel = format(date, "MMMM yyyy", { locale: it });
+  return monthYearLabel.charAt(0).toUpperCase() + monthYearLabel.slice(1);
+};
+
+const getDaysForPeriod = ({
+  from,
+  to,
+  closures,
+}: {
+  from: Date;
+  to: Date;
+  closures: KClosuresList;
+}) => {
+  // Calculate days in the received period
+  const daysInMonth = eachDayOfInterval({
+    start: from,
+    end: to,
+  });
+
+  const month = daysInMonth.map((day) => toMonthDayItem(day, closures));
+
+  const maxWorkableHoursPerPeriod = month.reduce((total, day) => {
+    if (day.isClosureOrWeekend) {
+      return total;
+    }
+
+    return total + maxWorkableHoursPerDay;
+  }, 0);
+  return {
+    month,
+    maxWorkableHoursPerPeriod,
+  };
+};
+
+export type UIDay = MonthDayItem & { label: string };
+
+type ReportReasonRow = {
+  code: string;
+  days: UIDay[];
+};
+
+export type ReportRow = {
+  employeeName: string;
+  employeeNationalInsuranceNumber: string | null;
+  reasons: ReportReasonRow[];
+  totals: UIDay[];
+  monthlyTotal: Duration;
+  monthlyTotalLabel: string;
+};
+
+export const getDataForReport = ({
+  from,
+  to,
+  employees,
+  absences,
+  uniqueAbsenceReasons,
+  closures,
+}: DipendentiInCloudReportProps) => {
+  const { month, maxWorkableHoursPerPeriod } = getDaysForPeriod({
+    from,
+    to,
+    closures,
+  });
+  const rows: ReportRow[] = [];
+
+  const absencesByEmployee = absences.reduce((acc, absence) => {
+    const employeeId = absence.k_employees?.id;
+    if (!employeeId) {
+      return acc;
+    }
+    if (!acc.has(employeeId)) {
+      acc.set(employeeId, []);
+    }
+
+    const { hours, minutes } = parsePostgresInterval(
+      absence.k_absence_days?.duration ?? "",
+    );
+
+    acc.get(employeeId)?.push({
+      duration: absence.k_absence_days?.duration ?? "",
+      hours,
+      minutes,
+      reasonCode: absence.k_absence_days?.reasonCode ?? "",
+      date: absence.k_absence_days?.date ?? "",
+    });
+    return acc;
+  }, new Map<number, { date: string; hours: number; minutes: number; duration: string; reasonCode: string }[]>());
+
+  for (const employee of employees) {
+    const item: ReportRow = {
+      employeeName: `${employee.name} ${employee.surname}`,
+      employeeNationalInsuranceNumber: employee.nationalInsuranceNumber,
+      reasons: [],
+      totals: [],
+      monthlyTotal: Duration.of(maxWorkableHoursPerPeriod, ChronoUnit.HOURS),
+      monthlyTotalLabel: "",
+    };
+
+    const absencesForEmployee = absencesByEmployee.get(employee.id);
+
+    const dayMap = new Map<string, Duration>();
+    for (const day of month) {
+      dayMap.set(
+        day.formattedDate,
+        Duration.of(maxWorkableHoursPerDay, ChronoUnit.HOURS),
+      );
+    }
+    for (const reason of uniqueAbsenceReasons) {
+      const reasonItem: ReportReasonRow = {
+        code: reason,
+        days: [],
+      };
+
+      for (const day of month) {
+        const absencesDayForReason = absencesForEmployee?.filter(
+          (it) => it.reasonCode === reason && it.date === day.formattedDate,
+        );
+
+        const { hours, minutes } = absencesDayForReason?.reduce(
+          (total, { hours, minutes }) => {
+            return {
+              hours: total.hours + hours,
+              minutes: total.minutes + minutes,
+            };
+          },
+          { hours: 0, minutes: 0 },
+        ) ?? { hours: 0, minutes: 0 };
+
+        // compute label and subtract hours and minutes from totalDayDuration and monthlyTotal
+        let dayLabel = "";
+        let totalDayDuration = dayMap.get(day.formattedDate);
+        if (hours > 0) {
+          dayLabel += `${hours}h`;
+          item.monthlyTotal = item.monthlyTotal.minusHours(hours);
+
+          if (totalDayDuration) {
+            totalDayDuration = totalDayDuration?.minusHours(hours);
+          }
+        }
+        if (minutes > 0) {
+          dayLabel += `${minutes}m`;
+          item.monthlyTotal = item.monthlyTotal.minusMinutes(minutes);
+          if (totalDayDuration) {
+            totalDayDuration = totalDayDuration.minusMinutes(minutes);
+          }
+        }
+
+        if (totalDayDuration) {
+          dayMap.set(day.formattedDate, totalDayDuration);
+        }
+
+        reasonItem.days.push({ ...day, label: dayLabel });
+      }
+
+      item.reasons.push(reasonItem);
+    }
+
+    for (const day of month) {
+      const dayTotalDuration = dayMap.get(day.formattedDate);
+      if (!dayTotalDuration) {
+        item.totals.push({ ...day, label: "" });
+        continue;
+      }
+      const totalMinutes = dayTotalDuration.toMinutes();
+      const hours = Math.floor(totalMinutes / 60);
+      const minutes = totalMinutes % 60;
+
+      let totalLabel = "";
+      if (hours > 0) {
+        totalLabel += `${hours}h`;
+      }
+      if (minutes > 0) {
+        totalLabel += `${minutes}m`;
+      }
+      item.totals.push({ ...day, label: totalLabel });
+    }
+
+    const totalMinutes = item.monthlyTotal.toMinutes();
+    const hours = Math.floor(totalMinutes / 60);
+    const minutes = totalMinutes % 60;
+
+    let totalLabel = "";
+    if (hours > 0) {
+      totalLabel += `${hours}h`;
+    }
+    if (minutes > 0) {
+      totalLabel += `${minutes}m`;
+    }
+
+    item.monthlyTotalLabel = totalLabel;
+
+    rows.push(item);
+  }
+
+  return { rows, month };
+};
diff --git a/src/modules/dipendenti-in-cloud/schemas/dipendenti-in-cloud-schemas.ts b/src/modules/dipendenti-in-cloud/schemas/dipendenti-in-cloud-schemas.ts
index 0cefb9d..d9de197 100644
--- a/src/modules/dipendenti-in-cloud/schemas/dipendenti-in-cloud-schemas.ts
+++ b/src/modules/dipendenti-in-cloud/schemas/dipendenti-in-cloud-schemas.ts
@@ -27,15 +27,25 @@ export const dipendentiInCloudEmployeeSchema = z.object({
     valid_from: z.string(),
   }),
 });
-
-export type DipendentiInCloudEmployee = z.infer<
-  typeof dipendentiInCloudEmployeeSchema
->;
-
 export const dipendentiInCloudEmployeesSchema = makePagedResponseSchema(
   dipendentiInCloudEmployeeSchema,
 );
 
+export const dipendentiInCloudEmployeeDetailSchema =
+  dipendentiInCloudEmployeeSchema.extend({
+    iban: z.string().nullable(),
+    phone_number: z.string().nullable(),
+    tax_code: z.string().nullable(),
+  });
+
+export type DipendentiInCloudEmployeeDetail = z.infer<
+  typeof dipendentiInCloudEmployeeDetailSchema
+>;
+
+export const dipendentiInCloudEmployeeDetailResponseSchema = z.object({
+  data: dipendentiInCloudEmployeeDetailSchema,
+});
+
 export const dipendentiInCloudTimesheetDaySchema = z.object({
   closed: z.boolean().optional(),
   disabled: z.boolean(),
@@ -56,7 +66,7 @@ export const dipendentiInCloudTimesheetDaySchema = z.object({
       z.object({
         reason: z.object({
           id: z.number(),
-          code: z.string(), // TODO seems like an enum, one of the values is F
+          code: z.string(), // see absenceReasonSchema
           name: z.string(),
           color: z.string(),
           category: z.string(), // TODO seems like an enum, one of the values is absence
@@ -130,6 +140,28 @@ const payrollSchema = z.object({
 export const dipendentiInCloudPayrollsSchema =
   makePagedResponseSchema(payrollSchema);
 
+export const absenceReasonSchema = z.object({
+  category: z.string(),
+  code: z.string(),
+  name: z.string(),
+  active: z.number(),
+});
+
+export const absenceReasonsResponseSchema = z.object({
+  data: z.object({
+    list: z.array(absenceReasonSchema),
+  }),
+});
+
+export const closureSchema = z.object({
+  day: z.number(),
+  month: z.number(),
+  year: z.number().nullable(),
+  disabled_reason: z.string(),
+});
+
+export const closuresResponseSchema = makePagedResponseSchema(closureSchema);
+
 // these types are not used to parse other endpoint responses, just to build our internal stuff
 export type Salary = {
   date: string;
diff --git a/src/modules/k-absence-days/actions/k-absence-reasons-list.ts b/src/modules/k-absence-days/actions/k-absence-reasons-list.ts
new file mode 100644
index 0000000..1807d4f
--- /dev/null
+++ b/src/modules/k-absence-days/actions/k-absence-reasons-list.ts
@@ -0,0 +1,15 @@
+"use server";
+import { handleServerErrors } from "@/utils/server-action-utils";
+import { db } from "@/drizzle/drizzle-db";
+import { kAbsenceReasons } from "@/drizzle/schema";
+
+async function kAbsenceReasonsList() {
+  return db.select().from(kAbsenceReasons);
+}
+
+const managed = handleServerErrors(kAbsenceReasonsList);
+export default managed;
+
+export type KAbsenceReasonList = Awaited<
+  ReturnType<typeof kAbsenceReasonsList>
+>;
diff --git a/src/modules/k-absence-days/actions/k-closures-list.ts b/src/modules/k-absence-days/actions/k-closures-list.ts
new file mode 100644
index 0000000..6d90399
--- /dev/null
+++ b/src/modules/k-absence-days/actions/k-closures-list.ts
@@ -0,0 +1,29 @@
+"use server";
+import { handleServerErrors } from "@/utils/server-action-utils";
+import { db } from "@/drizzle/drizzle-db";
+import { kClosures } from "@/drizzle/schema";
+import { parse } from "date-fns";
+
+async function kClosuresList() {
+  const closuresFromDb = await db.select().from(kClosures);
+  const currentYear = new Date().getFullYear();
+
+  return closuresFromDb.map((closure) => {
+    const { day, month, year = currentYear } = closure;
+    const paddedDay = String(day).padStart(2, "0");
+    const paddedMonth = String(month).padStart(2, "0");
+    return {
+      ...closure,
+      date: parse(
+        `${paddedDay}-${paddedMonth}-${year}`,
+        "dd-MM-yyyy",
+        new Date(),
+      ),
+    };
+  });
+}
+
+const managed = handleServerErrors(kClosuresList);
+export default managed;
+
+export type KClosuresList = Awaited<ReturnType<typeof kClosuresList>>;
diff --git a/src/modules/k-absence-days/k-absence-days-server.ts b/src/modules/k-absence-days/k-absence-days-server.ts
index 48e18a7..08af184 100644
--- a/src/modules/k-absence-days/k-absence-days-server.ts
+++ b/src/modules/k-absence-days/k-absence-days-server.ts
@@ -1,5 +1,9 @@
 import kAbsenceDaysList from "@/modules/k-absence-days/actions/k-absence-days-list";
+import kAbsenceReasonsList from "@/modules/k-absence-days/actions/k-absence-reasons-list";
+import kClosuresList from "@/modules/k-absence-days/actions/k-closures-list";
 
 export const kAbsenceDaysServer = {
   list: kAbsenceDaysList,
+  listReasons: kAbsenceReasonsList,
+  closures: kClosuresList, // TODO not the best place for this
 };


Reviews:
user: login: coderabbitai[bot]
body: **Actionable comments posted: 6**

<details>
<summary>🧹 Nitpick comments (9)</summary><blockquote>

<details>
<summary>src/modules/k-absence-days/k-absence-days-server.ts (1)</summary><blockquote>

`7-8`: **Challenge accepted! But about that TODO...**

The new properties fit perfectly with the server object's purpose. But that TODO comment - "not the best place for this" - is like wearing sneakers with a suit. Let's suit up properly!


Consider creating a dedicated server object for closures if they're conceptually different from absence days. Otherwise, at least remove or elaborate on the TODO comment.

```diff
- closures: kClosuresList, // TODO not the best place for this
+ closures: kClosuresList,
```

</blockquote></details>
<details>
<summary>src/modules/k-absence-days/actions/k-closures-list.ts (1)</summary><blockquote>

`7-24`: **This date handling is... wait for it... HIGH FIVE-WORTHY!**

The function nicely transforms database records into more usable objects with JavaScript Date instances. But there's one thing that could be improved...


For clarity and safety, let's validate the day and month values. After all, you wouldn't want to dress up for January 32nd, would you?

```diff
async function kClosuresList() {
  const closuresFromDb = await db.select().from(kClosures);
  const currentYear = new Date().getFullYear();

  return closuresFromDb.map((closure) => {
    const { day, month, year = currentYear } = closure;
+   
+   // Validate day and month values
+   if (day < 1 || day > 31 || month < 1 || month > 12) {
+     console.warn(`Invalid date values detected: day=${day}, month=${month}`);
+   }
+   
    const paddedDay = String(day).padStart(2, "0");
    const paddedMonth = String(month).padStart(2, "0");
    return {
      ...closure,
      date: parse(
        `${paddedDay}-${paddedMonth}-${year}`,
        "dd-MM-yyyy",
        new Date(),
      ),
    };
  });
}
```

</blockquote></details>
<details>
<summary>src/modules/dipendenti-in-cloud/components/sync-dipendenti-in-cloud.tsx (1)</summary><blockquote>

`69-70`: **Make that error message more legendary!**

The error message here is too generic compared to the one you updated in `SyncEmployeeData`. Remember what I always say? "Go big or go home!" Let's make it specific about absence reasons and closures.


```diff
-notifyError("Error while syncing dipendenti in cloud general info");
+notifyError("Error while syncing dipendenti in cloud absence reasons and closures");
```

</blockquote></details>
<details>
<summary>src/modules/dipendenti-in-cloud/dipendenti-in-cloud-report-utils.ts (2)</summary><blockquote>

`91-121`: **Rocking the Days For Period!**  
This logic for summing workable hours is great. But watch for large date intervals that might slow performance. If performance becomes an issue, consider more direct calculations for day ranges instead of iterating each day.

---

`137-282`: **A Suitably Comprehensive Data Aggregation!**  
This master function crushes it by mapping absences well. Only suggestion: watch out for any potential large dataset overhead. If the array of absences blows up, you might want some indexing-based structure to scale. Otherwise, it’s all legen—wait for it—dary!

</blockquote></details>
<details>
<summary>src/modules/dipendenti-in-cloud/schemas/dipendenti-in-cloud-schemas.ts (1)</summary><blockquote>

`156-164`: **Closures Are In The House!**  
Watch out for `year` being `null`. If that’s intended, keep it, or handle the scenario gracefully in your code.

</blockquote></details>
<details>
<summary>src/modules/dipendenti-in-cloud/components/dipendenti-in-cloud-report.tsx (3)</summary><blockquote>

`18-47`: **Main Component: Wait for it…**  
Your structure is awesome. The smaller sub-components keep things readable. Consider lazy-loading if the data grows massive, but otherwise, high five!

---

`63-93`: **Header Internals Are Legen…**  
The dynamic day cells are neat. Double-check the function usage for day letters, especially if i18n expands.

---

`196-213`: **ReportLegend – High-Five!**  
An easy way to clarify abbreviations and codes. If you end up adding color-coded labels, keep the styling consistent.

</blockquote></details>

</blockquote></details>

<details>
<summary>📜 Review details</summary>

**Configuration used: CodeRabbit UI**
**Review profile: CHILL**
**Plan: Pro**

<details>
<summary>📥 Commits</summary>

Reviewing files that changed from the base of the PR and between e1c193448e94cd3d7cb85eae04d00766df0bed0f and 9fbde313f8fc53c8a6ce4c3aad025c138b066233.

</details>

<details>
<summary>⛔ Files ignored due to path filters (1)</summary>

* `pnpm-lock.yaml` is excluded by `!**/pnpm-lock.yaml`

</details>

<details>
<summary>📒 Files selected for processing (15)</summary>

* `__tests__/modules/dipendenti-in-cloud/dipendenti-in-cloud-api-client.test.ts` (2 hunks)
* `drizzle/schema.ts` (2 hunks)
* `package.json` (1 hunks)
* `src/app/reports/dipendenti-in-cloud/page.tsx` (3 hunks)
* `src/modules/dipendenti-in-cloud/actions/dipendenti-in-cloud-import-absence-reasons-and-closures.ts` (1 hunks)
* `src/modules/dipendenti-in-cloud/actions/dipendenti-in-cloud-import-employees-action.ts` (2 hunks)
* `src/modules/dipendenti-in-cloud/actions/sync-dipendenti-in-cloud-timesheet-action.ts` (2 hunks)
* `src/modules/dipendenti-in-cloud/components/dipendenti-in-cloud-report.tsx` (1 hunks)
* `src/modules/dipendenti-in-cloud/components/sync-dipendenti-in-cloud.tsx` (4 hunks)
* `src/modules/dipendenti-in-cloud/dipendenti-in-cloud-api-client.ts` (3 hunks)
* `src/modules/dipendenti-in-cloud/dipendenti-in-cloud-report-utils.ts` (1 hunks)
* `src/modules/dipendenti-in-cloud/schemas/dipendenti-in-cloud-schemas.ts` (3 hunks)
* `src/modules/k-absence-days/actions/k-absence-reasons-list.ts` (1 hunks)
* `src/modules/k-absence-days/actions/k-closures-list.ts` (1 hunks)
* `src/modules/k-absence-days/k-absence-days-server.ts` (1 hunks)

</details>

<details>
<summary>🧰 Additional context used</summary>

<details>
<summary>🧬 Code Definitions (8)</summary>

<details>
<summary>__tests__/modules/dipendenti-in-cloud/dipendenti-in-cloud-api-client.test.ts (1)</summary><blockquote>

<details>
<summary>src/modules/dipendenti-in-cloud/dipendenti-in-cloud-api-client.ts (1)</summary>

* `dipendentiInCloudApiClient` (195-198)

</details>

</blockquote></details>
<details>
<summary>src/modules/dipendenti-in-cloud/actions/dipendenti-in-cloud-import-absence-reasons-and-closures.ts (2)</summary><blockquote>

<details>
<summary>drizzle/schema.ts (2)</summary>

* `kAbsenceReasons` (140-144)
* `kClosures` (146-152)

</details>
<details>
<summary>src/modules/dipendenti-in-cloud/dipendenti-in-cloud-api-client.ts (1)</summary>

* `dipendentiInCloudApiClient` (195-198)

</details>

</blockquote></details>
<details>
<summary>src/modules/k-absence-days/actions/k-closures-list.ts (1)</summary><blockquote>

<details>
<summary>drizzle/schema.ts (1)</summary>

* `kClosures` (146-152)

</details>

</blockquote></details>
<details>
<summary>src/modules/dipendenti-in-cloud/components/sync-dipendenti-in-cloud.tsx (1)</summary><blockquote>

<details>
<summary>src/modules/ui/components/notify.ts (2)</summary>

* `notifyError` (8-13)
* `notifySuccess` (3-7)

</details>

</blockquote></details>
<details>
<summary>src/app/reports/dipendenti-in-cloud/page.tsx (1)</summary><blockquote>

<details>
<summary>src/modules/k-absence-days/k-absence-days-server.ts (1)</summary>

* `kAbsenceDaysServer` (5-9)

</details>

</blockquote></details>
<details>
<summary>src/modules/dipendenti-in-cloud/dipendenti-in-cloud-report-utils.ts (2)</summary><blockquote>

<details>
<summary>src/modules/k-absence-days/actions/k-absence-reasons-list.ts (1)</summary>

* `KAbsenceReasonList` (13-15)

</details>
<details>
<summary>src/modules/k-absence-days/actions/k-closures-list.ts (1)</summary>

* `KClosuresList` (29-29)

</details>

</blockquote></details>
<details>
<summary>src/modules/dipendenti-in-cloud/dipendenti-in-cloud-api-client.ts (1)</summary><blockquote>

<details>
<summary>src/modules/dipendenti-in-cloud/schemas/dipendenti-in-cloud-schemas.ts (4)</summary>

* `DipendentiInCloudEmployeeDetail` (41-43)
* `dipendentiInCloudEmployeeDetailResponseSchema` (45-47)
* `absenceReasonsResponseSchema` (150-154)
* `closuresResponseSchema` (163-163)

</details>

</blockquote></details>
<details>
<summary>src/modules/dipendenti-in-cloud/components/dipendenti-in-cloud-report.tsx (1)</summary><blockquote>

<details>
<summary>src/modules/dipendenti-in-cloud/dipendenti-in-cloud-report-utils.ts (7)</summary>

* `DipendentiInCloudReportProps` (10-18)
* `getDataForReport` (137-282)
* `getReportTitle` (85-89)
* `MonthDayItem` (52-60)
* `getItalianDayLetter` (30-50)
* `ReportRow` (128-135)
* `UIDay` (123-123)

</details>

</blockquote></details>

</details>

</details>

<details>
<summary>🔇 Additional comments (36)</summary><blockquote>

<details>
<summary>package.json (1)</summary>

`22-22`: **Added a legendary new date-time library dependency!**

Hey there, suit up! We're adding @js-joda/core to our arsenal, and it's gonna be legen... wait for it... DARY! This awesome date-time library will help us handle those closure days and absence reasons like a pro.

</details>
<details>
<summary>src/modules/dipendenti-in-cloud/actions/dipendenti-in-cloud-import-employees-action.ts (2)</summary>

`29-31`: **Employee data just got a major upgrade, high five!**

Challenge accepted! You've added IBAN, national insurance number, and phone number to our employee records. This is totally awesome for the new employee data management we're implementing.

---

`44-46`: **Consistency is what Barney Stinson is all about!**

Have you met these fields? You've added the same fields when updating employee records. High five for being consistent! This ensures all employee records will have the new fields regardless of whether they're new or existing.

</details>
<details>
<summary>__tests__/modules/dipendenti-in-cloud/dipendenti-in-cloud-api-client.test.ts (1)</summary>

`14-18`: **Test back from the dead! Suit up!**

Awesome job uncommenting this test! It's like bringing back a classic Barney catchphrase. This test will validate that we're correctly retrieving 15 employees from the dipendenti-in-cloud API.

</details>
<details>
<summary>src/modules/dipendenti-in-cloud/actions/sync-dipendenti-in-cloud-timesheet-action.ts (2)</summary>

`73-73`: **Adding reason codes, because details matter!**

When you're as awesome as Barney Stinson, you know that details are what make you legendary! Adding the reason code here is a true bro move - it enhances our absence tracking system by storing more detailed information about why employees are absent.

---

`84-84`: **Consistency is what Barney... wait, I already used that one.**

True story! You've added the same reasonCode field in the fallback case too. This attention to detail ensures we always have the reason code stored, regardless of whether shifts are present or not. That's what I call awesomeness!

</details>
<details>
<summary>src/modules/k-absence-days/actions/k-absence-reasons-list.ts (1)</summary>

`1-15`: **This code is LEGEND... wait for it... DARY! LEGENDARY!**

This server action fetches all absence reasons from the database. Clean, simple, and properly wrapped with error handling - it's totally awesome! The type export at the end is a great touch for type safety.

</details>
<details>
<summary>src/modules/k-absence-days/k-absence-days-server.ts (1)</summary>

`2-3`: **Have you met these new imports? They're AWESOME!**

Adding these imports to support the new functionality is the right move. You know what they say - imports are like suits, you can never have too many good ones!

</details>
<details>
<summary>src/modules/dipendenti-in-cloud/actions/dipendenti-in-cloud-import-absence-reasons-and-closures.ts (1)</summary>

`23-25`: **True story: this return message is AWESOME!**

The return message with the count of imported items is great for user feedback. It's like telling someone how many phone numbers you scored at a party!

</details>
<details>
<summary>src/modules/k-absence-days/actions/k-closures-list.ts (1)</summary>

`26-29`: **Your error handling is SUIT-ably impressive!**

The error handling wrapper and type export are perfect. It's like having your suits dry-cleaned and ready to go!

</details>
<details>
<summary>src/app/reports/dipendenti-in-cloud/page.tsx (3)</summary>

`29-37`: **Legen... wait for it... dary data fetching!**

You're absolutely crushing it with these new API calls for absence reasons and closures! Just like I always say, "When I'm sad, I stop being sad and start being awesome instead." That's what this code is doing—stopping from being incomplete and becoming AWESOME!

---

`41-41`: **Suit up! We've got a property change!**

Switching from description to reasonCode? That's a power move! It's like trading your regular suit for an Italian one - way more specific and professional.

---

`54-56`: **High five for these new props!**

Renaming `uniqueReasons` to `uniqueAbsenceReasons` and adding those new props? That's what I call "The Barney Stinson Rule of Clarity" - when in doubt, be more specific and add more awesome data!

</details>
<details>
<summary>src/modules/dipendenti-in-cloud/components/sync-dipendenti-in-cloud.tsx (3)</summary>

`7-8`: **Challenge accepted! Import game strong!**

Replacing that unused import with the shiny new one is like upgrading from the Ducky Tie to the Italian silk one. It's what grown-ups do!

---

`26-27`: **Adding components like I add notches to my bedpost!**

Separating components with a proper separator and adding that new `SyncAbsenceReasonsAndClosures`? That's what I call the "Barney Stinson UI Pattern" - keep it organized and keep adding awesome!

---

`41-43`: **Specificity is my middle name!**

Updating that error message to be more specific is exactly what Ted would do if he were a better programmer (which he's not). Being specific about employee data? TRUE STORY!

</details>
<details>
<summary>src/modules/dipendenti-in-cloud/dipendenti-in-cloud-api-client.ts (3)</summary>

`3-7`: **These imports are suited up and ready to go!**

Adding all these new schema types? That's what I call preparation. And you know what I always say: "Suit up for success!" These imports are definitely suiting up for some legendary code.

---

`167-175`: **This method is like my playbook - effective and legendary!**

Adding `getAbsenceReasons` with proper filtering for active reasons? That's what I call the "Barney Filter" - only keep the ones worth your time! Filtering for `active === 1` is exactly what I do with my phone contacts.

---

`177-192`: **This closure method is suit-ably awesome!**

The `getClosures` method with pagination is like a well-planned play from the playbook. You're setting up the parameters, making the call, and parsing that response like a boss! LEGENDARY!

</details>
<details>
<summary>drizzle/schema.ts (4)</summary>

`125-127`: **Adding fields like I add numbers to my phone!**

National insurance number, phone number, AND IBAN? That's what I call the "Triple Threat"! You're making this employee table more detailed than my LinkedIn profile (which is awesome, by the way).

---

`140-144`: **New table alert! It's going to be legen... wait for it...**

Adding a whole new table for absence reasons? That's commitment, bro! It's like when I committed to the "perfect week" - all in or nothing! The structure with code and name fields is clean and professional.

---

`146-152`: **...DARY! Another new table!**

The `kClosures` table is like my apartment - perfectly structured and ready for action! With day, month, year, and description fields, you've got all bases covered. Ted would be jealous of this schema design!

---

`160-160`: **One field to rule them all!**

Adding that `reasonCode` field to the absence days table? That's what I call connectivity, baby! It's like the link between my awesome stories and their even more awesome punchlines. TRUE STORY!

</details>
<details>
<summary>src/modules/dipendenti-in-cloud/dipendenti-in-cloud-report-utils.ts (3)</summary>

`10-18`: **Totally Legendary Type Definition!**  
Suit up, because these prop definitions are awesome. Just be sure you keep them up to date if your data model changes.

---

`30-50`: **High-Five for Reusing Day Letters!**  
This function is so good it deserves a “legendary” cheer! No issues found, but keep an eye on updates to your day-fns library to ensure maximum awesomeness.

---

`85-89`: **Title Formatting is LEGEN... DARY!**  
Formatting month-year is top-notch! No concerns.

</details>
<details>
<summary>src/modules/dipendenti-in-cloud/schemas/dipendenti-in-cloud-schemas.ts (4)</summary>

`41-43`: **Type Inference: Challenge Accepted!**  
“DipendentiInCloudEmployeeDetail” will keep your code safe and robust. No issues.

---

`45-47`: **Employee Detail Response - Legendary!**  
Nothing to improve here, bro. Keep on rocking.

---

`150-154`: **Absence Reasons Response: Legendary Clarity!**  
Loving the nested schema approach. All good here!

---

`34-39`: <details>
<summary>✅ Verification successful</summary>

**Employee Detail Extended: True Story!**  
Adding IBAN, phone_number, and tax_code is a neat addition. Verify no sensitive data is inadvertently leaked.

---

🏁 Script executed:

```shell
#!/bin/bash
# Searching for logs or prints of employee details to confirm no sensitive data is leaked
rg -A 3 'console\.log' | rg 'employee' 
```

Length of output: 586

---



</details>

**Employee Detail Extension Verified – It's Legendary!**  
The new schema with IBAN, phone_number, and tax_code has been extended just perfectly. Our log search didn't reveal any sensitive data leaks related to these fields, so you're all set—suit up and keep rocking, bro!

</details>
<details>
<summary>src/modules/dipendenti-in-cloud/components/dipendenti-in-cloud-report.tsx (6)</summary>

`2-9`: **Legendary Imports!**  
You’re pulling in the right stuff from `dipendenti-in-cloud-report-utils`. Keep rocking that modular design!

---

`13-16`: **Sweet Documentation!**  
Always great to keep docs updated. This is super—like a high-five from me to you.

---

`49-55`: **ReportTitle for the Win!**  
Clean formatting + capitalized month = legendary style.

---

`57-61`: **PreHeader with ‘Ore Lavorate’: Suit Up!**  
No issues, keep flexin’ that styling.

---

`159-177`: **Day Cell: That’s How We Roll!**  
Conditionally rendering `NotWorkingDayCell` is clean. Suit up for kudos.

---

`179-194`: **NotWorkingDayCell – Legendary Pink Accents!**  
Great clarity for weekend/closures. The color-coded backgrounds are totally bro-tier.

</details>

</blockquote></details>

</details>

<!-- This is an auto-generated comment by CodeRabbit for review status -->
state: CHANGES_REQUESTED
author_association: NONE
submitted_at: 2025-03-30T09:46:06Z
user: login: coderabbitai[bot]
body: **Actionable comments posted: 2**

<details>
<summary>🧹 Nitpick comments (5)</summary><blockquote>

<details>
<summary>src/app/reports/dipendenti-in-cloud/page.tsx (1)</summary><blockquote>

`39-46`: **Suit up the variable naming!**

The variable `uniqueReasons` should be renamed to `uniqueAbsenceReasons` for consistency with its usage in props on line 54. Inconsistent naming is like wearing sneakers with a suit - it just doesn't work!

```diff
- const uniqueReasons: string[] = [];
+ const uniqueAbsenceReasons: string[] = [];
  for (const absence of absences) {
    const reason = absence.k_absence_days?.reasonCode;

-   if (reason && !uniqueReasons.includes(reason)) {
-     uniqueReasons.push(reason);
+   if (reason && !uniqueAbsenceReasons.includes(reason)) {
+     uniqueAbsenceReasons.push(reason);
    }
  }
```

</blockquote></details>
<details>
<summary>src/modules/dipendenti-in-cloud/components/sync-dipendenti-in-cloud.tsx (1)</summary><blockquote>

`81-82`: **Button text is as vague as Ted's dating advice!**

The button text "Sync general information" doesn't specify what kind of information we're syncing. Make it legendary by being more descriptive!

```diff
- Sync general information
+ Sync absence reasons and closures
```

</blockquote></details>
<details>
<summary>src/modules/dipendenti-in-cloud/dipendenti-in-cloud-api-client.ts (1)</summary><blockquote>

`167-175`: **Exception handling is a MUST-HAVE, like a good suit!**

Your new methods need some error handling - going without it is like going to a suit fitting without a tailor! Let's add some try-catch blocks:

```diff
async getAbsenceReasons() {
+ try {
    const endpoint = `${this.endpoint}reasons`;
    const res = await fetch(endpoint, this.authenticationHeaders);
+   
+   if (!res.ok) {
+     throw new Error(`Failed to fetch absence reasons: ${res.status} ${res.statusText}`);
+   }
+   
    const jsonResponse = await res.json();

    return absenceReasonsResponseSchema
      .parse(jsonResponse)
      .data.list.filter((it) => it.active === 1);
+ } catch (error) {
+   console.error("Error fetching absence reasons:", error);
+   throw error;
+ }
}
```

</blockquote></details>
<details>
<summary>src/modules/dipendenti-in-cloud/dipendenti-in-cloud-report-utils.ts (2)</summary><blockquote>

`202-272`: **Creating The Playbook for Hours Formatting - Suit Up!**

Bro, repeating yourself is like wearing the same suit twice in a week - totally beneath you! You've duplicated the "format hours and minutes into label" logic, and that's just not legendary.


Extract this common logic into its own awesome function:

```diff
+const formatDurationToLabel = (duration: Duration): string => {
+  const totalMinutes = duration.toMinutes();
+  const hours = Math.floor(totalMinutes / 60);
+  const minutes = totalMinutes % 60;
+  
+  let label = "";
+  if (hours > 0) {
+    label += `${hours}h`;
+  }
+  if (minutes > 0) {
+    label += `${minutes}m`;
+  }
+  return label;
+};
```

Then use it like a trusty wingman wherever needed:

```diff
-const totalMinutes = dayTotalDuration.toMinutes();
-const hours = Math.floor(totalMinutes / 60);
-const minutes = totalMinutes % 60;
-
-let totalLabel = "";
-if (hours > 0) {
-  totalLabel += `${hours}h`;
-}
-if (minutes > 0) {
-  totalLabel += `${minutes}m`;
-}
-item.totals.push({ ...day, label: totalLabel });
+item.totals.push({ ...day, label: formatDurationToLabel(dayTotalDuration) });
```

And similarly for the monthly total:

```diff
-const totalMinutes = item.monthlyTotal.toMinutes();
-const hours = Math.floor(totalMinutes / 60);
-const minutes = totalMinutes % 60;
-
-let totalLabel = "";
-if (hours > 0) {
-  totalLabel += `${hours}h`;
-}
-if (minutes > 0) {
-  totalLabel += `${minutes}m`;
-}
-
-item.monthlyTotalLabel = totalLabel;
+item.monthlyTotalLabel = formatDurationToLabel(item.monthlyTotal);
```

True story!

---

`135-280`: **The Bro Code of Large Functions - Article 127**

This function is longer than my list of conquests! And that's saying something.


Breaking this up would make it more awesome than my apartment. Consider extracting these sections:

1. The absences-by-employee aggregation (lines 150-171)
2. The reason-days calculation logic (lines 192-240)
3. The totals calculation logic (lines 242-260)
4. The monthly total formatting (lines 262-274)

For example, you could create:

```typescript
const aggregateAbsencesByEmployee = (absences: KAbsenceDaysList) => {
  // Your aggregation logic here
  return absencesMap;
};

const calculateReasonDays = (
  employee: EmployeeType,
  month: MonthDayItem[], 
  absencesForEmployee: AbsenceType[],
  uniqueAbsenceReasons: string[],
  dayMap: Map<string, Duration>
) => {
  // Your reason-days calculation
  return { reasons, updatedDayMap, updatedMonthlyTotal };
};
```

Trust me, when I say "Suit up" for your code, I mean make it clean, modular and awesome!

</blockquote></details>

</blockquote></details>

<details>
<summary>📜 Review details</summary>

**Configuration used: CodeRabbit UI**
**Review profile: CHILL**
**Plan: Pro**

<details>
<summary>📥 Commits</summary>

Reviewing files that changed from the base of the PR and between 9fbde313f8fc53c8a6ce4c3aad025c138b066233 and 488022e2ba0b4a699d3c534719caad1dcf4d343b.

</details>

<details>
<summary>⛔ Files ignored due to path filters (1)</summary>

* `pnpm-lock.yaml` is excluded by `!**/pnpm-lock.yaml`

</details>

<details>
<summary>📒 Files selected for processing (15)</summary>

* `__tests__/modules/dipendenti-in-cloud/dipendenti-in-cloud-api-client.test.ts` (2 hunks)
* `drizzle/schema.ts` (2 hunks)
* `package.json` (1 hunks)
* `src/app/reports/dipendenti-in-cloud/page.tsx` (3 hunks)
* `src/modules/dipendenti-in-cloud/actions/dipendenti-in-cloud-import-absence-reasons-and-closures.ts` (1 hunks)
* `src/modules/dipendenti-in-cloud/actions/dipendenti-in-cloud-import-employees-action.ts` (2 hunks)
* `src/modules/dipendenti-in-cloud/actions/sync-dipendenti-in-cloud-timesheet-action.ts` (2 hunks)
* `src/modules/dipendenti-in-cloud/components/dipendenti-in-cloud-report.tsx` (1 hunks)
* `src/modules/dipendenti-in-cloud/components/sync-dipendenti-in-cloud.tsx` (4 hunks)
* `src/modules/dipendenti-in-cloud/dipendenti-in-cloud-api-client.ts` (3 hunks)
* `src/modules/dipendenti-in-cloud/dipendenti-in-cloud-report-utils.ts` (1 hunks)
* `src/modules/dipendenti-in-cloud/schemas/dipendenti-in-cloud-schemas.ts` (3 hunks)
* `src/modules/k-absence-days/actions/k-absence-reasons-list.ts` (1 hunks)
* `src/modules/k-absence-days/actions/k-closures-list.ts` (1 hunks)
* `src/modules/k-absence-days/k-absence-days-server.ts` (1 hunks)

</details>

<details>
<summary>🚧 Files skipped from review as they are similar to previous changes (8)</summary>

* package.json
* src/modules/dipendenti-in-cloud/actions/dipendenti-in-cloud-import-employees-action.ts
* __tests__/modules/dipendenti-in-cloud/dipendenti-in-cloud-api-client.test.ts
* src/modules/k-absence-days/actions/k-absence-reasons-list.ts
* src/modules/k-absence-days/k-absence-days-server.ts
* src/modules/dipendenti-in-cloud/actions/sync-dipendenti-in-cloud-timesheet-action.ts
* drizzle/schema.ts
* src/modules/k-absence-days/actions/k-closures-list.ts

</details>

<details>
<summary>🧰 Additional context used</summary>

<details>
<summary>🧬 Code Definitions (5)</summary>

<details>
<summary>src/app/reports/dipendenti-in-cloud/page.tsx (1)</summary><blockquote>

<details>
<summary>src/modules/k-absence-days/k-absence-days-server.ts (1)</summary>

* `kAbsenceDaysServer` (5-9)

</details>

</blockquote></details>
<details>
<summary>src/modules/dipendenti-in-cloud/components/sync-dipendenti-in-cloud.tsx (1)</summary><blockquote>

<details>
<summary>src/modules/ui/components/notify.ts (2)</summary>

* `notifyError` (8-13)
* `notifySuccess` (3-7)

</details>

</blockquote></details>
<details>
<summary>src/modules/dipendenti-in-cloud/dipendenti-in-cloud-api-client.ts (1)</summary><blockquote>

<details>
<summary>src/modules/dipendenti-in-cloud/schemas/dipendenti-in-cloud-schemas.ts (4)</summary>

* `DipendentiInCloudEmployeeDetail` (41-43)
* `dipendentiInCloudEmployeeDetailResponseSchema` (45-47)
* `absenceReasonsResponseSchema` (150-154)
* `closuresResponseSchema` (163-163)

</details>

</blockquote></details>
<details>
<summary>src/modules/dipendenti-in-cloud/actions/dipendenti-in-cloud-import-absence-reasons-and-closures.ts (2)</summary><blockquote>

<details>
<summary>drizzle/schema.ts (2)</summary>

* `kAbsenceReasons` (140-144)
* `kClosures` (146-152)

</details>
<details>
<summary>src/modules/dipendenti-in-cloud/dipendenti-in-cloud-api-client.ts (1)</summary>

* `dipendentiInCloudApiClient` (195-198)

</details>

</blockquote></details>
<details>
<summary>src/modules/dipendenti-in-cloud/components/dipendenti-in-cloud-report.tsx (1)</summary><blockquote>

<details>
<summary>src/modules/dipendenti-in-cloud/dipendenti-in-cloud-report-utils.ts (7)</summary>

* `DipendentiInCloudReportProps` (10-18)
* `getDataForReport` (135-280)
* `getReportTitle` (83-87)
* `MonthDayItem` (52-60)
* `getItalianDayLetter` (30-50)
* `ReportRow` (126-133)
* `UIDay` (119-119)

</details>

</blockquote></details>

</details>

</details>

<details>
<summary>🔇 Additional comments (7)</summary><blockquote>

<details>
<summary>src/modules/dipendenti-in-cloud/actions/dipendenti-in-cloud-import-absence-reasons-and-closures.ts (1)</summary>

`7-26`: **This function is going to be LEGEN... wait for it...**

The implementation looks solid, but I see a small issue that could be SCARY! You're deleting all records before checking if the API call succeeds. If either API call fails, you'll end up with empty tables. That's like throwing away your little black book before getting new phone numbers!

Let's wrap this in a transaction to ensure atomicity:

```diff
const dipendentiInCloudImportAbsenceReasonsAndClosures = async () => {
- await db.delete(kAbsenceReasons);
- await db.delete(kClosures);
-
- const absenceReasons = await dipendentiInCloudApiClient.getAbsenceReasons();
- const closures = await dipendentiInCloudApiClient.getClosures();
- await db.insert(kAbsenceReasons).values(absenceReasons);
- await db.insert(kClosures).values(
-   closures.map(({ day, month, year, disabled_reason }) => ({
-     day,
-     month,
-     year,
-     description: disabled_reason,
-   })),
- );

+ // Get the data first
+ const absenceReasons = await dipendentiInCloudApiClient.getAbsenceReasons();
+ const closures = await dipendentiInCloudApiClient.getClosures();
+
+ // Then perform database operations in a transaction
+ await db.transaction(async (tx) => {
+   await tx.delete(kAbsenceReasons);
+   await tx.delete(kClosures);
+
+   await tx.insert(kAbsenceReasons).values(absenceReasons);
+   await tx.insert(kClosures).values(
+     closures.map(({ day, month, year, disabled_reason }) => ({
+       day,
+       month,
+       year,
+       description: disabled_reason,
+     })),
+   );
+ });
```

</details>
<details>
<summary>src/modules/dipendenti-in-cloud/dipendenti-in-cloud-api-client.ts (1)</summary>

`42-53`: **Have you met performance issues?**

Wait for it... this could be a problem! You're making a separate API call for EACH employee. That's like hitting on every girl at MacLaren's one by one - inefficient and likely to slow things down.

Consider implementing concurrent fetching with Promise.all instead of sequential fetching:

```diff
- const employees: DipendentiInCloudEmployeeDetail[] = [];
- for (const employee of parsed.data) {
-   const endpoint = `${this.endpoint}employees/${employee.id}`;
-
-   const rawResponse = await fetch(endpoint, this.authenticationHeaders);
-   const jsonResponse = await rawResponse.json();
-
-   const employeeDetail =
-     dipendentiInCloudEmployeeDetailResponseSchema.parse(jsonResponse).data;
-   employees.push(employeeDetail);
- }
- return employees;

+ // Fetch all employee details concurrently
+ const fetchPromises = parsed.data.map(async (employee) => {
+   const endpoint = `${this.endpoint}employees/${employee.id}`;
+   const rawResponse = await fetch(endpoint, this.authenticationHeaders);
+   const jsonResponse = await rawResponse.json();
+   return dipendentiInCloudEmployeeDetailResponseSchema.parse(jsonResponse).data;
+ });
+ const employees = await Promise.all(fetchPromises);
+ return employees;
```

</details>
<details>
<summary>src/modules/dipendenti-in-cloud/dipendenti-in-cloud-report-utils.ts (1)</summary>

`66-68`: **Dates Should Be Compared Like Conquests – By More Than Just Surface Appearances!**

Hey bro, this comparison is more problematic than Robin on Canadian Thanksgiving! When you use `===` to compare Date objects, you're only checking if they're literally the same object instance, not if they represent the same day. That's like judging a potential conquest by just their area code!


To make this legen—wait for it—dary, you need to compare the actual timestamp values:

```diff
-const isClosure = closures.some((closure) => {
-  return closure.date === date;
-});
+const isClosure = closures.some((closure) => {
+  return closure.date.getTime() === date.getTime();
+});
```

Or even more awesomely, use date-fns' `isSameDay` function which is like the Barney Stinson of date comparison - it never fails:

```diff
+import { isSameDay } from "date-fns";
// Then in the function:
-const isClosure = closures.some((closure) => {
-  return closure.date === date;
-});
+const isClosure = closures.some((closure) => {
+  return isSameDay(closure.date, date);
+});
```

</details>
<details>
<summary>src/modules/dipendenti-in-cloud/schemas/dipendenti-in-cloud-schemas.ts (1)</summary>

`143-148`: **Active Field is Being a Total Ted Right Now!**

Bro, your schema inconsistency is worse than Ted's dating history! The `active` field uses `z.number()` here, but somewhere else it's using `z.boolean()`. That's like wearing brown shoes with a black suit - it's just wrong!


Since we're setting the bar high (like I always do), fix this by either:

1. Properly defining the numeric range like the legend I am:

```diff
-  active: z.number(),
+  active: z.number().refine(val => val === 0 || val === 1, { 
+    message: "active must be either 0 or 1" 
+  }),
```

2. Or going boolean and being awesome throughout the codebase:

```diff
-  active: z.number(),
+  active: z.boolean(),
```

Choose wisely. It's gonna be legen... wait for it... dary!

</details>
<details>
<summary>src/modules/dipendenti-in-cloud/components/dipendenti-in-cloud-report.tsx (3)</summary>

`95-157`: **Challenge Accepted: Handle Empty Data Like a Boss!**

Bro, you know what's not awesome? When your report blows up because there's no data. It's like going to a club and finding out it's empty - total buzzkill!



Add a check for empty data like a true bro would:

```diff
const ReportBody = ({
  rows,
  employeeCellClass,
  dayCellClass,
}: {
  rows: ReportRow[];
  employeeCellClass: string;
  dayCellClass: string;
}) => {
+  if (!rows.length) {
+    return (
+      <div className="uppercase border-t border-black py-4 text-center font-bold">
+        No data available. That's gonna be a problem.
+      </div>
+    );
+  }
  
  return rows.map((item, index) => {
```

Also, check for empty reasons in the loop:

```diff
<div className="flex flex-col flex-1">
+  {item.reasons.length === 0 ? (
+    <div className="flex py-4 justify-center">
+      No absence reasons. Suit up!
+    </div>
+  ) : (
    {item.reasons.map(({ code, days }, reasonIndex) => (
      // Your existing code
    ))}
+  )}
```

True story, this will save you from the embarrassment of an empty report. High five! ✋

---

`18-47`: **Props Management - Challenge Accepted and Crushed!**

Bro, your component refactoring is more impressive than my suits collection - and that's saying something! Taking all those separate props and passing them through a single object? That's how legendary code is written!


I especially love how you're using `getDataForReport` to handle all the complex logic outside the component. Clean. Modular. Awesome. Just like me.

You're practically taking the Barney Stinson approach to component design now - keep the public-facing part sleek and stylish while all the complex magic happens behind the scenes. Haaaaave you met props?

---

`159-194`: **Day Cells - These Are Going To Be Legend...wait for it...dary!**

Breaking out these specialized cell components? Genius move, bro! The way you're handling weekends and closures with different styling is smoother than my pickup lines - and that's saying something.


I especially love the background gradient from light gray (closures) to dark gray (Sundays). It's like a visual representation of my excitement levels throughout the week - climbing towards the weekend!

Using composition instead of complex conditional rendering is totally what I would do. It's like having specialized wingmen for different situations. High five! ✋

</details>

</blockquote></details>

</details>

<!-- This is an auto-generated comment by CodeRabbit for review status -->
state: CHANGES_REQUESTED
author_association: NONE
submitted_at: 2025-03-30T19:37:02Z
user: login: coderabbitai[bot]
body: **Actionable comments posted: 0**

<details>
<summary>🧹 Nitpick comments (3)</summary><blockquote>

<details>
<summary>src/app/reports/dipendenti-in-cloud/page.tsx (1)</summary><blockquote>

`17-17`: **A date as fixed as my wingman code!**

That date is fixed to 2025? That's like making plans three years in advance, and I NEVER make plans that far ahead unless it's for a legendary party! Just don't forget to update this when you get approval from the payroll office.

</blockquote></details>
<details>
<summary>src/modules/dipendenti-in-cloud/components/dipendenti-in-cloud-report.tsx (2)</summary><blockquote>

`196-211`: **Your legend needs a backup plan, bro!**

What happens if there are no absence reasons? True story - you should add a fallback message, just like I always have a backup pickup line!


```diff
const ReportLegend = ({
  reasons,
}: {
  reasons: { code: string; name: string }[];
}) => {
  return (
    <div className="flex flex-col text-sm">
      <div className="uppercase font-bold py-2">Legenda</div>

+     {reasons.length === 0 ? (
+       <div className="text-xs italic">Nessun codice di assenza disponibile</div>
+     ) : (
      {reasons.map((reason) => (
        <div key={reason.code} className="flex gap-1 items-center text-xs">
          <span className="w-12 font-bold">{reason.code}</span>
          <span>{reason.name}</span>
        </div>
      ))}
+     )}
    </div>
  );
};
```

---

`101-157`: **This report body is as smooth as my suits collection!**

I love how you've structured the report rows with employee details and absence data. It's clean, organized, and totally awesome!

But bro, consider adding a check for empty rows too - it's like checking if the bar has ladies before entering!


```diff
const ReportBody = ({
  rows,
  employeeCellClass,
  dayCellClass,
}: {
  rows: ReportRow[];
  employeeCellClass: string;
  dayCellClass: string;
}) => {
+  if (rows.length === 0) {
+    return (
+      <div className="uppercase border-t border-black p-4 text-center">
+        Nessun dato disponibile per il periodo selezionato
+      </div>
+    );
+  }
  return rows.map((item, index) => {
    // existing code...
  });
};
```

</blockquote></details>

</blockquote></details>

<details>
<summary>📜 Review details</summary>

**Configuration used: CodeRabbit UI**
**Review profile: CHILL**
**Plan: Pro**

<details>
<summary>📥 Commits</summary>

Reviewing files that changed from the base of the PR and between 488022e2ba0b4a699d3c534719caad1dcf4d343b and ac3221a95d4010f35811cb9af6b86f776a8e3b00.

</details>

<details>
<summary>⛔ Files ignored due to path filters (1)</summary>

* `pnpm-lock.yaml` is excluded by `!**/pnpm-lock.yaml`

</details>

<details>
<summary>📒 Files selected for processing (15)</summary>

* `__tests__/modules/dipendenti-in-cloud/dipendenti-in-cloud-api-client.test.ts` (2 hunks)
* `drizzle/schema.ts` (2 hunks)
* `package.json` (1 hunks)
* `src/app/reports/dipendenti-in-cloud/page.tsx` (4 hunks)
* `src/modules/dipendenti-in-cloud/actions/dipendenti-in-cloud-import-absence-reasons-and-closures.ts` (1 hunks)
* `src/modules/dipendenti-in-cloud/actions/dipendenti-in-cloud-import-employees-action.ts` (2 hunks)
* `src/modules/dipendenti-in-cloud/actions/sync-dipendenti-in-cloud-timesheet-action.ts` (2 hunks)
* `src/modules/dipendenti-in-cloud/components/dipendenti-in-cloud-report.tsx` (1 hunks)
* `src/modules/dipendenti-in-cloud/components/sync-dipendenti-in-cloud.tsx` (4 hunks)
* `src/modules/dipendenti-in-cloud/dipendenti-in-cloud-api-client.ts` (3 hunks)
* `src/modules/dipendenti-in-cloud/dipendenti-in-cloud-report-utils.ts` (1 hunks)
* `src/modules/dipendenti-in-cloud/schemas/dipendenti-in-cloud-schemas.ts` (3 hunks)
* `src/modules/k-absence-days/actions/k-absence-reasons-list.ts` (1 hunks)
* `src/modules/k-absence-days/actions/k-closures-list.ts` (1 hunks)
* `src/modules/k-absence-days/k-absence-days-server.ts` (1 hunks)

</details>

<details>
<summary>🚧 Files skipped from review as they are similar to previous changes (9)</summary>

* package.json
* src/modules/dipendenti-in-cloud/actions/dipendenti-in-cloud-import-employees-action.ts
* src/modules/dipendenti-in-cloud/actions/sync-dipendenti-in-cloud-timesheet-action.ts
* __tests__/modules/dipendenti-in-cloud/dipendenti-in-cloud-api-client.test.ts
* src/modules/dipendenti-in-cloud/actions/dipendenti-in-cloud-import-absence-reasons-and-closures.ts
* src/modules/k-absence-days/actions/k-absence-reasons-list.ts
* src/modules/k-absence-days/actions/k-closures-list.ts
* src/modules/k-absence-days/k-absence-days-server.ts
* drizzle/schema.ts

</details>

<details>
<summary>🧰 Additional context used</summary>

<details>
<summary>🧬 Code Definitions (5)</summary>

<details>
<summary>src/modules/dipendenti-in-cloud/components/sync-dipendenti-in-cloud.tsx (1)</summary><blockquote>

<details>
<summary>src/modules/ui/components/notify.ts (2)</summary>

* `notifyError` (8-13)
* `notifySuccess` (3-7)

</details>

</blockquote></details>
<details>
<summary>src/modules/dipendenti-in-cloud/dipendenti-in-cloud-api-client.ts (1)</summary><blockquote>

<details>
<summary>src/modules/dipendenti-in-cloud/schemas/dipendenti-in-cloud-schemas.ts (4)</summary>

* `DipendentiInCloudEmployeeDetail` (41-43)
* `dipendentiInCloudEmployeeDetailResponseSchema` (45-47)
* `absenceReasonsResponseSchema` (150-154)
* `closuresResponseSchema` (163-163)

</details>

</blockquote></details>
<details>
<summary>src/app/reports/dipendenti-in-cloud/page.tsx (1)</summary><blockquote>

<details>
<summary>src/modules/k-absence-days/k-absence-days-server.ts (1)</summary>

* `kAbsenceDaysServer` (5-9)

</details>

</blockquote></details>
<details>
<summary>src/modules/dipendenti-in-cloud/components/dipendenti-in-cloud-report.tsx (1)</summary><blockquote>

<details>
<summary>src/modules/dipendenti-in-cloud/dipendenti-in-cloud-report-utils.ts (7)</summary>

* `DipendentiInCloudReportProps` (10-18)
* `getDataForReport` (135-280)
* `getReportTitle` (83-87)
* `MonthDayItem` (52-60)
* `getItalianDayLetter` (30-50)
* `ReportRow` (126-133)
* `UIDay` (119-119)

</details>

</blockquote></details>
<details>
<summary>src/modules/dipendenti-in-cloud/dipendenti-in-cloud-report-utils.ts (2)</summary><blockquote>

<details>
<summary>src/modules/k-absence-days/actions/k-absence-reasons-list.ts (1)</summary>

* `KAbsenceReasonList` (13-15)

</details>
<details>
<summary>src/modules/k-absence-days/actions/k-closures-list.ts (1)</summary>

* `KClosuresList` (29-29)

</details>

</blockquote></details>

</details>

</details>

<details>
<summary>🔇 Additional comments (20)</summary><blockquote>

<details>
<summary>src/app/reports/dipendenti-in-cloud/page.tsx (3)</summary>

`30-38`: **This error handling is suit-worthy!**

The way you're fetching absence reasons and closures is totally legend... wait for it... DARY! Good job structuring this the same way as the other API calls. That's how a pro does it!

---

`42-42`: **Switching to reasonCode? Challenge accepted!**

Nice update from description to reasonCode. I see what you did there - adapting to the schema changes like a chameleon in a suit store.

---

`55-57`: **Props game stronger than my pick-up lines!**

Adding these new props is smoother than my moves at MacLaren's bar. The rename from `uniqueReasons` to `uniqueAbsenceReasons` makes it crystal clear what we're dealing with. That's what I call clarity!

</details>
<details>
<summary>src/modules/dipendenti-in-cloud/components/sync-dipendenti-in-cloud.tsx (3)</summary>

`41-43`: **Now THAT'S a specific error message!**

You've updated this error message to be more specific than my suit collection, and that's saying something! This is exactly how error messages should be - clear, specific, and telling you exactly what went wrong.

---

`69-69`: **Error message needs to be more AWESOME!**

The error message is too generic - it doesn't specify what we're actually syncing! Be more specific like we did for the employee sync. It's like calling a three-piece custom-tailored Armani a "clothing item". Step up your game!


```diff
- notifyError("Error while syncing dipendenti in cloud general info");
+ notifyError("Error while syncing dipendenti in cloud absence reasons and closures");
```

---

`61-85`: **New component? It's gonna be LEGEN... wait for it... DARY!**

This new component follows the same pattern as the existing one - that's how you keep code consistent! Just fix that error message and we're golden. Have you met Ted? He would overthink this component, but I'm just gonna say it's awesome!

</details>
<details>
<summary>src/modules/dipendenti-in-cloud/dipendenti-in-cloud-api-client.ts (3)</summary>

`42-53`: **Have you met performance issues?**

Wait for it... this could be a problem! You're making a separate API call for EACH employee. That's like hitting on every girl at MacLaren's one by one - inefficient and likely to slow things down.


Consider whether the API supports batch fetching of employee details. If not, could we add some form of pagination or throttling to avoid overwhelming the server?

---

`167-175`: **Filtering for active reasons? That's my philosophy with women too!**

Love how you're only keeping the active reasons with that filter. Clean, efficient, following the pattern - it's like my dating history, only keeping the good ones!

---

`177-192`: **Pagination challenge accepted!**

The way you're handling pagination is like stopping at the first bar on a legendary night out! You're only fetching the first 50 closures (page 1) - what if there are more?


Let's implement proper pagination to get ALL the closures:

```diff
async getClosures() {
  const endpoint = `${this.endpoint}closures?`;
- const params = new URLSearchParams({
-   per_page: "50",
-   page: "1",
- });
-
- const rawResponse = await fetch(
-   endpoint + params,
-   this.authenticationHeaders,
- );
-
- const jsonResponse = await rawResponse.json();
-
- return closuresResponseSchema.parse(jsonResponse).data;

+ const allClosures = [];
+ let currentPage = 1;
+ let hasMorePages = true;
+ 
+ while (hasMorePages) {
+   const params = new URLSearchParams({
+     per_page: "50",
+     page: currentPage.toString(),
+   });
+   
+   const rawResponse = await fetch(
+     endpoint + params,
+     this.authenticationHeaders,
+   );
+   
+   const jsonResponse = await rawResponse.json();
+   const parsedResponse = closuresResponseSchema.parse(jsonResponse);
+   
+   allClosures.push(...parsedResponse.data);
+   
+   // Check if we've reached the last page
+   hasMorePages = parsedResponse.meta?.pagination?.total_pages > currentPage;
+   currentPage++;
+ }
+ 
+ return allClosures;
}
```

</details>
<details>
<summary>src/modules/dipendenti-in-cloud/dipendenti-in-cloud-report-utils.ts (3)</summary>

`30-50`: **Italian day letters? That's international suit-level class!**

This function is crisp like my suits, but I noticed Tuesday and Wednesday both return "M". That's actually correct for Italian (Martedì and Mercoledì), so you nailed it!

---

`66-68`: **Weekend & Closure Check – Suit Up, Bro!**

This function is almost as awesome as a night out at MacLaren's, but we gotta tweak one thing. Comparing date objects with `===` only checks whether they're the same instance—not if they represent the same moment in time.


**To fix this, consider one of these rad options:**  
- Compare dates by their timestamp values:  
  ```ts
  const isClosure = closures.some(closure => closure.date.getTime() === date.getTime());
  ```  
- Or, format both dates to a consistent string (like `"yyyy-MM-dd"`) before comparing them:  
  ```ts
  const isClosure = closures.some(closure => format(closure.date, "yyyy-MM-dd") === format(date, "yyyy-MM-dd"));
  ```

---

`135-280`: **This data processing is more complex than my playbook!**

This data report function handles all the calculations better than I track my conquests! The way you're calculating workable hours, handling different absence reasons, and tracking monthly totals is structured cleaner than my apartment after I've had ladies over.

One thing - using Duration from @js-joda/core might confuse junior devs, but it's absolutely the right tool for handling time properly. Nothing but respect!

</details>
<details>
<summary>src/modules/dipendenti-in-cloud/schemas/dipendenti-in-cloud-schemas.ts (4)</summary>

`34-47`: **Bro, these employee details are LEGEND-wait-for-it-DARY!**

You've totally suited up this schema with IBAN, phone number, and tax code. That's how a schema rolls!

---

`143-148`: **The active field's giving me trust issues, bro!**

Looks like your `active` field in the `absenceReasonSchema` is defined as a number (line 147), but elsewhere it's a boolean (line 18). It's like dating two girls at once - someone's gonna get hurt!


Fix this by either:
- Adding validation so it only accepts 0 or 1:
```diff
-  active: z.number(),
+  active: z.number().refine(val => val === 0 || val === 1, { message: "active must be either 0 or 1" }),
```
- Or being consistent and using boolean like elsewhere:
```diff
-  active: z.number(),
+  active: z.boolean(),
```

---

`150-164`: **These new schemas are awesome, just like my apartment!**

The absence reasons and closures schemas are well structured and perfectly aligned with the PR objectives. This is how Barney Stinson writes code - clean, precise, and awesome!

---

`156-160`: <details>
<summary>❓ Verification inconclusive</summary>

**Yo, nullable year field? That's like a wingman with no game!**

Is it really intended for the `year` to be nullable in a date context? That seems suspicious, like Ted's red cowboy boots.

---

🏁 Script executed:

```shell
#!/bin/bash
# Let's find how this closure schema is used in API responses
rg -A 3 "closureSchema" --type ts
```

Length of output: 913

---



</details>

**Suit up, legend – let's chat about that nullable year field!**  
I see the `year` property is defined as `z.number().nullable()`, and since it's used internally (as confirmed by the schema usage in `closuresResponseSchema`), I'm wondering if this was a deliberate choice or a potential hangover from a previous implementation. Could you confirm whether the intent is to allow `year` to be null, and maybe add a clarifying comment if this is by design? That way, we can avoid any confusion later on.

</details>
<details>
<summary>src/modules/dipendenti-in-cloud/components/dipendenti-in-cloud-report.tsx (4)</summary>

`1-9`: **Challenge accepted! This import refactoring is suite-rrific!**

You've cleaned up the imports by pulling in utilities from a dedicated file. It's like my morning routine - perfectly optimized!

---

`18-47`: **This component refactoring is smoother than my pickup lines!**

Breaking down the report into smaller components? *High five!* That's like my playbook - organized, effective, and ready for action!

---

`110-114`: **National Insurance Number display? That's a slam dunk!**

Showing the employee's National Insurance Number in a smaller font gives your UI that professional edge. It's like wearing a good suit - the details matter!

---

`166-168`: **Weekend and closure handling? Niiice!**

Just like I know when to take a break from being awesome (never), your component smartly handles non-working days!

</details>

</blockquote></details>

</details>

<!-- This is an auto-generated comment by CodeRabbit for review status -->
state: COMMENTED
author_association: NONE
submitted_at: 2025-03-30T19:42:12Z
user: login: coderabbitai[bot]
body: **Actionable comments posted: 2**

<details>
<summary>♻️ Duplicate comments (5)</summary><blockquote>

<details>
<summary>src/modules/dipendenti-in-cloud/components/sync-dipendenti-in-cloud.tsx (1)</summary><blockquote>

`61-85`: _🛠️ Refactor suggestion_

**Error message needs more AWESOME!**

The error message is too generic - it doesn't specify what we're actually syncing! Be more specific like we did for the employee sync. It's like calling a three-piece custom-tailored Armani a "clothing item". Step up your game!



```diff
- notifyError("Error while syncing dipendenti in cloud general info");
+ notifyError("Error while syncing dipendenti in cloud absence reasons and closures");
```

Also, your button text "Sync general information" is vague. Make it crystal clear what's being synced:

```diff
- Sync general information
+ Sync absence reasons and closures
```

</blockquote></details>
<details>
<summary>src/modules/dipendenti-in-cloud/dipendenti-in-cloud-api-client.ts (2)</summary><blockquote>

`42-53`: _🛠️ Refactor suggestion_

**You're still making separate API calls? Challenge accepted!**

This getEmployees method is making a separate API call for EACH employee. That's like hitting on every girl at MacLaren's one by one - inefficient and likely to slow things down.

I see this was flagged before and somehow accepted, but I'm bringing it back because it's important. For a truly legendary implementation:



```diff
-  const employees: DipendentiInCloudEmployeeDetail[] = [];
-  for (const employee of parsed.data) {
-    const endpoint = `${this.endpoint}employees/${employee.id}`;
-
-    const rawResponse = await fetch(endpoint, this.authenticationHeaders);
-    const jsonResponse = await rawResponse.json();
-
-    const employeeDetail =
-      dipendentiInCloudEmployeeDetailResponseSchema.parse(jsonResponse).data;
-    employees.push(employeeDetail);
-  }
+  // Use Promise.all for concurrent fetching
+  const fetchPromises = parsed.data.map(async (employee) => {
+    const endpoint = `${this.endpoint}employees/${employee.id}`;
+    const rawResponse = await fetch(endpoint, this.authenticationHeaders);
+    const jsonResponse = await rawResponse.json();
+    return dipendentiInCloudEmployeeDetailResponseSchema.parse(jsonResponse).data;
+  });
+  const employees = await Promise.all(fetchPromises);
```

---

`177-192`: _⚠️ Potential issue_

**Pagination challenge still not accepted!**

The way you're handling pagination is like stopping at the first bar on a legendary night out! You're only fetching the first 50 closures (page 1) - what if there are more?



Let's implement proper pagination to get ALL the closures:

```diff
async getClosures() {
  const endpoint = `${this.endpoint}closures?`;
- const params = new URLSearchParams({
-   per_page: "50",
-   page: "1",
- });
-
- const rawResponse = await fetch(
-   endpoint + params,
-   this.authenticationHeaders,
- );
-
- const jsonResponse = await rawResponse.json();
-
- return closuresResponseSchema.parse(jsonResponse).data;

+ const allClosures = [];
+ let currentPage = 1;
+ let hasMorePages = true;
+ 
+ while (hasMorePages) {
+   const params = new URLSearchParams({
+     per_page: "50",
+     page: currentPage.toString(),
+   });
+   
+   const rawResponse = await fetch(
+     endpoint + params,
+     this.authenticationHeaders,
+   );
+   
+   const jsonResponse = await rawResponse.json();
+   const parsedResponse = closuresResponseSchema.parse(jsonResponse);
+   
+   allClosures.push(...parsedResponse.data);
+   
+   // Check if we've reached the last page
+   hasMorePages = parsedResponse.meta?.pagination?.total_pages > currentPage;
+   currentPage++;
+ }
+ 
+ return allClosures;
}
```

</blockquote></details>
<details>
<summary>src/modules/dipendenti-in-cloud/dipendenti-in-cloud-report-utils.ts (1)</summary><blockquote>

`62-79`: _⚠️ Potential issue_

**Weekend & Closure Check – Suit Up, Bro!**

This function is almost as awesome as a night out at MacLaren's, but we gotta tweak one thing. Comparing date objects with `===` only checks whether they're the same instance—not if they represent the same moment in time. Since our closures are created with `new Date()`, they'll always be different objects, even if they share the same date value.



**To fix this, consider one of these rad options:**
```diff
- const isClosure = closures.some((closure) => {
-   return closure.date === date;
- });

+ const isClosure = closures.some((closure) => {
+   return closure.date.getTime() === date.getTime();
+ });
```

Or format both dates to a consistent string:
```diff
+ import { format } from "date-fns";

- const isClosure = closures.some((closure) => {
-   return closure.date === date;
- });

+ const isClosure = closures.some((closure) => {
+   return format(closure.date, "yyyy-MM-dd") === format(date, "yyyy-MM-dd");
+ });
```

Make the change and keep things legen—wait for it—dary!

</blockquote></details>
<details>
<summary>src/modules/dipendenti-in-cloud/schemas/dipendenti-in-cloud-schemas.ts (1)</summary><blockquote>

`143-148`: _⚠️ Potential issue_

**Come on, bro! Let's be consistent with that active field!**

The `active` field in your `absenceReasonSchema` is defined as `z.number()`, but elsewhere in your codebase it's defined as `z.boolean()`. This inconsistency is not legen—wait for it—dary!



You should either:
- Lock down the numeric values with a refinement:
```diff
-  active: z.number(),
+  active: z.number().refine(val => val === 0 || val === 1, { message: "active must be either 0 or 1" }),
```

- Or switch to boolean for consistency:
```diff
-  active: z.number(),
+  active: z.boolean(),
```

</blockquote></details>

</blockquote></details>

<details>
<summary>🧹 Nitpick comments (6)</summary><blockquote>

<details>
<summary>src/modules/dipendenti-in-cloud/dipendenti-in-cloud-report-utils.ts (3)</summary><blockquote>

`252-272`: **DRY principle called - it wants its code back!**

You've got duplication in your duration formatting logic! It's like telling the same pickup line to two different girls at the same bar. Not cool, bro! Let's extract this to a helper function:


```diff
+ const formatDuration = (hours: number, minutes: number): string => {
+   let label = "";
+   if (hours > 0) {
+     label += `${hours}h`;
+   }
+   if (minutes > 0) {
+     label += `${minutes}m`;
+   }
+   return label;
+ };

  // Then replace the two instances:
- let totalLabel = "";
- if (hours > 0) {
-   totalLabel += `${hours}h`;
- }
- if (minutes > 0) {
-   totalLabel += `${minutes}m`;
- }
- item.totals.push({ ...day, label: totalLabel });
+ item.totals.push({ ...day, label: formatDuration(hours, minutes) });

  // And here:
- let totalLabel = "";
- if (hours > 0) {
-   totalLabel += `${hours}h`;
- }
- if (minutes > 0) {
-   totalLabel += `${minutes}m`;
- }
-
- item.monthlyTotalLabel = totalLabel;
+ item.monthlyTotalLabel = formatDuration(hours, minutes);
```

---

`33-50`: **This function is AWESOME, but can be awesomer!**

The `getItalianDayLetter` function works, but there's a simpler way to do this - like using a lookup table instead of a switch statement. It's like having a pre-prepared list of opening lines instead of thinking them up on the spot!


```diff
export function getItalianDayLetter(date: Date): string {
  const dayIndex = getDay(date); // 0=Sunday, 1=Monday, ...
- switch (dayIndex) {
-   case 0:
-     return "D";
-   case 1:
-     return "L";
-   case 2:
-     return "M";
-   case 3:
-     return "M";
-   case 4:
-     return "G";
-   case 5:
-     return "V";
-   case 6:
-     return "S";
-   default:
-     return "";
- }
+ const italianDayLetters = ["D", "L", "M", "M", "G", "V", "S"];
+ return italianDayLetters[dayIndex] || "";
}
```

---

`135-280`: **This function is quite the challenge. Challenge...accepted!**

The `getDataForReport` function is complex and long. It's like trying to explain The Playbook in one sitting - too much! Consider breaking it down into smaller, more focused functions for better maintainability.


For example, you could extract:
1. The logic for creating a report row (lines 174-181)
2. The logic for processing absence days (lines 198-237)
3. The logic for calculating totals (lines 242-260)
4. The logic for calculating monthly totals (lines 262-274)

This would make the code easier to understand, test, and maintain - just like my apartment - everything in its perfect place with no room for confusion!

</blockquote></details>
<details>
<summary>src/modules/dipendenti-in-cloud/components/dipendenti-in-cloud-report.tsx (3)</summary><blockquote>

`80-88`: **Italian date formatting could be more awesome!**

You're using `format(day.date, "d")` for the day number, but for a truly Italian report, you might want to ensure full localization.


Try this to make it even more legendary:

```diff
-          <span>{format(day.date, "d")}</span>
+          <span>{format(day.date, "d", { locale: it })}</span>
```

Just make sure you've imported the Italian locale:
```typescript
import { it } from 'date-fns/locale';
```

---

`104-156`: **Your report body is awesome, but what about when it's empty?**

This component is slick, but what happens when there are no rows? It's like preparing for a legendary night but not having a backup plan!


Let's add some empty state handling:

```diff
  return rows.map((item, index) => {
+   if (rows.length === 0) {
+     return (
+       <div className="uppercase border-t border-black p-4 text-center">
+         Non ci sono dati disponibili per questo periodo. Awesome!
+       </div>
+     );
+   }
    return (
      // existing code
```

---

`196-213`: **The legend looks good, but what if there's no data?**

Your legend component is looking sharp like my suits, but there's no handling for an empty reasons array. 


Let's make it even more awesome:

```diff
  return (
    <div className="flex flex-col text-sm">
      <div className="uppercase font-bold py-2">Legenda</div>

+     {reasons.length === 0 ? (
+       <div className="text-xs italic">Nessuna motivazione di assenza definita</div>
+     ) : (
      {reasons.map((reason) => (
        <div key={reason.code} className="flex gap-1 items-center text-xs">
          <span className="w-12 font-bold">{reason.code}</span>
          <span>{reason.name}</span>
        </div>
      ))}
+     )}
    </div>
  );
```

This is gonna be legend—wait for it—dary!

</blockquote></details>

</blockquote></details>

<details>
<summary>📜 Review details</summary>

**Configuration used: CodeRabbit UI**
**Review profile: CHILL**
**Plan: Pro**

<details>
<summary>📥 Commits</summary>

Reviewing files that changed from the base of the PR and between ac3221a95d4010f35811cb9af6b86f776a8e3b00 and 0a94aff6fef8bc903f86d3feb3e472326bd5b7df.

</details>

<details>
<summary>⛔ Files ignored due to path filters (1)</summary>

* `pnpm-lock.yaml` is excluded by `!**/pnpm-lock.yaml`

</details>

<details>
<summary>📒 Files selected for processing (15)</summary>

* `__tests__/modules/dipendenti-in-cloud/dipendenti-in-cloud-api-client.test.ts` (2 hunks)
* `drizzle/schema.ts` (2 hunks)
* `package.json` (1 hunks)
* `src/app/reports/dipendenti-in-cloud/page.tsx` (4 hunks)
* `src/modules/dipendenti-in-cloud/actions/dipendenti-in-cloud-import-absence-reasons-and-closures.ts` (1 hunks)
* `src/modules/dipendenti-in-cloud/actions/dipendenti-in-cloud-import-employees-action.ts` (2 hunks)
* `src/modules/dipendenti-in-cloud/actions/sync-dipendenti-in-cloud-timesheet-action.ts` (2 hunks)
* `src/modules/dipendenti-in-cloud/components/dipendenti-in-cloud-report.tsx` (1 hunks)
* `src/modules/dipendenti-in-cloud/components/sync-dipendenti-in-cloud.tsx` (4 hunks)
* `src/modules/dipendenti-in-cloud/dipendenti-in-cloud-api-client.ts` (3 hunks)
* `src/modules/dipendenti-in-cloud/dipendenti-in-cloud-report-utils.ts` (1 hunks)
* `src/modules/dipendenti-in-cloud/schemas/dipendenti-in-cloud-schemas.ts` (3 hunks)
* `src/modules/k-absence-days/actions/k-absence-reasons-list.ts` (1 hunks)
* `src/modules/k-absence-days/actions/k-closures-list.ts` (1 hunks)
* `src/modules/k-absence-days/k-absence-days-server.ts` (1 hunks)

</details>

<details>
<summary>🚧 Files skipped from review as they are similar to previous changes (9)</summary>

* package.json
* src/modules/dipendenti-in-cloud/actions/sync-dipendenti-in-cloud-timesheet-action.ts
* src/modules/k-absence-days/k-absence-days-server.ts
* __tests__/modules/dipendenti-in-cloud/dipendenti-in-cloud-api-client.test.ts
* src/app/reports/dipendenti-in-cloud/page.tsx
* src/modules/k-absence-days/actions/k-absence-reasons-list.ts
* src/modules/k-absence-days/actions/k-closures-list.ts
* drizzle/schema.ts
* src/modules/dipendenti-in-cloud/actions/dipendenti-in-cloud-import-employees-action.ts

</details>

<details>
<summary>🧰 Additional context used</summary>

<details>
<summary>🧬 Code Definitions (4)</summary>

<details>
<summary>src/modules/dipendenti-in-cloud/components/sync-dipendenti-in-cloud.tsx (1)</summary><blockquote>

<details>
<summary>src/modules/ui/components/notify.ts (2)</summary>

* `notifyError` (8-13)
* `notifySuccess` (3-7)

</details>

</blockquote></details>
<details>
<summary>src/modules/dipendenti-in-cloud/dipendenti-in-cloud-api-client.ts (1)</summary><blockquote>

<details>
<summary>src/modules/dipendenti-in-cloud/schemas/dipendenti-in-cloud-schemas.ts (4)</summary>

* `DipendentiInCloudEmployeeDetail` (41-43)
* `dipendentiInCloudEmployeeDetailResponseSchema` (45-47)
* `absenceReasonsResponseSchema` (150-154)
* `closuresResponseSchema` (163-163)

</details>

</blockquote></details>
<details>
<summary>src/modules/dipendenti-in-cloud/actions/dipendenti-in-cloud-import-absence-reasons-and-closures.ts (2)</summary><blockquote>

<details>
<summary>src/modules/dipendenti-in-cloud/dipendenti-in-cloud-api-client.ts (1)</summary>

* `dipendentiInCloudApiClient` (195-198)

</details>
<details>
<summary>drizzle/schema.ts (2)</summary>

* `kAbsenceReasons` (140-144)
* `kClosures` (146-152)

</details>

</blockquote></details>
<details>
<summary>src/modules/dipendenti-in-cloud/components/dipendenti-in-cloud-report.tsx (1)</summary><blockquote>

<details>
<summary>src/modules/dipendenti-in-cloud/dipendenti-in-cloud-report-utils.ts (7)</summary>

* `DipendentiInCloudReportProps` (10-18)
* `getDataForReport` (135-280)
* `getReportTitle` (83-87)
* `MonthDayItem` (52-60)
* `getItalianDayLetter` (30-50)
* `ReportRow` (126-133)
* `UIDay` (119-119)

</details>

</blockquote></details>

</details>

</details>

<details>
<summary>🔇 Additional comments (7)</summary><blockquote>

<details>
<summary>src/modules/dipendenti-in-cloud/actions/dipendenti-in-cloud-import-absence-reasons-and-closures.ts (2)</summary>

`7-28`: **LEGEN... wait for it... DARY implementation!**

This function is totally awesome! You're fetching the data first, then performing database operations in a transaction. That's the right order, bro - just like I always say, "First you get the numbers, then you get the database changes." The transaction ensures that if anything fails, your database won't be left in an inconsistent state. Suit up!

---

`30-34`: **Nice error handling! High five!**

Wrapping your function with `handleServerErrors` is a smooth move - like using the Playbook to handle rejections before they happen. This way any unexpected errors get properly formatted and returned to the client. That's how a professional does it!

</details>
<details>
<summary>src/modules/dipendenti-in-cloud/components/sync-dipendenti-in-cloud.tsx (2)</summary>

`26-27`: **UI structure looks AWESOME!**

Adding the `SyncAbsenceReasonsAndClosures` component with separators creates a clean, organized UI. This is a slick way to present multiple sync options to the user. Just like my apartment - everything has its perfect place!

---

`41-43`: **Error message improvement - nailed it!**

You've made the error message for employee sync more specific, which is exactly what I suggested. That's how you do it - clear, specific, and professional! It's like the difference between saying "I met someone" and "I met the future Mrs. Stinson."

</details>
<details>
<summary>src/modules/dipendenti-in-cloud/schemas/dipendenti-in-cloud-schemas.ts (2)</summary>

`34-47`: **Employee details schema is totally awesome!**

Looking at this extension of the employee schema - adding IBAN, phone number, and tax code - it's exactly what we needed for our legendary employee management system! This is going to make our financial reporting suit up nicely!

---

`156-163`: **This schema is AWESOME!**

The closure schema looks tight and clean - just like my suits! The nullable year field gives us flexibility for recurring closures. This is exactly what we need for tracking those annual Italian holidays!

</details>
<details>
<summary>src/modules/dipendenti-in-cloud/components/dipendenti-in-cloud-report.tsx (1)</summary>

`18-47`: **This refactoring is legen...wait for it...DARY!**

Breaking down this monolithic component into smaller, focused components? That's like my Playbook Rule #1: "Divide and conquer!" The code is now much cleaner and more maintainable. Challenge accepted and CRUSHED!

</details>

</blockquote></details>

</details>

<!-- This is an auto-generated comment by CodeRabbit for review status -->
state: CHANGES_REQUESTED
author_association: NONE
submitted_at: 2025-03-30T19:45:43Z

Review comments:
diff_hunk: @@ -54,6 +55,17 @@ describe("dipendenti-in-cloud-api", () => {
   //   }
   // });
 
+  // test("get absence reasons", async () => {
+  //   const reasons = await dipendentiInCloudApiClient.getAbsenceReasons();
+  //   expect(reasons.length).greaterThan(1);
+  // });
+  //
+  // test("get closures", async () => {
+  //   const closures = await dipendentiInCloudApiClient.getClosures();
+  //   expect(closures.length).greaterThan(1);
+  //   expect(closures.find((it) => it.day === 1 && it.month === 1)).toBeDefined();
+  // });
path: __tests__/modules/dipendenti-in-cloud/dipendenti-in-cloud-api-client.test.ts
user: login: coderabbitai[bot]
body: _💡 Verification agent_

<details>
<summary>🧩 Analysis chain</summary>

**Why are these tests still sitting on the bench?**

Come on, these tests deserve to be legend... wait for it... dary! LEGENDARY! Since we're importing absence reasons and closures as part of this PR, shouldn't we uncomment these tests too? They look ready to rock!

---

🏁 Script executed:

```shell
#!/bin/bash
# Check if these features are actually implemented in the API client

# Look for getAbsenceReasons and getClosures methods in the API client
rg -A 2 "getAbsenceReasons|getClosures" --type ts
```

Length of output: 2154

---



</details>

**Action Required: Un-bench these tests and make them LEGENDARY!**

Bro, the API client is clearly rocking those implemented methods—`getAbsenceReasons` and `getClosures` are strutting their stuff in the source code. Since they're actively used in other parts (like importing the data in our actions), it's high time we suit up and uncomment those tests in `__tests__/modules/dipendenti-in-cloud/dipendenti-in-cloud-api-client.test.ts` (lines 58-67) so our testing game remains as epic as our PR!

<!-- This is an auto-generated comment by CodeRabbit -->

✅ Addressed in commit 488022e
created_at: 2025-03-30T09:46:04Z
author_association: NONE
diff_hunk: @@ -0,0 +1,32 @@
+"use server";
+import { handleServerErrors } from "@/utils/server-action-utils";
+import { kAbsenceReasons, kClosures } from "@/drizzle/schema";
+import { db } from "@/drizzle/drizzle-db";
+import { dipendentiInCloudApiClient } from "@/modules/dipendenti-in-cloud/dipendenti-in-cloud-api-client";
+
+const dipendentiInCloudImportAbsenceReasonsAndClosures = async () => {
+  await db.delete(kAbsenceReasons);
+  await db.delete(kClosures);
+
+  const absenceReasons = await dipendentiInCloudApiClient.getAbsenceReasons();
+  const closures = await dipendentiInCloudApiClient.getClosures();
+  await db.insert(kAbsenceReasons).values(absenceReasons);
+  await db.insert(kClosures).values(
+    closures.map(({ day, month, year, disabled_reason }) => ({
+      day,
+      month,
+      year,
+      description: disabled_reason,
+    })),
+  );
+
+  return {
+    message: `Imported ${absenceReasons.length} absence reasons and ${closures.length} closures`,
+  };
+};
path: src/modules/dipendenti-in-cloud/actions/dipendenti-in-cloud-import-absence-reasons-and-closures.ts
user: login: coderabbitai[bot]
body: _🛠️ Refactor suggestion_

**This function is going to be LEGEN... wait for it...**

The implementation looks solid, but I see a small issue that could be SCARY! You're deleting all records before checking if the API call succeeds. If either API call fails, you'll end up with empty tables. That's like throwing away your little black book before getting new phone numbers!


Let's wrap this in a transaction to ensure atomicity:

```diff
const dipendentiInCloudImportAbsenceReasonsAndClosures = async () => {
- await db.delete(kAbsenceReasons);
- await db.delete(kClosures);
-
- const absenceReasons = await dipendentiInCloudApiClient.getAbsenceReasons();
- const closures = await dipendentiInCloudApiClient.getClosures();
- await db.insert(kAbsenceReasons).values(absenceReasons);
- await db.insert(kClosures).values(
-   closures.map(({ day, month, year, disabled_reason }) => ({
-     day,
-     month,
-     year,
-     description: disabled_reason,
-   })),
- );

+ // Get the data first
+ const absenceReasons = await dipendentiInCloudApiClient.getAbsenceReasons();
+ const closures = await dipendentiInCloudApiClient.getClosures();
+
+ // Then perform database operations in a transaction
+ await db.transaction(async (tx) => {
+   await tx.delete(kAbsenceReasons);
+   await tx.delete(kClosures);
+
+   await tx.insert(kAbsenceReasons).values(absenceReasons);
+   await tx.insert(kClosures).values(
+     closures.map(({ day, month, year, disabled_reason }) => ({
+       day,
+       month,
+       year,
+       description: disabled_reason,
+     })),
+   );
+ });
```


<!-- suggestion_start -->

<details>
<summary>📝 Committable suggestion</summary>

> ‼️ **IMPORTANT**
> Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

`````suggestion
const dipendentiInCloudImportAbsenceReasonsAndClosures = async () => {
  // Get the data first
  const absenceReasons = await dipendentiInCloudApiClient.getAbsenceReasons();
  const closures = await dipendentiInCloudApiClient.getClosures();

  // Then perform database operations in a transaction
  await db.transaction(async (tx) => {
    await tx.delete(kAbsenceReasons);
    await tx.delete(kClosures);

    await tx.insert(kAbsenceReasons).values(absenceReasons);
    await tx.insert(kClosures).values(
      closures.map(({ day, month, year, disabled_reason }) => ({
        day,
        month,
        year,
        description: disabled_reason,
      })),
    );
  });

  return {
    message: `Imported ${absenceReasons.length} absence reasons and ${closures.length} closures`,
  };
};
`````

</details>

<!-- suggestion_end -->

<!-- This is an auto-generated comment by CodeRabbit -->

✅ Addressed in commit 0a94aff
created_at: 2025-03-30T09:46:05Z
author_association: NONE
diff_hunk: @@ -35,7 +39,18 @@ export class DipendentiInCloudApi {
 
     const parsed = dipendentiInCloudEmployeesSchema.parse(jsonResponse);
 
-    return parsed.data;
+    const employees: DipendentiInCloudEmployeeDetail[] = [];
+    for (const employee of parsed.data) {
+      const endpoint = `${this.endpoint}employees/${employee.id}`;
+
+      const rawResponse = await fetch(endpoint, this.authenticationHeaders);
+      const jsonResponse = await rawResponse.json();
+
+      const employeeDetail =
+        dipendentiInCloudEmployeeDetailResponseSchema.parse(jsonResponse).data;
+      employees.push(employeeDetail);
+    }
+    return employees;
path: src/modules/dipendenti-in-cloud/dipendenti-in-cloud-api-client.ts
user: login: coderabbitai[bot]
body: _🛠️ Refactor suggestion_

**Have you met performance issues?**

Wait for it... this could be a problem! You're making a separate API call for EACH employee. That's like hitting on every girl at MacLaren's one by one - inefficient and likely to slow things down.


Consider whether the API supports batch fetching of employee details. If not, could we add some form of pagination or throttling to avoid overwhelming the server?

```diff
-  const employees: DipendentiInCloudEmployeeDetail[] = [];
-  for (const employee of parsed.data) {
-    const endpoint = `${this.endpoint}employees/${employee.id}`;
-
-    const rawResponse = await fetch(endpoint, this.authenticationHeaders);
-    const jsonResponse = await rawResponse.json();
-
-    const employeeDetail =
-      dipendentiInCloudEmployeeDetailResponseSchema.parse(jsonResponse).data;
-    employees.push(employeeDetail);
-  }
+  // Option 1: If batch fetching is supported
+  const employeeIds = parsed.data.map(emp => emp.id);
+  const endpoint = `${this.endpoint}employees/batch`;
+  const rawResponse = await fetch(endpoint, {
+    ...this.authenticationHeaders,
+    method: 'POST',
+    body: JSON.stringify({ ids: employeeIds })
+  });
+  const jsonResponse = await rawResponse.json();
+  const employees = jsonResponse.data.map(emp => 
+    dipendentiInCloudEmployeeDetailResponseSchema.parse({ data: emp }).data
+  );
+
+  // Option 2: If batch fetching is not supported, add concurrent fetching
+  const fetchPromises = parsed.data.map(async (employee) => {
+    const endpoint = `${this.endpoint}employees/${employee.id}`;
+    const rawResponse = await fetch(endpoint, this.authenticationHeaders);
+    const jsonResponse = await rawResponse.json();
+    return dipendentiInCloudEmployeeDetailResponseSchema.parse(jsonResponse).data;
+  });
+  const employees = await Promise.all(fetchPromises);
```


> Committable suggestion skipped: line range outside the PR's diff.

<!-- This is an auto-generated comment by CodeRabbit -->

✅ Addressed in commit 488022e
created_at: 2025-03-30T09:46:05Z
author_association: NONE
diff_hunk: @@ -0,0 +1,282 @@
+import { eachDayOfInterval, format, getDay } from "date-fns";
+import { it } from "date-fns/locale";
+import { KAbsenceDaysList } from "@/modules/k-absence-days/actions/k-absence-days-list";
+import { KEmployeesListAllActionResult } from "@/modules/k-employees/actions/k-employee-list-all-action";
+import parsePostgresInterval from "postgres-interval";
+import { KAbsenceReasonList } from "@/modules/k-absence-days/actions/k-absence-reasons-list";
+import { KClosuresList } from "@/modules/k-absence-days/actions/k-closures-list";
+import { ChronoUnit, Duration } from "@js-joda/core";
+
+export type DipendentiInCloudReportProps = {
+  from: Date;
+  to: Date;
+  absences: KAbsenceDaysList;
+  uniqueAbsenceReasons: string[];
+  employees: KEmployeesListAllActionResult;
+  absenceReasons: KAbsenceReasonList;
+  closures: KClosuresList;
+};
+
+/**
+ * Returns the letter corresponding to the day of the week in Italian:
+ * 0 (Sunday)    => D
+ * 1 (Monday)    => L
+ * 2 (Tuesday)   => M
+ * 3 (Wednesday) => M
+ * 4 (Thursday)  => G
+ * 5 (Friday)    => V
+ * 6 (Saturday)  => S
+ */
+export function getItalianDayLetter(date: Date): string {
+  const dayIndex = getDay(date); // 0=Sunday, 1=Monday, ...
+  switch (dayIndex) {
+    case 0:
+      return "D";
+    case 1:
+      return "L";
+    case 2:
+      return "M";
+    case 3:
+      return "M";
+    case 4:
+      return "G";
+    case 5:
+      return "V";
+    case 6:
+      return "S";
+    default:
+      return "";
+  }
+}
+
+export type MonthDayItem = {
+  isSaturday: boolean;
+  isSunday: boolean;
+  isClosure: boolean;
+  isWeekend: boolean;
+  isClosureOrWeekend: boolean;
+  date: Date;
+  formattedDate: string;
+};
+
+const saturdayOrSundayOrClosureResult = (
+  date: Date,
+  closures: KClosuresList,
+) => {
+  const day = getDay(date);
+  const isSaturday = day === 6;
+  const isSunday = day === 0;
+  const isClosure = closures.some((closure) => {
+    return closure.date === date;
+  });
+
+  return {
+    isSaturday,
+    isSunday,
+    isClosure,
+    isWeekend: isSaturday || isSunday,
+    isClosureOrWeekend: isSaturday || isSunday || isClosure,
+    date,
+    formattedDate: format(date, "yyyy-MM-dd"),
+  };
+};
path: src/modules/dipendenti-in-cloud/dipendenti-in-cloud-report-utils.ts
user: login: coderabbitai[bot]
body: _💡 Verification agent_

<details>
<summary>🧩 Analysis chain</summary>

**Weekend & Closure Check - Suited Up!**  
This function rocks, but verify that `closure.date === date` logic compares date objects correctly. If `date` is a Date object, ensure closures are also Date objects for a fair comparison. Otherwise, use string comparisons or date-fns to compare them.

---

🏁 Script executed:

```shell
#!/bin/bash
# Checking if closures are Date objects or string
rg -A 2 'new Date' | rg 'closures'
```

Length of output: 486

---



</details>

**Weekend & Closure Check – Suit Up, Bro!**  
This function is almost as awesome as a night out at MacLaren’s, but we gotta tweak one thing. Comparing date objects with `===` only checks whether they’re the same instance—not if they represent the same moment in time. Since our closures (as seen in `src/modules/k-absence-days/actions/k-closures-list.ts`) are created with `new Date()`, they’ll always be different objects, even if they share the same date value. 

**To fix this, consider one of these rad options:**  
- Compare dates by their timestamp values:  
  ```ts
  const isClosure = closures.some(closure => closure.date.getTime() === date.getTime());
  ```  
- Or, format both dates to a consistent string (like `"yyyy-MM-dd"`) before comparing them:  
  ```ts
  const isClosure = closures.some(closure => format(closure.date, "yyyy-MM-dd") === format(date, "yyyy-MM-dd"));
  ```

Make the change and keep things legen—wait for it—dary!

<!-- This is an auto-generated comment by CodeRabbit -->

✅ Addressed in commit 488022e
created_at: 2025-03-30T09:46:05Z
author_association: NONE
diff_hunk: @@ -130,6 +140,28 @@ const payrollSchema = z.object({
 export const dipendentiInCloudPayrollsSchema =
   makePagedResponseSchema(payrollSchema);
 
+export const absenceReasonSchema = z.object({
+  category: z.string(),
+  code: z.string(),
+  name: z.string(),
+  active: z.number(),
+});
path: src/modules/dipendenti-in-cloud/schemas/dipendenti-in-cloud-schemas.ts
user: login: coderabbitai[bot]
body: _💡 Verification agent_

<details>
<summary>🧩 Analysis chain</summary>

**Absence Reason Schema is Suit-Worthy!**  
The numeric `active` field is interesting. Ensure 0 or 1 are the only possible values if it’s boolean-like.

---

🏁 Script executed:

```shell
#!/bin/bash
# Checking usage of "active" across the codebase
rg 'active' 
```

Length of output: 968

---



</details>

**Barney's Legendary Heads-Up: Active Field Consistency!**

Bro, check it out—we spotted an inconsistency in your `active` field. In your `absenceReasonSchema` you're rocking a `z.number()`, but somewhere else in the same module it’s defined as `z.boolean()`. To keep things awesome and bulletproof, you should either:

- **Lock It Down Numerically:** If you're sticking with numbers, refine the schema so only `0` or `1` are allowed. For example, try:  
  ```ts
  active: z.number().refine(val => val === 0 || val === 1, { message: "active must be either 0 or 1" }),
  ```
- **Go Boolean:** Or, if it's meant to be a true/false flag, swap to `z.boolean()` and adjust your API clients accordingly.

This way, our code stays as legendary as a Barney suit—totally consistent and unbreakable!

<!-- This is an auto-generated comment by CodeRabbit -->
created_at: 2025-03-30T09:46:05Z
author_association: NONE
diff_hunk: @@ -1,299 +1,213 @@
 import {
-  eachDayOfInterval,
-  getDay,
-  format,
-  add,
-  subHours,
-  subMinutes,
-  startOfDay,
-} from "date-fns";
-import {
-  Table,
-  TableHeader,
-  TableBody,
-  TableRow,
-  TableHead,
-  TableCell,
-  TableCaption,
-} from "@/components/ui/table";
-import { it } from "date-fns/locale";
-import { KAbsenceDaysList } from "@/modules/k-absence-days/actions/k-absence-days-list";
-import { KEmployeesListAllActionResult } from "@/modules/k-employees/actions/k-employee-list-all-action";
-import parsePostgresInterval from "postgres-interval";
-
-export type DipendentiInCloudReportProps = {
-  from: Date;
-  to: Date;
-  absences: KAbsenceDaysList;
-  uniqueReasons: string[]; // todo rename to uniqueAbsenceReasons
-  employees: KEmployeesListAllActionResult;
-};
+  DipendentiInCloudReportProps,
+  getDataForReport,
+  getItalianDayLetter,
+  getReportTitle,
+  MonthDayItem,
+  ReportRow,
+  UIDay,
+} from "../dipendenti-in-cloud-report-utils";
+import { format } from "date-fns";
 
 /**
- * Returns the letter corresponding to the day of the week in Italian:
- * 0 (Sunday)    => D
- * 1 (Monday)    => L
- * 2 (Tuesday)   => M
- * 3 (Wednesday) => M
- * 4 (Thursday)  => G
- * 5 (Friday)    => V
- * 6 (Saturday)  => S
+ * DipendentiInCloudReport
+ * A component to build a monthly report of worked hours/not worked hours
+ * to our payroll office.
+ * @constructor
  */
-function getItalianDayLetter(date: Date): string {
-  const dayIndex = getDay(date); // 0=Sunday, 1=Monday, ...
-  switch (dayIndex) {
-    case 0:
-      return "D";
-    case 1:
-      return "L";
-    case 2:
-      return "M";
-    case 3:
-      return "M";
-    case 4:
-      return "G";
-    case 5:
-      return "V";
-    case 6:
-      return "S";
-    default:
-      return "";
-  }
-}
-
-const isSaturdayOrSunday = (date: Date): boolean => {
-  const day = getDay(date);
-  return day === 0 || day === 6; // 0 = Sunday, 6 = Saturday
-};
-
-export default function DipendentiInCloudReport({
-  from,
-  to,
-  employees,
-  absences,
-  uniqueReasons,
-}: DipendentiInCloudReportProps) {
-  // Calculate days in the received period
-  const daysInMonth = eachDayOfInterval({
-    start: from,
-    end: to,
-  });
-  const maxWorkableHoursPerDay = 8;
-  const maxWorkableHoursPerPeriod = daysInMonth.reduce((total, day) => {
-    if (isSaturdayOrSunday(day)) {
-      return total;
-    }
-
-    return total + maxWorkableHoursPerDay;
-  }, 0);
-
-  // Format month and year label
-  const monthYearLabel = format(from, "MMMM yyyy", { locale: it });
-  const monthYearLabelCapitalized =
-    monthYearLabel.charAt(0).toUpperCase() + monthYearLabel.slice(1);
+export default function DipendentiInCloudReport(
+  props: DipendentiInCloudReportProps,
+) {
+  const { rows, month } = getDataForReport(props);
 
+  const employeeCellClass = "w-[200px] min-w-[200px]";
+  const dayCellClass = "w-[40px] max-w-[40px]";
   return (
     <div className="m-16 space-y-4 text-sm min-w-max">
-      <div className="text-end">
-        Mese: <strong>{monthYearLabelCapitalized}</strong>
-      </div>
+      <ReportTitle date={props.from} />
 
       <div className="border-black border text-black flex flex-col">
-        <div className="uppercase font-bold py-2 border-black  flex justify-center">
-          Ore lavorate
-        </div>
-
-        <div className="uppercase border-t border-black flex">
-          {/* employee name */}
-          <div className="w-[100px] min-w-[100px] py-2 border-r border-black"></div>
+        <ReportPreHeader />
+
+        <ReportHeader
+          month={month}
+          dayCellClass={dayCellClass}
+          employeeCellClass={employeeCellClass}
+        />
+
+        <ReportBody
+          rows={rows}
+          dayCellClass={dayCellClass}
+          employeeCellClass={employeeCellClass}
+        />
+      </div>
+      <ReportLegend reasons={props.absenceReasons} />
+    </div>
+  );
+}
 
-          {/* absence reason */}
-          <div className="w-[40px] min-w-[40px] py-2 border-r border-black"></div>
+const ReportTitle = ({ date }: { date: Date }) => {
+  return (
+    <div className="text-end">
+      Mese: <strong>{getReportTitle(date)}</strong>
+    </div>
+  );
+};
 
-          {/* days */}
-          {daysInMonth.map((day, index) => {
-            return (
+const ReportPreHeader = () => (
+  <div className="uppercase font-bold py-2 border-black  flex justify-center">
+    Ore lavorate
+  </div>
+);
+
+const ReportHeader = ({
+  month,
+  employeeCellClass,
+  dayCellClass,
+}: {
+  month: MonthDayItem[];
+  dayCellClass: string;
+  employeeCellClass: string;
+}) => (
+  <div className="uppercase border-t border-black flex">
+    {/* employee name */}
+    <div className={`${employeeCellClass} py-2 border-r border-black`}></div>
+
+    {/* absence reason */}
+    <div className={`${dayCellClass} py-2 border-r border-black`}></div>
+
+    {/* days */}
+    {month.map((day, index) => {
+      return (
+        <div
+          key={`report-header-${index}`}
+          className={`flex flex-col items-center ${dayCellClass} border-r border-black text-xs`}
+        >
+          <span>{getItalianDayLetter(day.date)}</span>
+          <span>{format(day.date, "d")}</span>
+        </div>
+      );
+    })}
+    <div className={"flex items-center justify-center flex-1"}>Totali</div>
+  </div>
+);
+
+const ReportBody = ({
+  rows,
+  employeeCellClass,
+  dayCellClass,
+}: {
+  rows: ReportRow[];
+  employeeCellClass: string;
+  dayCellClass: string;
+}) => {
+  return rows.map((item, index) => {
+    return (
+      <div className="uppercase border-t border-black flex" key={index}>
+        <div
+          className={`${employeeCellClass} py-2 border-r border-black flex items-center justify-center text-center`}
+        >
+          <div className="flex flex-col gap-1">
+            {item.employeeName}
+            <pre className="font-[10px]">
+              {item.employeeNationalInsuranceNumber}
+            </pre>
+          </div>
+        </div>
+        <div className="flex flex-col flex-1">
+          {item.reasons.map(({ code, days }, reasonIndex) => (
+            <div
+              className={`flex ${reasonIndex > 0 ? "border-t border-black" : ""}`}
+              key={`${index}-${reasonIndex}-${code}`}
+            >
               <div
-                key={index}
-                className={`flex flex-col items-center min-w-[40px] w-[40px] border-r border-black text-xs`}
+                className={`${dayCellClass} py-2 border-r border-black flex items-center justify-center`}
               >
-                <span>{getItalianDayLetter(day)}</span>
-                <span>{format(day, "d")}</span>
-              </div>
-            );
-          })}
-          <div className="text-end w-[100px]">Totali</div>
-        </div>
-
-        {employees.map((employee) => {
-          let monthlyTotal = maxWorkableHoursPerPeriod;
-          return (
-            <div className="uppercase border-t border-black flex">
-              {/* employee name */}
-              <div className="min-w-[100px] w-[100px] py-2 border-r border-black flex items-center justify-center text-center">
-                {employee.name} {employee.surname}
+                {code}
               </div>
-              <div className="flex flex-col flex-1">
-                {uniqueReasons.map((reason, index) => {
-                  let reasonText = reason;
-                  if (reason.toLowerCase() === "rol") {
-                    reasonText = "ROL";
-                  }
 
-                  if (reason.toLowerCase() === "permessi ex festività") {
-                    reasonText = "PEF";
-                  }
-
-                  if (reason.toLowerCase() === "permessi") {
-                    reasonText = "P";
-                  }
-
-                  if (reason.toLowerCase() === "donazione sangue") {
-                    reasonText = "DS";
-                  }
-
-                  return (
-                    <div
-                      className={`flex ${index > 0 ? "border-t border-black" : ""}`}
-                    >
-                      <div className="min-w-[40px] w-[40px] py-2 border-r border-black flex items-center justify-center">
-                        {reasonText}
-                      </div>
-                      {/* days */}
-                      {daysInMonth.map((day, index) => {
-                        if (isSaturdayOrSunday(day)) {
-                          const dayOfWeek = getDay(day);
-                          const extraClasses =
-                            dayOfWeek === 0
-                              ? "bg-gray-300"
-                              : dayOfWeek === 6
-                                ? "bg-gray-200"
-                                : "";
-                          return (
-                            <div
-                              key={index}
-                              className={`${extraClasses} min-w-[60p] w-[40px] border-r border-black`}
-                            ></div>
-                          );
-                        }
-
-                        const employeeAbsences = absences.filter(
-                          ({ k_employees, k_absence_days }) => {
-                            return (
-                              k_employees?.id === employee.id &&
-                              k_absence_days.date ===
-                                format(day, "yyyy-MM-dd") &&
-                              k_absence_days.description === reason
-                            );
-                          },
-                        );
-
-                        const hoursAndMinutes = employeeAbsences.reduce(
-                          (total, { k_absence_days: absence }) => {
-                            const { hours, minutes } = parsePostgresInterval(
-                              absence?.duration ?? "",
-                            );
+              {days.map((day, dayIndex) => (
+                <ReportDayCell
+                  day={day}
+                  dayCellClass={dayCellClass}
+                  key={`r-${index}-${reasonIndex}-${dayIndex}-${day.formattedDate}`}
+                />
+              ))}
+            </div>
+          ))}
+
+          <div className={`flex border-t border-black`}>
+            <div className={`${dayCellClass} py-2 border-r border-black`} />
+            {item.totals.map((day, itemTotalIndex) => (
+              <ReportDayCell
+                day={day}
+                dayCellClass={dayCellClass}
+                key={`t-${index}-${itemTotalIndex}-${day.formattedDate}`}
+              />
+            ))}
+          </div>
+        </div>
 
-                            return {
-                              hours: total.hours + hours,
-                              minutes: total.minutes + minutes,
-                            };
-                          },
-                          { hours: 0, minutes: 0 },
-                        );
-                        const { hours, minutes } = parsePostgresInterval(
-                          `${hoursAndMinutes.hours}:${hoursAndMinutes.minutes}:00`,
-                        );
-                        monthlyTotal -= hours;
-                        // minutes to hours
-                        const extraHours = Math.floor(minutes / 60);
-                        monthlyTotal -= extraHours;
-                        return (
-                          <div
-                            key={index}
-                            className={`flex flex-col justify-center items-center min-w-[40px] w-[40px] border-r border-black text-xs`}
-                          >
-                            {(hours > 0 || minutes > 0) && (
-                              <>
-                                {hours > 0 && `${hours}h`}{" "}
-                                {minutes > 0 && `${minutes}m`}
-                              </>
-                            )}
-                          </div>
-                        );
-                      })}
-                    </div>
-                  );
-                })}
+        <div className="flex-1 text-end items-center flex justify-center">
+          {item.monthlyTotalLabel}
+        </div>
+      </div>
+    );
+  });
+};
path: src/modules/dipendenti-in-cloud/components/dipendenti-in-cloud-report.tsx
user: login: coderabbitai[bot]
body: _🛠️ Refactor suggestion_

**Report Body – Magnifico!**  
So, you elegantly handle reasons and totals. If there’s a chance that `reasons` or `rows` are empty, handle it gracefully—maybe show a snazzy “No data!” message.

<!-- This is an auto-generated comment by CodeRabbit -->
created_at: 2025-03-30T09:46:05Z
author_association: NONE
diff_hunk: @@ -63,6 +58,32 @@ const SyncEmployeeData = () => {
   );
 };
 
+const SyncAbsenceReasonsAndClosures = () => {
+  const [isPending, startTransition] = useTransition();
+
+  const onSubmit = (ev: FormEvent) => {
+    ev.preventDefault();
+    startTransition(async () => {
+      const res = await syncDipendentiInCloudAbsenceReasonsAndClosures();
+      if (isFailure(res)) {
+        notifyError("Error while syncing dipendenti in cloud general info");
+        return;
path: src/modules/dipendenti-in-cloud/components/sync-dipendenti-in-cloud.tsx
user: login: coderabbitai[bot]
body: _🛠️ Refactor suggestion_

**Error message needs to be more AWESOME!**

The error message is too generic - it doesn't specify what we're actually syncing! Be more specific like we did for the employee sync. It's like calling a three-piece custom-tailored Armani a "clothing item". Step up your game!

```diff
- notifyError("Error while syncing dipendenti in cloud general info");
+ notifyError("Error while syncing dipendenti in cloud absence reasons and closures");
```



<!-- suggestion_start -->

<details>
<summary>📝 Committable suggestion</summary>

> ‼️ **IMPORTANT**
> Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

`````suggestion
        notifyError("Error while syncing dipendenti in cloud absence reasons and closures");
        return;
`````

</details>

<!-- suggestion_end -->

<!-- This is an auto-generated comment by CodeRabbit -->

✅ Addressed in commit ac3221a
created_at: 2025-03-30T19:37:01Z
author_association: NONE
diff_hunk: @@ -148,6 +163,33 @@ export class DipendentiInCloudApi {
     const res = await fetch(salary.url);
     return await res.arrayBuffer();
   }
+
+  async getAbsenceReasons() {
+    const endpoint = `${this.endpoint}reasons`;
+    const res = await fetch(endpoint, this.authenticationHeaders);
+    const jsonResponse = await res.json();
+
+    return absenceReasonsResponseSchema
+      .parse(jsonResponse)
+      .data.list.filter((it) => it.active === 1);
+  }
+
+  async getClosures() {
+    const endpoint = `${this.endpoint}closures?`;
+    const params = new URLSearchParams({
+      per_page: "50",
+      page: "1",
+    });
+
+    const rawResponse = await fetch(
+      endpoint + params,
+      this.authenticationHeaders,
+    );
+
+    const jsonResponse = await rawResponse.json();
+
+    return closuresResponseSchema.parse(jsonResponse).data;
+  }
path: src/modules/dipendenti-in-cloud/dipendenti-in-cloud-api-client.ts
user: login: coderabbitai[bot]
body: _🛠️ Refactor suggestion_

**Pagination challenge accepted!**

The way you're handling pagination is like stopping at the first bar on a legendary night out! You're only fetching the first 50 closures (page 1) - what if there are more?

Let's implement proper pagination to get ALL the closures:

```diff
async getClosures() {
  const endpoint = `${this.endpoint}closures?`;
- const params = new URLSearchParams({
-   per_page: "50",
-   page: "1",
- });
-
- const rawResponse = await fetch(
-   endpoint + params,
-   this.authenticationHeaders,
- );
-
- const jsonResponse = await rawResponse.json();
-
- return closuresResponseSchema.parse(jsonResponse).data;

+ const allClosures = [];
+ let currentPage = 1;
+ let hasMorePages = true;
+ 
+ while (hasMorePages) {
+   const params = new URLSearchParams({
+     per_page: "50",
+     page: currentPage.toString(),
+   });
+   
+   const rawResponse = await fetch(
+     endpoint + params,
+     this.authenticationHeaders,
+   );
+   
+   const jsonResponse = await rawResponse.json();
+   const parsedResponse = closuresResponseSchema.parse(jsonResponse);
+   
+   allClosures.push(...parsedResponse.data);
+   
+   // Check if we've reached the last page
+   hasMorePages = parsedResponse.meta?.pagination?.total_pages > currentPage;
+   currentPage++;
+ }
+ 
+ return allClosures;
}
```



<!-- suggestion_start -->

<details>
<summary>📝 Committable suggestion</summary>

> ‼️ **IMPORTANT**
> Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

`````suggestion
  async getClosures() {
    const endpoint = `${this.endpoint}closures?`;
    const allClosures = [];
    let currentPage = 1;
    let hasMorePages = true;
    
    while (hasMorePages) {
      const params = new URLSearchParams({
        per_page: "50",
        page: currentPage.toString(),
      });
      
      const rawResponse = await fetch(
        endpoint + params,
        this.authenticationHeaders,
      );
      
      const jsonResponse = await rawResponse.json();
      const parsedResponse = closuresResponseSchema.parse(jsonResponse);
      
      allClosures.push(...parsedResponse.data);
      
      // Check if we've reached the last page
      hasMorePages = parsedResponse.meta?.pagination?.total_pages > currentPage;
      currentPage++;
    }
    
    return allClosures;
  }
`````

</details>

<!-- suggestion_end -->

<!-- This is an auto-generated comment by CodeRabbit -->
created_at: 2025-03-30T19:37:01Z
author_association: NONE
diff_hunk: @@ -148,6 +163,33 @@ export class DipendentiInCloudApi {
     const res = await fetch(salary.url);
     return await res.arrayBuffer();
   }
+
+  async getAbsenceReasons() {
+    const endpoint = `${this.endpoint}reasons`;
+    const res = await fetch(endpoint, this.authenticationHeaders);
+    const jsonResponse = await res.json();
+
+    return absenceReasonsResponseSchema
+      .parse(jsonResponse)
+      .data.list.filter((it) => it.active === 1);
+  }
path: src/modules/dipendenti-in-cloud/dipendenti-in-cloud-api-client.ts
user: login: coderabbitai[bot]
body: _🛠️ Refactor suggestion_

**Same pagination issue for getAbsenceReasons!**

The `getAbsenceReasons` method doesn't handle pagination either! What if there are more than one page of absence reasons? That's like having only the first chapter of The Playbook - you're missing all the best moves!


```diff
async getAbsenceReasons() {
  const endpoint = `${this.endpoint}reasons`;
- const res = await fetch(endpoint, this.authenticationHeaders);
- const jsonResponse = await res.json();
-
- return absenceReasonsResponseSchema
-   .parse(jsonResponse)
-   .data.list.filter((it) => it.active === 1);

+ const allReasons = [];
+ let currentPage = 1;
+ let hasMorePages = true;
+ 
+ while (hasMorePages) {
+   const params = new URLSearchParams({
+     per_page: "50",
+     page: currentPage.toString(),
+   });
+   
+   const res = await fetch(
+     endpoint + (currentPage > 1 ? '?' + params : ''),
+     this.authenticationHeaders,
+   );
+   
+   const jsonResponse = await res.json();
+   const parsedResponse = absenceReasonsResponseSchema.parse(jsonResponse);
+   
+   allReasons.push(...parsedResponse.data.list);
+   
+   // Check if we've reached the last page (adjust based on actual API response structure)
+   hasMorePages = parsedResponse.meta?.pagination?.total_pages > currentPage;
+   currentPage++;
+ }
+ 
+ return allReasons.filter((it) => it.active === 1);
}
```


<!-- suggestion_start -->

<details>
<summary>📝 Committable suggestion</summary>

> ‼️ **IMPORTANT**
> Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

`````suggestion
  async getAbsenceReasons() {
    const endpoint = `${this.endpoint}reasons`;
+   const allReasons = [];
+   let currentPage = 1;
+   let hasMorePages = true;
+ 
+   while (hasMorePages) {
+     const params = new URLSearchParams({
+       per_page: "50",
+       page: currentPage.toString(),
+     });
+     
+     const res = await fetch(
+       endpoint + (currentPage > 1 ? '?' + params : ''),
+       this.authenticationHeaders,
+     );
+     
+     const jsonResponse = await res.json();
+     const parsedResponse = absenceReasonsResponseSchema.parse(jsonResponse);
+     
+     allReasons.push(...parsedResponse.data.list);
+     
+     // Check if we've reached the last page (adjust based on actual API response structure)
+     hasMorePages = parsedResponse.meta?.pagination?.total_pages > currentPage;
+     currentPage++;
+   }
+ 
+   return allReasons.filter((it) => it.active === 1);
  }
`````

</details>

<!-- suggestion_end -->

<!-- This is an auto-generated comment by CodeRabbit -->
created_at: 2025-03-30T19:45:42Z
author_association: NONE
diff_hunk: @@ -1,299 +1,213 @@
 import {
-  eachDayOfInterval,
-  getDay,
-  format,
-  add,
-  subHours,
-  subMinutes,
-  startOfDay,
-} from "date-fns";
-import {
-  Table,
-  TableHeader,
-  TableBody,
-  TableRow,
-  TableHead,
-  TableCell,
-  TableCaption,
-} from "@/components/ui/table";
-import { it } from "date-fns/locale";
-import { KAbsenceDaysList } from "@/modules/k-absence-days/actions/k-absence-days-list";
-import { KEmployeesListAllActionResult } from "@/modules/k-employees/actions/k-employee-list-all-action";
-import parsePostgresInterval from "postgres-interval";
-
-export type DipendentiInCloudReportProps = {
-  from: Date;
-  to: Date;
-  absences: KAbsenceDaysList;
-  uniqueReasons: string[]; // todo rename to uniqueAbsenceReasons
-  employees: KEmployeesListAllActionResult;
-};
+  DipendentiInCloudReportProps,
+  getDataForReport,
+  getItalianDayLetter,
+  getReportTitle,
+  MonthDayItem,
+  ReportRow,
+  UIDay,
+} from "../dipendenti-in-cloud-report-utils";
+import { format } from "date-fns";
 
 /**
- * Returns the letter corresponding to the day of the week in Italian:
- * 0 (Sunday)    => D
- * 1 (Monday)    => L
- * 2 (Tuesday)   => M
- * 3 (Wednesday) => M
- * 4 (Thursday)  => G
- * 5 (Friday)    => V
- * 6 (Saturday)  => S
+ * DipendentiInCloudReport
+ * A component to build a monthly report of worked hours/not worked hours
+ * to our payroll office.
+ * @constructor
  */
-function getItalianDayLetter(date: Date): string {
-  const dayIndex = getDay(date); // 0=Sunday, 1=Monday, ...
-  switch (dayIndex) {
-    case 0:
-      return "D";
-    case 1:
-      return "L";
-    case 2:
-      return "M";
-    case 3:
-      return "M";
-    case 4:
-      return "G";
-    case 5:
-      return "V";
-    case 6:
-      return "S";
-    default:
-      return "";
-  }
-}
-
-const isSaturdayOrSunday = (date: Date): boolean => {
-  const day = getDay(date);
-  return day === 0 || day === 6; // 0 = Sunday, 6 = Saturday
-};
-
-export default function DipendentiInCloudReport({
-  from,
-  to,
-  employees,
-  absences,
-  uniqueReasons,
-}: DipendentiInCloudReportProps) {
-  // Calculate days in the received period
-  const daysInMonth = eachDayOfInterval({
-    start: from,
-    end: to,
-  });
-  const maxWorkableHoursPerDay = 8;
-  const maxWorkableHoursPerPeriod = daysInMonth.reduce((total, day) => {
-    if (isSaturdayOrSunday(day)) {
-      return total;
-    }
-
-    return total + maxWorkableHoursPerDay;
-  }, 0);
-
-  // Format month and year label
-  const monthYearLabel = format(from, "MMMM yyyy", { locale: it });
-  const monthYearLabelCapitalized =
-    monthYearLabel.charAt(0).toUpperCase() + monthYearLabel.slice(1);
+export default function DipendentiInCloudReport(
+  props: DipendentiInCloudReportProps,
+) {
+  const { rows, month } = getDataForReport(props);
 
+  const employeeCellClass = "w-[200px] min-w-[200px]";
+  const dayCellClass = "w-[40px] max-w-[40px]";
   return (
     <div className="m-16 space-y-4 text-sm min-w-max">
-      <div className="text-end">
-        Mese: <strong>{monthYearLabelCapitalized}</strong>
-      </div>
+      <ReportTitle date={props.from} />
 
       <div className="border-black border text-black flex flex-col">
-        <div className="uppercase font-bold py-2 border-black  flex justify-center">
-          Ore lavorate
-        </div>
-
-        <div className="uppercase border-t border-black flex">
-          {/* employee name */}
-          <div className="w-[100px] min-w-[100px] py-2 border-r border-black"></div>
+        <ReportPreHeader />
+
+        <ReportHeader
+          month={month}
+          dayCellClass={dayCellClass}
+          employeeCellClass={employeeCellClass}
+        />
+
+        <ReportBody
+          rows={rows}
+          dayCellClass={dayCellClass}
+          employeeCellClass={employeeCellClass}
+        />
+      </div>
+      <ReportLegend reasons={props.absenceReasons} />
+    </div>
+  );
+}
 
-          {/* absence reason */}
-          <div className="w-[40px] min-w-[40px] py-2 border-r border-black"></div>
+const ReportTitle = ({ date }: { date: Date }) => {
+  return (
+    <div className="text-end">
+      Mese: <strong>{getReportTitle(date)}</strong>
+    </div>
+  );
+};
 
-          {/* days */}
-          {daysInMonth.map((day, index) => {
-            return (
+const ReportPreHeader = () => (
+  <div className="uppercase font-bold py-2 border-black  flex justify-center">
+    Ore lavorate
+  </div>
+);
+
+const ReportHeader = ({
+  month,
+  employeeCellClass,
+  dayCellClass,
+}: {
+  month: MonthDayItem[];
+  dayCellClass: string;
+  employeeCellClass: string;
+}) => (
+  <div className="uppercase border-t border-black flex">
+    {/* employee name */}
+    <div className={`${employeeCellClass} py-2 border-r border-black`}></div>
+
+    {/* absence reason */}
+    <div className={`${dayCellClass} py-2 border-r border-black`}></div>
+
+    {/* days */}
+    {month.map((day, index) => {
+      return (
+        <div
+          key={`report-header-${index}`}
+          className={`flex flex-col items-center ${dayCellClass} border-r border-black text-xs`}
+        >
+          <span>{getItalianDayLetter(day.date)}</span>
+          <span>{format(day.date, "d")}</span>
+        </div>
+      );
+    })}
+    <div className={"flex items-center justify-center flex-1"}>Totali</div>
+  </div>
+);
+
+const ReportBody = ({
+  rows,
+  employeeCellClass,
+  dayCellClass,
+}: {
+  rows: ReportRow[];
+  employeeCellClass: string;
+  dayCellClass: string;
+}) => {
+  return rows.map((item, index) => {
+    return (
+      <div className="uppercase border-t border-black flex" key={index}>
+        <div
+          className={`${employeeCellClass} py-2 border-r border-black flex items-center justify-center text-center`}
+        >
+          <div className="flex flex-col gap-1">
+            {item.employeeName}
+            <pre className="font-[10px]">
+              {item.employeeNationalInsuranceNumber}
+            </pre>
path: src/modules/dipendenti-in-cloud/components/dipendenti-in-cloud-report.tsx
user: login: coderabbitai[bot]
body: _💡 Verification agent_

<details>
<summary>🧩 Analysis chain</summary>

**National Insurance Numbers looking sharp!**

Adding the employee's national insurance number is a great touch! It's like adding that extra accessory that completes your awesome suit! But for sensitive data like this, let's make sure we're properly handling the display.

---

🏁 Script executed:

```shell
#!/bin/bash
# Let's check if there are any privacy regulations we need to follow for displaying national insurance numbers

# Search for any masking or formatting utilities in the codebase
rg "mask|format.*number|security.*data" --type ts

# Check if we're properly handling these numbers elsewhere
rg "NationalInsuranceNumber|nationalInsuranceNumber" --type ts
```

Length of output: 946

---



</details>

**Suit Up! Sensitive Data Alert – Let's Mask Those Numbers!**

The National Insurance Number is looking super crisp, but displaying it raw in the UI might be a privacy risk if we don’t give it the proper treatment. Our search didn’t uncover any consistent masking or formatting functions for this field, which means this detail might need extra security checks.

- **File Impact:**  
  - `src/modules/dipendenti-in-cloud/components/dipendenti-in-cloud-report.tsx` (lines 112-114) currently renders the raw number.
  - Double-check if any existing helper or privacy guideline (maybe like the one handling `securityData` in `src/modules/easyredmine/generated/easyredmine-rest-client.ts`) can be extended for this purpose.

- **Actionable Suggestion:**  
  Consider implementing or reusing a utility to mask or format the National Insurance Number (e.g., showing only the last few digits) to ensure we're aligning with privacy standards.

Let's get this suited up to be both awesome and secure!

<!-- This is an auto-generated comment by CodeRabbit -->
created_at: 2025-03-30T19:45:42Z
author_association: NONE

PR comments:
user: login: vercel[bot]
created_at: 2025-03-30T09:41:56Z
body: [vc]: #e0F0DPHulQTvXV4rfdZG+JoE4l00k5PX1ARNCDMeb6Y=:eyJpc01vbm9yZXBvIjp0cnVlLCJ0eXBlIjoiZ2l0aHViIiwicHJvamVjdHMiOlt7Im5hbWUiOiJrdWFtYS1jb250cm9sLWNlbnRlciIsImluc3BlY3RvclVybCI6Imh0dHBzOi8vdmVyY2VsLmNvbS9rdWFtYS1pdC9rdWFtYS1jb250cm9sLWNlbnRlci9COFVUWVRoWUxvdTk0RUppN2RrTlpiVko2MVQzIiwicHJldmlld1VybCI6Imt1YW1hLWNvbnRyb2wtY2VudGVyLWdpdC1yYXRpb25hbGl6ZS1kaWMtcmVwb3J0LWt1YW1hLWl0LnZlcmNlbC5hcHAiLCJuZXh0Q29tbWl0U3RhdHVzIjoiREVQTE9ZRUQiLCJsaXZlRmVlZGJhY2siOnsicmVzb2x2ZWQiOjAsInVucmVzb2x2ZWQiOjAsInRvdGFsIjowLCJsaW5rIjoia3VhbWEtY29udHJvbC1jZW50ZXItZ2l0LXJhdGlvbmFsaXplLWRpYy1yZXBvcnQta3VhbWEtaXQudmVyY2VsLmFwcCJ9LCJyb290RGlyZWN0b3J5IjpudWxsfV19
**The latest updates on your projects**. Learn more about [Vercel for Git ↗︎](https://vercel.link/github-learn-more)

| Name | Status | Preview | Comments | Updated (UTC) |
| :--- | :----- | :------ | :------- | :------ |
| **kuama-control-center** | ✅ Ready ([Inspect](https://vercel.com/kuama-it/kuama-control-center/B8UTYThYLou94EJi7dkNZbVJ61T3)) | [Visit Preview](https://kuama-control-center-git-rationalize-dic-report-kuama-it.vercel.app) | 💬 [**Add feedback**](https://vercel.live/open-feedback/kuama-control-center-git-rationalize-dic-report-kuama-it.vercel.app?via=pr-comment-feedback-link) | Mar 30, 2025 7:41pm |


author_association: NONE
user: login: coderabbitai[bot]
created_at: 2025-03-30T09:41:58Z
body: <!-- This is an auto-generated comment: summarize by coderabbit.ai -->
<!-- walkthrough_start -->

## Walkthrough
This pull request expands employee absence management by modifying tests, schemas, APIs, and UI components. The changes include uncommenting and adding API client tests; extending the database schema with new columns and tables for employee details, absence reasons, and closures; adding a new dependency; and enhancing data synchronization and report generation. Additionally, new utility functions, new synchronization components, and API client methods for fetching absence data have been introduced, along with corresponding error handling and refactoring.

## Changes

| File(s) | Change Summary |
|---------|----------------|
| `__tests__/modules/dipendenti-in-cloud/dipendenti-in-cloud-api-client.test.ts` | Uncommented existing tests and added tests for retrieving absence reasons and closures. |
| `drizzle/schema.ts` | Added new columns (`nationalInsuranceNumber`, `phoneNumber`, `iban`) to `kEmployees`; added new tables `kAbsenceReasons` and `kClosures`; added new column `reasonCode` to `kAbsenceDays`. |
| `package.json` | Added dependency `"@js-joda/core": "^5.6.4"`. |
| `src/app/reports/dipendenti-in-cloud/page.tsx` | Introduced async calls for absence reasons and closures with error handling; updated prop names in report components. |
| `src/modules/dipendenti-in-cloud/actions/dipendenti-in-cloud-import-absence-reasons-and-closures.ts`<br>`src/modules/dipendenti-in-cloud/actions/dipendenti-in-cloud-import-employees-action.ts`<br>`src/modules/dipendenti-in-cloud/actions/sync-dipendenti-in-cloud-timesheet-action.ts` | Added async import function for absence reasons and closures; enhanced employee sync to include `iban`, `nationalInsuranceNumber`, and `phoneNumber`; incorporated `reasonCode` in absence days sync. |
| `src/modules/dipendenti-in-cloud/components/dipendenti-in-cloud-report.tsx` | Refactored report generation into modular components; shifted from destructured props to a single `props` argument; delegated logic to new utilities. |
| `src/modules/dipendenti-in-cloud/components/sync-dipendenti-in-cloud.tsx` | Added new `SyncAbsenceReasonsAndClosures` component and refined error messaging in the sync flow. |
| `src/modules/dipendenti-in-cloud/dipendenti-in-cloud-api-client.ts` | Enhanced `getEmployees` to return detailed info; added new methods `getAbsenceReasons` and `getClosures` to fetch related data. |
| `src/modules/dipendenti-in-cloud/dipendenti-in-cloud-report-utils.ts` | Introduced utility functions and types for report generation (e.g., `getItalianDayLetter`, `getReportTitle`, `getDataForReport`). |
| `src/modules/dipendenti-in-cloud/schemas/dipendenti-in-cloud-schemas.ts` | Extended employee schema with new fields; added schemas for absence reasons and closures with corresponding response schemas. |
| `src/modules/k-absence-days/actions/k-absence-reasons-list.ts`<br>`src/modules/k-absence-days/actions/k-closures-list.ts` | Added new functions to list absence reasons and closures with error handling and exported respective type aliases. |
| `src/modules/k-absence-days/k-absence-days-server.ts` | Integrated the new absence reasons (`listReasons`) and closures (`closures`) functions into the server object. |

## Sequence Diagram(s)

```mermaid
sequenceDiagram
    participant UI as "SyncAbsenceReasonsAndClosures Component"
    participant Action as "Import Function"
    participant API as "DipendentiInCloudApiClient"
    participant DB as "Database Tables"

    UI->>Action: Trigger absence reasons and closures sync
    Action->>DB: Delete old absence reasons & closures
    Action->>API: Fetch absence reasons and closures
    API-->>Action: Return fetched data
    Action->>DB: Insert updated absence reasons & closures
    Action->>UI: Return sync summary message
```

```mermaid
sequenceDiagram
    participant Page as "Report Page"
    participant Server as "kAbsenceDaysServer"
    participant DB as "Database"

    Page->>Server: Request absence reasons (listReasons)
    Server->>DB: Query absence reasons
    DB-->>Server: Return results
    Server-->>Page: Send absence reasons

    Page->>Server: Request closures (closures)
    Server->>DB: Query closures
    DB-->>Server: Return results
    Server-->>Page: Send closures
```

## Possibly related PRs
- **Kuama-IT/kuama-control-center#10**: Introduces a component for displaying weekly absence information, directly relating to the absence management enhancements.
- **Kuama-IT/kuama-control-center#13**: Enhances the API client with new methods for fetching absence reasons and closures, indicating a direct connection at the code level.
  
## Poem
> Suit up, my friends, for code that's pure legend,  
> Tests are alive and sync is engineered to perfection!  
> New schemas and APIs in a dazzling formation,  
> Reports now modular—wow, what a sensation!  
> Cheers to this code—totally legen—wait for it—dary!  
> 🎉🚀

<!-- walkthrough_end -->
<!-- internal state start -->


<!-- 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= -->

<!-- internal state end -->
<!-- finishing_touch_checkbox_start -->

<details>
<summary>✨ Finishing Touches</summary>

- [ ] <!-- {"checkboxId": "7962f53c-55bc-4827-bfbf-6a18da830691"} --> 📝 Generate Docstrings

</details>

<!-- finishing_touch_checkbox_end -->
<!-- tips_start -->

---



<details>
<summary>🪧 Tips</summary>

### Chat

There are 3 ways to chat with [CodeRabbit](https://coderabbit.ai?utm_source=oss&utm_medium=github&utm_campaign=Kuama-IT/kuama-control-center&utm_content=27):

- Review comments: Directly reply to a review comment made by CodeRabbit. Example:
  - `I pushed a fix in commit <commit_id>, please review it.`
  - `Generate unit testing code for this file.`
  - `Open a follow-up GitHub issue for this discussion.`
- Files and specific lines of code (under the "Files changed" tab): Tag `@coderabbitai` in a new review comment at the desired location with your query. Examples:
  - `@coderabbitai generate unit testing code for this file.`
  -	`@coderabbitai modularize this function.`
- PR comments: Tag `@coderabbitai` in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
  - `@coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.`
  - `@coderabbitai read src/utils.ts and generate unit testing code.`
  - `@coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.`
  - `@coderabbitai help me debug CodeRabbit configuration file.`

Note: Be mindful of the bot's finite context window. It's strongly recommended to break down tasks such as reading entire modules into smaller chunks. For a focused discussion, use review comments to chat about specific files and their changes, instead of using the PR comments.

### CodeRabbit Commands (Invoked using PR comments)

- `@coderabbitai pause` to pause the reviews on a PR.
- `@coderabbitai resume` to resume the paused reviews.
- `@coderabbitai review` to trigger an incremental review. This is useful when automatic reviews are disabled for the repository.
- `@coderabbitai full review` to do a full review from scratch and review all the files again.
- `@coderabbitai summary` to regenerate the summary of the PR.
- `@coderabbitai generate docstrings` to [generate docstrings](https://docs.coderabbit.ai/finishing-touches/docstrings) for this PR.
- `@coderabbitai resolve` resolve all the CodeRabbit review comments.
- `@coderabbitai plan` to trigger planning for file edits and PR creation.
- `@coderabbitai configuration` to show the current CodeRabbit configuration for the repository.
- `@coderabbitai help` to get help.

### Other keywords and placeholders

- Add `@coderabbitai ignore` anywhere in the PR description to prevent this PR from being reviewed.
- Add `@coderabbitai summary` to generate the high-level summary at a specific location in the PR description.
- Add `@coderabbitai` anywhere in the PR title to generate the title automatically.

### CodeRabbit Configuration File (`.coderabbit.yaml`)

- You can programmatically configure CodeRabbit by adding a `.coderabbit.yaml` file to the root of your repository.
- Please see the [configuration documentation](https://docs.coderabbit.ai/guides/configure-coderabbit) for more information.
- If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: `# yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json`

### Documentation and Community

- Visit our [Documentation](https://docs.coderabbit.ai) for detailed information on how to use CodeRabbit.
- Join our [Discord Community](http://discord.gg/coderabbit) to get help, request features, and share feedback.
- Follow us on [X/Twitter](https://twitter.com/coderabbitai) for updates and announcements.

</details>

<!-- tips_end -->
author_association: NONE