Added Application HolidayViewer
This commit is contained in:
0
src/components/hoildays/__init__.py
Normal file
0
src/components/hoildays/__init__.py
Normal file
0
src/components/hoildays/assets/Holidays.css
Normal file
0
src/components/hoildays/assets/Holidays.css
Normal file
0
src/components/hoildays/assets/__init__.py
Normal file
0
src/components/hoildays/assets/__init__.py
Normal file
8
src/components/hoildays/assets/icons.py
Normal file
8
src/components/hoildays/assets/icons.py
Normal file
@@ -0,0 +1,8 @@
|
||||
from fastcore.basics import NotStr
|
||||
|
||||
# Material - HolidayVillageTwotone
|
||||
icon_holidays = NotStr("""<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 24 24">
|
||||
<path opacity=".3" d="M8 6.83l-4 4V18h3v-3h2v3h3v-7.17l-4-4zM9 13H7v-2h2v2z" fill="currentColor"></path>
|
||||
<path d="M8 4l-6 6v10h12V10L8 4zm4 14H9v-3H7v3H4v-7.17l4-4l4 4V18zm-3-5H7v-2h2v2zm9 7V8.35L13.65 4h-2.83L16 9.18V20h2zm4 0V6.69L19.31 4h-2.83L20 7.52V20h2z" fill="currentColor"></path>
|
||||
</svg>
|
||||
""")
|
||||
0
src/components/hoildays/commands.py
Normal file
0
src/components/hoildays/commands.py
Normal file
33
src/components/hoildays/components/HolidaysViewer.py
Normal file
33
src/components/hoildays/components/HolidaysViewer.py
Normal file
@@ -0,0 +1,33 @@
|
||||
from fasthtml.components import *
|
||||
|
||||
from components.BaseComponent import BaseComponent
|
||||
from components.hoildays.constants import HOLIDAYS_VIEWER_INSTANCE_ID
|
||||
from components.datagrid_new.components.DataGrid import DataGrid
|
||||
from components.hoildays.helpers.calendar_helper import CalendarHelper
|
||||
from components.hoildays.helpers.nibelisparser import OffPeriodDetails
|
||||
from components.repositories.constants import USERS_REPOSITORY_NAME, HOLIDAYS_TABLE_NAME
|
||||
from helpers.Datahelper import DataHelper
|
||||
|
||||
|
||||
class HolidaysViewer(BaseComponent):
|
||||
def __init__(self, session, _id, settings_manager, boundaries=None):
|
||||
super().__init__(session, _id)
|
||||
self._settings_manager = settings_manager
|
||||
self._boundaries = boundaries
|
||||
|
||||
def __ft__(self):
|
||||
records = DataHelper.get(self._session,
|
||||
self._settings_manager,
|
||||
USERS_REPOSITORY_NAME,
|
||||
HOLIDAYS_TABLE_NAME,
|
||||
OffPeriodDetails)
|
||||
names, holidays = CalendarHelper.create_calendar(records)
|
||||
calendar = DataGrid.new(self._session, holidays, index=names)
|
||||
return Div(
|
||||
calendar,
|
||||
cls="mt-2",
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def create_component_id(session):
|
||||
return f"{HOLIDAYS_VIEWER_INSTANCE_ID}{session['user_id']}"
|
||||
0
src/components/hoildays/components/__init__.py
Normal file
0
src/components/hoildays/components/__init__.py
Normal file
1
src/components/hoildays/constants.py
Normal file
1
src/components/hoildays/constants.py
Normal file
@@ -0,0 +1 @@
|
||||
HOLIDAYS_VIEWER_INSTANCE_ID = "__HolidaysViewer__"
|
||||
0
src/components/hoildays/helpers/__init__.py
Normal file
0
src/components/hoildays/helpers/__init__.py
Normal file
81
src/components/hoildays/helpers/calendar_helper.py
Normal file
81
src/components/hoildays/helpers/calendar_helper.py
Normal file
@@ -0,0 +1,81 @@
|
||||
from datetime import date, timedelta
|
||||
|
||||
from components.hoildays.helpers.nibelisparser import OffPeriodDetails
|
||||
|
||||
|
||||
class CalendarHelper:
|
||||
@staticmethod
|
||||
def create_calendar(records: list[OffPeriodDetails], start_date: date | None = None, end_date: date | None = None):
|
||||
"""
|
||||
|
||||
:param records:
|
||||
:param start_date:
|
||||
:param end_date:
|
||||
:return:
|
||||
"""
|
||||
# multiple algorithms possible. The chosen one
|
||||
# two steps
|
||||
# step one
|
||||
# create a dict [date, [name, holiday]] for all record in records
|
||||
# in the loop, records all the names
|
||||
#
|
||||
# step two
|
||||
# create a dict [date, list[holiday]] according to the sorted list names
|
||||
|
||||
# step 1
|
||||
temp = {}
|
||||
names = set()
|
||||
for record in records:
|
||||
full_name = record.first_name + " " + record.last_name
|
||||
names.add(full_name)
|
||||
duration = CalendarHelper.get_period(record.start_date, record.end_date)
|
||||
last_index = len(duration) - 1
|
||||
for index, current_date in enumerate(duration):
|
||||
calendar = temp.setdefault(current_date, {})
|
||||
if full_name in calendar:
|
||||
calendar[full_name].append(CalendarHelper.get_reason(record, index == 0, index == last_index))
|
||||
else:
|
||||
calendar[full_name] = [CalendarHelper.get_reason(record, index == 0, index == last_index)]
|
||||
|
||||
res = {}
|
||||
names = list(sorted(names))
|
||||
start_date_to_use = start_date or min(temp.keys())
|
||||
end_date_to_use = end_date or max(temp.keys())
|
||||
|
||||
for current_date in CalendarHelper.get_period(start_date_to_use, end_date_to_use):
|
||||
if current_date in temp:
|
||||
values = []
|
||||
res[current_date] = values
|
||||
for name in names:
|
||||
if name in temp[current_date]:
|
||||
values.append(temp[current_date][name])
|
||||
else:
|
||||
values.append(None)
|
||||
|
||||
else:
|
||||
res[current_date] = [None] * len(names)
|
||||
|
||||
return names, res
|
||||
|
||||
@staticmethod
|
||||
def get_reason(record: OffPeriodDetails, is_start, is_end):
|
||||
suffix = ""
|
||||
if is_start and record.start_am_pm:
|
||||
suffix += "_" + record.start_am_pm
|
||||
if is_end and record.end_am_pm:
|
||||
suffix += "_" + record.end_am_pm
|
||||
|
||||
return record.reason + suffix
|
||||
|
||||
@staticmethod
|
||||
def get_period(start_date: date, end_date: date):
|
||||
if end_date < start_date:
|
||||
raise ValueError("end date is before start date.")
|
||||
|
||||
current_date = start_date
|
||||
res = [current_date]
|
||||
while current_date < end_date:
|
||||
current_date = current_date + timedelta(days=1)
|
||||
res.append(current_date)
|
||||
|
||||
return res
|
||||
240
src/components/hoildays/helpers/nibelisparser.py
Normal file
240
src/components/hoildays/helpers/nibelisparser.py
Normal file
@@ -0,0 +1,240 @@
|
||||
import dataclasses
|
||||
import datetime
|
||||
from typing import Literal
|
||||
|
||||
|
||||
@dataclasses.dataclass
|
||||
class ParsingError:
|
||||
def __init__(self, reason: str, parser):
|
||||
self.reason = reason
|
||||
self.pos = parser.pos
|
||||
self.line = parser.line
|
||||
self.column = parser.column
|
||||
|
||||
|
||||
@dataclasses.dataclass
|
||||
class OffPeriodDetails:
|
||||
first_name: str
|
||||
last_name: str
|
||||
start_date: datetime.date
|
||||
start_am_pm: Literal["am", "pm"] | None
|
||||
end_date: datetime.date
|
||||
end_am_pm: Literal["am", "pm"] | None
|
||||
total: float
|
||||
reason: str
|
||||
date_import: datetime.date
|
||||
|
||||
def get_key(self):
|
||||
return (f"{self.first_name}|{self.last_name}|"
|
||||
f"{self.start_date}|{self.start_am_pm}|"
|
||||
f"{self.end_date}|{self.end_am_pm}")
|
||||
|
||||
|
||||
class NibelisParser:
|
||||
def __init__(self, data: str):
|
||||
self.data = data
|
||||
self.pos = 0
|
||||
self.line = 1
|
||||
self.column = 1
|
||||
self.error_sink: list[ParsingError] = []
|
||||
|
||||
def parse(self):
|
||||
return self.read_input()
|
||||
|
||||
def read_input(self):
|
||||
# self.data = self.data.replace("\n\t\n", "\t\n")
|
||||
|
||||
while (line := self.read_line().rstrip("\t")) != "Demandes qualifiées" and line is not None:
|
||||
pass
|
||||
|
||||
res = []
|
||||
|
||||
try:
|
||||
while True:
|
||||
if (detail := self.read_detail()) is not None:
|
||||
res.append(detail)
|
||||
except StopIteration:
|
||||
pass
|
||||
|
||||
return res
|
||||
|
||||
def read_detail(self):
|
||||
start = self.read_line() # Détail de la demande
|
||||
if start is None:
|
||||
raise StopIteration()
|
||||
|
||||
if start.strip() == "" or start == "Demandes qualifiées":
|
||||
return None
|
||||
|
||||
if start != "Détail de la demande":
|
||||
self.error_sink.append(ParsingError("'Détail de la demande' not found.", self))
|
||||
return None
|
||||
|
||||
try:
|
||||
self.read_word() # ENGI402
|
||||
names = []
|
||||
while (token := self.read_word()) not in ["Le", "Du"]:
|
||||
names.append(token)
|
||||
first_name, last_name = self.get_first_name_last_name(names)
|
||||
|
||||
if token == "Le":
|
||||
start_date, start_am_pm, end_date, end_am_pm = self.read_one_date()
|
||||
else:
|
||||
start_date, start_am_pm, end_date, end_am_pm = self.read_period()
|
||||
total = self.read_total()
|
||||
reason = " ".join([self.read_word(), self.read_word()])
|
||||
self.read_line() # finish the line
|
||||
|
||||
return OffPeriodDetails(first_name,
|
||||
last_name,
|
||||
start_date,
|
||||
start_am_pm,
|
||||
end_date,
|
||||
end_am_pm,
|
||||
total,
|
||||
reason,
|
||||
datetime.date.today())
|
||||
|
||||
except Exception as ex:
|
||||
self.error_sink.append(ParsingError(str(ex), self))
|
||||
self.read_line() # finish the line
|
||||
return None
|
||||
|
||||
def read_one_date(self):
|
||||
"""
|
||||
|
||||
:return:
|
||||
"""
|
||||
# ... Le] lundi 20/05/2024 [0.50 J. ...
|
||||
# ... Le] jeudi 04/01/2024 après-midi exclu [0.50 J. ...
|
||||
# ... Le] lundi 29/01/2024 matin exclu [0.50 J. ...
|
||||
|
||||
self.read_word() # read day
|
||||
date_as_str = self.read_word()
|
||||
start_date = datetime.datetime.strptime(date_as_str, "%d/%m/%Y").date()
|
||||
end_date = start_date + datetime.timedelta(days=1)
|
||||
am_pm_as_str = self.read_until(lambda c: c.isdigit(), "").strip()
|
||||
am_pm = self.decode_am_pm(am_pm_as_str)
|
||||
return start_date, am_pm, end_date, am_pm
|
||||
|
||||
def read_period(self):
|
||||
"""
|
||||
|
||||
:return:
|
||||
"""
|
||||
# ... Du] <= lundi 27/05/2024 au mardi 28/05/2024 => [3.50 J. ..
|
||||
# ... Du] <= lundi 19/02/2024 matin inclus au mardi 20/02/2024 après-midi exclu => [3.50 J. ..
|
||||
# ... Du] <= mardi 20/02/2024 matin exclu au vendredi 23/02/2024 après-midi inclus => [3.50 J. ..
|
||||
self.read_word() # read day
|
||||
date_as_str = self.read_word()
|
||||
start_am_pm_content = self.read_words_until(["au"])
|
||||
self.read_word() # read day
|
||||
end_as_str = self.read_word()
|
||||
end_am_pm_content = self.read_until(lambda c: c.isdigit())
|
||||
|
||||
start_date = datetime.datetime.strptime(date_as_str, "%d/%m/%Y").date()
|
||||
end_date = datetime.datetime.strptime(end_as_str, "%d/%m/%Y").date()
|
||||
end_date = end_date + datetime.timedelta(days=1)
|
||||
start_am_pm = self.decode_am_pm(start_am_pm_content)
|
||||
end_am_pm = self.decode_am_pm(end_am_pm_content)
|
||||
|
||||
return start_date, start_am_pm, end_date, end_am_pm
|
||||
|
||||
def read_total(self):
|
||||
total_as_str = self.read_word()
|
||||
self.read_word() # parse J.
|
||||
return float(total_as_str)
|
||||
|
||||
def read_line(self, strip=False):
|
||||
return self._read_content(["\n"], strip=strip)
|
||||
|
||||
def read_word(self, strip=True):
|
||||
return self._read_content(["\n", "\t", " "], strip=strip)
|
||||
|
||||
def read_words_until(self, words: list):
|
||||
names = []
|
||||
while True:
|
||||
token = self.read_word()
|
||||
if token is None or token in words:
|
||||
break
|
||||
names.append(token)
|
||||
|
||||
return names
|
||||
|
||||
def read_until(self, predicate, default=None):
|
||||
if self.pos == len(self.data):
|
||||
return default
|
||||
|
||||
buffer = ""
|
||||
while self.pos < len(self.data) and not predicate(self.data[self.pos]):
|
||||
buffer += self.data[self.pos]
|
||||
self._move_forward()
|
||||
|
||||
return buffer
|
||||
|
||||
def _read_content(self, tokens: list, strip):
|
||||
if self.pos >= len(self.data):
|
||||
return None
|
||||
|
||||
buffer = ""
|
||||
while self.pos < len(self.data) and self.data[self.pos] not in tokens:
|
||||
buffer += self.data[self.pos]
|
||||
self._move_forward()
|
||||
|
||||
# eat the token
|
||||
self._move_forward()
|
||||
|
||||
if strip:
|
||||
while self.pos < len(self.data) and self.data[self.pos] in tokens:
|
||||
self._move_forward()
|
||||
|
||||
return buffer
|
||||
|
||||
def _move_forward(self):
|
||||
self.pos += 1
|
||||
try:
|
||||
if self.data[self.pos - 1] == "\n": # \n is a 'new line', so it's counted in the new line
|
||||
self.line += 1
|
||||
self.column = 1
|
||||
|
||||
else:
|
||||
self.column += 1
|
||||
except IndexError:
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
@staticmethod
|
||||
def get_first_name_last_name(names: list[str]):
|
||||
if names[0][0].isdigit():
|
||||
names.pop(0) # sometimes, the code (ex PROD134) is in two parts
|
||||
|
||||
for i, name in enumerate(names):
|
||||
if name.isupper():
|
||||
break
|
||||
|
||||
return " ".join(names[:i]), " ".join(names[i:])
|
||||
|
||||
@staticmethod
|
||||
def decode_am_pm(input_):
|
||||
if not input_:
|
||||
return None
|
||||
|
||||
if isinstance(input_, list):
|
||||
input_ = " ".join(input_)
|
||||
else:
|
||||
input_ = input_.replace("\t", " ")
|
||||
input_ = input_.strip()
|
||||
input_ = " ".join(input_.split())
|
||||
|
||||
match input_:
|
||||
case "matin inclus":
|
||||
return "am"
|
||||
case "matin exclu":
|
||||
return "pm"
|
||||
case "après-midi inclus":
|
||||
return "pm"
|
||||
case "après-midi exclu":
|
||||
return "am"
|
||||
case _:
|
||||
return None
|
||||
Reference in New Issue
Block a user