Răsfoiți Sursa

Первый коммит

Вадим Королёв 1 an în urmă
comite
7d7479e5ee

+ 2 - 0
.gitignore

@@ -0,0 +1,2 @@
+pockit.geany
+src/vendor

+ 251 - 0
doc/pockit.drawio

@@ -0,0 +1,251 @@
+<mxfile host="app.diagrams.net" modified="2023-12-14T18:43:50.696Z" agent="Mozilla/5.0 (X11; Linux x86_64; rv:109.0) Gecko/20100101 Firefox/115.0" etag="JCQS5pvwg_oaY510Vpyx" version="22.1.9" type="device">
+  <diagram id="R2lEEEUBdFMjLlhIrx00" name="Page-1">
+    <mxGraphModel dx="733" dy="366" grid="1" gridSize="10" guides="1" tooltips="1" connect="1" arrows="1" fold="1" page="1" pageScale="1" pageWidth="850" pageHeight="1100" math="0" shadow="0" extFonts="Permanent Marker^https://fonts.googleapis.com/css?family=Permanent+Marker">
+      <root>
+        <mxCell id="0" />
+        <mxCell id="1" parent="0" />
+        <mxCell id="yovVQwwNwtsDAoFcyGfq-1" value="reports" style="shape=table;startSize=30;container=1;collapsible=1;childLayout=tableLayout;fixedRows=1;rowLines=0;fontStyle=1;align=center;resizeLast=1;html=1;" vertex="1" parent="1">
+          <mxGeometry x="110" y="140" width="180" height="150" as="geometry" />
+        </mxCell>
+        <mxCell id="yovVQwwNwtsDAoFcyGfq-2" value="" style="shape=tableRow;horizontal=0;startSize=0;swimlaneHead=0;swimlaneBody=0;fillColor=none;collapsible=0;dropTarget=0;points=[[0,0.5],[1,0.5]];portConstraint=eastwest;top=0;left=0;right=0;bottom=1;" vertex="1" parent="yovVQwwNwtsDAoFcyGfq-1">
+          <mxGeometry y="30" width="180" height="30" as="geometry" />
+        </mxCell>
+        <mxCell id="yovVQwwNwtsDAoFcyGfq-3" value="PK" style="shape=partialRectangle;connectable=0;fillColor=none;top=0;left=0;bottom=0;right=0;fontStyle=1;overflow=hidden;whiteSpace=wrap;html=1;" vertex="1" parent="yovVQwwNwtsDAoFcyGfq-2">
+          <mxGeometry width="30" height="30" as="geometry">
+            <mxRectangle width="30" height="30" as="alternateBounds" />
+          </mxGeometry>
+        </mxCell>
+        <mxCell id="yovVQwwNwtsDAoFcyGfq-4" value="id" style="shape=partialRectangle;connectable=0;fillColor=none;top=0;left=0;bottom=0;right=0;align=left;spacingLeft=6;fontStyle=5;overflow=hidden;whiteSpace=wrap;html=1;" vertex="1" parent="yovVQwwNwtsDAoFcyGfq-2">
+          <mxGeometry x="30" width="150" height="30" as="geometry">
+            <mxRectangle width="150" height="30" as="alternateBounds" />
+          </mxGeometry>
+        </mxCell>
+        <mxCell id="yovVQwwNwtsDAoFcyGfq-5" value="" style="shape=tableRow;horizontal=0;startSize=0;swimlaneHead=0;swimlaneBody=0;fillColor=none;collapsible=0;dropTarget=0;points=[[0,0.5],[1,0.5]];portConstraint=eastwest;top=0;left=0;right=0;bottom=0;" vertex="1" parent="yovVQwwNwtsDAoFcyGfq-1">
+          <mxGeometry y="60" width="180" height="30" as="geometry" />
+        </mxCell>
+        <mxCell id="yovVQwwNwtsDAoFcyGfq-6" value="FK" style="shape=partialRectangle;connectable=0;fillColor=none;top=0;left=0;bottom=0;right=0;editable=1;overflow=hidden;whiteSpace=wrap;html=1;" vertex="1" parent="yovVQwwNwtsDAoFcyGfq-5">
+          <mxGeometry width="30" height="30" as="geometry">
+            <mxRectangle width="30" height="30" as="alternateBounds" />
+          </mxGeometry>
+        </mxCell>
+        <mxCell id="yovVQwwNwtsDAoFcyGfq-7" value="discipline_id" style="shape=partialRectangle;connectable=0;fillColor=none;top=0;left=0;bottom=0;right=0;align=left;spacingLeft=6;overflow=hidden;whiteSpace=wrap;html=1;" vertex="1" parent="yovVQwwNwtsDAoFcyGfq-5">
+          <mxGeometry x="30" width="150" height="30" as="geometry">
+            <mxRectangle width="150" height="30" as="alternateBounds" />
+          </mxGeometry>
+        </mxCell>
+        <mxCell id="yovVQwwNwtsDAoFcyGfq-8" value="" style="shape=tableRow;horizontal=0;startSize=0;swimlaneHead=0;swimlaneBody=0;fillColor=none;collapsible=0;dropTarget=0;points=[[0,0.5],[1,0.5]];portConstraint=eastwest;top=0;left=0;right=0;bottom=0;" vertex="1" parent="yovVQwwNwtsDAoFcyGfq-1">
+          <mxGeometry y="90" width="180" height="30" as="geometry" />
+        </mxCell>
+        <mxCell id="yovVQwwNwtsDAoFcyGfq-9" value="FK" style="shape=partialRectangle;connectable=0;fillColor=none;top=0;left=0;bottom=0;right=0;editable=1;overflow=hidden;whiteSpace=wrap;html=1;" vertex="1" parent="yovVQwwNwtsDAoFcyGfq-8">
+          <mxGeometry width="30" height="30" as="geometry">
+            <mxRectangle width="30" height="30" as="alternateBounds" />
+          </mxGeometry>
+        </mxCell>
+        <mxCell id="yovVQwwNwtsDAoFcyGfq-10" value="work_type_id" style="shape=partialRectangle;connectable=0;fillColor=none;top=0;left=0;bottom=0;right=0;align=left;spacingLeft=6;overflow=hidden;whiteSpace=wrap;html=1;" vertex="1" parent="yovVQwwNwtsDAoFcyGfq-8">
+          <mxGeometry x="30" width="150" height="30" as="geometry">
+            <mxRectangle width="150" height="30" as="alternateBounds" />
+          </mxGeometry>
+        </mxCell>
+        <mxCell id="yovVQwwNwtsDAoFcyGfq-11" value="" style="shape=tableRow;horizontal=0;startSize=0;swimlaneHead=0;swimlaneBody=0;fillColor=none;collapsible=0;dropTarget=0;points=[[0,0.5],[1,0.5]];portConstraint=eastwest;top=0;left=0;right=0;bottom=0;" vertex="1" parent="yovVQwwNwtsDAoFcyGfq-1">
+          <mxGeometry y="120" width="180" height="30" as="geometry" />
+        </mxCell>
+        <mxCell id="yovVQwwNwtsDAoFcyGfq-12" value="" style="shape=partialRectangle;connectable=0;fillColor=none;top=0;left=0;bottom=0;right=0;editable=1;overflow=hidden;whiteSpace=wrap;html=1;" vertex="1" parent="yovVQwwNwtsDAoFcyGfq-11">
+          <mxGeometry width="30" height="30" as="geometry">
+            <mxRectangle width="30" height="30" as="alternateBounds" />
+          </mxGeometry>
+        </mxCell>
+        <mxCell id="yovVQwwNwtsDAoFcyGfq-13" value="comment" style="shape=partialRectangle;connectable=0;fillColor=none;top=0;left=0;bottom=0;right=0;align=left;spacingLeft=6;overflow=hidden;whiteSpace=wrap;html=1;" vertex="1" parent="yovVQwwNwtsDAoFcyGfq-11">
+          <mxGeometry x="30" width="150" height="30" as="geometry">
+            <mxRectangle width="150" height="30" as="alternateBounds" />
+          </mxGeometry>
+        </mxCell>
+        <mxCell id="yovVQwwNwtsDAoFcyGfq-14" value="disciplines" style="shape=table;startSize=30;container=1;collapsible=1;childLayout=tableLayout;fixedRows=1;rowLines=0;fontStyle=1;align=center;resizeLast=1;html=1;" vertex="1" parent="1">
+          <mxGeometry x="450" y="120" width="180" height="120" as="geometry" />
+        </mxCell>
+        <mxCell id="yovVQwwNwtsDAoFcyGfq-15" value="" style="shape=tableRow;horizontal=0;startSize=0;swimlaneHead=0;swimlaneBody=0;fillColor=none;collapsible=0;dropTarget=0;points=[[0,0.5],[1,0.5]];portConstraint=eastwest;top=0;left=0;right=0;bottom=1;" vertex="1" parent="yovVQwwNwtsDAoFcyGfq-14">
+          <mxGeometry y="30" width="180" height="30" as="geometry" />
+        </mxCell>
+        <mxCell id="yovVQwwNwtsDAoFcyGfq-16" value="PK" style="shape=partialRectangle;connectable=0;fillColor=none;top=0;left=0;bottom=0;right=0;fontStyle=1;overflow=hidden;whiteSpace=wrap;html=1;" vertex="1" parent="yovVQwwNwtsDAoFcyGfq-15">
+          <mxGeometry width="30" height="30" as="geometry">
+            <mxRectangle width="30" height="30" as="alternateBounds" />
+          </mxGeometry>
+        </mxCell>
+        <mxCell id="yovVQwwNwtsDAoFcyGfq-17" value="id" style="shape=partialRectangle;connectable=0;fillColor=none;top=0;left=0;bottom=0;right=0;align=left;spacingLeft=6;fontStyle=5;overflow=hidden;whiteSpace=wrap;html=1;" vertex="1" parent="yovVQwwNwtsDAoFcyGfq-15">
+          <mxGeometry x="30" width="150" height="30" as="geometry">
+            <mxRectangle width="150" height="30" as="alternateBounds" />
+          </mxGeometry>
+        </mxCell>
+        <mxCell id="yovVQwwNwtsDAoFcyGfq-18" value="" style="shape=tableRow;horizontal=0;startSize=0;swimlaneHead=0;swimlaneBody=0;fillColor=none;collapsible=0;dropTarget=0;points=[[0,0.5],[1,0.5]];portConstraint=eastwest;top=0;left=0;right=0;bottom=0;" vertex="1" parent="yovVQwwNwtsDAoFcyGfq-14">
+          <mxGeometry y="60" width="180" height="30" as="geometry" />
+        </mxCell>
+        <mxCell id="yovVQwwNwtsDAoFcyGfq-19" value="" style="shape=partialRectangle;connectable=0;fillColor=none;top=0;left=0;bottom=0;right=0;editable=1;overflow=hidden;whiteSpace=wrap;html=1;" vertex="1" parent="yovVQwwNwtsDAoFcyGfq-18">
+          <mxGeometry width="30" height="30" as="geometry">
+            <mxRectangle width="30" height="30" as="alternateBounds" />
+          </mxGeometry>
+        </mxCell>
+        <mxCell id="yovVQwwNwtsDAoFcyGfq-20" value="name" style="shape=partialRectangle;connectable=0;fillColor=none;top=0;left=0;bottom=0;right=0;align=left;spacingLeft=6;overflow=hidden;whiteSpace=wrap;html=1;" vertex="1" parent="yovVQwwNwtsDAoFcyGfq-18">
+          <mxGeometry x="30" width="150" height="30" as="geometry">
+            <mxRectangle width="150" height="30" as="alternateBounds" />
+          </mxGeometry>
+        </mxCell>
+        <mxCell id="yovVQwwNwtsDAoFcyGfq-21" value="" style="shape=tableRow;horizontal=0;startSize=0;swimlaneHead=0;swimlaneBody=0;fillColor=none;collapsible=0;dropTarget=0;points=[[0,0.5],[1,0.5]];portConstraint=eastwest;top=0;left=0;right=0;bottom=0;" vertex="1" parent="yovVQwwNwtsDAoFcyGfq-14">
+          <mxGeometry y="90" width="180" height="30" as="geometry" />
+        </mxCell>
+        <mxCell id="yovVQwwNwtsDAoFcyGfq-22" value="FK" style="shape=partialRectangle;connectable=0;fillColor=none;top=0;left=0;bottom=0;right=0;editable=1;overflow=hidden;whiteSpace=wrap;html=1;" vertex="1" parent="yovVQwwNwtsDAoFcyGfq-21">
+          <mxGeometry width="30" height="30" as="geometry">
+            <mxRectangle width="30" height="30" as="alternateBounds" />
+          </mxGeometry>
+        </mxCell>
+        <mxCell id="yovVQwwNwtsDAoFcyGfq-23" value="teacher_id" style="shape=partialRectangle;connectable=0;fillColor=none;top=0;left=0;bottom=0;right=0;align=left;spacingLeft=6;overflow=hidden;whiteSpace=wrap;html=1;" vertex="1" parent="yovVQwwNwtsDAoFcyGfq-21">
+          <mxGeometry x="30" width="150" height="30" as="geometry">
+            <mxRectangle width="150" height="30" as="alternateBounds" />
+          </mxGeometry>
+        </mxCell>
+        <mxCell id="yovVQwwNwtsDAoFcyGfq-27" value="teachers" style="shape=table;startSize=30;container=1;collapsible=1;childLayout=tableLayout;fixedRows=1;rowLines=0;fontStyle=1;align=center;resizeLast=1;html=1;" vertex="1" parent="1">
+          <mxGeometry x="480" y="300" width="180" height="150" as="geometry" />
+        </mxCell>
+        <mxCell id="yovVQwwNwtsDAoFcyGfq-28" value="" style="shape=tableRow;horizontal=0;startSize=0;swimlaneHead=0;swimlaneBody=0;fillColor=none;collapsible=0;dropTarget=0;points=[[0,0.5],[1,0.5]];portConstraint=eastwest;top=0;left=0;right=0;bottom=1;" vertex="1" parent="yovVQwwNwtsDAoFcyGfq-27">
+          <mxGeometry y="30" width="180" height="30" as="geometry" />
+        </mxCell>
+        <mxCell id="yovVQwwNwtsDAoFcyGfq-29" value="PK" style="shape=partialRectangle;connectable=0;fillColor=none;top=0;left=0;bottom=0;right=0;fontStyle=1;overflow=hidden;whiteSpace=wrap;html=1;" vertex="1" parent="yovVQwwNwtsDAoFcyGfq-28">
+          <mxGeometry width="30" height="30" as="geometry">
+            <mxRectangle width="30" height="30" as="alternateBounds" />
+          </mxGeometry>
+        </mxCell>
+        <mxCell id="yovVQwwNwtsDAoFcyGfq-30" value="id" style="shape=partialRectangle;connectable=0;fillColor=none;top=0;left=0;bottom=0;right=0;align=left;spacingLeft=6;fontStyle=5;overflow=hidden;whiteSpace=wrap;html=1;" vertex="1" parent="yovVQwwNwtsDAoFcyGfq-28">
+          <mxGeometry x="30" width="150" height="30" as="geometry">
+            <mxRectangle width="150" height="30" as="alternateBounds" />
+          </mxGeometry>
+        </mxCell>
+        <mxCell id="yovVQwwNwtsDAoFcyGfq-31" value="" style="shape=tableRow;horizontal=0;startSize=0;swimlaneHead=0;swimlaneBody=0;fillColor=none;collapsible=0;dropTarget=0;points=[[0,0.5],[1,0.5]];portConstraint=eastwest;top=0;left=0;right=0;bottom=0;" vertex="1" parent="yovVQwwNwtsDAoFcyGfq-27">
+          <mxGeometry y="60" width="180" height="30" as="geometry" />
+        </mxCell>
+        <mxCell id="yovVQwwNwtsDAoFcyGfq-32" value="" style="shape=partialRectangle;connectable=0;fillColor=none;top=0;left=0;bottom=0;right=0;editable=1;overflow=hidden;whiteSpace=wrap;html=1;" vertex="1" parent="yovVQwwNwtsDAoFcyGfq-31">
+          <mxGeometry width="30" height="30" as="geometry">
+            <mxRectangle width="30" height="30" as="alternateBounds" />
+          </mxGeometry>
+        </mxCell>
+        <mxCell id="yovVQwwNwtsDAoFcyGfq-33" value="name" style="shape=partialRectangle;connectable=0;fillColor=none;top=0;left=0;bottom=0;right=0;align=left;spacingLeft=6;overflow=hidden;whiteSpace=wrap;html=1;" vertex="1" parent="yovVQwwNwtsDAoFcyGfq-31">
+          <mxGeometry x="30" width="150" height="30" as="geometry">
+            <mxRectangle width="150" height="30" as="alternateBounds" />
+          </mxGeometry>
+        </mxCell>
+        <mxCell id="yovVQwwNwtsDAoFcyGfq-34" value="" style="shape=tableRow;horizontal=0;startSize=0;swimlaneHead=0;swimlaneBody=0;fillColor=none;collapsible=0;dropTarget=0;points=[[0,0.5],[1,0.5]];portConstraint=eastwest;top=0;left=0;right=0;bottom=0;" vertex="1" parent="yovVQwwNwtsDAoFcyGfq-27">
+          <mxGeometry y="90" width="180" height="30" as="geometry" />
+        </mxCell>
+        <mxCell id="yovVQwwNwtsDAoFcyGfq-35" value="" style="shape=partialRectangle;connectable=0;fillColor=none;top=0;left=0;bottom=0;right=0;editable=1;overflow=hidden;whiteSpace=wrap;html=1;" vertex="1" parent="yovVQwwNwtsDAoFcyGfq-34">
+          <mxGeometry width="30" height="30" as="geometry">
+            <mxRectangle width="30" height="30" as="alternateBounds" />
+          </mxGeometry>
+        </mxCell>
+        <mxCell id="yovVQwwNwtsDAoFcyGfq-36" value="surname" style="shape=partialRectangle;connectable=0;fillColor=none;top=0;left=0;bottom=0;right=0;align=left;spacingLeft=6;overflow=hidden;whiteSpace=wrap;html=1;" vertex="1" parent="yovVQwwNwtsDAoFcyGfq-34">
+          <mxGeometry x="30" width="150" height="30" as="geometry">
+            <mxRectangle width="150" height="30" as="alternateBounds" />
+          </mxGeometry>
+        </mxCell>
+        <mxCell id="yovVQwwNwtsDAoFcyGfq-37" value="" style="shape=tableRow;horizontal=0;startSize=0;swimlaneHead=0;swimlaneBody=0;fillColor=none;collapsible=0;dropTarget=0;points=[[0,0.5],[1,0.5]];portConstraint=eastwest;top=0;left=0;right=0;bottom=0;" vertex="1" parent="yovVQwwNwtsDAoFcyGfq-27">
+          <mxGeometry y="120" width="180" height="30" as="geometry" />
+        </mxCell>
+        <mxCell id="yovVQwwNwtsDAoFcyGfq-38" value="" style="shape=partialRectangle;connectable=0;fillColor=none;top=0;left=0;bottom=0;right=0;editable=1;overflow=hidden;whiteSpace=wrap;html=1;" vertex="1" parent="yovVQwwNwtsDAoFcyGfq-37">
+          <mxGeometry width="30" height="30" as="geometry">
+            <mxRectangle width="30" height="30" as="alternateBounds" />
+          </mxGeometry>
+        </mxCell>
+        <mxCell id="yovVQwwNwtsDAoFcyGfq-39" value="patronymic" style="shape=partialRectangle;connectable=0;fillColor=none;top=0;left=0;bottom=0;right=0;align=left;spacingLeft=6;overflow=hidden;whiteSpace=wrap;html=1;" vertex="1" parent="yovVQwwNwtsDAoFcyGfq-37">
+          <mxGeometry x="30" width="150" height="30" as="geometry">
+            <mxRectangle width="150" height="30" as="alternateBounds" />
+          </mxGeometry>
+        </mxCell>
+        <mxCell id="yovVQwwNwtsDAoFcyGfq-40" value="" style="edgeStyle=entityRelationEdgeStyle;fontSize=12;html=1;endArrow=ERoneToMany;rounded=0;" edge="1" parent="1" source="yovVQwwNwtsDAoFcyGfq-28" target="yovVQwwNwtsDAoFcyGfq-21">
+          <mxGeometry width="100" height="100" relative="1" as="geometry">
+            <mxPoint x="230" y="420" as="sourcePoint" />
+            <mxPoint x="330" y="320" as="targetPoint" />
+          </mxGeometry>
+        </mxCell>
+        <mxCell id="yovVQwwNwtsDAoFcyGfq-41" value="" style="edgeStyle=entityRelationEdgeStyle;fontSize=12;html=1;endArrow=ERoneToMany;rounded=0;" edge="1" parent="1" source="yovVQwwNwtsDAoFcyGfq-15" target="yovVQwwNwtsDAoFcyGfq-5">
+          <mxGeometry width="100" height="100" relative="1" as="geometry">
+            <mxPoint x="300" y="250" as="sourcePoint" />
+            <mxPoint x="400" y="150" as="targetPoint" />
+          </mxGeometry>
+        </mxCell>
+        <mxCell id="yovVQwwNwtsDAoFcyGfq-42" value="work_types" style="shape=table;startSize=30;container=1;collapsible=1;childLayout=tableLayout;fixedRows=1;rowLines=0;fontStyle=1;align=center;resizeLast=1;html=1;" vertex="1" parent="1">
+          <mxGeometry x="160" y="340" width="180" height="90" as="geometry" />
+        </mxCell>
+        <mxCell id="yovVQwwNwtsDAoFcyGfq-43" value="" style="shape=tableRow;horizontal=0;startSize=0;swimlaneHead=0;swimlaneBody=0;fillColor=none;collapsible=0;dropTarget=0;points=[[0,0.5],[1,0.5]];portConstraint=eastwest;top=0;left=0;right=0;bottom=1;" vertex="1" parent="yovVQwwNwtsDAoFcyGfq-42">
+          <mxGeometry y="30" width="180" height="30" as="geometry" />
+        </mxCell>
+        <mxCell id="yovVQwwNwtsDAoFcyGfq-44" value="PK" style="shape=partialRectangle;connectable=0;fillColor=none;top=0;left=0;bottom=0;right=0;fontStyle=1;overflow=hidden;whiteSpace=wrap;html=1;" vertex="1" parent="yovVQwwNwtsDAoFcyGfq-43">
+          <mxGeometry width="30" height="30" as="geometry">
+            <mxRectangle width="30" height="30" as="alternateBounds" />
+          </mxGeometry>
+        </mxCell>
+        <mxCell id="yovVQwwNwtsDAoFcyGfq-45" value="id" style="shape=partialRectangle;connectable=0;fillColor=none;top=0;left=0;bottom=0;right=0;align=left;spacingLeft=6;fontStyle=5;overflow=hidden;whiteSpace=wrap;html=1;" vertex="1" parent="yovVQwwNwtsDAoFcyGfq-43">
+          <mxGeometry x="30" width="150" height="30" as="geometry">
+            <mxRectangle width="150" height="30" as="alternateBounds" />
+          </mxGeometry>
+        </mxCell>
+        <mxCell id="yovVQwwNwtsDAoFcyGfq-46" value="" style="shape=tableRow;horizontal=0;startSize=0;swimlaneHead=0;swimlaneBody=0;fillColor=none;collapsible=0;dropTarget=0;points=[[0,0.5],[1,0.5]];portConstraint=eastwest;top=0;left=0;right=0;bottom=0;" vertex="1" parent="yovVQwwNwtsDAoFcyGfq-42">
+          <mxGeometry y="60" width="180" height="30" as="geometry" />
+        </mxCell>
+        <mxCell id="yovVQwwNwtsDAoFcyGfq-47" value="" style="shape=partialRectangle;connectable=0;fillColor=none;top=0;left=0;bottom=0;right=0;editable=1;overflow=hidden;whiteSpace=wrap;html=1;" vertex="1" parent="yovVQwwNwtsDAoFcyGfq-46">
+          <mxGeometry width="30" height="30" as="geometry">
+            <mxRectangle width="30" height="30" as="alternateBounds" />
+          </mxGeometry>
+        </mxCell>
+        <mxCell id="yovVQwwNwtsDAoFcyGfq-48" value="name" style="shape=partialRectangle;connectable=0;fillColor=none;top=0;left=0;bottom=0;right=0;align=left;spacingLeft=6;overflow=hidden;whiteSpace=wrap;html=1;" vertex="1" parent="yovVQwwNwtsDAoFcyGfq-46">
+          <mxGeometry x="30" width="150" height="30" as="geometry">
+            <mxRectangle width="150" height="30" as="alternateBounds" />
+          </mxGeometry>
+        </mxCell>
+        <mxCell id="yovVQwwNwtsDAoFcyGfq-55" value="" style="edgeStyle=entityRelationEdgeStyle;fontSize=12;html=1;endArrow=ERoneToMany;rounded=0;" edge="1" parent="1" source="yovVQwwNwtsDAoFcyGfq-43" target="yovVQwwNwtsDAoFcyGfq-8">
+          <mxGeometry width="100" height="100" relative="1" as="geometry">
+            <mxPoint x="110" y="440" as="sourcePoint" />
+            <mxPoint x="210" y="340" as="targetPoint" />
+          </mxGeometry>
+        </mxCell>
+        <mxCell id="yovVQwwNwtsDAoFcyGfq-56" value="settings" style="shape=table;startSize=30;container=1;collapsible=1;childLayout=tableLayout;fixedRows=1;rowLines=0;fontStyle=1;align=center;resizeLast=1;html=1;" vertex="1" parent="1">
+          <mxGeometry x="160" y="490" width="180" height="120" as="geometry" />
+        </mxCell>
+        <mxCell id="yovVQwwNwtsDAoFcyGfq-57" value="" style="shape=tableRow;horizontal=0;startSize=0;swimlaneHead=0;swimlaneBody=0;fillColor=none;collapsible=0;dropTarget=0;points=[[0,0.5],[1,0.5]];portConstraint=eastwest;top=0;left=0;right=0;bottom=1;" vertex="1" parent="yovVQwwNwtsDAoFcyGfq-56">
+          <mxGeometry y="30" width="180" height="30" as="geometry" />
+        </mxCell>
+        <mxCell id="yovVQwwNwtsDAoFcyGfq-58" value="PK" style="shape=partialRectangle;connectable=0;fillColor=none;top=0;left=0;bottom=0;right=0;fontStyle=1;overflow=hidden;whiteSpace=wrap;html=1;" vertex="1" parent="yovVQwwNwtsDAoFcyGfq-57">
+          <mxGeometry width="30" height="30" as="geometry">
+            <mxRectangle width="30" height="30" as="alternateBounds" />
+          </mxGeometry>
+        </mxCell>
+        <mxCell id="yovVQwwNwtsDAoFcyGfq-59" value="id" style="shape=partialRectangle;connectable=0;fillColor=none;top=0;left=0;bottom=0;right=0;align=left;spacingLeft=6;fontStyle=5;overflow=hidden;whiteSpace=wrap;html=1;" vertex="1" parent="yovVQwwNwtsDAoFcyGfq-57">
+          <mxGeometry x="30" width="150" height="30" as="geometry">
+            <mxRectangle width="150" height="30" as="alternateBounds" />
+          </mxGeometry>
+        </mxCell>
+        <mxCell id="yovVQwwNwtsDAoFcyGfq-60" value="" style="shape=tableRow;horizontal=0;startSize=0;swimlaneHead=0;swimlaneBody=0;fillColor=none;collapsible=0;dropTarget=0;points=[[0,0.5],[1,0.5]];portConstraint=eastwest;top=0;left=0;right=0;bottom=0;" vertex="1" parent="yovVQwwNwtsDAoFcyGfq-56">
+          <mxGeometry y="60" width="180" height="30" as="geometry" />
+        </mxCell>
+        <mxCell id="yovVQwwNwtsDAoFcyGfq-61" value="" style="shape=partialRectangle;connectable=0;fillColor=none;top=0;left=0;bottom=0;right=0;editable=1;overflow=hidden;whiteSpace=wrap;html=1;" vertex="1" parent="yovVQwwNwtsDAoFcyGfq-60">
+          <mxGeometry width="30" height="30" as="geometry">
+            <mxRectangle width="30" height="30" as="alternateBounds" />
+          </mxGeometry>
+        </mxCell>
+        <mxCell id="yovVQwwNwtsDAoFcyGfq-62" value="name" style="shape=partialRectangle;connectable=0;fillColor=none;top=0;left=0;bottom=0;right=0;align=left;spacingLeft=6;overflow=hidden;whiteSpace=wrap;html=1;" vertex="1" parent="yovVQwwNwtsDAoFcyGfq-60">
+          <mxGeometry x="30" width="150" height="30" as="geometry">
+            <mxRectangle width="150" height="30" as="alternateBounds" />
+          </mxGeometry>
+        </mxCell>
+        <mxCell id="yovVQwwNwtsDAoFcyGfq-63" value="" style="shape=tableRow;horizontal=0;startSize=0;swimlaneHead=0;swimlaneBody=0;fillColor=none;collapsible=0;dropTarget=0;points=[[0,0.5],[1,0.5]];portConstraint=eastwest;top=0;left=0;right=0;bottom=0;" vertex="1" parent="yovVQwwNwtsDAoFcyGfq-56">
+          <mxGeometry y="90" width="180" height="30" as="geometry" />
+        </mxCell>
+        <mxCell id="yovVQwwNwtsDAoFcyGfq-64" value="" style="shape=partialRectangle;connectable=0;fillColor=none;top=0;left=0;bottom=0;right=0;editable=1;overflow=hidden;whiteSpace=wrap;html=1;" vertex="1" parent="yovVQwwNwtsDAoFcyGfq-63">
+          <mxGeometry width="30" height="30" as="geometry">
+            <mxRectangle width="30" height="30" as="alternateBounds" />
+          </mxGeometry>
+        </mxCell>
+        <mxCell id="yovVQwwNwtsDAoFcyGfq-65" value="value" style="shape=partialRectangle;connectable=0;fillColor=none;top=0;left=0;bottom=0;right=0;align=left;spacingLeft=6;overflow=hidden;whiteSpace=wrap;html=1;" vertex="1" parent="yovVQwwNwtsDAoFcyGfq-63">
+          <mxGeometry x="30" width="150" height="30" as="geometry">
+            <mxRectangle width="150" height="30" as="alternateBounds" />
+          </mxGeometry>
+        </mxCell>
+      </root>
+    </mxGraphModel>
+  </diagram>
+</mxfile>

