Skip to content

feat: Add shift duration tracking to stats and enhance UI#61

Merged
panteLx merged 2 commits intomainfrom
feat/shift-duration
Dec 11, 2025
Merged

feat: Add shift duration tracking to stats and enhance UI#61
panteLx merged 2 commits intomainfrom
feat/shift-duration

Conversation

@panteLx
Copy link
Owner

@panteLx panteLx commented Dec 11, 2025

Adds duration tracking to shift statistics and surfaces total and per-type durations in the UI.

Introduces duration calculation (handles overnight shifts and excludes all-day shifts) and aggregates totalMinutes per shift title and overall. Updates the stats API shape to return count and totalMinutes instead of a raw SQL count, and removes an unused SQL import.

Adds utility functions to format durations and uses them in the UI. Enhances the stats component to show total shifts, formatted total hours, per-shift progress bars, count badges, and duration badges for clearer visibility of time spent.

Improves readability and usability of shift metrics for easier analysis of distribution and time allocation.

Summary by CodeRabbit

  • New Features
    • Shift statistics now include total duration (hours) alongside counts; UI updated with total stats card and per-type duration badges and progress bars.
  • Breaking Changes
    • Stats API response shape changed: each type includes count and totalMinutes, and a top-level totalMinutes is now returned; error handling added for failures.
  • Localization
    • Added common "shifts" and "hours" keys; removed some legacy shift/title keys and a version label.

✏️ Tip: You can customize this high-level summary in your review settings.

Adds duration tracking to shift statistics and surfaces total and per-type durations in the UI.

Introduces duration calculation (handles overnight shifts and excludes all-day shifts) and aggregates totalMinutes per shift title and overall. Updates the stats API shape to return count and totalMinutes instead of a raw SQL count, and removes an unused SQL import.

Adds utility functions to format durations and uses them in the UI. Enhances the stats component to show total shifts, formatted total hours, per-shift progress bars, count badges, and duration badges for clearer visibility of time spent.

Improves readability and usability of shift metrics for easier analysis of distribution and time allocation.
Copilot AI review requested due to automatic review settings December 11, 2025 17:46
@coderabbitai
Copy link

coderabbitai bot commented Dec 11, 2025

Note

Other AI code review bot(s) detected

CodeRabbit has detected other AI code review bot(s) in this pull request and will avoid duplicating their findings in the review comments. This may lead to a less comprehensive review.

Walkthrough

API and UI now compute and expose per-shift durations. The stats route returns counts and totalMinutes per title plus an overall totalMinutes. A date-utils module adds duration calculation/formatting. The UI and messages are updated to display durations and use consolidated translation keys.

Changes

Cohort / File(s) Summary
API + Duration utils
app/api/shifts/stats/route.ts, lib/date-utils.ts
Stats route now selects start/end times and isAllDay, computes per-shift duration (skips all-day), aggregates in-memory to produce stats: Record<string, { count: number; totalMinutes: number }> and top-level totalMinutes. Added calculateShiftDuration() and formatDuration() utilities. Error handling added.
Shift statistics UI
components/shift-stats.tsx
ShiftStats interface updated to include totalMinutes and per-title objects. UI shows both counts and formatted durations, adds total statistics card, per-type progress rows, and conditional duration badges.
Localization changes
messages/en.json, messages/de.json, messages/it.json
Introduced common.shifts and stats.hours; removed shift.title, shift.shifts, and version keys. Minor string/format edits.
Minor UI text changes
components/day-shifts-dialog.tsx, components/shifts-list.tsx, .github/copilot-instructions.md
Replaced several shift.* translation keys with common.shifts (pluralization adjustments) and adjusted a toast label key in copilot instructions.

Sequence Diagram(s)

sequenceDiagram
    participant Client as Browser / UI
    participant Server as app/api/shifts/stats
    participant DB as Database

    Client->>Server: GET /api/shifts/stats
    Server->>DB: SELECT id, title, startTime, endTime, isAllDay FROM shifts...
    DB-->>Server: rows[]
    Note over Server: iterate rows, for each row\nif not isAllDay -> calculateShiftDuration(startTime,endTime)\naccumulate count & totalMinutes per title
    Server-->>Client: 200 { stats: { title: {count,totalMinutes} }, totalMinutes }
    Client->>Client: render ShiftStats (counts, formatted durations)
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

