Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
52 changes: 49 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
![Redux](https://img.shields.io/badge/Redux_Toolkit-2.11.0-764ABC?logo=redux&logoColor=white)
![MUI](https://img.shields.io/badge/Material_UI-7.0.2-007FFF?logo=mui&logoColor=white)
![Vitest](https://img.shields.io/badge/Vitest-4-6E9F18?logo=vitest&logoColor=white)
![Supabase](https://img.shields.io/badge/Supabase-2.112.4-3ECF8E?logo=supabase&logoColor=white)

[![CI](https://github.com/dmaman86/shiftly/actions/workflows/ci.yml/badge.svg)](https://github.com/dmaman86/shiftly/actions/workflows/ci.yml)
![TypeScript](https://img.shields.io/badge/TypeScript-5.7.2-blue?logo=typescript)
Expand Down Expand Up @@ -67,6 +68,28 @@ This makes it possible to **recalculate past months accurately** using the same
- Monthly aggregated breakdown
- Incremental recalculation (add / update / remove shifts)
- Fully reactive UI
- Optional Google sign-in with cross-device data persistence

---

## Authentication & Data Persistence

Shiftly works fully **without an account** — everything runs in memory, and nothing is stored anywhere.

Signing in with a **Google account** (via Supabase Auth) additionally persists your data to Supabase, tied to your account, so it carries over across sessions and devices:

- **Monthly configuration** — year, month, standard hours, hourly rate
- **Day status** — sick / vacation marks per day
- **Shifts** — start, end, and duty flag for each saved shift
- **Shabbat credit carry-over** — unused Shabbat credit hours roll forward to the next month instead of being lost

| Table | Stores |
| --- | --- |
| `monthly_configs` | Per-(user, year, month) settings, plus the running Shabbat-credit carry-over balance |
| `work_days` | Per-(user, date) status (`sick` / `vacation`) — a missing row means `normal` |
| `shifts` | Per-user saved shifts, with the date denormalized onto each row for direct querying |

All three tables are protected by Postgres Row Level Security: each user can only read or write their own rows. The schema lives in `supabase/migrations/`.

---

Expand Down Expand Up @@ -243,6 +266,7 @@ Create specific calculator instances:
- **Axios** 1.18.1 (HTTP client)
- **date-fns** 4.1.0 (date manipulation)
- **Hebcal API** (holiday detection)
- **Supabase** 2.112.4 (Google OAuth + Postgres persistence)

### Testing

Expand Down Expand Up @@ -301,6 +325,19 @@ bun run dev

Visit `http://localhost:5173/shiftly` in your browser.

### Supabase Setup (optional)

Guest mode works with no setup at all — nothing is persisted, no account required.

To enable Google sign-in and cross-device data persistence, create a `.env.local` file with your Supabase project's credentials:

```bash
VITE_SUPABASE_URL=
VITE_SUPABASE_PUBLISHABLE_KEY=
```

Then run the SQL files under `supabase/migrations/`, in order, in your Supabase project's SQL Editor to create the `monthly_configs`, `work_days`, and `shifts` tables with their Row Level Security policies.

---

## Project Structure
Expand All @@ -310,6 +347,7 @@ src/
├── app/ # Application shell
│ ├── domain/ # Domain instance & wiring
│ ├── providers/ # Context providers
│ │ └── auth/ # Supabase auth context & provider
│ └── routes/ # Route configuration
├── domain/ # Business logic (framework-agnostic)
│ ├── builder/ # Domain structure builders
Expand All @@ -326,11 +364,12 @@ src/
│ └── types/ # Type definitions
├── adapters/ # Domain -> UI view models
├── features/ # Feature-specific UI modules
│ ├── auth/ # Google sign-in UI
│ ├── calculation-rules/
│ ├── config/
│ ├── config/ # Config panel + monthly-config persistence sync
│ ├── info-dialog/
│ ├── salary-summary/
│ ├── work-table/
│ ├── work-table/ # Work table + day-status/shift persistence sync
│ └── workday-timeline/
├── hooks/ # React hooks (orchestration layer)
├── hoc/ # Higher-order components
Expand All @@ -340,9 +379,16 @@ src/
│ └── states/ # Redux slices
├── services/ # External services
│ ├── analytics/ # Salary feedback service
│ └── hebcal/ # Holiday API integration
│ ├── hebcal/ # Holiday API integration
│ ├── supabase/ # Supabase client
│ ├── monthlyConfig/# Monthly config persistence
│ ├── workDay/ # Day status persistence
│ └── shift/ # Shift persistence
├── constants/ # Application constants
└── utils/ # Helper utilities

supabase/
└── migrations/ # Postgres schema (tables, RLS policies)
```

---
Expand Down
52 changes: 49 additions & 3 deletions README_HE.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
![Redux](https://img.shields.io/badge/Redux_Toolkit-2.11.0-764ABC?logo=redux&logoColor=white)
![MUI](https://img.shields.io/badge/Material_UI-7.0.2-007FFF?logo=mui&logoColor=white)
![Vitest](https://img.shields.io/badge/Vitest-4-6E9F18?logo=vitest&logoColor=white)
![Supabase](https://img.shields.io/badge/Supabase-2.112.4-3ECF8E?logo=supabase&logoColor=white)

[![CI](https://github.com/dmaman86/shiftly/actions/workflows/ci.yml/badge.svg)](https://github.com/dmaman86/shiftly/actions/workflows/ci.yml)
![TypeScript](https://img.shields.io/badge/TypeScript-5.7.2-blue?logo=typescript)
Expand Down Expand Up @@ -68,6 +69,28 @@
- פירוט חודשי מצטבר
- חישוב אינקרמנטלי (הוספה / עדכון / הסרה של משמרות)
- ממשק משתמש ריאקטיבי לחלוטין
- התחברות אופציונלית עם Google ושמירת נתונים בין מכשירים

---

## אימות ושמירת נתונים

Shiftly פועלת במלואה **גם ללא חשבון** — הכל רץ בזיכרון, ושום דבר לא נשמר.

התחברות עם **חשבון Google** (דרך Supabase Auth) שומרת בנוסף את הנתונים שלך ב-Supabase, מקושרים לחשבון שלך, כך שהם נשארים זמינים בין הפעלות ובין מכשירים:

- **הגדרות חודשיות** — שנה, חודש, שעות תקן, שכר שעתי
- **סטטוס יום** — סימון מחלה / חופשה לכל יום
- **משמרות** — שעת התחלה, סיום ודגל תפקיד לכל משמרת שנשמרה
- **העברת זכות שבת** — שעות זכות שבת שלא נוצלו עוברות לחודש הבא במקום להיעלם

| טבלה | שומרת |
| --- | --- |
| `monthly_configs` | הגדרות לכל (משתמש, שנה, חודש), וגם יתרת זכות השבת המועברת |
| `work_days` | סטטוס לכל (משתמש, תאריך) (`sick` / `vacation`) — שורה חסרה משמעה `normal` |
| `shifts` | משמרות שנשמרו לכל משתמש, עם התאריך על כל שורה לצורך שאילתות ישירות |

שלוש הטבלאות מוגנות באמצעות Row Level Security של Postgres: כל משתמש יכול לקרוא ולכתוב רק את השורות שלו. הסכמה נמצאת ב-`supabase/migrations/`.

---

Expand Down Expand Up @@ -243,6 +266,7 @@ totalHours = worked hours + sick hours + vacation hours + appliedShabbatCredit
- **Axios** 1.18.1 (HTTP client)
- **date-fns** 4.1.0 (טיפול בתאריכים)
- **Hebcal API** (זיהוי חגים)
- **Supabase** 2.112.4 (התחברות Google + שמירת נתונים ב-Postgres)

### בדיקות

Expand All @@ -258,6 +282,7 @@ src/
├── app/ # מעטפת האפליקציה
│ ├── domain/ # אתחול וחיבור הדומיין
│ ├── providers/ # ספקי Context
│ │ └── auth/ # Context ו-Provider לאימות Supabase
│ └── routes/ # הגדרות ניתוב
├── domain/ # לוגיקה עסקית (בלתי תלויה בפריימוורקים)
│ ├── builder/ # בוני מבני דומיין
Expand All @@ -274,11 +299,12 @@ src/
│ └── types/ # הגדרות טיפוסים
├── adapters/ # המרת דומיין ל־UI
├── features/ # מודולי UI ספציפיים
│ ├── auth/ # ממשק התחברות Google
│ ├── calculation-rules/
│ ├── config/
│ ├── config/ # פאנל הגדרות + סנכרון שמירת קונפיג חודשי
│ ├── info-dialog/
│ ├── salary-summary/
│ ├── work-table/
│ ├── work-table/ # טבלת עבודה + סנכרון שמירת סטטוס יום ומשמרות
│ └── workday-timeline/
├── hooks/ # React hooks (שכבת תיאום)
├── hoc/ # רכיבי Higher-order
Expand All @@ -288,9 +314,16 @@ src/
│ └── states/ # Redux slices
├── services/ # שירותים חיצוניים
│ ├── analytics/ # שירות משוב שכר
│ └── hebcal/ # אינטגרציה עם API חגים
│ ├── hebcal/ # אינטגרציה עם API חגים
│ ├── supabase/ # קליינט Supabase
│ ├── monthlyConfig/# שמירת קונפיג חודשי
│ ├── workDay/ # שמירת סטטוס יום
│ └── shift/ # שמירת משמרות
├── constants/ # קבועי אפליקציה
└── utils/ # עזרי שירות

supabase/
└── migrations/ # סכמת Postgres (טבלאות, מדיניות RLS)
```

---
Expand Down Expand Up @@ -448,6 +481,19 @@ bun run dev

כניסה ל־`http://localhost:5173/shiftly` בדפדפן.

### הגדרת Supabase (אופציונלי)

מצב אורח עובד בלי שום הגדרה — שום דבר לא נשמר, ואין צורך בחשבון.

כדי להפעיל התחברות עם Google ושמירת נתונים בין מכשירים, צרו קובץ `.env.local` עם פרטי הפרויקט שלכם ב-Supabase:

```bash
VITE_SUPABASE_URL=
VITE_SUPABASE_PUBLISHABLE_KEY=
```

לאחר מכן הריצו את קבצי ה-SQL תחת `supabase/migrations/`, לפי הסדר, ב-SQL Editor של פרויקט ה-Supabase שלכם, כדי ליצור את הטבלאות `monthly_configs`, `work_days` ו-`shifts` עם מדיניות ה-Row Level Security שלהן.

---

## רישיון
Expand Down
17 changes: 17 additions & 0 deletions bun.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@
"@mui/material": "^7.0.2",
"@mui/x-date-pickers": "^8.1.0",
"@reduxjs/toolkit": "^2.11.0",
"@supabase/supabase-js": "^2.112.4",
"axios": "^1.18.1",
"date-fns": "^4.1.0",
"i18next": "^26.0.4",
Expand Down
16 changes: 9 additions & 7 deletions src/app/providers/AppProviders.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import { LocalizationProvider } from "@mui/x-date-pickers";
import { AdapterDateFns } from "@mui/x-date-pickers/AdapterDateFns";

import { store } from "@/redux/store";
import { DomainProvider, AppSnackbarProvider } from "@/app/providers";
import { AuthProvider, DomainProvider, AppSnackbarProvider } from "@/app/providers";

import createCache from "@emotion/cache";
import rtlPlugin from "stylis-plugin-rtl";
Expand Down Expand Up @@ -40,12 +40,14 @@ export const AppProviders = ({ children }: AppProvidersProps) => {
<CacheProvider value={direction === "rtl" ? rtlCache : ltrCache}>
<ThemeProvider theme={theme}>
<AppSnackbarProvider>
<DomainProvider>
<LocalizationProvider dateAdapter={AdapterDateFns}>
<CssBaseline />
{children}
</LocalizationProvider>
</DomainProvider>
<AuthProvider>
<DomainProvider>
<LocalizationProvider dateAdapter={AdapterDateFns}>
<CssBaseline />
{children}
</LocalizationProvider>
</DomainProvider>
</AuthProvider>
</AppSnackbarProvider>
</ThemeProvider>
</CacheProvider>
Expand Down
63 changes: 63 additions & 0 deletions src/app/providers/auth/AuthProvider.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import { useEffect, useMemo, useState } from "react";
import type { Session } from "@supabase/supabase-js";

import { supabase } from "@/services/supabase/supabase.client";
import { AuthContext, type AuthContextValue } from "./authContext";

type AuthProviderProps = {
children: React.ReactNode;
};

export const AuthProvider = ({ children }: AuthProviderProps) => {
const [session, setSession] = useState<Session | null>(null);
const [isLoading, setIsLoading] = useState(true);
const [initializationError, setInitializationError] = useState<string | null>(
null,
);

useEffect(() => {
let isMounted = true;
let receivedAuthEvent = false;

const {
data: { subscription },
} = supabase.auth.onAuthStateChange((_event, nextSession) => {
if (!isMounted) return;

receivedAuthEvent = true;
setSession(nextSession);
setInitializationError(null);
setIsLoading(false);
});

void supabase.auth.getSession().then(({ data, error }) => {
if (!isMounted || receivedAuthEvent) return;

if (error) {
setInitializationError(error.message);
setSession(null);
} else {
setSession(data.session);
}

setIsLoading(false);
});

return () => {
isMounted = false;
subscription.unsubscribe();
};
}, []);

const value = useMemo<AuthContextValue>(
() => ({
session,
user: session?.user ?? null,
isLoading,
initializationError,
}),
[session, isLoading, initializationError],
);

return <AuthContext.Provider value={value}>{children}</AuthContext.Provider>;
};
11 changes: 11 additions & 0 deletions src/app/providers/auth/authContext.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { createContext } from "react";
import type { Session, User } from "@supabase/supabase-js";

export type AuthContextValue = {
session: Session | null;
user: User | null;
isLoading: boolean;
initializationError: string | null;
};

export const AuthContext = createContext<AuthContextValue | null>(null);
3 changes: 3 additions & 0 deletions src/app/providers/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,6 @@ export { DomainContext } from "./domain/domainContext";
export { AppSnackbarContext } from "./snackbar/snackbarContext";
export { DirectionContext } from "./direction/directionContext";
export type { Direction } from "./direction/directionContext";
export { AuthProvider } from "./auth/AuthProvider";
export { AuthContext } from "./auth/authContext";
export type { AuthContextValue } from "./auth/authContext";
Loading
Loading