BIN
doc/pockit.drawio.png


+ 4 - 0
src/.env

@@ -0,0 +1,4 @@
+user_name="Вадим"
+journal_login="Korolevvs"
+journal_password="4jUPF0WqAX"
+period_id="573"

+ 5 - 0
src/composer.json

@@ -0,0 +1,5 @@
+{
+    "require": {
+        "vlucas/phpdotenv": "^5.6"
+    }
+}

+ 488 - 0
src/composer.lock

@@ -0,0 +1,488 @@
+{
+    "_readme": [
+        "This file locks the dependencies of your project to a known state",
+        "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
+        "This file is @generated automatically"
+    ],
+    "content-hash": "108be68e4e2b97fed51d36a10eed0849",
+    "packages": [
+        {
+            "name": "graham-campbell/result-type",
+            "version": "v1.1.2",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/GrahamCampbell/Result-Type.git",
+                "reference": "fbd48bce38f73f8a4ec8583362e732e4095e5862"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/GrahamCampbell/Result-Type/zipball/fbd48bce38f73f8a4ec8583362e732e4095e5862",
+                "reference": "fbd48bce38f73f8a4ec8583362e732e4095e5862",
+                "shasum": ""
+            },
+            "require": {
+                "php": "^7.2.5 || ^8.0",
+                "phpoption/phpoption": "^1.9.2"
+            },
+            "require-dev": {
+                "phpunit/phpunit": "^8.5.34 || ^9.6.13 || ^10.4.2"
+            },
+            "type": "library",
+            "autoload": {
+                "psr-4": {
+                    "GrahamCampbell\\ResultType\\": "src/"
+                }
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "MIT"
+            ],
+            "authors": [
+                {
+                    "name": "Graham Campbell",
+                    "email": "hello@gjcampbell.co.uk",
+                    "homepage": "https://github.com/GrahamCampbell"
+                }
+            ],
+            "description": "An Implementation Of The Result Type",
+            "keywords": [
+                "Graham Campbell",
+                "GrahamCampbell",
+                "Result Type",
+                "Result-Type",
+                "result"
+            ],
+            "support": {
+                "issues": "https://github.com/GrahamCampbell/Result-Type/issues",
+                "source": "https://github.com/GrahamCampbell/Result-Type/tree/v1.1.2"
+            },
+            "funding": [
+                {
+                    "url": "https://github.com/GrahamCampbell",
+                    "type": "github"
+                },
+                {
+                    "url": "https://tidelift.com/funding/github/packagist/graham-campbell/result-type",
+                    "type": "tidelift"
+                }
+            ],
+            "time": "2023-11-12T22:16:48+00:00"
+        },
+        {
+            "name": "phpoption/phpoption",
+            "version": "1.9.2",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/schmittjoh/php-option.git",
+                "reference": "80735db690fe4fc5c76dfa7f9b770634285fa820"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/schmittjoh/php-option/zipball/80735db690fe4fc5c76dfa7f9b770634285fa820",
+                "reference": "80735db690fe4fc5c76dfa7f9b770634285fa820",
+                "shasum": ""
+            },
+            "require": {
+                "php": "^7.2.5 || ^8.0"
+            },
+            "require-dev": {
+                "bamarni/composer-bin-plugin": "^1.8.2",
+                "phpunit/phpunit": "^8.5.34 || ^9.6.13 || ^10.4.2"
+            },
+            "type": "library",
+            "extra": {
+                "bamarni-bin": {
+                    "bin-links": true,
+                    "forward-command": true
+                },
+                "branch-alias": {
+                    "dev-master": "1.9-dev"
+                }
+            },
+            "autoload": {
+                "psr-4": {
+                    "PhpOption\\": "src/PhpOption/"
+                }
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "Apache-2.0"
+            ],
+            "authors": [
+                {
+                    "name": "Johannes M. Schmitt",
+                    "email": "schmittjoh@gmail.com",
+                    "homepage": "https://github.com/schmittjoh"
+                },
+                {
+                    "name": "Graham Campbell",
+                    "email": "hello@gjcampbell.co.uk",
+                    "homepage": "https://github.com/GrahamCampbell"
+                }
+            ],
+            "description": "Option Type for PHP",
+            "keywords": [
+                "language",
+                "option",
+                "php",
+                "type"
+            ],
+            "support": {
+                "issues": "https://github.com/schmittjoh/php-option/issues",
+                "source": "https://github.com/schmittjoh/php-option/tree/1.9.2"
+            },
+            "funding": [
+                {
+                    "url": "https://github.com/GrahamCampbell",
+                    "type": "github"
+                },
+                {
+                    "url": "https://tidelift.com/funding/github/packagist/phpoption/phpoption",
+                    "type": "tidelift"
+                }
+            ],
+            "time": "2023-11-12T21:59:55+00:00"
+        },
+        {
+            "name": "symfony/polyfill-ctype",
+            "version": "v1.28.0",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/symfony/polyfill-ctype.git",
+                "reference": "ea208ce43cbb04af6867b4fdddb1bdbf84cc28cb"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/ea208ce43cbb04af6867b4fdddb1bdbf84cc28cb",
+                "reference": "ea208ce43cbb04af6867b4fdddb1bdbf84cc28cb",
+                "shasum": ""
+            },
+            "require": {
+                "php": ">=7.1"
+            },
+            "provide": {
+                "ext-ctype": "*"
+            },
+            "suggest": {
+                "ext-ctype": "For best performance"
+            },
+            "type": "library",
+            "extra": {
+                "branch-alias": {
+                    "dev-main": "1.28-dev"
+                },
+                "thanks": {
+                    "name": "symfony/polyfill",
+                    "url": "https://github.com/symfony/polyfill"
+                }
+            },
+            "autoload": {
+                "files": [
+                    "bootstrap.php"
+                ],
+                "psr-4": {
+                    "Symfony\\Polyfill\\Ctype\\": ""
+                }
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "MIT"
+            ],
+            "authors": [
+                {
+                    "name": "Gert de Pagter",
+                    "email": "BackEndTea@gmail.com"
+                },
+                {
+                    "name": "Symfony Community",
+                    "homepage": "https://symfony.com/contributors"
+                }
+            ],
+            "description": "Symfony polyfill for ctype functions",
+            "homepage": "https://symfony.com",
+            "keywords": [
+                "compatibility",
+                "ctype",
+                "polyfill",
+                "portable"
+            ],
+            "support": {
+                "source": "https://github.com/symfony/polyfill-ctype/tree/v1.28.0"
+            },
+            "funding": [
+                {
+                    "url": "https://symfony.com/sponsor",
+                    "type": "custom"
+                },
+                {
+                    "url": "https://github.com/fabpot",
+                    "type": "github"
+                },
+                {
+                    "url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
+                    "type": "tidelift"
+                }
+            ],
+            "time": "2023-01-26T09:26:14+00:00"
+        },
+        {
+            "name": "symfony/polyfill-mbstring",
+            "version": "v1.28.0",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/symfony/polyfill-mbstring.git",
+                "reference": "42292d99c55abe617799667f454222c54c60e229"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/42292d99c55abe617799667f454222c54c60e229",
+                "reference": "42292d99c55abe617799667f454222c54c60e229",
+                "shasum": ""
+            },
+            "require": {
+                "php": ">=7.1"
+            },
+            "provide": {
+                "ext-mbstring": "*"
+            },
+            "suggest": {
+                "ext-mbstring": "For best performance"
+            },
+            "type": "library",
+            "extra": {
+                "branch-alias": {
+                    "dev-main": "1.28-dev"
+                },
+                "thanks": {
+                    "name": "symfony/polyfill",
+                    "url": "https://github.com/symfony/polyfill"
+                }
+            },
+            "autoload": {
+                "files": [
+                    "bootstrap.php"
+                ],
+                "psr-4": {
+                    "Symfony\\Polyfill\\Mbstring\\": ""
+                }
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "MIT"
+            ],
+            "authors": [
+                {
+                    "name": "Nicolas Grekas",
+                    "email": "p@tchwork.com"
+                },
+                {
+                    "name": "Symfony Community",
+                    "homepage": "https://symfony.com/contributors"
+                }
+            ],
+            "description": "Symfony polyfill for the Mbstring extension",
+            "homepage": "https://symfony.com",
+            "keywords": [
+                "compatibility",
+                "mbstring",
+                "polyfill",
+                "portable",
+                "shim"
+            ],
+            "support": {
+                "source": "https://github.com/symfony/polyfill-mbstring/tree/v1.28.0"
+            },
+            "funding": [
+                {
+                    "url": "https://symfony.com/sponsor",
+                    "type": "custom"
+                },
+                {
+                    "url": "https://github.com/fabpot",
+                    "type": "github"
+                },
+                {
+                    "url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
+                    "type": "tidelift"
+                }
+            ],
+            "time": "2023-07-28T09:04:16+00:00"
+        },
+        {
+            "name": "symfony/polyfill-php80",
+            "version": "v1.28.0",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/symfony/polyfill-php80.git",
+                "reference": "6caa57379c4aec19c0a12a38b59b26487dcfe4b5"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/symfony/polyfill-php80/zipball/6caa57379c4aec19c0a12a38b59b26487dcfe4b5",
+                "reference": "6caa57379c4aec19c0a12a38b59b26487dcfe4b5",
+                "shasum": ""
+            },
+            "require": {
+                "php": ">=7.1"
+            },
+            "type": "library",
+            "extra": {
+                "branch-alias": {
+                    "dev-main": "1.28-dev"
+                },
+                "thanks": {
+                    "name": "symfony/polyfill",
+                    "url": "https://github.com/symfony/polyfill"
+                }
+            },
+            "autoload": {
+                "files": [
+                    "bootstrap.php"
+                ],
+                "psr-4": {
+                    "Symfony\\Polyfill\\Php80\\": ""
+                },
+                "classmap": [
+                    "Resources/stubs"
+                ]
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "MIT"
+            ],
+            "authors": [
+                {
+                    "name": "Ion Bazan",
+                    "email": "ion.bazan@gmail.com"
+                },
+                {
+                    "name": "Nicolas Grekas",
+                    "email": "p@tchwork.com"
+                },
+                {
+                    "name": "Symfony Community",
+                    "homepage": "https://symfony.com/contributors"
+                }
+            ],
+            "description": "Symfony polyfill backporting some PHP 8.0+ features to lower PHP versions",
+            "homepage": "https://symfony.com",
+            "keywords": [
+                "compatibility",
+                "polyfill",
+                "portable",
+                "shim"
+            ],
+            "support": {
+                "source": "https://github.com/symfony/polyfill-php80/tree/v1.28.0"
+            },
+            "funding": [
+                {
+                    "url": "https://symfony.com/sponsor",
+                    "type": "custom"
+                },
+                {
+                    "url": "https://github.com/fabpot",
+                    "type": "github"
+                },
+                {
+                    "url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
+                    "type": "tidelift"
+                }
+            ],
+            "time": "2023-01-26T09:26:14+00:00"
+        },
+        {
+            "name": "vlucas/phpdotenv",
+            "version": "v5.6.0",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/vlucas/phpdotenv.git",
+                "reference": "2cf9fb6054c2bb1d59d1f3817706ecdb9d2934c4"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/vlucas/phpdotenv/zipball/2cf9fb6054c2bb1d59d1f3817706ecdb9d2934c4",
+                "reference": "2cf9fb6054c2bb1d59d1f3817706ecdb9d2934c4",
+                "shasum": ""
+            },
+            "require": {
+                "ext-pcre": "*",
+                "graham-campbell/result-type": "^1.1.2",
+                "php": "^7.2.5 || ^8.0",
+                "phpoption/phpoption": "^1.9.2",
+                "symfony/polyfill-ctype": "^1.24",
+                "symfony/polyfill-mbstring": "^1.24",
+                "symfony/polyfill-php80": "^1.24"
+            },
+            "require-dev": {
+                "bamarni/composer-bin-plugin": "^1.8.2",
+                "ext-filter": "*",
+                "phpunit/phpunit": "^8.5.34 || ^9.6.13 || ^10.4.2"
+            },
+            "suggest": {
+                "ext-filter": "Required to use the boolean validator."
+            },
+            "type": "library",
+            "extra": {
+                "bamarni-bin": {
+                    "bin-links": true,
+                    "forward-command": true
+                },
+                "branch-alias": {
+                    "dev-master": "5.6-dev"
+                }
+            },
+            "autoload": {
+                "psr-4": {
+                    "Dotenv\\": "src/"
+                }
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "BSD-3-Clause"
+            ],
+            "authors": [
+                {
+                    "name": "Graham Campbell",
+                    "email": "hello@gjcampbell.co.uk",
+                    "homepage": "https://github.com/GrahamCampbell"
+                },
+                {
+                    "name": "Vance Lucas",
+                    "email": "vance@vancelucas.com",
+                    "homepage": "https://github.com/vlucas"
+                }
+            ],
+            "description": "Loads environment variables from `.env` to `getenv()`, `$_ENV` and `$_SERVER` automagically.",
+            "keywords": [
+                "dotenv",
+                "env",
+                "environment"
+            ],
+            "support": {
+                "issues": "https://github.com/vlucas/phpdotenv/issues",
+                "source": "https://github.com/vlucas/phpdotenv/tree/v5.6.0"
+            },
+            "funding": [
+                {
+                    "url": "https://github.com/GrahamCampbell",
+                    "type": "github"
+                },
+                {
+                    "url": "https://tidelift.com/funding/github/packagist/vlucas/phpdotenv",
+                    "type": "tidelift"
+                }
+            ],
+            "time": "2023-11-12T22:43:29+00:00"
+        }
+    ],
+    "packages-dev": [],
+    "aliases": [],
+    "minimum-stability": "stable",
+    "stability-flags": [],
+    "prefer-stable": false,
+    "prefer-lowest": false,
+    "platform": [],
+    "platform-dev": [],
+    "plugin-api-version": "2.0.0"
+}