-重点审查点:

  • app/api/shifts/stats/route.ts — correctness of in-memory grouping and error handling
  • lib/date-utils.ts — edge cases for overnight shifts and input validation
  • components/shift-stats.tsx — UI bindings for new stats shape and formatting use

Possibly related PRs

Poem

🐰 I hopped through start and end, counting time,
Minutes stacked like carrots in a line.
Per-title tallies, totals bright and true,
Progress bars gleam — a carrot-colored view,
Hooray — stats now show both count and time! 🌿

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 57.14% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and accurately summarizes the main changes: adding duration tracking to shift statistics (backend) and enhancing the UI to display this new data. It aligns well with the PR objectives.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feat/shift-duration

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@panteLx panteLx linked an issue Dec 11, 2025 that may be closed by this pull request
3 tasks
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🧹 Nitpick comments (4)
lib/date-utils.ts (2)

18-34: Missing input validation for malformed time strings.

The function assumes valid HH:MM format but doesn't guard against malformed input. If startTime or endTime is empty, malformed, or missing the colon, split(":").map(Number) will produce NaN values, leading to incorrect calculations.

Consider adding validation or defensive parsing:

 export function calculateShiftDuration(
   startTime: string,
   endTime: string
 ): number {
+  if (!startTime || !endTime || !startTime.includes(":") || !endTime.includes(":")) {
+    return 0;
+  }
+
   const [startHour, startMinute] = startTime.split(":").map(Number);
   const [endHour, endMinute] = endTime.split(":").map(Number);
+
+  if (isNaN(startHour) || isNaN(startMinute) || isNaN(endHour) || isNaN(endMinute)) {
+    return 0;
+  }

   const startMinutes = startHour * 60 + startMinute;

41-45: Handle negative minutes input.

If a negative minutes value is passed (e.g., due to a bug upstream), Math.floor on a negative number and the modulo operator will produce unexpected results (e.g., -90"-2:30" instead of a meaningful fallback).

 export function formatDuration(minutes: number): string {
+  if (minutes < 0) {
+    return "0:00";
+  }
   const hours = Math.floor(minutes / 60);
   const mins = minutes % 60;
   return `${hours}:${String(mins).padStart(2, "0")}`;
 }
app/api/shifts/stats/route.ts (1)

151-160: Consider using Object.fromEntries for cleaner transformation.

The reduce pattern works but Object.fromEntries is more idiomatic for this use case.

-    const stats = Array.from(statsMap.entries()).reduce(
-      (acc, [title, data]) => {
-        acc[title] = {
-          count: data.count,
-          totalMinutes: data.totalMinutes,
-        };
-        return acc;
-      },
-      {} as Record<string, { count: number; totalMinutes: number }>
-    );
+    const stats: Record<string, { count: number; totalMinutes: number }> = 
+      Object.fromEntries(statsMap);
components/shift-stats.tsx (1)

263-270: Progress bar width calculation may cause division by zero.

If totalShifts is 0, the width calculation (data.count / totalShifts) * 100 will result in NaN or Infinity. Although the parent condition Object.keys(stats.stats).length > 0 suggests there are stats, it's possible totalShifts could still be 0 if all entries have count: 0.

                           style={{
-                            width: `${(data.count / totalShifts) * 100}%`,
+                            width: `${totalShifts > 0 ? (data.count / totalShifts) * 100 : 0}%`,
                           }}
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between a517332 and ee1fb9f.

📒 Files selected for processing (3)
  • app/api/shifts/stats/route.ts (3 hunks)
  • components/shift-stats.tsx (4 hunks)
  • lib/date-utils.ts (1 hunks)
🧰 Additional context used
📓 Path-based instructions (5)
**/*.{ts,tsx,js,json}

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

All code, comments, variable names, and messages must be in English

Files:

  • lib/date-utils.ts
  • components/shift-stats.tsx
  • app/api/shifts/stats/route.ts
**/*.{ts,tsx,js}

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

Only add comments for complex logic or non-obvious behavior; prefer self-documenting code with clear variable and function names

Files:

  • lib/date-utils.ts
  • components/shift-stats.tsx
  • app/api/shifts/stats/route.ts
components/**/*.tsx

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

components/**/*.tsx: All dialog components must follow the unified design pattern with gradient backgrounds, consistent padding (p-6, px-6, pb-6), border styling (border-border/50), backdrop blur effects, and gradient text for titles
Dialog components must control state via props (open, onOpenChange), reset local state when open changes to false, and include onSubmit and optional onDelete callbacks

Files:

  • components/shift-stats.tsx
components/**/*shift*.tsx

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

Shift dialog must have saveAsPreset enabled by default

Files:

  • components/shift-stats.tsx
app/api/**/*.ts

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

app/api/**/*.ts: Store passwords as SHA-256 hashes in the passwordHash field and verify using utilities from lib/password-utils.ts
For GET endpoints on password-protected calendars, check both calendar.passwordHash && calendar.isLocked before requiring password; for mutation endpoints (POST/PUT/PATCH/DELETE), check only calendar.passwordHash
For new API routes, follow request pattern: extract search params/body, validate required fields, return 400 for missing data, return 401 for auth failures

Files:

  • app/api/shifts/stats/route.ts
🧠 Learnings (13)
📚 Learning: 2025-12-07T04:12:10.807Z
Learnt from: CR
Repo: panteLx/BetterShift PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2025-12-07T04:12:10.807Z
Learning: Applies to app/**/*.tsx : Shift toggle logic: delete if shift exists, create if not (no modifications to existing shifts on simple toggle)

Applied to files:

  • components/shift-stats.tsx
📚 Learning: 2025-12-07T04:12:10.807Z
Learnt from: CR
Repo: panteLx/BetterShift PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2025-12-07T04:12:10.807Z
Learning: Applies to app/page.tsx : Use `statsRefreshTrigger` counter state in main page to track and trigger mutation-related data refreshes

Applied to files:

  • components/shift-stats.tsx
📚 Learning: 2025-12-07T04:12:10.807Z
Learnt from: CR
Repo: panteLx/BetterShift PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2025-12-07T04:12:10.807Z
Learning: Applies to app/page.tsx : State management on main page (`app/page.tsx`) must use `useState` for shifts, presets, notes, calendars and `useEffect` for data fetching on calendar/date changes

Applied to files:

  • components/shift-stats.tsx
  • app/api/shifts/stats/route.ts
📚 Learning: 2025-12-07T04:12:10.807Z
Learnt from: CR
Repo: panteLx/BetterShift PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2025-12-07T04:12:10.807Z
Learning: Applies to components/**/*calendar*.tsx : Calendar interactions must support left-click to toggle shift with selected preset, right-click to open note dialog (prevent default context menu), and display `<StickyNote>` icon for days with notes

Applied to files:

  • components/shift-stats.tsx
📚 Learning: 2025-12-07T04:12:10.807Z
Learnt from: CR
Repo: panteLx/BetterShift PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2025-12-07T04:12:10.807Z
Learning: Applies to hooks/*.ts : All data-fetching hooks (`useShifts`, `usePresets`, `useNotes`) must automatically call `getCachedPassword(calendarId)` before each fetch and return empty arrays on 401 responses for graceful degradation

Applied to files:

  • components/shift-stats.tsx
  • app/api/shifts/stats/route.ts
📚 Learning: 2025-12-07T04:12:10.807Z
Learnt from: CR
Repo: panteLx/BetterShift PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2025-12-07T04:12:10.807Z
Learning: Applies to app/**/*.tsx : Client-side components must use `getCachedPassword(calendarId)` to retrieve and `verifyAndCachePassword(calendarId, password)` to verify and cache passwords from `lib/password-cache.ts`

Applied to files:

  • components/shift-stats.tsx
  • app/api/shifts/stats/route.ts
📚 Learning: 2025-12-07T04:12:10.807Z
Learnt from: CR
Repo: panteLx/BetterShift PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2025-12-07T04:12:10.807Z
Learning: Applies to lib/db/schema.ts : Define cascade delete relationships in the schema (e.g., `calendars` → `shifts`, `shiftPresets`, `calendarNotes` cascade delete; `shiftPresets` → `shifts` set null on delete)

Applied to files:

  • app/api/shifts/stats/route.ts
📚 Learning: 2025-12-07T04:12:10.807Z
Learnt from: CR
Repo: panteLx/BetterShift PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2025-12-07T04:12:10.807Z
Learning: Applies to lib/db/schema.ts : Generate database migrations with `npm run db:generate` and apply with `npm run db:migrate` after schema changes in `lib/db/schema.ts`; never use `db:push`

Applied to files:

  • app/api/shifts/stats/route.ts
📚 Learning: 2025-12-07T04:12:10.808Z
Learnt from: CR
Repo: panteLx/BetterShift PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2025-12-07T04:12:10.808Z
Learning: Use Drizzle Studio (`npm run db:studio`) for local database inspection and `npm run build` to validate TypeScript before deployment

Applied to files:

  • app/api/shifts/stats/route.ts
📚 Learning: 2025-12-07T04:12:10.807Z
Learnt from: CR
Repo: panteLx/BetterShift PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2025-12-07T04:12:10.807Z
Learning: Applies to app/api/**/*.ts : For GET endpoints on password-protected calendars, check both `calendar.passwordHash && calendar.isLocked` before requiring password; for mutation endpoints (POST/PUT/PATCH/DELETE), check only `calendar.passwordHash`

Applied to files:

  • app/api/shifts/stats/route.ts
📚 Learning: 2025-12-07T04:12:10.807Z
Learnt from: CR
Repo: panteLx/BetterShift PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2025-12-07T04:12:10.807Z
Learning: Applies to lib/db/schema.ts : Store timestamps in SQLite as integers using `{ mode: 'timestamp' }` in schema; they auto-convert to Date objects

Applied to files:

  • app/api/shifts/stats/route.ts
📚 Learning: 2025-12-07T04:12:10.808Z
Learnt from: CR
Repo: panteLx/BetterShift PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2025-12-07T04:12:10.808Z
Learning: When creating new database tables, add table definition to `lib/db/schema.ts`, export type with `$inferSelect`, run migrations, create API routes, and add translations

Applied to files:

  • app/api/shifts/stats/route.ts
📚 Learning: 2025-12-07T04:12:10.807Z
Learnt from: CR
Repo: panteLx/BetterShift PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2025-12-07T04:12:10.807Z
Learning: Applies to app/**/*.tsx : For mutations on password-protected calendars, check cached password before the mutation: `const result = await verifyAndCachePassword(calendarId, password)`. If `result.protected && !result.valid`, set `pendingAction` and show `PasswordDialog`

Applied to files:

  • app/api/shifts/stats/route.ts
🧬 Code graph analysis (2)
components/shift-stats.tsx (1)
lib/date-utils.ts (1)
  • formatDuration (41-45)
app/api/shifts/stats/route.ts (1)
lib/db/schema.ts (1)
  • shifts (49-82)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (3)
  • GitHub Check: Agent
  • GitHub Check: build-dev
  • GitHub Check: Tests & Checks
🔇 Additional comments (3)
app/api/shifts/stats/route.ts (1)

142-146: Potential issue with null isAllDay from database.

The schema shows isAllDay has a default of false, but for older records or edge cases, accessing shift.isAllDay might return null instead of false. The fallback || false handles this, but TypeScript may not catch potential issues if the type suggests it's always boolean.

The || false fallback is appropriate defensive coding here.

components/shift-stats.tsx (2)

8-8: LGTM!

The import of formatDuration and the updated ShiftStats interface correctly align with the API changes in route.ts. The interface now properly reflects the enriched stats structure with count and totalMinutes per shift type.

Also applies to: 14-15


250-252: LGTM on sorting logic.

Sorting by b.count - a.count correctly orders shift types by count in descending order, showing the most frequent shifts first.

Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR adds shift duration tracking capabilities to the statistics feature, calculating and displaying total hours worked in addition to shift counts. The implementation adds duration calculation utilities, updates the stats API to aggregate duration data per shift type, and enhances the UI with formatted duration displays using progress bars and badges.

Key Changes:

  • Introduces calculateShiftDuration and formatDuration utility functions for time calculations
  • Refactors stats API to fetch individual shift times and compute durations client-side instead of using SQL aggregation
  • Enhances stats UI with separate badges for shift counts and total hours, plus improved visual styling

Reviewed changes

Copilot reviewed 3 out of 3 changed files in this pull request and generated 5 comments.

File Description
lib/date-utils.ts Adds utility functions for calculating shift duration in minutes and formatting duration to HH:MM format
app/api/shifts/stats/route.ts Removes SQL aggregation, fetches individual shift times, calculates durations excluding all-day shifts, and returns count + totalMinutes per shift type
components/shift-stats.tsx Updates interface to include totalMinutes, displays duration badges alongside count badges with enhanced gradient styling

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

🧹 Nitpick comments (1)
app/api/shifts/stats/route.ts (1)

128-137: Object transformation and totalMinutes computation align with new API contract

Reducing the statsMap into a plain object and separately summing totalMinutes produces a clear, serializable { [title]: { count, totalMinutes } } plus an overall totalMinutes field that the UI can consume directly.

If this endpoint ever becomes a hot path, you could micro-optimize by tracking a running total while building statsMap instead of re-reducing the object, but as written it’s perfectly readable and efficient for typical calendar sizes.

Also applies to: 140-143, 145-151

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between ee1fb9f and f8bc681.

📒 Files selected for processing (9)
  • .github/copilot-instructions.md (1 hunks)
  • app/api/shifts/stats/route.ts (3 hunks)
  • components/day-shifts-dialog.tsx (1 hunks)
  • components/shift-stats.tsx (4 hunks)
  • components/shifts-list.tsx (2 hunks)
  • lib/date-utils.ts (1 hunks)
  • messages/de.json (2 hunks)
  • messages/en.json (2 hunks)
  • messages/it.json (2 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • lib/date-utils.ts
🧰 Additional context used
📓 Path-based instructions (7)
components/**/*.tsx

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

components/**/*.tsx: All dialog components must follow the unified design pattern with gradient backgrounds, consistent padding (p-6, px-6, pb-6), border styling (border-border/50), backdrop blur effects, and gradient text for titles
Dialog components must control state via props (open, onOpenChange), reset local state when open changes to false, and include onSubmit and optional onDelete callbacks

Files:

  • components/day-shifts-dialog.tsx
  • components/shifts-list.tsx
  • components/shift-stats.tsx
**/*.{ts,tsx,js,json}

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

All code, comments, variable names, and messages must be in English

Files:

  • components/day-shifts-dialog.tsx
  • components/shifts-list.tsx
  • components/shift-stats.tsx
  • messages/de.json
  • app/api/shifts/stats/route.ts
  • messages/en.json
  • messages/it.json
**/*.{ts,tsx,js}

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

Only add comments for complex logic or non-obvious behavior; prefer self-documenting code with clear variable and function names

Files:

  • components/day-shifts-dialog.tsx
  • components/shifts-list.tsx
  • components/shift-stats.tsx
  • app/api/shifts/stats/route.ts
components/**/*shift*.tsx

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

Shift dialog must have saveAsPreset enabled by default

Files:

  • components/day-shifts-dialog.tsx
  • components/shifts-list.tsx
  • components/shift-stats.tsx
messages/de.json

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

Use German translations with informal 'du' form (never formal 'Sie' form) - e.g., 'Möchtest du...' not 'Möchten Sie...'

Files:

  • messages/de.json
messages/{de,en}.json

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

messages/{de,en}.json: Never duplicate CRUD success/error messages in translation files; always use common.created, common.updated, common.deleted, common.createError, common.updateError, common.deleteError with {item} parameter
Centralize validation messages in validation.* namespace and form field labels in form.* namespace in translation files to prevent duplication

Files:

  • messages/de.json
  • messages/en.json
app/api/**/*.ts

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

app/api/**/*.ts: Store passwords as SHA-256 hashes in the passwordHash field and verify using utilities from lib/password-utils.ts
For GET endpoints on password-protected calendars, check both calendar.passwordHash && calendar.isLocked before requiring password; for mutation endpoints (POST/PUT/PATCH/DELETE), check only calendar.passwordHash
For new API routes, follow request pattern: extract search params/body, validate required fields, return 400 for missing data, return 401 for auth failures

Files:

  • app/api/shifts/stats/route.ts
🧠 Learnings (22)
📚 Learning: 2025-12-07T04:12:10.807Z
Learnt from: CR
Repo: panteLx/BetterShift PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2025-12-07T04:12:10.807Z
Learning: Applies to app/**/*.tsx : Shift toggle logic: delete if shift exists, create if not (no modifications to existing shifts on simple toggle)

Applied to files:

  • components/day-shifts-dialog.tsx
  • components/shifts-list.tsx
  • components/shift-stats.tsx
  • app/api/shifts/stats/route.ts
  • messages/en.json
📚 Learning: 2025-12-07T04:12:10.807Z
Learnt from: CR
Repo: panteLx/BetterShift PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2025-12-07T04:12:10.807Z
Learning: Applies to components/**/*shift*.tsx : Shift dialog must have `saveAsPreset` enabled by default

Applied to files:

  • components/day-shifts-dialog.tsx
  • components/shifts-list.tsx
📚 Learning: 2025-12-07T04:12:10.807Z
Learnt from: CR
Repo: panteLx/BetterShift PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2025-12-07T04:12:10.807Z
Learning: Applies to app/**/*.tsx : All user-facing text and messages in components must use `useTranslations()` from next-intl and reference translation keys

Applied to files:

  • components/day-shifts-dialog.tsx
  • components/shifts-list.tsx
  • components/shift-stats.tsx
📚 Learning: 2025-12-07T04:12:10.808Z
Learnt from: CR
Repo: panteLx/BetterShift PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2025-12-07T04:12:10.808Z
Learning: When creating new dialog components, place in `components/` directory using shadcn/ui Dialog, use `useTranslations()` for all text, and integrate in `app/page.tsx`

Applied to files:

  • components/day-shifts-dialog.tsx
  • components/shifts-list.tsx
📚 Learning: 2025-12-07T04:12:10.807Z
Learnt from: CR
Repo: panteLx/BetterShift PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2025-12-07T04:12:10.807Z
Learning: Applies to app/**/*.tsx : Use `showMobileCalendarDialog` to separate calendar selector on mobile UI

Applied to files:

  • components/day-shifts-dialog.tsx
  • components/shifts-list.tsx
📚 Learning: 2025-12-07T04:12:10.807Z
Learnt from: CR
Repo: panteLx/BetterShift PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2025-12-07T04:12:10.807Z
Learning: Applies to messages/{de,en}.json : Never duplicate CRUD success/error messages in translation files; always use `common.created`, `common.updated`, `common.deleted`, `common.createError`, `common.updateError`, `common.deleteError` with `{item}` parameter

Applied to files:

  • .github/copilot-instructions.md
  • components/shift-stats.tsx
  • messages/de.json
  • messages/en.json
  • messages/it.json
📚 Learning: 2025-12-07T04:12:10.807Z
Learnt from: CR
Repo: panteLx/BetterShift PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2025-12-07T04:12:10.807Z
Learning: Applies to app/**/*.tsx : Always use `formatDateToLocal()` for displaying dates in YYYY-MM-DD format in UI components

Applied to files:

  • components/shifts-list.tsx
📚 Learning: 2025-12-07T04:12:10.807Z
Learnt from: CR
Repo: panteLx/BetterShift PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2025-12-07T04:12:10.807Z
Learning: Applies to components/**/*calendar*.tsx : Calendar interactions must support left-click to toggle shift with selected preset, right-click to open note dialog (prevent default context menu), and display `<StickyNote>` icon for days with notes

Applied to files:

  • components/shifts-list.tsx
  • components/shift-stats.tsx
  • messages/de.json
  • messages/en.json
  • messages/it.json
📚 Learning: 2025-12-07T04:12:10.807Z
Learnt from: CR
Repo: panteLx/BetterShift PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2025-12-07T04:12:10.807Z
Learning: Applies to app/**/*.tsx : Use locale-specific date formatting: `locale === 'de' ? de : enUS` from date-fns for calendar displays

Applied to files:

  • components/shifts-list.tsx
  • components/shift-stats.tsx
📚 Learning: 2025-12-07T04:12:10.807Z
Learnt from: CR
Repo: panteLx/BetterShift PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2025-12-07T04:12:10.807Z
Learning: Applies to app/page.tsx : Use `statsRefreshTrigger` counter state in main page to track and trigger mutation-related data refreshes

Applied to files:

  • components/shift-stats.tsx
📚 Learning: 2025-12-07T04:12:10.807Z
Learnt from: CR
Repo: panteLx/BetterShift PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2025-12-07T04:12:10.807Z
Learning: Applies to **/*.{ts,tsx,js,json} : All code, comments, variable names, and messages must be in English

Applied to files:

  • components/shift-stats.tsx
📚 Learning: 2025-12-07T04:12:10.807Z
Learnt from: CR
Repo: panteLx/BetterShift PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2025-12-07T04:12:10.807Z
Learning: Applies to messages/de.json : Use German translations with informal 'du' form (never formal 'Sie' form) - e.g., 'Möchtest du...' not 'Möchten Sie...'

Applied to files:

  • components/shift-stats.tsx
📚 Learning: 2025-12-07T04:12:10.807Z
Learnt from: CR
Repo: panteLx/BetterShift PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2025-12-07T04:12:10.807Z
Learning: Applies to messages/{de,en}.json : Centralize validation messages in `validation.*` namespace and form field labels in `form.*` namespace in translation files to prevent duplication

Applied to files:

  • components/shift-stats.tsx
  • messages/de.json
  • messages/it.json
📚 Learning: 2025-12-07T04:12:10.807Z
Learnt from: CR
Repo: panteLx/BetterShift PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2025-12-07T04:12:10.807Z
Learning: Applies to app/page.tsx : State management on main page (`app/page.tsx`) must use `useState` for shifts, presets, notes, calendars and `useEffect` for data fetching on calendar/date changes

Applied to files:

  • components/shift-stats.tsx
  • app/api/shifts/stats/route.ts
📚 Learning: 2025-12-07T04:12:10.807Z
Learnt from: CR
Repo: panteLx/BetterShift PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2025-12-07T04:12:10.807Z
Learning: Applies to hooks/*.ts : All data-fetching hooks (`useShifts`, `usePresets`, `useNotes`) must automatically call `getCachedPassword(calendarId)` before each fetch and return empty arrays on 401 responses for graceful degradation

Applied to files:

  • components/shift-stats.tsx
  • app/api/shifts/stats/route.ts
📚 Learning: 2025-12-07T04:12:10.807Z
Learnt from: CR
Repo: panteLx/BetterShift PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2025-12-07T04:12:10.807Z
Learning: Applies to app/**/*.tsx : Client-side components must use `getCachedPassword(calendarId)` to retrieve and `verifyAndCachePassword(calendarId, password)` to verify and cache passwords from `lib/password-cache.ts`

Applied to files:

  • components/shift-stats.tsx
  • app/api/shifts/stats/route.ts
📚 Learning: 2025-12-07T04:12:10.807Z
Learnt from: CR
Repo: panteLx/BetterShift PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2025-12-07T04:12:10.807Z
Learning: Applies to lib/db/schema.ts : Define cascade delete relationships in the schema (e.g., `calendars` → `shifts`, `shiftPresets`, `calendarNotes` cascade delete; `shiftPresets` → `shifts` set null on delete)

Applied to files:

  • app/api/shifts/stats/route.ts
📚 Learning: 2025-12-07T04:12:10.807Z
Learnt from: CR
Repo: panteLx/BetterShift PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2025-12-07T04:12:10.807Z
Learning: Applies to lib/db/schema.ts : Generate database migrations with `npm run db:generate` and apply with `npm run db:migrate` after schema changes in `lib/db/schema.ts`; never use `db:push`

Applied to files:

  • app/api/shifts/stats/route.ts
📚 Learning: 2025-12-07T04:12:10.808Z
Learnt from: CR
Repo: panteLx/BetterShift PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2025-12-07T04:12:10.808Z
Learning: Use Drizzle Studio (`npm run db:studio`) for local database inspection and `npm run build` to validate TypeScript before deployment

Applied to files:

  • app/api/shifts/stats/route.ts
📚 Learning: 2025-12-07T04:12:10.807Z
Learnt from: CR
Repo: panteLx/BetterShift PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2025-12-07T04:12:10.807Z
Learning: Applies to lib/db/schema.ts : Store timestamps in SQLite as integers using `{ mode: 'timestamp' }` in schema; they auto-convert to Date objects

Applied to files:

  • app/api/shifts/stats/route.ts
📚 Learning: 2025-12-07T04:12:10.807Z
Learnt from: CR
Repo: panteLx/BetterShift PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2025-12-07T04:12:10.807Z
Learning: Applies to app/api/**/*.ts : For GET endpoints on password-protected calendars, check both `calendar.passwordHash && calendar.isLocked` before requiring password; for mutation endpoints (POST/PUT/PATCH/DELETE), check only `calendar.passwordHash`

Applied to files:

  • app/api/shifts/stats/route.ts
📚 Learning: 2025-12-07T04:12:10.807Z
Learnt from: CR
Repo: panteLx/BetterShift PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2025-12-07T04:12:10.807Z
Learning: Applies to app/**/*.tsx : For mutations on password-protected calendars, check cached password before the mutation: `const result = await verifyAndCachePassword(calendarId, password)`. If `result.protected && !result.valid`, set `pendingAction` and show `PasswordDialog`

Applied to files:

  • app/api/shifts/stats/route.ts
🧬 Code graph analysis (2)
components/shift-stats.tsx (1)
lib/date-utils.ts (1)
  • formatDuration (63-73)
app/api/shifts/stats/route.ts (2)
lib/db/schema.ts (1)
  • shifts (49-82)
lib/date-utils.ts (1)
  • calculateShiftDuration (18-56)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: build-dev
🔇 Additional comments (10)
components/shifts-list.tsx (1)

61-61: Consistent use of shared common.shifts translation key

Both header and per-day plural labels now use t("common.shifts"), matching the updated locale files and keeping i18n usage consistent across components.

Also applies to: 150-151

components/day-shifts-dialog.tsx (1)

51-53: Pluralization now aligned with shared common.shifts key

Using t("shift.shift_one") for singular and t("common.shifts") for plural keeps wording correct and consistent with the updated translation structure.

.github/copilot-instructions.md (1)

165-174: Example toast now matches centralized common.* + common.shifts pattern

toast.success(t("common.created", { item: t("common.shifts") })) correctly demonstrates the shared CRUD message pattern with a reusable item label.

messages/it.json (1)

26-28: Italian keys for common.shifts and stats.hours match UI usage

The added "common.shifts": "Turni" and "stats.hours": "Ore" align with the new stats UI and keep terminology consistent with the rest of the Italian locale.

Also applies to: 180-182

messages/de.json (1)

26-28: German translations for common.shifts and stats.hours are consistent

Adding "common.shifts": "Schichten" and "stats.hours": "Stunden" fits existing wording and supports the updated stats UI without introducing any formal “Sie” phrasing.

Also applies to: 181-182

messages/en.json (1)

26-28: English common.shifts and stats.hours keys correctly support stats UI

The new "common.shifts": "Shifts" and "stats.hours": "Hours" entries provide the labels needed by the updated stats components and stay consistent with the rest of the locale structure.

Also applies to: 181-182

app/api/shifts/stats/route.ts (1)

9-12: Duration-aware stats aggregation is correct and reuses shared utility

The handler now:

  • Selects title, startTime, endTime, and isAllDay for each shift.
  • Aggregates per title into a Map<string, { count; totalMinutes }> using calculateShiftDuration, treating all-day shifts as zero minutes.
  • Honors both external sync and preset hideFromStats flags in the query filter.

This removes the earlier duplicated duration logic and centralizes time calculations via calculateShiftDuration, which is a solid improvement for maintainability and correctness.

Also applies to: 85-125

components/shift-stats.tsx (3)

8-16: Client stats model correctly matches the new API and aggregates totals safely

Updating ShiftStats to stats: Record<string, { count; totalMinutes }> plus a top-level totalMinutes and deriving:

  • totalShifts via sum(data.count) and
  • totalMinutes via stats?.totalMinutes || 0

keeps the client in sync with the API and handles the “no data” case without runtime issues.

Also applies to: 82-88


105-120: Collapsed header summary cleanly surfaces total shifts and duration

When collapsed, the component now shows:

  • A badge for totalShifts, and
  • A conditional badge for formatted total duration via formatDuration(totalMinutes),

giving a quick overview without extra text and without impacting loading behavior.


210-247: Expanded stats UI provides clear total and per-type breakdown with proper i18n

The new expanded view:

  • Adds a “Total” card that shows:
    • Overall shift count labeled with t("common.shifts")
    • Overall duration labeled with t("stats.hours") and rendered via formatDuration(totalMinutes)
  • Renders per-title rows sorted by count with:
    • A progress bar proportional to data.count / totalShifts
    • Count and (when > 0) duration badges, both using formatDuration(data.totalMinutes).

This fixes the earlier hardcoded German labels, fully adheres to the translation guidelines, and gives a very readable visualization of shift distribution and time spent.

Also applies to: 249-289

@panteLx panteLx merged commit 91a883e into main Dec 11, 2025
3 checks passed
@panteLx panteLx deleted the feat/shift-duration branch December 11, 2025 18:14
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

feature request: add duration to shift statistics

1 participant