Add and document typst template

This commit is contained in:
Joscha 2024-04-30 21:27:13 +02:00
parent 3a687cdb36
commit 2131c2fb0d
4 changed files with 562 additions and 0 deletions

View file

@ -5,3 +5,12 @@ inspired by the [SDQ TimeSheetGenerator][tsg] and a friend's very useful wrapper
website that spits out PDFs directly.
[tsg]: https://github.com/kit-sdq/TimeSheetGenerator
## Typst template
The centerpiece of the repository is a [typst][typst] template that generates
and validates time sheets. It aims to mirror the look of the official form as
closely as possible. For usage information, see [its docs][tdocs].
[typst]: https://github.com/typst/typst
[tdocs]: kit_timesheet.md

47
kit_logo.svg Normal file
View file

@ -0,0 +1,47 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 196.18 90.32" width="196.18px" height="90.32px" x="0px" y="0px" role="img" aria-labelledby="kit-logo-alt-title">
<title id="kit-logo-alt-title">KIT - Karlsruher Institut für Technologie</title>
<defs>
<style>.cls-1,.cls-2,.cls-3{fill-rule:evenodd;}.cls-2{fill:#fff;}.cls-3{fill:#009682;}</style></defs>
<path class="cls-1" d="M195.75.47h-46.3V16.23H163V70.59h19.22V16.23h13.54V.47ZM144.84,70.52H125.62V.47h19.22V70.52Zm-22.46,0H101L75.58,43.23V27.61L100.2.39h22L93.87,35.67l28.51,34.85Z" transform="translate(0 -0.39)"></path>
<polygon class="cls-2" points="70.97 70.13 70.97 70.13 70.97 70.13 70.97 70.13 70.97 70.13"></polygon>
<polygon class="cls-2" points="70.97 70.13 70.97 70.13 70.97 70.13 70.97 70.13 70.97 70.13"></polygon>
<path class="cls-3" d="M16.18,26.1A68.63,68.63,0,0,0,6.89,40.71L71,70.52,16.18,26.1Z" transform="translate(0 -0.39)"></path>
<path class="cls-3" d="M71,70.52,41.09,7A69.32,69.32,0,0,0,26.4,16L71,70.52Z" transform="translate(0 -0.39)"></path>
<path class="cls-1" d="M71,70.52,71,.47h-.58a70.22,70.22,0,0,0-16.55,2L71,70.52Z" transform="translate(0 -0.39)"></path>
<polygon class="cls-2" points="70.97 70.13 70.97 70.13 70.97 70.13 70.97 70.13 70.97 70.13 70.97 70.13"></polygon>
<polygon class="cls-2" points="70.97 70.13 70.97 70.13 70.97 70.13 70.97 70.13 70.97 70.13 70.97 70.13"></polygon>
<path class="cls-3" d="M2.28,53.38A70.75,70.75,0,0,0,.05,70.52H71L2.28,53.38Z" transform="translate(0 -0.39)"></path>
<g class="logo-text">
<polygon points="0 87.54 1.25 87.54 1.25 83.28 5.13 87.54 6.95 87.54 2.53 82.95 6.63 78.83 4.97 78.83 1.25 82.67 1.25 78.83 0 78.83 0 87.54 0 87.54"></polygon>
<path d="M8.44,83A2.47,2.47,0,0,1,9,82.68a2.87,2.87,0,0,1,.64-.23,3,3,0,0,1,.67-.08,1.88,1.88,0,0,1,.92.19,1,1,0,0,1,.5.55,2.5,2.5,0,0,1,.15.92H10.61a5.19,5.19,0,0,0-.77.06,4.2,4.2,0,0,0-.79.22,2.55,2.55,0,0,0-.71.38,2.05,2.05,0,0,0-.5.6,2,2,0,0,0-.19.83A2.12,2.12,0,0,0,8,87.23a1.71,1.71,0,0,0,.82.65,3.25,3.25,0,0,0,1.22.21,2.66,2.66,0,0,0,.72-.1,2.2,2.2,0,0,0,.56-.24,2.89,2.89,0,0,0,.42-.32,3.29,3.29,0,0,0,.27-.31h0v.82h1.1a1.47,1.47,0,0,1,0-.25,3.9,3.9,0,0,1,0-.4c0-.16,0-.36,0-.6V84.06a3.5,3.5,0,0,0-.26-1.46,1.78,1.78,0,0,0-.83-.88,3.09,3.09,0,0,0-1.47-.3,4.11,4.11,0,0,0-1.13.17,4.19,4.19,0,0,0-1,.43l.06,1Zm3.42,2.45a1.81,1.81,0,0,1-.2.85,1.57,1.57,0,0,1-.6.6,2,2,0,0,1-1,.22,1.23,1.23,0,0,1-.42-.07,1.5,1.5,0,0,1-.38-.19A1,1,0,0,1,9,86.55a.94.94,0,0,1-.11-.45.88.88,0,0,1,.17-.55,1.22,1.22,0,0,1,.42-.36,2.27,2.27,0,0,1,.57-.2,3.05,3.05,0,0,1,.57-.09,4.36,4.36,0,0,1,.48,0h.37l.38,0v.56Z" transform="translate(0 -0.39)"></path>
<path d="M14.94,87.94h1.18V85a4.09,4.09,0,0,1,.21-1.38,2,2,0,0,1,.6-.89,1.38,1.38,0,0,1,.92-.32,1.77,1.77,0,0,1,.32,0,2,2,0,0,1,.33.09v-1l-.23-.05-.25,0-.29,0a1.76,1.76,0,0,0-.67.15,2,2,0,0,0-.57.4,1.87,1.87,0,0,0-.42.59h0v-1h-1.1v6.37Z" transform="translate(0 -0.39)"></path>
<polygon points="19.87 87.54 21.05 87.54 21.05 78.19 19.87 78.19 19.87 87.54 19.87 87.54"></polygon>
<path d="M22.58,87.74a2.93,2.93,0,0,0,.85.28,5.46,5.46,0,0,0,.9.07,2.87,2.87,0,0,0,.83-.12,2.27,2.27,0,0,0,.71-.36,1.75,1.75,0,0,0,.49-.62,1.91,1.91,0,0,0,.19-.89,1.61,1.61,0,0,0-.15-.73,1.58,1.58,0,0,0-.4-.52,3.29,3.29,0,0,0-.53-.36l-.59-.28a4.54,4.54,0,0,1-.54-.26,1.31,1.31,0,0,1-.39-.34.8.8,0,0,1-.16-.46.71.71,0,0,1,.16-.44.89.89,0,0,1,.41-.25,1.64,1.64,0,0,1,.56-.09l.27,0,.35.07.35.11a1.06,1.06,0,0,1,.26.12l.1-1c-.23-.07-.46-.12-.69-.17a4.66,4.66,0,0,0-.77-.08,2.8,2.8,0,0,0-.88.13,1.91,1.91,0,0,0-.71.36,1.52,1.52,0,0,0-.48.61,2.06,2.06,0,0,0-.17.86,1.34,1.34,0,0,0,.15.66,1.69,1.69,0,0,0,.39.49,5.07,5.07,0,0,0,.54.36l.59.29.54.29a1.62,1.62,0,0,1,.39.33.64.64,0,0,1,.15.43.79.79,0,0,1-.39.71,1.32,1.32,0,0,1-.39.15,1.48,1.48,0,0,1-.41,0,2.58,2.58,0,0,1-.43,0,3.06,3.06,0,0,1-.53-.15,2.59,2.59,0,0,1-.5-.25l-.07,1Z" transform="translate(0 -0.39)"></path>
<path d="M27.9,87.94h1.17V85a4.34,4.34,0,0,1,.22-1.38,1.9,1.9,0,0,1,.6-.89,1.38,1.38,0,0,1,.92-.32,1.77,1.77,0,0,1,.32,0,2,2,0,0,1,.33.09v-1l-.23-.05-.25,0-.3,0a1.76,1.76,0,0,0-.66.15,2.23,2.23,0,0,0-.58.4,2.26,2.26,0,0,0-.42.59h0v-1H27.9v6.37Z" transform="translate(0 -0.39)"></path>
<path d="M38.31,81.57H37.14v3A4.35,4.35,0,0,1,37,85.58a2.31,2.31,0,0,1-.35.83,1.72,1.72,0,0,1-.6.54,1.8,1.8,0,0,1-.87.19,1.31,1.31,0,0,1-.8-.29A1.37,1.37,0,0,1,34,86.1a4.37,4.37,0,0,1-.12-1.23v-3.3H32.68v3.79a3.94,3.94,0,0,0,.25,1.46,1.93,1.93,0,0,0,.79.94,2.5,2.5,0,0,0,1.35.33,2.7,2.7,0,0,0,.85-.14,2.24,2.24,0,0,0,.73-.4,1.76,1.76,0,0,0,.5-.62h0v1h1.13V81.57Z" transform="translate(0 -0.39)"></path>
<path d="M40.23,87.94h1.18V85a4.93,4.93,0,0,1,.11-1.06,2.84,2.84,0,0,1,.35-.83,1.67,1.67,0,0,1,.61-.54,2.06,2.06,0,0,1,.86-.19,1.34,1.34,0,0,1,.81.29,1.44,1.44,0,0,1,.42.75,5.32,5.32,0,0,1,.12,1.23v3.3h1.17V84.16a3.62,3.62,0,0,0-.25-1.47,2,2,0,0,0-.78-.94,2.69,2.69,0,0,0-1.35-.33,2.75,2.75,0,0,0-1.2.28,2.19,2.19,0,0,0-.85.78h0v-3.9H40.23v9.36Z" transform="translate(0 -0.39)"></path>
<path d="M52.67,86.5a2,2,0,0,1-.32.2,3.63,3.63,0,0,1-.48.21,3.41,3.41,0,0,1-.57.16,2.44,2.44,0,0,1-.59.07,2.15,2.15,0,0,1-.8-.15,2.29,2.29,0,0,1-.67-.44,2.1,2.1,0,0,1-.46-.67,2.2,2.2,0,0,1-.17-.86h4.54A5.88,5.88,0,0,0,53,83.59a3.41,3.41,0,0,0-.46-1.14,2.26,2.26,0,0,0-.87-.75,3.11,3.11,0,0,0-1.33-.28,3.21,3.21,0,0,0-1.23.24,2.74,2.74,0,0,0-.94.67,2.87,2.87,0,0,0-.6,1,4.05,4.05,0,0,0-.22,1.29,4.87,4.87,0,0,0,.2,1.42,2.85,2.85,0,0,0,.6,1.08,2.59,2.59,0,0,0,1,.7,3.87,3.87,0,0,0,1.47.25,5.64,5.64,0,0,0,.89-.07,5.74,5.74,0,0,0,.63-.16l.49-.18V86.5Zm-4.06-2.36a2.9,2.9,0,0,1,.14-.62,2,2,0,0,1,.32-.57,1.51,1.51,0,0,1,.51-.42,1.84,1.84,0,0,1,.74-.16,1.74,1.74,0,0,1,.67.14,1.36,1.36,0,0,1,.5.38,1.57,1.57,0,0,1,.31.57,2.15,2.15,0,0,1,.11.68Z" transform="translate(0 -0.39)"></path> <path d="M54.65,87.94h1.18V85A4.09,4.09,0,0,1,56,83.66a2,2,0,0,1,.61-.89,1.33,1.33,0,0,1,.91-.32,1.77,1.77,0,0,1,.32,0,2,2,0,0,1,.33.09v-1L58,81.46l-.24,0-.3,0a1.76,1.76,0,0,0-.67.15,2,2,0,0,0-.57.4,1.87,1.87,0,0,0-.42.59h0v-1h-1.1v6.37Z" transform="translate(0 -0.39)"></path>
<polygon points="63.02 87.54 64.26 87.54 64.26 78.83 63.02 78.83 63.02 87.54 63.02 87.54"></polygon>
<path d="M66.3,87.94h1.17V85a4.35,4.35,0,0,1,.12-1.06,2.39,2.39,0,0,1,.35-.83,1.72,1.72,0,0,1,.6-.54,2.14,2.14,0,0,1,.87-.19,1.28,1.28,0,0,1,.8.29,1.37,1.37,0,0,1,.42.75,4.78,4.78,0,0,1,.13,1.23v3.3h1.17V84.16a3.8,3.8,0,0,0-.25-1.47,2,2,0,0,0-.78-.94,2.76,2.76,0,0,0-1.36-.33,2.7,2.7,0,0,0-.85.14A2.58,2.58,0,0,0,68,82a1.88,1.88,0,0,0-.5.62h0v-1H66.3v6.37Z" transform="translate(0 -0.39)"></path>
<path d="M73.32,87.74a2.83,2.83,0,0,0,.85.28,5.26,5.26,0,0,0,.89.07,2.82,2.82,0,0,0,.83-.12,2.27,2.27,0,0,0,.71-.36,1.9,1.9,0,0,0,.5-.62,2,2,0,0,0,.18-.89,1.61,1.61,0,0,0-.15-.73,1.69,1.69,0,0,0-.39-.52,3.38,3.38,0,0,0-.54-.36l-.59-.28c-.19-.08-.37-.17-.54-.26a1.45,1.45,0,0,1-.39-.34.73.73,0,0,1-.15-.46.65.65,0,0,1,.15-.44,1,1,0,0,1,.41-.25,1.69,1.69,0,0,1,.56-.09l.27,0,.35.07a2.89,2.89,0,0,1,.35.11,1.14,1.14,0,0,1,.27.12l.1-1c-.23-.07-.47-.12-.7-.17a4.55,4.55,0,0,0-.77-.08,2.8,2.8,0,0,0-.88.13,1.91,1.91,0,0,0-.71.36,1.72,1.72,0,0,0-.48.61,2.06,2.06,0,0,0-.17.86,1.34,1.34,0,0,0,.15.66,1.57,1.57,0,0,0,.4.49,4.1,4.1,0,0,0,.53.36l.59.29c.2.1.38.19.54.29a1.44,1.44,0,0,1,.39.33.71.71,0,0,1,.16.43.83.83,0,0,1-.12.43.75.75,0,0,1-.28.28,1.32,1.32,0,0,1-.39.15,1.48,1.48,0,0,1-.41,0,2.58,2.58,0,0,1-.43,0,2.74,2.74,0,0,1-.52-.15,3,3,0,0,1-.51-.25l-.06,1Z" transform="translate(0 -0.39)"></path>
<path d="M82,81.57H80.33V79.73l-1.17.37v1.47H77.72v1h1.44v3.72a2.07,2.07,0,0,0,.24,1.05,1.45,1.45,0,0,0,.67.6,2.55,2.55,0,0,0,1.05.2,2.35,2.35,0,0,0,.54-.06l.46-.12v-1a1.7,1.7,0,0,1-.33.15,1.37,1.37,0,0,1-.47.07,1,1,0,0,1-.5-.13.92.92,0,0,1-.35-.38,1.33,1.33,0,0,1-.14-.63V82.52H82v-1Z" transform="translate(0 -0.39)"></path>
<path d="M83.57,87.94h1.17V81.57H83.57v6.37Zm1.24-9.11H83.49v1.32h1.32V78.83Z" transform="translate(0 -0.39)"></path>
<path d="M90.19,81.57H88.51V79.73l-1.17.37v1.47H85.9v1h1.44v3.72a2.07,2.07,0,0,0,.24,1.05,1.4,1.4,0,0,0,.67.6,2.55,2.55,0,0,0,1,.2,2.42,2.42,0,0,0,.54-.06l.45-.12v-1a1.43,1.43,0,0,1-.79.22A.93.93,0,0,1,89,87a.86.86,0,0,1-.35-.38,1.33,1.33,0,0,1-.14-.63V82.52h1.68v-1Z" transform="translate(0 -0.39)"></path>
<path d="M97.22,81.57H96.05v3a4.35,4.35,0,0,1-.12,1.06,2.31,2.31,0,0,1-.35.83A1.72,1.72,0,0,1,95,87a1.8,1.8,0,0,1-.87.19,1.33,1.33,0,0,1-.8-.29,1.37,1.37,0,0,1-.42-.75,4.37,4.37,0,0,1-.12-1.23v-3.3H91.59v3.79a3.94,3.94,0,0,0,.25,1.46,2,2,0,0,0,.79.94,2.5,2.5,0,0,0,1.35.33,2.7,2.7,0,0,0,.85-.14,2.24,2.24,0,0,0,.73-.4,1.76,1.76,0,0,0,.5-.62h0v1h1.13V81.57Z" transform="translate(0 -0.39)"></path>
<path d="M102.53,81.57h-1.69V79.73l-1.17.37v1.47H98.23v1h1.44v3.72a2.07,2.07,0,0,0,.24,1.05,1.45,1.45,0,0,0,.67.6,2.59,2.59,0,0,0,1,.2,2.35,2.35,0,0,0,.54-.06l.46-.12v-1a1.7,1.7,0,0,1-.33.15,1.33,1.33,0,0,1-.47.07,1,1,0,0,1-.5-.13,1,1,0,0,1-.35-.38,1.33,1.33,0,0,1-.14-.63V82.52h1.69v-1Z" transform="translate(0 -0.39)"></path>
<path d="M108.16,87.94h1.17V82.52H111v-1h-1.69v-.68a3.7,3.7,0,0,1,0-.6,1.23,1.23,0,0,1,.15-.48.81.81,0,0,1,.34-.32,1.48,1.48,0,0,1,.61-.11h.3l.26.06,0-1-.37,0-.42,0a2.69,2.69,0,0,0-1.06.18,1.49,1.49,0,0,0-.66.52,2.13,2.13,0,0,0-.32.8,5,5,0,0,0-.09,1.06v.58h-1.5v1h1.5v5.42Z" transform="translate(0 -0.39)"></path>
<path d="M113.39,80.6h1.17V79.35h-1.17V80.6Zm2,0h1.17V79.35h-1.17V80.6Zm2.37,1h-1.18v3a4.37,4.37,0,0,1-.11,1.06,2.5,2.5,0,0,1-.35.83,1.67,1.67,0,0,1-.61.54,1.79,1.79,0,0,1-.86.19,1.34,1.34,0,0,1-.81-.29,1.51,1.51,0,0,1-.42-.75,5.26,5.26,0,0,1-.12-1.23v-3.3h-1.17v3.79a3.75,3.75,0,0,0,.25,1.46,2,2,0,0,0,.78.94,2.5,2.5,0,0,0,1.35.33,2.72,2.72,0,0,0,.86-.14,2.47,2.47,0,0,0,.73-.4,1.88,1.88,0,0,0,.5-.62h0v1h1.14V81.57Z" transform="translate(0 -0.39)"></path>
<path d="M119.73,87.94h1.17V85a4.34,4.34,0,0,1,.22-1.38,1.9,1.9,0,0,1,.6-.89,1.38,1.38,0,0,1,.92-.32,1.77,1.77,0,0,1,.32,0,2,2,0,0,1,.33.09v-1l-.24-.05-.24,0-.3,0a1.76,1.76,0,0,0-.66.15,2.23,2.23,0,0,0-.58.4,2.26,2.26,0,0,0-.42.59h0v-1h-1.1v6.37Z" transform="translate(0 -0.39)"></path>
<polygon points="129.83 87.54 131.08 87.54 131.08 79.93 133.78 79.93 133.78 78.83 127.12 78.83 127.12 79.93 129.83 79.93 129.83 87.54 129.83 87.54"></polygon>
<path d="M138.8,86.5a1.52,1.52,0,0,1-.31.2,3.63,3.63,0,0,1-.48.21,3.41,3.41,0,0,1-.57.16,2.46,2.46,0,0,1-.6.07,2.09,2.09,0,0,1-.79-.15,2.16,2.16,0,0,1-.67-.44,2,2,0,0,1-.46-.67,2.2,2.2,0,0,1-.17-.86h4.54a7.08,7.08,0,0,0-.14-1.43,3.23,3.23,0,0,0-.47-1.14,2.23,2.23,0,0,0-.86-.75,3.13,3.13,0,0,0-1.34-.28,3.16,3.16,0,0,0-1.22.24,2.65,2.65,0,0,0-.94.67,2.75,2.75,0,0,0-.61,1,3.79,3.79,0,0,0-.21,1.29,4.56,4.56,0,0,0,.2,1.42,2.85,2.85,0,0,0,.6,1.08,2.52,2.52,0,0,0,1,.7,3.81,3.81,0,0,0,1.46.25,4.67,4.67,0,0,0,1.53-.23l.48-.18V86.5Zm-4.05-2.36a2.9,2.9,0,0,1,.14-.62,1.78,1.78,0,0,1,.32-.57,1.51,1.51,0,0,1,.51-.42,1.8,1.8,0,0,1,.74-.16,1.74,1.74,0,0,1,.67.14,1.45,1.45,0,0,1,.5.38,1.74,1.74,0,0,1,.31.57,2.15,2.15,0,0,1,.1.68Z" transform="translate(0 -0.39)"></path>
<path d="M144.93,81.67l-.41-.12a4,4,0,0,0-.5-.09,4.75,4.75,0,0,0-.51,0,3.75,3.75,0,0,0-1.33.25,3.06,3.06,0,0,0-1,.69,3,3,0,0,0-.66,1.06,3.59,3.59,0,0,0-.23,1.34,3.73,3.73,0,0,0,.23,1.29,3.2,3.2,0,0,0,.67,1,3.05,3.05,0,0,0,1,.72,3.35,3.35,0,0,0,1.32.27l.46,0a3,3,0,0,0,.51-.08,2.24,2.24,0,0,0,.5-.16l-.08-1a3,3,0,0,1-.4.2,3.6,3.6,0,0,1-.44.12,2.15,2.15,0,0,1-.43,0,2.24,2.24,0,0,1-.8-.14,1.69,1.69,0,0,1-.59-.37,1.93,1.93,0,0,1-.41-.54,2.54,2.54,0,0,1-.24-.65,3.23,3.23,0,0,1-.08-.68,2.91,2.91,0,0,1,.14-.89,2.41,2.41,0,0,1,.4-.76,1.87,1.87,0,0,1,.64-.54,2,2,0,0,1,.87-.2,2.72,2.72,0,0,1,.61.07,2.81,2.81,0,0,1,.66.21l.1-1Z" transform="translate(0 -0.39)"></path>
<path d="M146.26,87.94h1.17V85a4.93,4.93,0,0,1,.11-1.06,2.61,2.61,0,0,1,.36-.83,1.64,1.64,0,0,1,.6-.54,2.07,2.07,0,0,1,.87-.19,1.33,1.33,0,0,1,.8.29,1.37,1.37,0,0,1,.42.75,4.82,4.82,0,0,1,.12,1.23v3.3h1.18V84.16a3.79,3.79,0,0,0-.26-1.47,1.91,1.91,0,0,0-.78-.94,2.8,2.8,0,0,0-2.54,0,2.13,2.13,0,0,0-.85.78h0v-3.9h-1.17v9.36Z" transform="translate(0 -0.39)"></path>
<path d="M153.81,87.94H155V85a4.35,4.35,0,0,1,.12-1.06,2.39,2.39,0,0,1,.35-.83,1.72,1.72,0,0,1,.6-.54,2.1,2.1,0,0,1,.87-.19,1.28,1.28,0,0,1,.8.29,1.37,1.37,0,0,1,.42.75,4.78,4.78,0,0,1,.13,1.23v3.3h1.17V84.16a3.8,3.8,0,0,0-.25-1.47,1.93,1.93,0,0,0-.79-.94,2.82,2.82,0,0,0-2.2-.19,2.58,2.58,0,0,0-.73.4,1.88,1.88,0,0,0-.5.62h0v-1h-1.14v6.37Z" transform="translate(0 -0.39)"></path>
<path d="M160.94,84.76a3.73,3.73,0,0,0,.23,1.29,3.2,3.2,0,0,0,.67,1,3.05,3.05,0,0,0,1,.72,3.35,3.35,0,0,0,1.32.27,3.26,3.26,0,0,0,1.31-.27,2.86,2.86,0,0,0,1-.72,3.2,3.2,0,0,0,.67-1,4,4,0,0,0,.24-1.29,3.8,3.8,0,0,0-.24-1.34,3.09,3.09,0,0,0-.65-1.06,3.21,3.21,0,0,0-1-.69,3.65,3.65,0,0,0-1.32-.25,3.75,3.75,0,0,0-1.33.25,3.28,3.28,0,0,0-1,.69,3.12,3.12,0,0,0-.66,1.06,3.8,3.8,0,0,0-.23,1.34Zm1.25,0a2.9,2.9,0,0,1,.13-.89,2.6,2.6,0,0,1,.39-.76,1.8,1.8,0,0,1,1.47-.74,1.89,1.89,0,0,1,.84.2,2,2,0,0,1,.62.54,2.6,2.6,0,0,1,.39.76,2.91,2.91,0,0,1,.14.89,3.23,3.23,0,0,1-.08.68,2.52,2.52,0,0,1-.23.65,2.07,2.07,0,0,1-.39.54,1.62,1.62,0,0,1-.55.37,1.91,1.91,0,0,1-.74.14,2,2,0,0,1-.74-.14,1.76,1.76,0,0,1-.56-.37,2.07,2.07,0,0,1-.39-.54,3,3,0,0,1-.23-.65,3.21,3.21,0,0,1-.07-.68Z" transform="translate(0 -0.39)"></path>
<polygon points="169.06 87.54 170.24 87.54 170.24 78.19 169.06 78.19 169.06 87.54 169.06 87.54"></polygon>
<path d="M171.89,84.76a3.73,3.73,0,0,0,.23,1.29,3.2,3.2,0,0,0,.67,1,3.05,3.05,0,0,0,1,.72,3.35,3.35,0,0,0,1.32.27,3.26,3.26,0,0,0,1.31-.27,2.86,2.86,0,0,0,1-.72,3.2,3.2,0,0,0,.67-1,3.73,3.73,0,0,0,.23-1.29,3.59,3.59,0,0,0-.23-1.34,3,3,0,0,0-.66-1.06,3.06,3.06,0,0,0-1-.69,3.65,3.65,0,0,0-1.32-.25,3.75,3.75,0,0,0-1.33.25,3.17,3.17,0,0,0-1,.69,3,3,0,0,0-.66,1.06,3.8,3.8,0,0,0-.23,1.34Zm1.24,0a2.91,2.91,0,0,1,.14-.89,2.6,2.6,0,0,1,.39-.76,1.93,1.93,0,0,1,.63-.54,1.89,1.89,0,0,1,.84-.2,1.93,1.93,0,0,1,.84.2,2,2,0,0,1,.62.54,2.6,2.6,0,0,1,.39.76,2.91,2.91,0,0,1,.14.89,3.23,3.23,0,0,1-.08.68,2.52,2.52,0,0,1-.23.65,2.07,2.07,0,0,1-.39.54,1.62,1.62,0,0,1-.55.37,2,2,0,0,1-1.48,0,1.66,1.66,0,0,1-.56-.37,2.07,2.07,0,0,1-.39-.54,3,3,0,0,1-.23-.65,3.23,3.23,0,0,1-.08-.68Z" transform="translate(0 -0.39)"></path>
<path d="M185.49,81.57h-1.08v1h0a1.85,1.85,0,0,0-.45-.59,1.78,1.78,0,0,0-.68-.37,3.06,3.06,0,0,0-1-.14,2.82,2.82,0,0,0-1,.2,2.69,2.69,0,0,0-.8.52,3,3,0,0,0-.56.77,3.62,3.62,0,0,0-.34.92,4.6,4.6,0,0,0-.11,1,4.16,4.16,0,0,0,.17,1.18,3,3,0,0,0,.51,1,2.4,2.4,0,0,0,.85.69,2.75,2.75,0,0,0,1.19.26,3.09,3.09,0,0,0,.8-.11,2.58,2.58,0,0,0,.74-.33,2.06,2.06,0,0,0,.59-.55h0v.63a2.58,2.58,0,0,1-.24,1.16,1.64,1.64,0,0,1-.67.75,2,2,0,0,1-1.06.27,5.6,5.6,0,0,1-.6,0,5.12,5.12,0,0,1-.54-.1l-.54-.18-.62-.26-.12,1.08a7.74,7.74,0,0,0,.89.27,4.17,4.17,0,0,0,.73.13,4.87,4.87,0,0,0,.66,0,4.24,4.24,0,0,0,1.87-.36,2.3,2.3,0,0,0,1.07-1,3.6,3.6,0,0,0,.34-1.65V81.57Zm-3,5.42a1.5,1.5,0,0,1-.74-.18,1.76,1.76,0,0,1-.56-.49,2.41,2.41,0,0,1-.35-.72,3,3,0,0,1-.12-.86,3,3,0,0,1,.07-.66,2.4,2.4,0,0,1,.21-.64,2.06,2.06,0,0,1,.35-.55,1.7,1.7,0,0,1,.5-.38,1.63,1.63,0,0,1,.64-.14,1.81,1.81,0,0,1,.82.2,1.83,1.83,0,0,1,.58.53,2.45,2.45,0,0,1,.34.79,3.56,3.56,0,0,1,.12.94,2.55,2.55,0,0,1-.14.84,2.31,2.31,0,0,1-.38.69,1.78,1.78,0,0,1-.59.46,1.66,1.66,0,0,1-.75.17Z" transform="translate(0 -0.39)"></path>
<path d="M187.57,87.94h1.17V81.57h-1.17v6.37Zm1.24-9.11h-1.32v1.32h1.32V78.83Z" transform="translate(0 -0.39)"></path>
<path d="M195.69,86.5a1.64,1.64,0,0,1-.32.2,3.13,3.13,0,0,1-.48.21,3.58,3.58,0,0,1-.56.16,2.46,2.46,0,0,1-.6.07,2.09,2.09,0,0,1-.79-.15,2.16,2.16,0,0,1-.67-.44,2,2,0,0,1-.46-.67,2.2,2.2,0,0,1-.17-.86h4.54a7.08,7.08,0,0,0-.14-1.43,3.43,3.43,0,0,0-.47-1.14,2.19,2.19,0,0,0-.87-.75,3.08,3.08,0,0,0-1.33-.28,3.25,3.25,0,0,0-1.23.24,2.7,2.7,0,0,0-.93.67,2.75,2.75,0,0,0-.61,1,3.79,3.79,0,0,0-.21,1.29,4.88,4.88,0,0,0,.19,1.42,3,3,0,0,0,.61,1.08,2.55,2.55,0,0,0,1,.7,3.9,3.9,0,0,0,1.47.25,5.44,5.44,0,0,0,.89-.07,6,6,0,0,0,.64-.16l.48-.18V86.5Zm-4.05-2.36a2.4,2.4,0,0,1,.14-.62,2,2,0,0,1,.31-.57,1.66,1.66,0,0,1,.52-.42,1.8,1.8,0,0,1,.74-.16,1.78,1.78,0,0,1,.67.14,1.45,1.45,0,0,1,.5.38,1.55,1.55,0,0,1,.3.57,2.15,2.15,0,0,1,.11.68Z" transform="translate(0 -0.39)"></path>
</g>
</svg>

After

Width:  |  Height:  |  Size: 16 KiB

132
kit_timesheet.md Normal file
View file

@ -0,0 +1,132 @@
# The `kit_timesheet` typst module
This file exhaustively documents the `kit_timesheet` typst module and its
exported values.
## Quickstart
```typst
#import "kit_timesheet.typ" as ts
#ts.timesheet(
name: [McStudentface, Student],
staff_id: 1337420,
department: [Institut für Informatik],
working_area: ts.areas.Großforschung,
monthly_hours: 40,
hourly_wage: [14.09],
carry_prev_month: "02:30",
year: 2024,
month: 1,
ts.entry("Urlaub", 2, "12:00", "14:00", note: ts.notes.Urlaub),
ts.entry("Mate trinken", 3, "14:00", "15:30"),
ts.entry("Im Bett liegen", 4, "10:00", "12:00"),
ts.entry("Stundenzettel ausfüllen", 31, "14:00", "15:30", rest: "01:00"),
)
```
## Times and durations
Some function arguments represent a time of day or a duration. They expect a
string of the form `HH:MM` or `-HH:MM`, e.g. `12:34`. More specifically, the
string must match the regex `^-?[0-9]+:[0-5][0-9]$`.
This makes it possible to specify negative durations, which can be useful if you
didn't work enough hours last month and want to specify a negative
`carry_last_month` value.
A time of day is just a duration starting from midnight (00:00).
## Members
### `areas` (dictionary)
The `areas` dict contains the enum-like string values accepted by the
`working_area` parameter of the `timesheet` function. See its documentation for
more detail.
Available entries are:
- `areas.Großforschung` (value: `"GF"`)
- `areas.Unibereich` (value: `"UB"`)
### `notes` (dictionary)
The `notes` dict contains the enum-like string values accepted by the `note`
parameter of the `entry` function. See its documentation as well as footnote 1
in the generated document for more detail.
Available entries are:
- `notes.Urlaub` (value: `"U"`)
- `notes.Krankheit` (value: `"K"`)
- `notes.Feiertage` (value: `"F"`)
- `notes.Sonstiges` (value: `"S"`)
### `entry` (function)
Create a single entry for the `entries` parameter of the `timesheet` function.
An entry corresponds exactly to a row in the resulting document.
Positional arguments:
- `task`:
What you worked on. Corresponds to the _Tätigkeit_ column of the table.
- `day`:
The day of month. Corresponds to the _Datum_ column of the table.
- `start`:
When you started working. Corresponds to the _Beginn_ column of the table.
- `end`:
When you stopped working. Corresponds to the _Ende_ column of the table.
Named arguments:
- `rest` (default: `"00:00"`):
Break time. Corresponds to the _Pause_ column of the table. If it wasn't a
keyword, I'd have called it `break` :D
- `note` (default: `none`):
Additional note for the _Arbeitszeit_ column (whose value is automatically
calculated). Entries with a note of `notes.Urlaub` are used to calculate the
_Urlaub anteilig_ field in the summary table. See the `notes` dictionary for
all available values.
### `timesheet` (function)
Generate and validate a full timesheet.
Positional arguments:
- `..entries`:
All positional arguments are entries for the big table, created using the
`entry` function.
Named arguments:
- `name`:
Your name (Name, Vorname).
- `staff_id`:
Your staff id (Personalnummer).
- `department`:
Your department (Organisationseinheit/OE).
- `working_area`:
Your working area (Großforschung/GF or Unibereich/UB). Corresponds to the _GF_
and _UB_ checkboxes on the form. See the `areas` dictionary for all available
values.
- `monthly_hours`:
How many hours per month your contract says you should work (Vertraglich
vereinbarte Arbeitszeit).
- `hourly_wage`:
Your hourly wage (Stundensatz).
- `validate` (default: `true`):
Whether the template should try to validate the data you entered (check if
values look wrong, if you worked on a holiday, ...). If you turn this off, you
can do funky things like work a negative amount of time or on Sundays.
- `year`:
The year this time sheet is being generated for.
- `month`:
The month this time sheet is being generated for.
### `timesheet_empty` (function)
Generate an empty timesheet. Useful if you want to fill it out by hand. Not sure
why anyone would want to do this though :P

374
kit_timesheet.typ Normal file
View file

@ -0,0 +1,374 @@
/////////////
// "Enums" //
/////////////
#let areas = (Großforschung: "GF", Unibereich: "UB")
#let notes = (Urlaub: "U", Krankheit: "K", Feiertag: "F", Sonstiges: "S")
////////////
// Layout //
////////////
#let _kit_green = rgb("#009682")
#let _kit_stroke = 0.2mm
#let _frame(body) = {
set text(lang: "de", font: "Liberation Sans")
set page(margin: (top: 9.5mm, bottom: 12mm, left: 15mm, right: 10mm))
set block(spacing: 1mm)
set par(leading: 5pt)
// Weird vertical text in the bottom left
place(bottom + left, dx: -6mm, dy: -1mm,
rotate(-90deg, origin: bottom + left,
text(size: 6pt, tracking: 3.1pt,
"K_PSE_PB_AZDoku_01_04-20"
)
)
)
// Main box
rect(
width: 100%,
height: 100%,
stroke: _kit_stroke,
radius: (top-right: 4.5mm, bottom-left: 4.5mm),
inset: 0mm
)[
// Logo
#place(top + left, dx: 9.5mm, dy: 3.5mm,
image("kit_logo.svg", width: 29mm)
)
// Heading
#place(top + left, dx: 78mm, dy: 9mm,
text(weight: "bold", size: 14pt, fill: _kit_green,
[Arbeitszeitdokumentation]
)
)
// Page number
#place(bottom + right, dx: -15mm, dy: -1.5mm,
text(size: 9pt, [Seite 1 von 1])
)
// Main content
#block(
inset: (top: 24.5mm, left: 7.5mm, right: 13mm),
width: 100%,
height: 100%,
body
)
]
}
#let _underlined(body) = box(
baseline: 0%,
stroke: (bottom: _kit_stroke),
outset: (bottom: 1.5mm),
inset: (x: 1mm),
body,
)
#let _checkbox(checked) = box(
width: 3.5mm,
outset: (y: 0.4mm),
stroke: _kit_stroke,
align(center + horizon, if checked { "X" } else { " " })
)
#let _header(
year: " ",
month: " ",
name: " ",
staff_id: " ",
working_area: " ",
department: " ",
monthly_hours: " ",
hourly_wage: " ",
) = [
#set text(size: 11pt)
#pad(left: 2.5mm, grid(
columns: (1fr, 92mm),
rows: 6mm,
[],
align(right)[
*Monat / Jahr:* #h(6mm)
#_underlined(align(center)[
#box(width: 21mm)[#month] / #box(width: 21mm)[#year]
])
],
[*Name, Vorname des/r Beschäftigten:*],
_underlined(box(width: 100%, name)),
[*Personalnummer:*],
_underlined[
#box(width: 1fr)[#staff_id]
#h(8mm)
*GF:* #h(1mm)
#_checkbox(working_area == areas.Großforschung)
#h(8mm)
*UB:* #h(1mm)
#_checkbox(working_area == areas.Unibereich)
#h(8mm)
],
[*OE:*], // Institut / Organisationseinheit
_underlined(box(width: 100%)[#department]),
[*Vertraglich vereinbarte Arbeitszeit:*],
[
#_underlined(align(center)[
#h(4mm)
#box(width: 10mm)[#monthly_hours]
Std.
#h(4mm)
])
#h(1fr)
*Stundensatz:*
#h(4mm)
#_underlined(align(center)[
#box(width: 18mm)[#hourly_wage]
*€*
])
]
))
]
#let _log(..entries) = {
set text(size: 10pt)
table(
columns: (1fr, 23.3mm, 23.3mm, 23.3mm, 23.3mm, 23.3mm),
rows: array.range(24).map(_ => 5.05mm),
align: center + horizon,
stroke: _kit_stroke,
inset: 1mm,
table.header(
table.cell(rowspan: 2)[
*Tätigkeit* \
*(Stichwort, Projekt)*
],
[*Datum*],
[*Beginn*],
[*Ende*],
[*Pause*],
[*Arbeitszeit#super[1]*],
[*(tt.mm.jj)*],
[*(hh:mm)*],
[*(hh:mm)*],
[*(hh:mm)*],
[*(hh:mm)*],
),
..entries.pos()
)
}
#let _summary(
holiday: [],
total: [],
monthly_hours: [],
carry_prev_month: [],
carry_next_month: [],
) = {
set text(size: 10pt)
align(right, table(
columns: (54mm, 23.3mm),
rows: 5.05mm,
align: center + horizon,
stroke: _kit_stroke,
inset: 1mm,
[*Urlaub anteilig:*], [#holiday],
[*Summe:*], [#total],
[*monatliche Soll-Arbeitszeit:*], [#monthly_hours],
[*Übertrag vom Vormonat:*], [#carry_prev_month],
[*Übertrag in den Folgemonat:*], [#carry_next_month],
))
}
#let _footer() = pad(left: 2.5mm)[
#v(3.5mm)
#grid(
columns: (1fr, 77.5mm),
column-gutter: 6.5mm,
row-gutter: (12mm, 3mm),
[Ich bestätige die Richtigkeit der Angaben:],
[Geprüft:],
pad(left: -2.5mm, line(length: 100%, stroke: stroke(thickness: _kit_stroke, dash: "densely-dotted"))),
pad(left: -2.5mm, line(length: 100%, stroke: stroke(thickness: _kit_stroke, dash: "densely-dotted"))),
[Datum, Unterschrift Beschäftigte/r],
[Datum, Unterschrift Dienstvorgesetzte/r],
)
#v(5.5mm)
#set text(size: 10pt)
Nach *§ 17 Mindestlohngesetz (MiLoG)* müssen für geringfügig entlohnte und kurzfristig beschäftigte
Arbeitnehmer/innen u.a. Beginn, Ende und Dauer der täglichen Arbeitszeit aufgezeichnet und für Kon-
trollzwecke mindestens zwei Jahre am Ort der Beschäftigung aufbewahrt werden.
#v(12.5mm)
#line(length: 51mm, stroke: _kit_stroke)
#v(1mm)
#set text(size: 9pt)
#super[1] Summe in vollen Stunden und Minuten ohne Pause (Std:Min); bei Abwesenheit können auch folgende Kürzel
eingetragen werden: U=Urlaub, K=Krankheit, F=Feiertag, S=Sonstiges
]
//////////
// Util //
//////////
#let _pad_int(n, char: "0", width: 2) = {
let s = str(n)
for _ in array.range(width - s.clusters().len()) { char }
s
}
#let _parse_duration(s) = {
let matched = s.match(regex("^(-?)([0-9]+):([0-5][0-9])$"))
assert(matched != none, message: "invalid duration or time: " + s)
let groups = matched.captures
let sign = if groups.at(0) == "-" { -1 } else { 1 }
let h = int(groups.at(1))
let m = int(groups.at(2))
sign * (h * 60 + m)
}
#let _fmt_duration(mins) = {
if mins < 0 {
"-"
mins = -mins
}
let h = int(mins / 60)
let m = mins - h * 60
_pad_int(h, char: "0", width: 2)
":"
_pad_int(m, char: "0", width: 2)
}
////////////////
// Validation //
////////////////
#let _assert_entry(row, entry, condition, message) = {
message = "row " + str(row) + " (day " + str(entry.day) + "): " + message
assert(condition, message: message)
}
#let _check_entry(row, entry) = {
let e = entry
_assert_entry(row, e, e.start <= e.end, "start must be before end")
_assert_entry(row, e, e.rest <= e.end - e.start, "rest too long")
// I think the previous two checks should make it impossible for this assert
// to fail, but just to be careful...
_assert_entry(row, e, e.duration >= 0, "duration must be positive")
}
//////////////////
// Entry points //
//////////////////
#let timesheet_empty() = _frame[
#_header()
#_log()
#_summary()
#_footer()
]
#let entry(
task,
day,
start,
end,
rest: "0:00",
note: none,
) = {
assert(type(day) == int)
assert(note == none or notes.values().contains(note))
start = _parse_duration(start)
end = _parse_duration(end)
rest = _parse_duration(rest)
(
task: task,
day: day,
start: start,
end: end,
rest: rest,
duration: end - start - rest,
note: note,
)
}
#let timesheet(
name: "Name, Vorname",
staff_id: 1234567,
department: "Institut für Informatik",
working_area: none,
monthly_hours: 40,
hourly_wage: [14.09],
validate: true,
carry_prev_month: "00:00",
year: 2024,
month: 1,
..entries,
) = {
assert(working_area == none or areas.values().contains(working_area))
assert(type(monthly_hours) == int)
assert(type(year) == int)
assert(type(month) == int)
carry_prev_month = _parse_duration(carry_prev_month)
entries = entries.pos()
let monthly = monthly_hours * 60
let holiday = entries.filter(e => e.note == notes.Urlaub).map(e => e.duration).sum(default: 0)
let total = entries.map(e => e.duration).sum(default: 0)
let carry_next_month = carry_prev_month + total - monthly
if validate {
for (row, entry) in entries.enumerate(start: 1) {
_check_entry(row, entry)
}
}
let rows = entries.map(e => (
e.task,
datetime(year: year, month: month, day: e.day).display("[day].[month].[year]"),
_fmt_duration(e.start),
_fmt_duration(e.end),
_fmt_duration(e.rest),
{
_fmt_duration(e.duration)
if e.note != none { " "; e.note }
},
))
_frame[
#_header(
year: year,
month: month,
name: name,
staff_id: staff_id,
working_area: working_area,
department: department,
monthly_hours: monthly_hours,
hourly_wage: hourly_wage,
)
#_log(..rows.flatten())
#_summary(
holiday: _fmt_duration(holiday),
total: _fmt_duration(total),
monthly_hours: _fmt_duration(monthly),
carry_prev_month: _fmt_duration(carry_prev_month),
carry_next_month: _fmt_duration(carry_next_month),
)
#_footer()
]
}