+ 18 - 0
src/config.php

@@ -0,0 +1,18 @@
+<?php
+// Файл настроек
+
+// Установка временного пояса по умолчанию (GMT+3)
+date_default_timezone_set("Europe/Kirov");
+
+define('rootdir', __DIR__);
+
+require_once rootdir."/vendor/autoload.php";
+
+if (strtoupper(substr(php_uname("s"), 0, 3)) === 'WIN') {
+    define("server_os", "windows");
+} else {
+    define("server_os", "linux");
+}
+
+$dotenv = Dotenv\Dotenv::createImmutable(rootdir);
+$dotenv->load();

+ 11 - 0
src/controllers/Controller.php

@@ -0,0 +1,11 @@
+<?php
+// Контроллер
+
+class Controller {
+	public string $request_uri;
+	
+	// Родительский класс контроллеров
+	public function __construct($request_uri = null) {
+		$this->request_uri = $request_uri;
+	}
+}

+ 82 - 0
src/controllers/GradesController.php

@@ -0,0 +1,82 @@
+<?php
+// Контроллер оценок
+
+class GradesController extends Controller {
+
+	// Оценки
+	public function index() {
+		$view = new GradesView([
+			"page_title"=>"Оценки",
+			"crumbs" => ["Главная" => "/"]
+		]);
+		$view->view();
+	}
+
+	// Сбор оценок
+	public function collect() {
+		
+		// Создаём разделяемый обработчик
+		$sh = curl_share_init();
+		curl_share_setopt($sh, CURLSHOPT_SHARE, CURL_LOCK_DATA_COOKIE); // Делимся куками
+
+		// Подаём запрос в электронный дневник на авторизацию
+		$auth = curl_init('http://vpmt.ru:8081/region_pou/region.cgi/login');
+		curl_setopt($auth, CURLOPT_COOKIEFILE, "");
+		curl_setopt($auth, CURLOPT_SHARE, $sh);
+		curl_setopt($auth, CURLOPT_POST, 1);
+		curl_setopt($auth, CURLOPT_POSTFIELDS, 'username='.$_ENV["journal_login"].'&userpass='.sha1($_ENV["journal_password"]));
+		curl_setopt($auth, CURLOPT_ENCODING, 'windows-1251');
+		curl_setopt($auth, CURLOPT_RETURNTRANSFER, 1);
+		curl_exec($auth);
+
+		// Запрос на экспорт оценок
+		$grades = curl_init('http://vpmt.ru:8081/region_pou/region.cgi/journal_och?page=1&marks=1&period_id='.$_ENV['period_id'].'&export=1');
+		curl_setopt($grades, CURLOPT_COOKIEFILE, "");
+		curl_setopt($grades, CURLOPT_SHARE, $sh);
+		curl_setopt($grades, CURLOPT_RETURNTRANSFER, 1);
+		$data = curl_exec($grades);
+
+		// Разрыв сессии с журналом
+		$logout = curl_init('http://vpmt.ru:8081/region_pou/region.cgi/logout');
+		curl_setopt($logout, CURLOPT_COOKIEFILE, "");
+		curl_setopt($logout, CURLOPT_SHARE, $sh);
+		curl_setopt($logout, CURLOPT_RETURNTRANSFER, 1);
+		curl_exec($logout);
+
+		// Парсинг экспортного XML
+		// Данные хранятся в строках с тэгом Row
+		// Первые 3 не содержат оценок, их пропускаем
+		// Последний ряд тоже не содержит оценок, его не обрабатываем
+		// Если XML загрузить не удаётся, то **скорее всего** логин и пароль неверны
+		$doc = new DOMDocument();
+		$doc->loadXML($data);
+		$rows = $doc->getElementsByTagName("Row");
+
+		$output = [];
+		for ($y = 4; $y < count($rows) - 1; $y++) {
+			$output_row = [];
+			$children = $rows[$y]->childNodes;
+			// Children - дочерние узлы тэга Row
+			// Переносы строк в документе считаются узлом текста, поэтому
+			// [0] - текстовый узел
+			// [1] - содержит название дисциплины
+			// [2] - текстовый узел
+			// [3] - содержит оценки
+			// [4] - текстовый узел
+			// [5] - средний балл
+			// [6] - текстовый узел
+			$output[] = [
+				trim($children[1]->nodeValue),
+				trim($children[3]->nodeValue),
+				trim($children[5]->nodeValue)
+			];
+		}
+
+		// Закрываем сессии curl
+		curl_share_close($sh);
+		curl_close($auth);
+		curl_close($grades);
+
+		echo json_encode($output);
+	}
+}

