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
10 changes: 10 additions & 0 deletions .env.sample
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
# https://api.demo.factorial.dev for demo or "https://api.factorialhr.com" for production environment
FACTORIALHR_ENVIRONMENT_URL="https://api.factorialhr.com"

# oauth settings, required if not using api key authentication
FACTORIALHR_CLIENT_ID="<client-id>"
FACTORIALHR_CLIENT_SECRET="<client-secret>"
FACTORIALHR_REDIRECT_URI="<redirect-uri>"

# if you want to use api key authentication instead of oauth, you can set it here
#FACTORIALHR_API_KEY="<api-key>"
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
*.db
.web
assets/external/
.states
# Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]
Expand Down
20 changes: 4 additions & 16 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,13 @@
# * Run "pre-commit install".
repos:
- repo: https://github.com/adrienverge/yamllint
rev: v1.35.1
rev: v1.37.1
hooks:
- id: yamllint
args: ['-d {extends: relaxed, rules: { line-length: disable }}', '-s']

- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v5.0.0
rev: v6.0.0
hooks:
- id: check-toml
- id: check-yaml
Expand All @@ -24,30 +24,18 @@ repos:
- id: check-illegal-windows-names

- repo: https://github.com/astral-sh/uv-pre-commit
rev: 0.6.3
rev: 0.8.13
hooks:
# Update the uv lockfile
- id: uv-lock

- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.9.7
rev: v0.12.10
hooks:
- id: ruff
args: [--fix, --show-fixes]
- id: ruff-format

- repo: https://github.com/RobertCraigie/pyright-python
rev: v1.1.394
hooks:
- id: pyright
additional_dependencies:
- pytest >= 8
- factorialhr==4.0.0b3
- tabulate>=0.9.0
- pyside6-essentials>=6.8.2.1
- outcome>=1.3.0.post0
- trio>=0.29.0

- repo: https://github.com/pre-commit/pygrep-hooks
rev: v1.10.0
hooks:
Expand Down
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,12 @@ All notable changes to fwtv module will be documented in this file.

The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0)

## [Unreleased]

## [v3.0.0] -

### Changed app to run in the browser using reflex

## [2.4.1] - 2025-07-07

### Fixed
Expand Down
15 changes: 6 additions & 9 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,9 @@ This script verifies attendances whether they comply with german law. In particu
- Whether the work time is longer than 6 hours without a break of 30 min
- Whether the work time is longer than 9 hours without a break of 45 min
- Whether the work time is longer than 10 hours without a break of 11 hours
- Whether the work time is within the time of 6am and 10pm

