fixed: pause time calculation from work instead of presence time
All checks were successful
Tests / Run Go Tests (push) Successful in 1m44s
All checks were successful
Tests / Run Go Tests (push) Successful in 1m44s
This commit is contained in:
@@ -45,3 +45,218 @@ var testBookings10hrs = []models.Booking{{
|
|||||||
Timestamp: time.Date(2025, 01, 01, 18, 0, 0, 0, time.UTC),
|
Timestamp: time.Date(2025, 01, 01, 18, 0, 0, 0, time.UTC),
|
||||||
BookingType: testBookingType,
|
BookingType: testBookingType,
|
||||||
}}
|
}}
|
||||||
|
|
||||||
|
var testBookings6hrsBreak30min = []models.Booking{
|
||||||
|
{
|
||||||
|
CardUID: "aaaa-aaaa",
|
||||||
|
CheckInOut: 1,
|
||||||
|
Timestamp: time.Date(2025, 01, 01, 8, 0, 0, 0, time.UTC),
|
||||||
|
BookingType: testBookingType,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
CardUID: "aaaa-aaaa",
|
||||||
|
CheckInOut: 2,
|
||||||
|
Timestamp: time.Date(2025, 01, 01, 9, 0, 0, 0, time.UTC),
|
||||||
|
BookingType: testBookingType,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
CardUID: "aaaa-aaaa",
|
||||||
|
CheckInOut: 1,
|
||||||
|
Timestamp: time.Date(2025, 01, 01, 9, 30, 0, 0, time.UTC),
|
||||||
|
BookingType: testBookingType,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
CardUID: "aaaa-aaaa",
|
||||||
|
CheckInOut: 2,
|
||||||
|
Timestamp: time.Date(2025, 01, 01, 14, 30, 0, 0, time.UTC),
|
||||||
|
BookingType: testBookingType,
|
||||||
|
}}
|
||||||
|
|
||||||
|
var testBookings610hrsBreak30min = []models.Booking{
|
||||||
|
{
|
||||||
|
CardUID: "aaaa-aaaa",
|
||||||
|
CheckInOut: 1,
|
||||||
|
Timestamp: time.Date(2025, 01, 01, 8, 0, 0, 0, time.UTC),
|
||||||
|
BookingType: testBookingType,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
CardUID: "aaaa-aaaa",
|
||||||
|
CheckInOut: 2,
|
||||||
|
Timestamp: time.Date(2025, 01, 01, 9, 0, 0, 0, time.UTC),
|
||||||
|
BookingType: testBookingType,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
CardUID: "aaaa-aaaa",
|
||||||
|
CheckInOut: 1,
|
||||||
|
Timestamp: time.Date(2025, 01, 01, 9, 30, 0, 0, time.UTC),
|
||||||
|
BookingType: testBookingType,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
CardUID: "aaaa-aaaa",
|
||||||
|
CheckInOut: 2,
|
||||||
|
Timestamp: time.Date(2025, 01, 01, 14, 40, 0, 0, time.UTC),
|
||||||
|
BookingType: testBookingType,
|
||||||
|
}}
|
||||||
|
|
||||||
|
var testBookings9hrsBreak30min = []models.Booking{
|
||||||
|
{
|
||||||
|
CardUID: "aaaa-aaaa",
|
||||||
|
CheckInOut: 1,
|
||||||
|
Timestamp: time.Date(2025, 01, 01, 8, 0, 0, 0, time.UTC),
|
||||||
|
BookingType: testBookingType,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
CardUID: "aaaa-aaaa",
|
||||||
|
CheckInOut: 2,
|
||||||
|
Timestamp: time.Date(2025, 01, 01, 9, 0, 0, 0, time.UTC),
|
||||||
|
BookingType: testBookingType,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
CardUID: "aaaa-aaaa",
|
||||||
|
CheckInOut: 1,
|
||||||
|
Timestamp: time.Date(2025, 01, 01, 9, 30, 0, 0, time.UTC),
|
||||||
|
BookingType: testBookingType,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
CardUID: "aaaa-aaaa",
|
||||||
|
CheckInOut: 2,
|
||||||
|
Timestamp: time.Date(2025, 01, 01, 17, 30, 0, 0, time.UTC),
|
||||||
|
BookingType: testBookingType,
|
||||||
|
}}
|
||||||
|
|
||||||
|
var testBookings930hrs = []models.Booking{
|
||||||
|
{
|
||||||
|
CardUID: "aaaa-aaaa",
|
||||||
|
CheckInOut: 1,
|
||||||
|
Timestamp: time.Date(2025, 01, 01, 8, 0, 0, 0, time.UTC),
|
||||||
|
BookingType: testBookingType,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
CardUID: "aaaa-aaaa",
|
||||||
|
CheckInOut: 2,
|
||||||
|
Timestamp: time.Date(2025, 01, 01, 17, 30, 0, 0, time.UTC),
|
||||||
|
BookingType: testBookingType,
|
||||||
|
}}
|
||||||
|
|
||||||
|
var testBookings910hrsBreak30min = []models.Booking{
|
||||||
|
{
|
||||||
|
CardUID: "aaaa-aaaa",
|
||||||
|
CheckInOut: 1,
|
||||||
|
Timestamp: time.Date(2025, 01, 01, 8, 0, 0, 0, time.UTC),
|
||||||
|
BookingType: testBookingType,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
CardUID: "aaaa-aaaa",
|
||||||
|
CheckInOut: 2,
|
||||||
|
Timestamp: time.Date(2025, 01, 01, 9, 0, 0, 0, time.UTC),
|
||||||
|
BookingType: testBookingType,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
CardUID: "aaaa-aaaa",
|
||||||
|
CheckInOut: 1,
|
||||||
|
Timestamp: time.Date(2025, 01, 01, 9, 30, 0, 0, time.UTC),
|
||||||
|
BookingType: testBookingType,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
CardUID: "aaaa-aaaa",
|
||||||
|
CheckInOut: 2,
|
||||||
|
Timestamp: time.Date(2025, 01, 01, 17, 40, 0, 0, time.UTC),
|
||||||
|
BookingType: testBookingType,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
var testBookings910hrsBreak35min = []models.Booking{
|
||||||
|
{
|
||||||
|
CardUID: "aaaa-aaaa",
|
||||||
|
CheckInOut: 1,
|
||||||
|
Timestamp: time.Date(2025, 01, 01, 8, 0, 0, 0, time.UTC),
|
||||||
|
BookingType: testBookingType,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
CardUID: "aaaa-aaaa",
|
||||||
|
CheckInOut: 2,
|
||||||
|
Timestamp: time.Date(2025, 01, 01, 9, 0, 0, 0, time.UTC),
|
||||||
|
BookingType: testBookingType,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
CardUID: "aaaa-aaaa",
|
||||||
|
CheckInOut: 1,
|
||||||
|
Timestamp: time.Date(2025, 01, 01, 9, 35, 0, 0, time.UTC),
|
||||||
|
BookingType: testBookingType,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
CardUID: "aaaa-aaaa",
|
||||||
|
CheckInOut: 2,
|
||||||
|
Timestamp: time.Date(2025, 01, 01, 17, 45, 0, 0, time.UTC),
|
||||||
|
BookingType: testBookingType,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
var testBookings945hrs = []models.Booking{
|
||||||
|
{
|
||||||
|
CardUID: "aaaa-aaaa",
|
||||||
|
CheckInOut: 1,
|
||||||
|
Timestamp: time.Date(2025, 01, 01, 8, 0, 0, 0, time.UTC),
|
||||||
|
BookingType: testBookingType,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
CardUID: "aaaa-aaaa",
|
||||||
|
CheckInOut: 2,
|
||||||
|
Timestamp: time.Date(2025, 01, 01, 17, 45, 0, 0, time.UTC),
|
||||||
|
BookingType: testBookingType,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
var testBookings10hrsBreak45min = []models.Booking{
|
||||||
|
{
|
||||||
|
CardUID: "aaaa-aaaa",
|
||||||
|
CheckInOut: 1,
|
||||||
|
Timestamp: time.Date(2025, 01, 01, 8, 0, 0, 0, time.UTC),
|
||||||
|
BookingType: testBookingType,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
CardUID: "aaaa-aaaa",
|
||||||
|
CheckInOut: 2,
|
||||||
|
Timestamp: time.Date(2025, 01, 01, 9, 0, 0, 0, time.UTC),
|
||||||
|
BookingType: testBookingType,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
CardUID: "aaaa-aaaa",
|
||||||
|
CheckInOut: 1,
|
||||||
|
Timestamp: time.Date(2025, 01, 01, 9, 45, 0, 0, time.UTC),
|
||||||
|
BookingType: testBookingType,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
CardUID: "aaaa-aaaa",
|
||||||
|
CheckInOut: 2,
|
||||||
|
Timestamp: time.Date(2025, 01, 01, 18, 00, 0, 0, time.UTC),
|
||||||
|
BookingType: testBookingType,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
var testBookings1030hrsBreak45min = []models.Booking{
|
||||||
|
{
|
||||||
|
CardUID: "aaaa-aaaa",
|
||||||
|
CheckInOut: 1,
|
||||||
|
Timestamp: time.Date(2025, 01, 01, 8, 0, 0, 0, time.UTC),
|
||||||
|
BookingType: testBookingType,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
CardUID: "aaaa-aaaa",
|
||||||
|
CheckInOut: 2,
|
||||||
|
Timestamp: time.Date(2025, 01, 01, 9, 0, 0, 0, time.UTC),
|
||||||
|
BookingType: testBookingType,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
CardUID: "aaaa-aaaa",
|
||||||
|
CheckInOut: 1,
|
||||||
|
Timestamp: time.Date(2025, 01, 01, 9, 45, 0, 0, time.UTC),
|
||||||
|
BookingType: testBookingType,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
CardUID: "aaaa-aaaa",
|
||||||
|
CheckInOut: 2,
|
||||||
|
Timestamp: time.Date(2025, 01, 01, 18, 30, 0, 0, time.UTC),
|
||||||
|
BookingType: testBookingType,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|||||||
@@ -116,7 +116,8 @@ func correctWorkPause(workIn, pauseIn time.Duration) (work, pause time.Duration)
|
|||||||
}
|
}
|
||||||
|
|
||||||
var diff time.Duration
|
var diff time.Duration
|
||||||
if workIn <= (9*time.Hour) && pauseIn < 30*time.Minute {
|
|
||||||
|
if (workIn+pauseIn) <= (9*time.Hour+30*time.Minute) && pauseIn <= 30*time.Minute {
|
||||||
diff = 30*time.Minute - pauseIn
|
diff = 30*time.Minute - pauseIn
|
||||||
} else if pauseIn < 45*time.Minute {
|
} else if pauseIn < 45*time.Minute {
|
||||||
diff = 45*time.Minute - pauseIn
|
diff = 45*time.Minute - pauseIn
|
||||||
|
|||||||
@@ -30,18 +30,63 @@ func TestWorkdayWorktimeDay(t *testing.T) {
|
|||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
testName: "Bookings6hrs",
|
testName: "Bookings6hrs",
|
||||||
bookings: testBookings6hrs,
|
bookings: testBookings6hrs, //work 6h
|
||||||
expectedTime: time.Hour * 6,
|
expectedTime: time.Hour * 6, //pause 0
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
testName: "Bookings8hrs",
|
testName: "Bookings8hrs",
|
||||||
bookings: testBookings8hrs,
|
bookings: testBookings8hrs, //work 8 pause 0
|
||||||
expectedTime: time.Hour*7 + time.Minute*30,
|
expectedTime: time.Hour*7 + time.Minute*30, //pause 30 --> corrected
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
testName: "Bookings10hrs",
|
testName: "Bookings10hrs",
|
||||||
bookings: testBookings10hrs,
|
bookings: testBookings10hrs, //work 10 pause 0
|
||||||
expectedTime: time.Hour*9 + time.Minute*15,
|
expectedTime: time.Hour*9 + time.Minute*15, //pause 45 --> corrected
|
||||||
|
},
|
||||||
|
{
|
||||||
|
testName: "Booking 6h with 30 min Break",
|
||||||
|
bookings: testBookings6hrsBreak30min, //work 6 pause 30
|
||||||
|
expectedTime: time.Hour * 6, //pause 30 --> bc real pause
|
||||||
|
},
|
||||||
|
{
|
||||||
|
testName: "Booking 6h 10min with 30 min Break",
|
||||||
|
bookings: testBookings610hrsBreak30min, //work 6 10 pause 30
|
||||||
|
expectedTime: time.Hour*6 + time.Minute*10, //pause 30 --> real pause
|
||||||
|
},
|
||||||
|
{
|
||||||
|
testName: "Booking 9h with 30 min Break",
|
||||||
|
bookings: testBookings9hrsBreak30min, //work 9 pause 30
|
||||||
|
expectedTime: time.Hour * 9, //pause 30 --> real pause
|
||||||
|
},
|
||||||
|
{
|
||||||
|
testName: "Booking 9h 30min",
|
||||||
|
bookings: testBookings930hrs, //work 9 30 pause 0
|
||||||
|
expectedTime: time.Hour * 9, //pause 30 --> corrected
|
||||||
|
},
|
||||||
|
{
|
||||||
|
testName: "Booking 9h 40min with 30min Break",
|
||||||
|
bookings: testBookings910hrsBreak30min, //work 9 10 pause 30
|
||||||
|
expectedTime: time.Hour*8 + time.Minute*55, //pause 45 --> real + corrected
|
||||||
|
},
|
||||||
|
{
|
||||||
|
testName: "Booking 9h 40min with 35min Break",
|
||||||
|
bookings: testBookings910hrsBreak35min, //work 9 10 pause 35
|
||||||
|
expectedTime: time.Hour * 9, //pause 45 --> real + corrected
|
||||||
|
},
|
||||||
|
{
|
||||||
|
testName: "Booking 9h 45min",
|
||||||
|
bookings: testBookings945hrs, //work 9 45 pause 0
|
||||||
|
expectedTime: time.Hour * 9, //pause 45 --> corrected
|
||||||
|
},
|
||||||
|
{
|
||||||
|
testName: "Booking 10h Break 45min",
|
||||||
|
bookings: testBookings10hrsBreak45min, //work 9 15 pause 45
|
||||||
|
expectedTime: time.Hour*9 + time.Minute*15, //pause 45 --> real
|
||||||
|
},
|
||||||
|
{
|
||||||
|
testName: "Booking 10h 30min Break 45min",
|
||||||
|
bookings: testBookings1030hrsBreak45min, //work 9 45 pause 45
|
||||||
|
expectedTime: time.Hour*9 + time.Minute*45, //pause 45 --> real
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -113,6 +158,51 @@ func TestWorkdayPausetimeDay(t *testing.T) {
|
|||||||
bookings: testBookings10hrs,
|
bookings: testBookings10hrs,
|
||||||
expectedTime: time.Minute * 45,
|
expectedTime: time.Minute * 45,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
testName: "Booking 6h with 30 min Break",
|
||||||
|
bookings: testBookings6hrsBreak30min, //work 6 pause 30
|
||||||
|
expectedTime: time.Minute * 30, //pause 30 --> bc real pause
|
||||||
|
},
|
||||||
|
{
|
||||||
|
testName: "Booking 6h 10min with 30 min Break",
|
||||||
|
bookings: testBookings610hrsBreak30min, //work 6 10 pause 30
|
||||||
|
expectedTime: time.Minute * 30, //pause 30 --> real pause
|
||||||
|
},
|
||||||
|
{
|
||||||
|
testName: "Booking 9h with 30 min Break",
|
||||||
|
bookings: testBookings9hrsBreak30min, //work 9 pause 30
|
||||||
|
expectedTime: time.Minute * 30, //pause 30 --> real pause
|
||||||
|
},
|
||||||
|
{
|
||||||
|
testName: "Booking 9h 30min",
|
||||||
|
bookings: testBookings930hrs, //work 9 30 pause 0
|
||||||
|
expectedTime: time.Minute * 30, //pause 30 --> corrected
|
||||||
|
},
|
||||||
|
{
|
||||||
|
testName: "Booking 9h 40min with 30min Break",
|
||||||
|
bookings: testBookings910hrsBreak30min, //work 9 10 pause 30
|
||||||
|
expectedTime: time.Minute * 45, //pause 45 --> real + corrected
|
||||||
|
},
|
||||||
|
{
|
||||||
|
testName: "Booking 9h 40min with 35min Break",
|
||||||
|
bookings: testBookings910hrsBreak35min, //work 9 10 pause 35
|
||||||
|
expectedTime: time.Minute * 45, //pause 45 --> real + corrected
|
||||||
|
},
|
||||||
|
{
|
||||||
|
testName: "Booking 9h 45min",
|
||||||
|
bookings: testBookings945hrs, //work 9 45 pause 0
|
||||||
|
expectedTime: time.Minute * 45, //pause 45 --> corrected
|
||||||
|
},
|
||||||
|
{
|
||||||
|
testName: "Booking 10h Break 45min",
|
||||||
|
bookings: testBookings10hrsBreak45min, //work 9 15 pause 45
|
||||||
|
expectedTime: time.Minute * 45, //pause 45 --> real
|
||||||
|
},
|
||||||
|
{
|
||||||
|
testName: "Booking 10h 30min Break 45min",
|
||||||
|
bookings: testBookings1030hrsBreak45min, //work 9 45 pause 45
|
||||||
|
expectedTime: time.Minute * 45, //pause 45 --> real
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, tc := range testCases {
|
for _, tc := range testCases {
|
||||||
|
|||||||
91
Readme.md
91
Readme.md
@@ -50,9 +50,100 @@ cd arbeitszeitmessung
|
|||||||
- `BACKUP_FOLDER` Pfad für DB Backup Datein
|
- `BACKUP_FOLDER` Pfad für DB Backup Datein
|
||||||
- `LOG_PATH` Pfad für Audit Logs
|
- `LOG_PATH` Pfad für Audit Logs
|
||||||
|
|
||||||
|
## Administration:
|
||||||
|
|
||||||
|
### Nutzer erstellen:
|
||||||
|
|
||||||
|
Nutzerdaten erstellen:
|
||||||
|
|
||||||
|
```sql
|
||||||
|
INSERT INTO "s_personal_daten"
|
||||||
|
(
|
||||||
|
"personal_nummer",
|
||||||
|
"vorname",
|
||||||
|
"nachname",
|
||||||
|
"card_uid",
|
||||||
|
"geburtsdatum",
|
||||||
|
"geschlecht",
|
||||||
|
"adresse",
|
||||||
|
"plz",
|
||||||
|
"hauptbeschaeftigungs_ort",
|
||||||
|
"aktiv_beschaeftigt",
|
||||||
|
"vorgesetzter_pers_nr",
|
||||||
|
"arbeitszeit_min_start",
|
||||||
|
"arbeitszeit_max_ende",
|
||||||
|
"arbeitszeit_per_tag",
|
||||||
|
"arbeitszeit_per_woche",
|
||||||
|
)
|
||||||
|
VALUES (
|
||||||
|
1,
|
||||||
|
'Max',
|
||||||
|
'Mustermann',
|
||||||
|
'acde-edca',
|
||||||
|
'2003-02-01',
|
||||||
|
1,
|
||||||
|
'Musterstr. 42',
|
||||||
|
'00001',
|
||||||
|
1,
|
||||||
|
true,
|
||||||
|
123,
|
||||||
|
'07:00:00',
|
||||||
|
'20:00:00',
|
||||||
|
8,
|
||||||
|
40
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
|
Nutzerpasswort generieren (kann auch später als Passwort reset genutzt werden):
|
||||||
|
|
||||||
|
```sql
|
||||||
|
INSERT INTO "user_password"
|
||||||
|
("personal_nummer", "pass_hash")
|
||||||
|
VALUES (123, crypt('password', gen_salt('bf')));
|
||||||
|
```
|
||||||
|
|
||||||
|
### Buchungstypen erstellen:
|
||||||
|
|
||||||
|
Ohne definierte Anwesenheits und Abwesenheitstypen funktioniert die Anwendung nicht!
|
||||||
|
|
||||||
|
Anwesenheiten:
|
||||||
|
|
||||||
|
```sql
|
||||||
|
INSERT INTO "s_anwesenheit_typen"
|
||||||
|
("anwesenheit_id", "anwesenheit_name")
|
||||||
|
VALUES (1, 'Büro');
|
||||||
|
```
|
||||||
|
|
||||||
|
Abwesenheiten:
|
||||||
|
|
||||||
|
```sql
|
||||||
|
INSERT INTO "s_abwesenheit_typen"
|
||||||
|
("abwesenheit_id", "abwesenheit_name", "arbeitszeit_equivalent")
|
||||||
|
VALUES (1, 'Urlaub', 100);
|
||||||
|
```
|
||||||
|
|
||||||
|
### Feiertage erstellen:
|
||||||
|
|
||||||
|
Die gesetzlichen Feiertage für Deutschland/Sachsen werden automatisch mit der Route `auto/feiertage` für das aktuelle Kalenderjahr erzeugt. Um weitere Unternehmensspezifische Feiertage (z.B. 24.12. oder 31.12.) mit in die Liste der Feiertage aufzunehmen, müssen diese manuell erstellt werden.
|
||||||
|
|
||||||
|
```sql
|
||||||
|
INSERT INTO "s_feiertage"
|
||||||
|
("datum", "name", "arbeitszeit_equivalent", "wiederholen")
|
||||||
|
VALUES ('2026-12-24', 'Helligabend', 50, 1);
|
||||||
|
```
|
||||||
|
|
||||||
|
Wenn `wiederholen` == 1 wird der Feiertag automatisch beim Aufruf von `auto/feiertage` mit ins nächste Jahr (am selben Datum) übernommen.
|
||||||
|
|
||||||
|
Das Feld `arbeitszeit_equivalent` `arbeitszeit_equivalent` ist die prozentuelle Zeit am Tag welche durch diesen Eintrag eingenommen wird. (dies gilt auch für die [Buchungstypen](#buchungstypen-erstellen))
|
||||||
|
|
||||||
|
Alle weiteren Tabellen sollte ausschließlich über die Weboberfläche oder per API befüllt werden.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
# Filestrukture
|
# Filestrukture
|
||||||
|
|
||||||
```
|
```
|
||||||
|
|
||||||
├── Backend (Webserver)
|
├── Backend (Webserver)
|
||||||
│ ├── doc (Templates for Document Creator --> typst used to create PDF Reports)
|
│ ├── doc (Templates for Document Creator --> typst used to create PDF Reports)
|
||||||
│ │ ├── static
|
│ │ ├── static
|
||||||
|
|||||||
Reference in New Issue
Block a user