+ 29 - 0
src/controllers/HomeController.php

@@ -0,0 +1,29 @@
+<?php
+// Контроллер домашней страницы
+
+class HomeController extends Controller {
+
+	// Главная домашняя страница
+	public function index() {
+		// Определяем текст приветствия
+		$now_hour = localtime(time(), true)['tm_hour'];
+		if ($now_hour < 6 || $now_hour > 20) {
+			$welcome_text = 'С возвращением';
+		} else if ($now_hour < 9) {
+			$welcome_text = 'Доброе утро';
+		} else if ($now_hour < 16) {
+			$welcome_text = 'Добрый день';
+		} else {
+			$welcome_text = 'Добрый вечер';
+		}
+		$welcome_text .= ", ".$_ENV["user_name"];
+
+		//~ echo $welcome_text;
+
+		$view = new HomeView([
+			"welcome_text" => $welcome_text,
+			"page_title" => "ДОМ"
+		]);
+		$view->view();
+	}
+}

+ 10 - 0
src/controllers/NotFoundController.php

@@ -0,0 +1,10 @@
+<?php
+// Отображает сообщения "не найдено"
+
+class NotFoundController {
+	// Главная страница
+	public function index() {
+		$view = new NotFoundView();
+		$view->view();
+	}
+}

BIN
src/img/favicons/book.png