It also provides a way to fetch the attendances of all employees from [FactorialHR](https://apidoc.factorialhr.com/docs) using a [company api-key](https://help.factorialhr.com/how-to-create-api-keys-in-factorial).

![main_window](./docs/images/main_window.png "Main Window")
![main_window](./docs/images/working_time_verification.png "Main Window")

## Disclaimer

Expand All @@ -19,12 +18,10 @@ Errors where the time attended is 1 min above the limit are ignored, because fac

## Usage

- Install the tool with `pip install fwtv`
- Run tool with `factorial-working-time`

### Preconditions

Preconditions errors are syntactical errors like an attendance that starts and end and the same time, or if a `clock_in` or `clock_out` parameter is missing.
- clone this repository
- install dependencies using `uv sync --frozen`
- copy [`.env.sample`](.env.sample) to `.env` and adjust the environment variables
- run app `uv run reflex run --env prod`

## Contributing

Expand Down
Binary file removed docs/images/main_window.png
Binary file not shown.
Binary file added docs/images/working_time_verification.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
File renamed without changes.
4 changes: 4 additions & 0 deletions factorialhr_analysis/components/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
from factorialhr_analysis.components.date_range_selector import date_inputs, date_range_picker
from factorialhr_analysis.components.navbar import navbar

__all__ = ['date_inputs', 'date_range_picker', 'navbar']
53 changes: 53 additions & 0 deletions factorialhr_analysis/components/date_range_selector.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
"""A date range selector component using Reflex framework."""

import reflex as rx


class DateRangeState(rx.State):
"""State for the date range picker component."""

start_date: str = ''
end_date: str = ''
temp_start_date: str = ''
temp_end_date: str = ''

@rx.event
def apply_date_range(self):
"""Apply the selected date range."""
self.start_date = self.temp_start_date
self.end_date = self.temp_end_date


def date_range_picker() -> rx.Component:
"""Date range picker component."""
return rx.dialog.root(
rx.dialog.trigger(rx.button('Select Date Range')),
rx.dialog.content(
rx.dialog.title('Select Date Range'),
rx.dialog.description('Choose start and end dates'),
rx.vstack(
rx.text('Start Date:'),
rx.input(
type='date', value=DateRangeState.temp_start_date, on_change=DateRangeState.set_temp_start_date
),
rx.text('End Date:'),
rx.input(type='date', value=DateRangeState.temp_end_date, on_change=DateRangeState.set_temp_end_date),
rx.flex(
rx.dialog.close(rx.button('Cancel')),
rx.dialog.close(rx.button('Apply', on_click=DateRangeState.apply_date_range)),
spacing='3',
justify='end',
),
spacing='4',
),
),
)


def date_inputs() -> rx.Component:
"""Display the selected date range in read-only input fields."""
return rx.hstack(
rx.input(placeholder='Start Date', value=DateRangeState.start_date, read_only=True),
rx.input(placeholder='End Date', value=DateRangeState.end_date, read_only=True),
spacing='2',
)
71 changes: 71 additions & 0 deletions factorialhr_analysis/components/navbar.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
"""The navigation bar component."""

import reflex as rx
from reflex.style import color_mode, set_color_mode

from factorialhr_analysis import routes, state


def dark_mode_toggle() -> rx.Component:
"""Toggle for dark/light mode."""
return rx.segmented_control.root(
rx.segmented_control.item(
rx.icon(tag='monitor', size=20),
value='system',
),
rx.segmented_control.item(
rx.icon(tag='sun', size=20),
value='light',
),
rx.segmented_control.item(
rx.icon(tag='moon', size=20),
value='dark',
),
on_change=set_color_mode,
variant='classic',
radius='large',
value=color_mode,
)


def navbar_link(text: str, url: str) -> rx.Component:
"""Link in the navigation bar."""
return rx.link(rx.text(text, size='4', weight='medium'), href=url)


def navbar() -> rx.Component:
"""Navigation bar component."""
return rx.box(
rx.desktop_only(
rx.hstack(
rx.heading('Factorialhr analysis', size='7', weight='bold'),
rx.hstack(
navbar_link('Working time verification', '/#'),
spacing='5',
),
rx.hstack(
rx.menu.root(
rx.menu.trigger(
rx.icon_button(
rx.icon('user'),
size='2',
radius='full',
)
),
rx.menu.content(
rx.menu.item(
rx.link(rx.text('Log out'), href=routes.INDEX, on_click=state.LoginState.logout)
),
),
justify='end',
),
dark_mode_toggle(),
),
justify='between',
align_items='center',
),
),
bg=rx.color('accent', 3),
padding='1em',
width='100%',
)
13 changes: 13 additions & 0 deletions factorialhr_analysis/factorialhr_analysis.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
"""Main app file for the FactorialHR Analysis application."""

import dotenv
import reflex as rx

from factorialhr_analysis import pages, routes

dotenv.load_dotenv()


app = rx.App()
app.add_page(pages.index_page, route=routes.INDEX)
app.add_page(pages.login_page, route=routes.LOGIN_ROUTE)
4 changes: 4 additions & 0 deletions factorialhr_analysis/pages/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
from factorialhr_analysis.pages.index import index_page
from factorialhr_analysis.pages.login import login_page

__all__ = ['index_page', 'login_page']
Loading
Loading