BIN
src/img/favicons/cross.png


BIN
src/img/favicons/file.png


BIN
src/img/favicons/home.png


BIN
src/img/favicons/shield.png


+ 17 - 0
src/index.php

@@ -0,0 +1,17 @@
+<?php
+// Главный файл
+
+require_once "config.php";
+require_once "pockit.php";
+
+// Автозагрузка
+spl_autoload_register("Pockit::autoload");
+
+// Определение маршрутов
+$router = new Router();
+$router->register('', ['HomeController', 'index']);
+$router->register('/grades', ['GradesController', 'index']);
+$router->register('/grades/get', ['GradesController', 'collect']);
+$router->register404(['NotFoundController', 'index']);
+
+return $router->handle($_SERVER['REQUEST_URI']);

+ 56 - 0
src/pockit.php

@@ -0,0 +1,56 @@
+<?php
+
+// Функции, необходимые везде
+class Pockit {
+	public static function autoload($classname) {
+		if (preg_match('/Controller$/', $classname)) {
+			require_once rootdir.'/controllers/'.$classname.'.php';
+		} else if (preg_match('/View$/', $classname)) {
+			require_once rootdir.'/views/'.$classname.'.php';
+		} else if (preg_match('/Model$/', $classname)) {
+			require_once rootdir.'/models/'.$classname.'.php';
+		}
+	}
+}
+
+class Router {
+	private $routes = array();
+	private $not_found_handler;
+
+	// Регистрирует маршрут
+	public function register(string $uri, callable $handler) {
+		$this->routes[$uri] = $handler;
+	}
+
+	// Регистрирует маршрут для 404
+	public function register404(callable $handler) {
+		$this->not_found_handler = $handler;
+	}
+
+	// Выполняет маршрутизацию
+	public function handle(string $request_uri) {
+		if (preg_match('/^\/(?:css|fonts|img|doc|js)\//', $request_uri)) {
+			// Подача как есть
+			return false;
+		}
+
+		// Ищем подходящий маршрут
+		foreach ($this->routes as $route => $callback) {
+			$pattern = '/^'.str_replace('/', '\/', $route).'\/?((?:\?|\&)\w+=\w*)*$/';
+			if (preg_match($pattern, $request_uri)) {
+				$this->invoke($callback, $request_uri);
+			}
+		}
+
+		// Маршрут не найден, вызываем 404!
+		$this->invoke($this->not_found_handler, $request_uri);
+	}
+
+	// Вызывает функцию, которую определил маршрут
+	private function invoke(callable $callback, string $request_uri) {
+		list($handler, $handle_method) = $callback;
+		$h = new $handler($request_uri);
+		$h->$handle_method();
+		exit();
+	}
+}

+ 108 - 0
src/views/GradesView.php

@@ -0,0 +1,108 @@
+<?php
+// Оценки
+
+class GradesView extends LayoutView {
+	public function content():void { ?>
+
+<h1>Оценки</h1>
+
+<svg id="loading" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" style="margin:auto;background:transparent;display:block;" width="200px" height="200px" viewBox="0 0 100 100" preserveAspectRatio="xMidYMid">
+<g transform="translate(80,50)">
+<g transform="rotate(0)">
+<circle cx="0" cy="0" r="6" fill="#7286ff" fill-opacity="1">
+<animateTransform attributeName="transform" type="scale" begin="-0.875s" values="1.5 1.5;1 1" keyTimes="0;1" dur="1s" repeatCount="indefinite"></animateTransform>
+<animate attributeName="fill-opacity" keyTimes="0;1" dur="1s" repeatCount="indefinite" values="1;0" begin="-0.875s"></animate>
+</circle>
+</g>
+</g><g transform="translate(71.21320343559643,71.21320343559643)">
+<g transform="rotate(45)">
+<circle cx="0" cy="0" r="6" fill="#7286ff" fill-opacity="0.875">
+<animateTransform attributeName="transform" type="scale" begin="-0.75s" values="1.5 1.5;1 1" keyTimes="0;1" dur="1s" repeatCount="indefinite"></animateTransform>
+<animate attributeName="fill-opacity" keyTimes="0;1" dur="1s" repeatCount="indefinite" values="1;0" begin="-0.75s"></animate>
+</circle>
+</g>
+</g><g transform="translate(50,80)">
+<g transform="rotate(90)">
+<circle cx="0" cy="0" r="6" fill="#7286ff" fill-opacity="0.75">
+<animateTransform attributeName="transform" type="scale" begin="-0.625s" values="1.5 1.5;1 1" keyTimes="0;1" dur="1s" repeatCount="indefinite"></animateTransform>
+<animate attributeName="fill-opacity" keyTimes="0;1" dur="1s" repeatCount="indefinite" values="1;0" begin="-0.625s"></animate>
+</circle>
+</g>
+</g><g transform="translate(28.786796564403577,71.21320343559643)">
+<g transform="rotate(135)">
+<circle cx="0" cy="0" r="6" fill="#7286ff" fill-opacity="0.625">
+<animateTransform attributeName="transform" type="scale" begin="-0.5s" values="1.5 1.5;1 1" keyTimes="0;1" dur="1s" repeatCount="indefinite"></animateTransform>
+<animate attributeName="fill-opacity" keyTimes="0;1" dur="1s" repeatCount="indefinite" values="1;0" begin="-0.5s"></animate>
+</circle>
+</g>
+</g><g transform="translate(20,50.00000000000001)">
+<g transform="rotate(180)">
+<circle cx="0" cy="0" r="6" fill="#7286ff" fill-opacity="0.5">
+<animateTransform attributeName="transform" type="scale" begin="-0.375s" values="1.5 1.5;1 1" keyTimes="0;1" dur="1s" repeatCount="indefinite"></animateTransform>
+<animate attributeName="fill-opacity" keyTimes="0;1" dur="1s" repeatCount="indefinite" values="1;0" begin="-0.375s"></animate>
+</circle>
+</g>
+</g><g transform="translate(28.78679656440357,28.786796564403577)">
+<g transform="rotate(225)">
+<circle cx="0" cy="0" r="6" fill="#7286ff" fill-opacity="0.375">
+<animateTransform attributeName="transform" type="scale" begin="-0.25s" values="1.5 1.5;1 1" keyTimes="0;1" dur="1s" repeatCount="indefinite"></animateTransform>
+<animate attributeName="fill-opacity" keyTimes="0;1" dur="1s" repeatCount="indefinite" values="1;0" begin="-0.25s"></animate>
+</circle>
+</g>
+</g><g transform="translate(49.99999999999999,20)">
+<g transform="rotate(270)">
+<circle cx="0" cy="0" r="6" fill="#7286ff" fill-opacity="0.25">
+<animateTransform attributeName="transform" type="scale" begin="-0.125s" values="1.5 1.5;1 1" keyTimes="0;1" dur="1s" repeatCount="indefinite"></animateTransform>
+<animate attributeName="fill-opacity" keyTimes="0;1" dur="1s" repeatCount="indefinite" values="1;0" begin="-0.125s"></animate>
+</circle>
+</g>
+</g><g transform="translate(71.21320343559643,28.78679656440357)">
+<g transform="rotate(315)">
+<circle cx="0" cy="0" r="6" fill="#7286ff" fill-opacity="0.125">
+<animateTransform attributeName="transform" type="scale" begin="0s" values="1.5 1.5;1 1" keyTimes="0;1" dur="1s" repeatCount="indefinite"></animateTransform>
+<animate attributeName="fill-opacity" keyTimes="0;1" dur="1s" repeatCount="indefinite" values="1;0" begin="0s"></animate>
+</circle>
+</g>
+</g>
+</svg>
+
+<script>
+// Создаём запрос на сервер для получения оценок
+const xhr = new XMLHttpRequest();
+xhr.onload = function () {
+	document.getElementById("loading").remove();
+	let data = JSON.parse(this.responseText);
+	let table = document.createElement("table");
+
+	// Заголовки
+	let header = document.createElement("tr");
+	let cell = document.createElement("th"); cell.textContent = "Дисциплина"; header.appendChild(cell);
+	cell = document.createElement("th"); cell.textContent = "Оценки"; header.appendChild(cell);
+	cell = document.createElement("th"); cell.textContent = "Средний балл"; header.appendChild(cell);
+	table.appendChild(header);
+
+	for (let y = 0; y < data.length; y++) {
+		let row = document.createElement("tr");
+
+		cell = document.createElement("td"); cell.textContent = data[y][0]; row.appendChild(cell);
+		cell = document.createElement("td"); cell.textContent = data[y][1]; row.appendChild(cell);
+		cell = document.createElement("td"); cell.textContent = data[y][2]; row.appendChild(cell);
+
+		if (data[y][1].indexOf('2') > -1) {
+			row.classList.add("problem");
+		} else {
+			if (data[y][1].indexOf('3') == -1 && data[y][1].indexOf('4') == -1) {
+				row.classList.add("perfect");
+			}
+		}
+
+		table.appendChild(row);
+	}
+	document.body.appendChild(table);
+	
+}
+xhr.open("GET", "/grades/get");
+xhr.send();
+</script>
+		
+<?php }}

+ 50 - 0
src/views/HomeView.php

@@ -0,0 +1,50 @@
+<?php
+// Главная страница приложения
+
+class HomeView extends LayoutView {
+	protected $welcome_text;
+	public function content():void { ?>
+
+<!--Добро пожаловать-->
+<h1><?= $this->welcome_text ?></h1>
+<h3>Выбери действие</h3>
+
+<div>
+	<div>
+		<p><a href="/regen/new">Создать отчёт</a></p>
+		<p><a href="/regen/new">Архив отчёт</a></p>
+		<p><a href="/grades">Оценки</a></p>
+	</div>
+</div>
+
+<!--Меню действий-->
+<!--
+<div id='home-actions'>
+	<div class='home-action'>
+		<img src="/img/home-actions/file.png">
+		<a class='full-link' href="/regen/new" target="_blank">Создать отчёт<span></span></a>
+	</div>
+	<div class='home-action'>
+		<img src="/img/home-actions/archive.png">
+		<a class='full-link' href="/regen/archive">Архив отчётов<span></span></a>
+	</div>
+	<div class='home-action'>
+		<img src="/img/home-actions/table.png">
+		<a class='full-link' href="/regen/tabgen">Генератор таблиц<span></span></a>
+	</div>
+	<div class='home-action'>
+		<img src="/img/home-actions/settings.png">
+		<a class='full-link' href="#">Настройки<span></span></a>
+	</div>
+	<div class='home-action'>
+		<img src="/img/home-actions/lock.png">
+		<a class='full-link' href="/passwords">Менеджер паролей<span></span></a>
+	</div>
+	<div class='home-action'>
+		<img src="/img/home-actions/search.png">
+		<a class='full-link' href="/knowledge">Учебные материалы<span></span></a>
+	</div>
+</div>
+-->
+		
+<?php }}

+ 29 - 0
src/views/LayoutView.php

@@ -0,0 +1,29 @@
+<?php
+// Класс разметки
+
+class LayoutView extends View {
+	protected $page_title;
+	protected $crumbs;
+
+	protected function breadcrumbs() {
+		foreach ($this->crumbs as $crumb => $url) { ?>
+			<a href="<?= $url ?>"><?= $crumb ?></a>
+		<?php }
+	}
+	
+	public function view():void { ?>
+
+<!DOCTYPE html>
+<html lang="ru">
+	<head>
+		<meta charset="UTF-8">
+		<meta name="viewport" content="width=device-width, initial-scale=1.0">
+		<title><?= $this->page_title ?></title>
+	</head>
+	<body>
+		<?php $this->breadcrumbs(); ?>
+		<?php $this->content(); ?>
+	</body>
+</html>
+
+<?php }}

+ 14 - 0
src/views/NotFoundView.php

@@ -0,0 +1,14 @@
+<?php
+// Страница "404"
+
+class NotFoundView extends LayoutView {
+	protected $page_title = "Ничего не найдено";
+	public function content():void { ?>
+		
+<div style='text-align:center'>
+	<h1>Ничего не найдено</h1>
+	<h2>404</h2>
+	<a style='display:block' href='/'>На главную</a>
+</div>
+
+<?php }}

+ 23 - 0
src/views/View.php

@@ -0,0 +1,23 @@
+<?php
+// Родительский класс View
+
+class View {
+	public function __construct($data=array()) {
+		$this->assignData($data);
+	}
+
+	// Загрузка переменных из $data в $this-><переменная>
+	public function assignData($data) {
+		extract($data, EXTR_REFS);
+		foreach ($data as $key=>$value) {
+			$this->$key = $value;
+		}
+	}
+
+	// Возвращает контент как HTML, используя буферизацию вывода
+	public function render() {
+		ob_start();
+		$this->view();
+		return ob_get_clean();
+	}
+}

+ 1 - 0
start.sh

@@ -0,0 +1 @@
+php -S localhost:9000 -t ./src ./src/index.php