diff --git a/frontend/assets/mod.css b/frontend/assets/mod.css
new file mode 100644
index 0000000..4e8c596
--- /dev/null
+++ b/frontend/assets/mod.css
@@ -0,0 +1,944 @@
+/* From Uiverse.io by Galahhad */
+/* REMASTERED */
+/* RTX-ON */
+/* completely redone toggle and droid */
+
+.bb8-toggle {
+ --toggle-size: 10px;
+ /* finally I removed the scale now everything depends on the font-size */
+ /* --margin-top-for-head: 1.75em; */
+ /* it's just in case 👆 */
+ --toggle-width: 10.625em;
+ --toggle-height: 5.625em;
+ --toggle-offset: calc((var(--toggle-height) - var(--bb8-diameter)) / 2);
+ --toggle-bg: linear-gradient(#2c4770, #070e2b 35%, #628cac 50% 70%, #a6c5d4)
+ no-repeat;
+ --bb8-diameter: 4.375em;
+ --radius: 99em;
+ --transition: 0.4s;
+ --accent: #de7d2f;
+ --bb8-bg: #fff;
+}
+
+.bb8-toggle,
+.bb8-toggle *,
+.bb8-toggle *::before,
+.bb8-toggle *::after {
+ -webkit-box-sizing: border-box;
+ box-sizing: border-box;
+}
+
+.bb8-toggle {
+ cursor: pointer;
+ margin-top: var(--margin-top-for-head);
+ font-size: var(--toggle-size);
+}
+
+.bb8-toggle__checkbox {
+ -webkit-appearance: none;
+ -moz-appearance: none;
+ appearance: none;
+ display: none;
+}
+
+.bb8-toggle__container {
+ width: var(--toggle-width);
+ height: var(--toggle-height);
+ background: var(--toggle-bg);
+ background-size: 100% 11.25em;
+ background-position-y: -5.625em;
+ border-radius: var(--radius);
+ position: relative;
+ -webkit-transition: var(--transition);
+ -o-transition: var(--transition);
+ transition: var(--transition);
+}
+
+.bb8 {
+ display: -webkit-box;
+ display: -ms-flexbox;
+ display: flex;
+ -webkit-box-orient: vertical;
+ -webkit-box-direction: normal;
+ -ms-flex-direction: column;
+ flex-direction: column;
+ -webkit-box-align: center;
+ -ms-flex-align: center;
+ align-items: center;
+ position: absolute;
+ top: calc(var(--toggle-offset) - 1.688em + 0.188em);
+ left: var(--toggle-offset);
+ -webkit-transition: var(--transition);
+ -o-transition: var(--transition);
+ transition: var(--transition);
+ z-index: 2;
+}
+
+.bb8__head-container {
+ position: relative;
+ -webkit-transition: var(--transition);
+ -o-transition: var(--transition);
+ transition: var(--transition);
+ z-index: 2;
+ -webkit-transform-origin: 1.25em 3.75em;
+ -ms-transform-origin: 1.25em 3.75em;
+ transform-origin: 1.25em 3.75em;
+}
+
+.bb8__head {
+ overflow: hidden;
+ margin-bottom: -0.188em;
+ width: 2.5em;
+ height: 1.688em;
+ background: -o-linear-gradient(
+ transparent 0.063em,
+ dimgray 0.063em 0.313em,
+ transparent 0.313em 0.375em,
+ var(--accent) 0.375em 0.5em,
+ transparent 0.5em 1.313em,
+ silver 1.313em 1.438em,
+ transparent 1.438em
+ ),
+ -o-linear-gradient(45deg, transparent 0.188em, var(--bb8-bg) 0.188em 1.25em, transparent
+ 1.25em),
+ -o-linear-gradient(135deg, transparent 0.188em, var(--bb8-bg) 0.188em 1.25em, transparent
+ 1.25em),
+ -o-linear-gradient(var(--bb8-bg) 1.25em, transparent 1.25em);
+ background: -o-linear-gradient(
+ transparent 0.063em,
+ dimgray 0.063em 0.313em,
+ transparent 0.313em 0.375em,
+ var(--accent) 0.375em 0.5em,
+ transparent 0.5em 1.313em,
+ silver 1.313em 1.438em,
+ transparent 1.438em
+ ),
+ -o-linear-gradient(45deg, transparent 0.188em, var(--bb8-bg) 0.188em 1.25em, transparent
+ 1.25em),
+ -o-linear-gradient(135deg, transparent 0.188em, var(--bb8-bg) 0.188em 1.25em, transparent
+ 1.25em),
+ -o-linear-gradient(var(--bb8-bg) 1.25em, transparent 1.25em);
+ background: -o-linear-gradient(
+ transparent 0.063em,
+ dimgray 0.063em 0.313em,
+ transparent 0.313em 0.375em,
+ var(--accent) 0.375em 0.5em,
+ transparent 0.5em 1.313em,
+ silver 1.313em 1.438em,
+ transparent 1.438em
+ ),
+ -o-linear-gradient(45deg, transparent 0.188em, var(--bb8-bg) 0.188em 1.25em, transparent
+ 1.25em),
+ -o-linear-gradient(135deg, transparent 0.188em, var(--bb8-bg) 0.188em 1.25em, transparent
+ 1.25em),
+ -o-linear-gradient(var(--bb8-bg) 1.25em, transparent 1.25em);
+ background: -o-linear-gradient(
+ transparent 0.063em,
+ dimgray 0.063em 0.313em,
+ transparent 0.313em 0.375em,
+ var(--accent) 0.375em 0.5em,
+ transparent 0.5em 1.313em,
+ silver 1.313em 1.438em,
+ transparent 1.438em
+ ),
+ -o-linear-gradient(45deg, transparent 0.188em, var(--bb8-bg) 0.188em 1.25em, transparent
+ 1.25em),
+ -o-linear-gradient(135deg, transparent 0.188em, var(--bb8-bg) 0.188em 1.25em, transparent
+ 1.25em),
+ -o-linear-gradient(var(--bb8-bg) 1.25em, transparent 1.25em);
+ background: linear-gradient(
+ transparent 0.063em,
+ dimgray 0.063em 0.313em,
+ transparent 0.313em 0.375em,
+ var(--accent) 0.375em 0.5em,
+ transparent 0.5em 1.313em,
+ silver 1.313em 1.438em,
+ transparent 1.438em
+ ),
+ linear-gradient(
+ 45deg,
+ transparent 0.188em,
+ var(--bb8-bg) 0.188em 1.25em,
+ transparent 1.25em
+ ),
+ linear-gradient(
+ -45deg,
+ transparent 0.188em,
+ var(--bb8-bg) 0.188em 1.25em,
+ transparent 1.25em
+ ),
+ linear-gradient(var(--bb8-bg) 1.25em, transparent 1.25em);
+ border-radius: var(--radius) var(--radius) 0 0;
+ position: relative;
+ z-index: 1;
+ -webkit-filter: drop-shadow(0 0.063em 0.125em gray);
+ filter: drop-shadow(0 0.063em 0.125em gray);
+}
+
+.bb8__head::before {
+ content: "";
+ position: absolute;
+ width: 0.563em;
+ height: 0.563em;
+ background: -o-radial-gradient(
+ 0.25em 0.375em,
+ 0.125em circle,
+ red,
+ transparent
+ ),
+ -o-radial-gradient(0.375em 0.188em, 0.063em circle, var(--bb8-bg) 50%, transparent
+ 100%),
+ -o-linear-gradient(45deg, #000 0.188em, dimgray 0.313em 0.375em, #000 0.5em);
+ background: -o-radial-gradient(
+ 0.25em 0.375em,
+ 0.125em circle,
+ red,
+ transparent
+ ),
+ -o-radial-gradient(0.375em 0.188em, 0.063em circle, var(--bb8-bg) 50%, transparent
+ 100%),
+ -o-linear-gradient(45deg, #000 0.188em, dimgray 0.313em 0.375em, #000 0.5em);
+ background: -o-radial-gradient(
+ 0.25em 0.375em,
+ 0.125em circle,
+ red,
+ transparent
+ ),
+ -o-radial-gradient(0.375em 0.188em, 0.063em circle, var(--bb8-bg) 50%, transparent
+ 100%),
+ -o-linear-gradient(45deg, #000 0.188em, dimgray 0.313em 0.375em, #000 0.5em);
+ background: -o-radial-gradient(
+ 0.25em 0.375em,
+ 0.125em circle,
+ red,
+ transparent
+ ),
+ -o-radial-gradient(0.375em 0.188em, 0.063em circle, var(--bb8-bg) 50%, transparent
+ 100%),
+ -o-linear-gradient(45deg, #000 0.188em, dimgray 0.313em 0.375em, #000 0.5em);
+ background: radial-gradient(
+ 0.125em circle at 0.25em 0.375em,
+ red,
+ transparent
+ ),
+ radial-gradient(
+ 0.063em circle at 0.375em 0.188em,
+ var(--bb8-bg) 50%,
+ transparent 100%
+ ),
+ linear-gradient(45deg, #000 0.188em, dimgray 0.313em 0.375em, #000 0.5em);
+ border-radius: var(--radius);
+ top: 0.413em;
+ left: 50%;
+ -webkit-transform: translate(-50%);
+ -ms-transform: translate(-50%);
+ transform: translate(-50%);
+ -webkit-box-shadow: 0 0 0 0.089em lightgray, 0.563em 0.281em 0 -0.148em,
+ 0.563em 0.281em 0 -0.1em var(--bb8-bg), 0.563em 0.281em 0 -0.063em;
+ box-shadow: 0 0 0 0.089em lightgray, 0.563em 0.281em 0 -0.148em,
+ 0.563em 0.281em 0 -0.1em var(--bb8-bg), 0.563em 0.281em 0 -0.063em;
+ z-index: 1;
+ -webkit-transition: var(--transition);
+ -o-transition: var(--transition);
+ transition: var(--transition);
+}
+
+.bb8__head::after {
+ content: "";
+ position: absolute;
+ bottom: 0.375em;
+ left: 0;
+ width: 100%;
+ height: 0.188em;
+ background: -o-linear-gradient(
+ left,
+ var(--accent) 0.125em,
+ transparent 0.125em 0.188em,
+ var(--accent) 0.188em 0.313em,
+ transparent 0.313em 0.375em,
+ var(--accent) 0.375em 0.938em,
+ transparent 0.938em 1em,
+ var(--accent) 1em 1.125em,
+ transparent 1.125em 1.875em,
+ var(--accent) 1.875em 2em,
+ transparent 2em 2.063em,
+ var(--accent) 2.063em 2.25em,
+ transparent 2.25em 2.313em,
+ var(--accent) 2.313em 2.375em,
+ transparent 2.375em 2.438em,
+ var(--accent) 2.438em
+ );
+ background: -webkit-gradient(
+ linear,
+ left top,
+ right top,
+ color-stop(0.125em, var(--accent)),
+ color-stop(0.125em, transparent),
+ color-stop(0.188em, var(--accent)),
+ color-stop(0.313em, transparent),
+ color-stop(0.375em, var(--accent)),
+ color-stop(0.938em, transparent),
+ color-stop(1em, var(--accent)),
+ color-stop(1.125em, transparent),
+ color-stop(1.875em, var(--accent)),
+ color-stop(2em, transparent),
+ color-stop(2.063em, var(--accent)),
+ color-stop(2.25em, transparent),
+ color-stop(2.313em, var(--accent)),
+ color-stop(2.375em, transparent),
+ color-stop(2.438em, var(--accent))
+ );
+ background: linear-gradient(
+ to right,
+ var(--accent) 0.125em,
+ transparent 0.125em 0.188em,
+ var(--accent) 0.188em 0.313em,
+ transparent 0.313em 0.375em,
+ var(--accent) 0.375em 0.938em,
+ transparent 0.938em 1em,
+ var(--accent) 1em 1.125em,
+ transparent 1.125em 1.875em,
+ var(--accent) 1.875em 2em,
+ transparent 2em 2.063em,
+ var(--accent) 2.063em 2.25em,
+ transparent 2.25em 2.313em,
+ var(--accent) 2.313em 2.375em,
+ transparent 2.375em 2.438em,
+ var(--accent) 2.438em
+ );
+ -webkit-transition: var(--transition);
+ -o-transition: var(--transition);
+ transition: var(--transition);
+}
+
+.bb8__antenna {
+ position: absolute;
+ -webkit-transform: translateY(-90%);
+ -ms-transform: translateY(-90%);
+ transform: translateY(-90%);
+ width: 0.059em;
+ border-radius: var(--radius) var(--radius) 0 0;
+ -webkit-transition: var(--transition);
+ -o-transition: var(--transition);
+ transition: var(--transition);
+}
+
+.bb8__antenna:nth-child(1) {
+ height: 0.938em;
+ right: 0.938em;
+ background: -o-linear-gradient(#000 0.188em, silver 0.188em);
+ background: -webkit-gradient(
+ linear,
+ left top,
+ left bottom,
+ color-stop(0.188em, #000),
+ color-stop(0.188em, silver)
+ );
+ background: linear-gradient(#000 0.188em, silver 0.188em);
+}
+
+.bb8__antenna:nth-child(2) {
+ height: 0.375em;
+ left: 50%;
+ -webkit-transform: translate(-50%, -90%);
+ -ms-transform: translate(-50%, -90%);
+ transform: translate(-50%, -90%);
+ background: silver;
+}
+
+.bb8__body {
+ width: 4.375em;
+ height: 4.375em;
+ background: var(--bb8-bg);
+ border-radius: var(--radius);
+ position: relative;
+ overflow: hidden;
+ -webkit-transition: var(--transition);
+ -o-transition: var(--transition);
+ transition: var(--transition);
+ z-index: 1;
+ -webkit-transform: rotate(45deg);
+ -ms-transform: rotate(45deg);
+ transform: rotate(45deg);
+ background: -webkit-gradient(
+ linear,
+ right top,
+ left top,
+ color-stop(4%, var(--bb8-bg)),
+ color-stop(4%, var(--accent)),
+ color-stop(10%, transparent),
+ color-stop(90%, var(--accent)),
+ color-stop(96%, var(--bb8-bg))
+ ),
+ -webkit-gradient(linear, left top, left bottom, color-stop(4%, var(--bb8-bg)), color-stop(4%, var(--accent)), color-stop(10%, transparent), color-stop(90%, var(--accent)), color-stop(96%, var(--bb8-bg))),
+ -webkit-gradient(linear, left top, right top, color-stop(2.156em, transparent), color-stop(2.156em, silver), color-stop(2.188em, transparent)),
+ -webkit-gradient(linear, left top, left bottom, color-stop(2.156em, transparent), color-stop(2.156em, silver), color-stop(2.188em, transparent));
+ background: -o-linear-gradient(
+ right,
+ var(--bb8-bg) 4%,
+ var(--accent) 4% 10%,
+ transparent 10% 90%,
+ var(--accent) 90% 96%,
+ var(--bb8-bg) 96%
+ ),
+ -o-linear-gradient(var(--bb8-bg) 4%, var(--accent) 4% 10%, transparent 10%
+ 90%, var(--accent) 90% 96%, var(--bb8-bg) 96%),
+ -o-linear-gradient(left, transparent 2.156em, silver 2.156em 2.219em, transparent
+ 2.188em),
+ -o-linear-gradient(transparent 2.156em, silver 2.156em 2.219em, transparent
+ 2.188em);
+ background: linear-gradient(
+ -90deg,
+ var(--bb8-bg) 4%,
+ var(--accent) 4% 10%,
+ transparent 10% 90%,
+ var(--accent) 90% 96%,
+ var(--bb8-bg) 96%
+ ),
+ linear-gradient(
+ var(--bb8-bg) 4%,
+ var(--accent) 4% 10%,
+ transparent 10% 90%,
+ var(--accent) 90% 96%,
+ var(--bb8-bg) 96%
+ ),
+ linear-gradient(
+ to right,
+ transparent 2.156em,
+ silver 2.156em 2.219em,
+ transparent 2.188em
+ ),
+ linear-gradient(
+ transparent 2.156em,
+ silver 2.156em 2.219em,
+ transparent 2.188em
+ );
+ background-color: var(--bb8-bg);
+}
+
+.bb8__body::after {
+ content: "";
+ bottom: 1.5em;
+ left: 0.563em;
+ position: absolute;
+ width: 0.188em;
+ height: 0.188em;
+ background: rgb(236, 236, 236);
+ color: rgb(236, 236, 236);
+ border-radius: 50%;
+ -webkit-box-shadow: 0.875em 0.938em, 0 -1.25em, 0.875em -2.125em,
+ 2.125em -2.125em, 3.063em -1.25em, 3.063em 0, 2.125em 0.938em;
+ box-shadow: 0.875em 0.938em, 0 -1.25em, 0.875em -2.125em, 2.125em -2.125em,
+ 3.063em -1.25em, 3.063em 0, 2.125em 0.938em;
+}
+
+.bb8__body::before {
+ content: "";
+ width: 2.625em;
+ height: 2.625em;
+ position: absolute;
+ border-radius: 50%;
+ z-index: 0.1;
+ overflow: hidden;
+ top: 50%;
+ left: 50%;
+ -webkit-transform: translate(-50%, -50%);
+ -ms-transform: translate(-50%, -50%);
+ transform: translate(-50%, -50%);
+ border: 0.313em solid var(--accent);
+ background: -o-radial-gradient(
+ center,
+ 1em circle,
+ rgb(236, 236, 236) 50%,
+ transparent 51%
+ ),
+ -o-radial-gradient(center, 1.25em circle, var(--bb8-bg) 50%, transparent 51%),
+ -o-linear-gradient(right, transparent 42%, var(--accent) 42% 58%, transparent
+ 58%),
+ -o-linear-gradient(var(--bb8-bg) 42%, var(--accent) 42% 58%, var(--bb8-bg)
+ 58%);
+ background: -o-radial-gradient(
+ center,
+ 1em circle,
+ rgb(236, 236, 236) 50%,
+ transparent 51%
+ ),
+ -o-radial-gradient(center, 1.25em circle, var(--bb8-bg) 50%, transparent 51%),
+ -o-linear-gradient(right, transparent 42%, var(--accent) 42% 58%, transparent
+ 58%),
+ -o-linear-gradient(var(--bb8-bg) 42%, var(--accent) 42% 58%, var(--bb8-bg)
+ 58%);
+ background: radial-gradient(
+ 1em circle at center,
+ rgb(236, 236, 236) 50%,
+ transparent 51%
+ ),
+ radial-gradient(1.25em circle at center, var(--bb8-bg) 50%, transparent 51%),
+ -webkit-gradient(linear, right top, left top, color-stop(42%, transparent), color-stop(42%, var(--accent)), color-stop(58%, transparent)),
+ -webkit-gradient(linear, left top, left bottom, color-stop(42%, var(--bb8-bg)), color-stop(42%, var(--accent)), color-stop(58%, var(--bb8-bg)));
+ background: radial-gradient(
+ 1em circle at center,
+ rgb(236, 236, 236) 50%,
+ transparent 51%
+ ),
+ radial-gradient(1.25em circle at center, var(--bb8-bg) 50%, transparent 51%),
+ linear-gradient(
+ -90deg,
+ transparent 42%,
+ var(--accent) 42% 58%,
+ transparent 58%
+ ),
+ linear-gradient(var(--bb8-bg) 42%, var(--accent) 42% 58%, var(--bb8-bg) 58%);
+}
+
+.artificial__hidden {
+ position: absolute;
+ border-radius: inherit;
+ inset: 0;
+ pointer-events: none;
+ overflow: hidden;
+}
+
+.bb8__shadow {
+ content: "";
+ width: var(--bb8-diameter);
+ height: 20%;
+ border-radius: 50%;
+ background: #3a271c;
+ -webkit-box-shadow: 0.313em 0 3.125em #3a271c;
+ box-shadow: 0.313em 0 3.125em #3a271c;
+ opacity: 0.25;
+ position: absolute;
+ bottom: 0;
+ left: calc(var(--toggle-offset) - 0.938em);
+ -webkit-transition: var(--transition);
+ -o-transition: var(--transition);
+ transition: var(--transition);
+ -webkit-transform: skew(-70deg);
+ -ms-transform: skew(-70deg);
+ transform: skew(-70deg);
+ z-index: 1;
+}
+
+.bb8-toggle__scenery {
+ width: 100%;
+ height: 100%;
+ pointer-events: none;
+ overflow: hidden;
+ position: relative;
+ border-radius: inherit;
+}
+
+.bb8-toggle__scenery::before {
+ content: "";
+ position: absolute;
+ width: 100%;
+ height: 30%;
+ bottom: 0;
+ background: #b18d71;
+ z-index: 1;
+}
+
+.bb8-toggle__cloud {
+ z-index: 1;
+ position: absolute;
+ border-radius: 50%;
+}
+
+.bb8-toggle__cloud:nth-last-child(1) {
+ width: 0.875em;
+ height: 0.625em;
+ -webkit-filter: blur(0.125em) drop-shadow(0.313em 0.313em #ffffffae)
+ drop-shadow(-0.625em 0 #fff) drop-shadow(-0.938em -0.125em #fff);
+ filter: blur(0.125em) drop-shadow(0.313em 0.313em #ffffffae)
+ drop-shadow(-0.625em 0 #fff) drop-shadow(-0.938em -0.125em #fff);
+ right: 1.875em;
+ top: 2.813em;
+ background: -o-linear-gradient(bottom left, #ffffffae, #ffffffae);
+ background: -webkit-gradient(
+ linear,
+ left bottom,
+ right top,
+ from(#ffffffae),
+ to(#ffffffae)
+ );
+ background: linear-gradient(to top right, #ffffffae, #ffffffae);
+ -webkit-transition: var(--transition);
+ -o-transition: var(--transition);
+ transition: var(--transition);
+}
+
+.bb8-toggle__cloud:nth-last-child(2) {
+ top: 0.625em;
+ right: 4.375em;
+ width: 0.875em;
+ height: 0.375em;
+ background: #dfdedeae;
+ -webkit-filter: blur(0.125em) drop-shadow(-0.313em -0.188em #e0dfdfae)
+ drop-shadow(-0.625em -0.188em #bbbbbbae) drop-shadow(-1em 0.063em #cfcfcfae);
+ filter: blur(0.125em) drop-shadow(-0.313em -0.188em #e0dfdfae)
+ drop-shadow(-0.625em -0.188em #bbbbbbae) drop-shadow(-1em 0.063em #cfcfcfae);
+ -webkit-transition: 0.6s;
+ -o-transition: 0.6s;
+ transition: 0.6s;
+}
+
+.bb8-toggle__cloud:nth-last-child(3) {
+ top: 1.25em;
+ right: 0.938em;
+ width: 0.875em;
+ height: 0.375em;
+ background: #ffffffae;
+ -webkit-filter: blur(0.125em) drop-shadow(0.438em 0.188em #ffffffae)
+ drop-shadow(-0.625em 0.313em #ffffffae);
+ filter: blur(0.125em) drop-shadow(0.438em 0.188em #ffffffae)
+ drop-shadow(-0.625em 0.313em #ffffffae);
+ -webkit-transition: 0.8s;
+ -o-transition: 0.8s;
+ transition: 0.8s;
+}
+
+.gomrassen,
+.hermes,
+.chenini {
+ position: absolute;
+ border-radius: var(--radius);
+ background: -o-linear-gradient(#fff, #6e8ea2);
+ background: -webkit-gradient(
+ linear,
+ left top,
+ left bottom,
+ from(#fff),
+ to(#6e8ea2)
+ );
+ background: linear-gradient(#fff, #6e8ea2);
+ top: 100%;
+}
+
+.gomrassen {
+ left: 0.938em;
+ width: 1.875em;
+ height: 1.875em;
+ -webkit-box-shadow: 0 0 0.188em #ffffff52, 0 0 0.188em #6e8ea24b;
+ box-shadow: 0 0 0.188em #ffffff52, 0 0 0.188em #6e8ea24b;
+ -webkit-transition: var(--transition);
+ -o-transition: var(--transition);
+ transition: var(--transition);
+}
+
+.gomrassen::before,
+.gomrassen::after {
+ content: "";
+ position: absolute;
+ border-radius: inherit;
+ -webkit-box-shadow: inset 0 0 0.063em rgb(140, 162, 169);
+ box-shadow: inset 0 0 0.063em rgb(140, 162, 169);
+ background: rgb(184, 196, 200);
+}
+
+.gomrassen::before {
+ left: 0.313em;
+ top: 0.313em;
+ width: 0.438em;
+ height: 0.438em;
+}
+
+.gomrassen::after {
+ width: 0.25em;
+ height: 0.25em;
+ left: 1.25em;
+ top: 0.75em;
+}
+
+.hermes {
+ left: 3.438em;
+ width: 0.625em;
+ height: 0.625em;
+ -webkit-box-shadow: 0 0 0.125em #ffffff52, 0 0 0.125em #6e8ea24b;
+ box-shadow: 0 0 0.125em #ffffff52, 0 0 0.125em #6e8ea24b;
+ -webkit-transition: 0.6s;
+ -o-transition: 0.6s;
+ transition: 0.6s;
+}
+
+.chenini {
+ left: 4.375em;
+ width: 0.5em;
+ height: 0.5em;
+ -webkit-box-shadow: 0 0 0.125em #ffffff52, 0 0 0.125em #6e8ea24b;
+ box-shadow: 0 0 0.125em #ffffff52, 0 0 0.125em #6e8ea24b;
+ -webkit-transition: 0.8s;
+ -o-transition: 0.8s;
+ transition: 0.8s;
+}
+
+.tatto-1,
+.tatto-2 {
+ position: absolute;
+ width: 1.25em;
+ height: 1.25em;
+ border-radius: var(--radius);
+}
+
+.tatto-1 {
+ background: #fefefe;
+ right: 3.125em;
+ top: 0.625em;
+ -webkit-box-shadow: 0 0 0.438em #fdf4e1;
+ box-shadow: 0 0 0.438em #fdf4e1;
+ -webkit-transition: var(--transition);
+ -o-transition: var(--transition);
+ transition: var(--transition);
+}
+
+.tatto-2 {
+ background: -o-linear-gradient(#e6ac5c, #d75449);
+ background: -webkit-gradient(
+ linear,
+ left top,
+ left bottom,
+ from(#e6ac5c),
+ to(#d75449)
+ );
+ background: linear-gradient(#e6ac5c, #d75449);
+ right: 1.25em;
+ top: 2.188em;
+ -webkit-box-shadow: 0 0 0.438em #e6ad5c3d, 0 0 0.438em #d755494f;
+ box-shadow: 0 0 0.438em #e6ad5c3d, 0 0 0.438em #d755494f;
+ -webkit-transition: 0.7s;
+ -o-transition: 0.7s;
+ transition: 0.7s;
+}
+
+.bb8-toggle__star {
+ position: absolute;
+ width: 0.063em;
+ height: 0.063em;
+ background: #fff;
+ border-radius: var(--radius);
+ -webkit-filter: drop-shadow(0 0 0.063em #fff);
+ filter: drop-shadow(0 0 0.063em #fff);
+ color: #fff;
+ top: 100%;
+}
+
+.bb8-toggle__star:nth-child(1) {
+ left: 3.75em;
+ -webkit-box-shadow: 1.25em 0.938em, -1.25em 2.5em, 0 1.25em, 1.875em 0.625em,
+ -3.125em 1.875em, 1.25em 2.813em;
+ box-shadow: 1.25em 0.938em, -1.25em 2.5em, 0 1.25em, 1.875em 0.625em,
+ -3.125em 1.875em, 1.25em 2.813em;
+ -webkit-transition: 0.2s;
+ -o-transition: 0.2s;
+ transition: 0.2s;
+}
+
+.bb8-toggle__star:nth-child(2) {
+ left: 4.688em;
+ -webkit-box-shadow: 0.625em 0, 0 0.625em, -0.625em -0.625em, 0.625em 0.938em,
+ -3.125em 1.25em, 1.25em -1.563em;
+ box-shadow: 0.625em 0, 0 0.625em, -0.625em -0.625em, 0.625em 0.938em,
+ -3.125em 1.25em, 1.25em -1.563em;
+ -webkit-transition: 0.3s;
+ -o-transition: 0.3s;
+ transition: 0.3s;
+}
+
+.bb8-toggle__star:nth-child(3) {
+ left: 5.313em;
+ -webkit-box-shadow: -0.625em -0.625em, -2.188em 1.25em, -2.188em 0,
+ -3.75em -0.625em, -3.125em -0.625em, -2.5em -0.313em, 0.75em -0.625em;
+ box-shadow: -0.625em -0.625em, -2.188em 1.25em, -2.188em 0, -3.75em -0.625em,
+ -3.125em -0.625em, -2.5em -0.313em, 0.75em -0.625em;
+ -webkit-transition: var(--transition);
+ -o-transition: var(--transition);
+ transition: var(--transition);
+}
+
+.bb8-toggle__star:nth-child(4) {
+ left: 1.875em;
+ width: 0.125em;
+ height: 0.125em;
+ -webkit-transition: 0.5s;
+ -o-transition: 0.5s;
+ transition: 0.5s;
+}
+
+.bb8-toggle__star:nth-child(5) {
+ left: 5em;
+ width: 0.125em;
+ height: 0.125em;
+ -webkit-transition: 0.6s;
+ -o-transition: 0.6s;
+ transition: 0.6s;
+}
+
+.bb8-toggle__star:nth-child(6) {
+ left: 2.5em;
+ width: 0.125em;
+ height: 0.125em;
+ -webkit-transition: 0.7s;
+ -o-transition: 0.7s;
+ transition: 0.7s;
+}
+
+.bb8-toggle__star:nth-child(7) {
+ left: 3.438em;
+ width: 0.125em;
+ height: 0.125em;
+ -webkit-transition: 0.8s;
+ -o-transition: 0.8s;
+ transition: 0.8s;
+}
+
+/* actions */
+
+.bb8-toggle__checkbox:checked
+ + .bb8-toggle__container
+ .bb8-toggle__star:nth-child(1) {
+ top: 0.625em;
+}
+
+.bb8-toggle__checkbox:checked
+ + .bb8-toggle__container
+ .bb8-toggle__star:nth-child(2) {
+ top: 1.875em;
+}
+
+.bb8-toggle__checkbox:checked
+ + .bb8-toggle__container
+ .bb8-toggle__star:nth-child(3) {
+ top: 1.25em;
+}
+
+.bb8-toggle__checkbox:checked
+ + .bb8-toggle__container
+ .bb8-toggle__star:nth-child(4) {
+ top: 3.438em;
+}
+
+.bb8-toggle__checkbox:checked
+ + .bb8-toggle__container
+ .bb8-toggle__star:nth-child(5) {
+ top: 3.438em;
+}
+
+.bb8-toggle__checkbox:checked
+ + .bb8-toggle__container
+ .bb8-toggle__star:nth-child(6) {
+ top: 0.313em;
+}
+
+.bb8-toggle__checkbox:checked
+ + .bb8-toggle__container
+ .bb8-toggle__star:nth-child(7) {
+ top: 1.875em;
+}
+
+.bb8-toggle__checkbox:checked + .bb8-toggle__container .bb8-toggle__cloud {
+ right: -100%;
+}
+
+.bb8-toggle__checkbox:checked + .bb8-toggle__container .gomrassen {
+ top: 0.938em;
+}
+
+.bb8-toggle__checkbox:checked + .bb8-toggle__container .hermes {
+ top: 2.5em;
+}
+
+.bb8-toggle__checkbox:checked + .bb8-toggle__container .chenini {
+ top: 2.75em;
+}
+
+.bb8-toggle__checkbox:checked + .bb8-toggle__container {
+ background-position-y: 0;
+}
+
+.bb8-toggle__checkbox:checked + .bb8-toggle__container .tatto-1 {
+ top: 100%;
+}
+
+.bb8-toggle__checkbox:checked + .bb8-toggle__container .tatto-2 {
+ top: 100%;
+}
+
+.bb8-toggle__checkbox:checked + .bb8-toggle__container .bb8 {
+ left: calc(100% - var(--bb8-diameter) - var(--toggle-offset));
+}
+
+.bb8-toggle__checkbox:checked + .bb8-toggle__container .bb8__shadow {
+ left: calc(100% - var(--bb8-diameter) - var(--toggle-offset) + 0.938em);
+ -webkit-transform: skew(70deg);
+ -ms-transform: skew(70deg);
+ transform: skew(70deg);
+}
+
+.bb8-toggle__checkbox:checked + .bb8-toggle__container .bb8__body {
+ -webkit-transform: rotate(180deg);
+ -ms-transform: rotate(180deg);
+ transform: rotate(225deg);
+}
+
+.bb8-toggle__checkbox:hover + .bb8-toggle__container .bb8__head::before {
+ left: 100%;
+}
+
+.bb8-toggle__checkbox:not(:checked):hover
+ + .bb8-toggle__container
+ .bb8__antenna:nth-child(1) {
+ right: 1.5em;
+}
+
+.bb8-toggle__checkbox:hover
+ + .bb8-toggle__container
+ .bb8__antenna:nth-child(2) {
+ left: 0.938em;
+}
+
+.bb8-toggle__checkbox:hover + .bb8-toggle__container .bb8__head::after {
+ background-position: 1.375em 0;
+}
+
+.bb8-toggle__checkbox:checked:hover
+ + .bb8-toggle__container
+ .bb8__head::before {
+ left: 0;
+}
+
+.bb8-toggle__checkbox:checked:hover
+ + .bb8-toggle__container
+ .bb8__antenna:nth-child(2) {
+ left: calc(100% - 0.938em);
+}
+
+.bb8-toggle__checkbox:checked:hover + .bb8-toggle__container .bb8__head::after {
+ background-position: -1.375em 0;
+}
+
+.bb8-toggle__checkbox:active + .bb8-toggle__container .bb8__head-container {
+ -webkit-transform: rotate(25deg);
+ -ms-transform: rotate(25deg);
+ transform: rotate(25deg);
+}
+
+.bb8-toggle__checkbox:checked:active
+ + .bb8-toggle__container
+ .bb8__head-container {
+ -webkit-transform: rotate(-25deg);
+ -ms-transform: rotate(-25deg);
+ transform: rotate(-25deg);
+}
+
+.bb8:hover .bb8__head::before,
+.bb8:hover .bb8__antenna:nth-child(2) {
+ left: 50% !important;
+}
+
+.bb8:hover .bb8__antenna:nth-child(1) {
+ right: 0.938em !important;
+}
+
+.bb8:hover .bb8__head::after {
+ background-position: 0 0 !important;
+}
\ No newline at end of file
diff --git a/frontend/assets/mod.html b/frontend/assets/mod.html
new file mode 100644
index 0000000..10c5c39
--- /dev/null
+++ b/frontend/assets/mod.html
@@ -0,0 +1,34 @@
+
+
\ No newline at end of file
diff --git a/frontend/assets/pdd.svg b/frontend/assets/pdd.svg
new file mode 100644
index 0000000..ab63c6b
--- /dev/null
+++ b/frontend/assets/pdd.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/frontend/assets/yt.svg b/frontend/assets/yt.svg
new file mode 100644
index 0000000..075977d
--- /dev/null
+++ b/frontend/assets/yt.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/frontend/js/components/bom.js b/frontend/js/components/bom.js
new file mode 100644
index 0000000..b02e0e0
--- /dev/null
+++ b/frontend/js/components/bom.js
@@ -0,0 +1,369 @@
+// BOM物料清单管理
+(() => {
+ let bomList = [];
+ let currentPage = 1;
+ const pageSize = 20;
+ let editingId = null;
+
+ Router.register('/plan-mgmt/bom', async () => {
+ const html = `
+
+
+
+
+
+
+ `;
+
+ setTimeout(() => {
+ document.getElementById('add-bom-btn')?.addEventListener('click', () => openModal());
+ document.getElementById('import-bom-btn')?.addEventListener('click', () => showImportDialog());
+ document.getElementById('download-template-btn')?.addEventListener('click', () => downloadTemplate());
+ document.getElementById('batch-delete-btn')?.addEventListener('click', () => batchDelete());
+ document.getElementById('search-keyword')?.addEventListener('keypress', (e) => {
+ if (e.key === 'Enter') BOM.search();
+ });
+ loadList();
+ }, 100);
+
+ return html;
+ });
+
+ function showImportDialog() {
+ const input = document.createElement('input');
+ input.type = 'file';
+ input.accept = '.xlsx,.xls,.csv';
+ input.onchange = async (e) => {
+ const file = e.target.files[0];
+ if (!file) return;
+
+ const formData = new FormData();
+ formData.append('file', file);
+
+ try {
+ const overlay = document.getElementById('overlay');
+ overlay.classList.remove('hidden');
+
+ const res = await fetch('/api/bom/import', {
+ method: 'POST',
+ body: formData,
+ credentials: 'include'
+ });
+
+ overlay.classList.add('hidden');
+
+ const data = await res.json();
+ if (data.ok) {
+ alert(data.message || '导入成功');
+ loadList();
+ } else {
+ alert(data.error || '导入失败');
+ }
+ } catch (err) {
+ document.getElementById('overlay').classList.add('hidden');
+ alert('导入失败: ' + err.message);
+ }
+ };
+ input.click();
+ }
+
+ function downloadTemplate() {
+ const headers = ['产品编码', '产品名称', '物料编码', '物料名称', '单机用量', '单位', '最小包装', '供应商', '备注'];
+ const example = ['AP05', 'AP05基站', 'PCB-001', '主控PCB板', '1', 'pcs', '10', '深圳电子', ''];
+
+ const csvContent = [headers.join(','), example.join(',')].join('\n');
+ const blob = new Blob(['\ufeff' + csvContent], { type: 'text/csv;charset=utf-8;' });
+ const link = document.createElement('a');
+ link.href = URL.createObjectURL(blob);
+ link.download = 'BOM导入模板.csv';
+ link.click();
+ }
+
+ async function loadList() {
+ try {
+ const res = await API.get('/api/bom');
+ bomList = res.list || [];
+ renderList();
+ } catch (e) {
+ console.error('加载BOM列表失败:', e);
+ document.getElementById('bom-list').innerHTML = '| 加载失败 |
';
+ }
+ }
+
+ function renderList() {
+ const tbody = document.getElementById('bom-list');
+ const keyword = (document.getElementById('search-keyword')?.value || '').toLowerCase();
+
+ let filtered = bomList;
+ if (keyword) {
+ filtered = bomList.filter(item =>
+ (item.product_code || '').toLowerCase().includes(keyword) ||
+ (item.product_name || '').toLowerCase().includes(keyword) ||
+ (item.material_code || '').toLowerCase().includes(keyword) ||
+ (item.material_name || '').toLowerCase().includes(keyword)
+ );
+ }
+
+ const totalPages = Math.ceil(filtered.length / pageSize);
+ const start = (currentPage - 1) * pageSize;
+ const pageData = filtered.slice(start, start + pageSize);
+
+ if (pageData.length === 0) {
+ tbody.innerHTML = '| 暂无数据 |
';
+ } else {
+ tbody.innerHTML = pageData.map(item => `
+
+ |
+ ${escapeHtml(item.product_code || '')} |
+ ${escapeHtml(item.product_name || '')} |
+ ${escapeHtml(item.material_code || '')} |
+ ${escapeHtml(item.material_name || '')} |
+ ${item.unit_qty || 1} |
+ ${escapeHtml(item.unit || 'pcs')} |
+ ${item.min_package || 1} |
+ ${escapeHtml(item.supplier || '-')} |
+ ${escapeHtml(item.remark || '-')} |
+
+
+
+ |
+
+ `).join('');
+ }
+
+ renderPagination(totalPages);
+ }
+
+ function renderPagination(totalPages) {
+ const container = document.getElementById('pagination');
+ if (totalPages <= 1) {
+ container.innerHTML = '';
+ return;
+ }
+
+ let html = '';
+ html += ``;
+ html += `第 ${currentPage} / ${totalPages} 页`;
+ html += ``;
+ container.innerHTML = html;
+ }
+
+ function openModal(item = null) {
+ editingId = item?.id || null;
+ document.getElementById('modal-title').textContent = item ? '编辑BOM' : '新增BOM';
+ document.getElementById('product-code').value = item?.product_code || '';
+ document.getElementById('product-name').value = item?.product_name || '';
+ document.getElementById('material-code').value = item?.material_code || '';
+ document.getElementById('material-name').value = item?.material_name || '';
+ document.getElementById('unit-qty').value = item?.unit_qty || 1;
+ document.getElementById('unit').value = item?.unit || 'pcs';
+ document.getElementById('min-package').value = item?.min_package || 1;
+ document.getElementById('supplier').value = item?.supplier || '';
+ document.getElementById('remark').value = item?.remark || '';
+ document.getElementById('bom-modal').style.display = 'flex';
+ }
+
+ function closeModal() {
+ document.getElementById('bom-modal').style.display = 'none';
+ editingId = null;
+ }
+
+ async function save() {
+ const data = {
+ product_code: document.getElementById('product-code').value.trim(),
+ product_name: document.getElementById('product-name').value.trim(),
+ material_code: document.getElementById('material-code').value.trim(),
+ material_name: document.getElementById('material-name').value.trim(),
+ unit_qty: parseFloat(document.getElementById('unit-qty').value) || 1,
+ unit: document.getElementById('unit').value.trim() || 'pcs',
+ min_package: parseInt(document.getElementById('min-package').value) || 1,
+ supplier: document.getElementById('supplier').value.trim(),
+ remark: document.getElementById('remark').value.trim()
+ };
+
+ if (!data.product_code || !data.product_name || !data.material_code || !data.material_name) {
+ alert('请填写所有必填项');
+ return;
+ }
+
+ try {
+ if (editingId) {
+ await API.put(`/api/bom/${editingId}`, data);
+ alert('更新成功');
+ } else {
+ await API.post('/api/bom', data);
+ alert('创建成功');
+ }
+ closeModal();
+ loadList();
+ } catch (e) {
+ alert(e.message || '操作失败');
+ }
+ }
+
+ async function edit(id) {
+ const item = bomList.find(x => x.id === id);
+ if (item) openModal(item);
+ }
+
+ async function deleteBom(id) {
+ if (!confirm('确定要删除这条BOM吗?')) return;
+ try {
+ await API.delete(`/api/bom/${id}`);
+ alert('删除成功');
+ loadList();
+ } catch (e) {
+ alert(e.message || '删除失败');
+ }
+ }
+
+ async function batchDelete() {
+ const checked = document.querySelectorAll('.row-checkbox:checked');
+ if (checked.length === 0) {
+ alert('请先选择要删除的项');
+ return;
+ }
+ if (!confirm(`确定要删除选中的 ${checked.length} 条BOM吗?`)) return;
+
+ const ids = Array.from(checked).map(cb => parseInt(cb.dataset.id));
+ try {
+ await API.post('/api/bom/batch-delete', { ids });
+ alert('批量删除成功');
+ loadList();
+ } catch (e) {
+ alert(e.message || '批量删除失败');
+ }
+ }
+
+ function toggleSelectAll(checkbox) {
+ document.querySelectorAll('.row-checkbox').forEach(cb => cb.checked = checkbox.checked);
+ }
+
+ function search() {
+ currentPage = 1;
+ renderList();
+ }
+
+ function resetSearch() {
+ document.getElementById('search-keyword').value = '';
+ currentPage = 1;
+ renderList();
+ }
+
+ function goPage(page) {
+ currentPage = page;
+ renderList();
+ }
+
+ function escapeHtml(str) {
+ if (!str) return '';
+ return String(str).replace(/&/g, '&').replace(//g, '>').replace(/"/g, '"');
+ }
+
+ window.BOM = {
+ search,
+ resetSearch,
+ edit,
+ delete: deleteBom,
+ closeModal,
+ save,
+ toggleSelectAll,
+ goPage
+ };
+})();
diff --git a/frontend/js/components/initial-stock.js b/frontend/js/components/initial-stock.js
new file mode 100644
index 0000000..e1625f9
--- /dev/null
+++ b/frontend/js/components/initial-stock.js
@@ -0,0 +1,365 @@
+// 期初库存管理
+(() => {
+ let stockList = [];
+ let currentPage = 1;
+ const pageSize = 20;
+ let editingId = null;
+
+ Router.register('/plan-mgmt/initial-stock', async () => {
+ const html = `
+
+
+
+
+
+
+ `;
+
+ setTimeout(() => {
+ document.getElementById('add-stock-btn')?.addEventListener('click', () => openModal());
+ document.getElementById('import-stock-btn')?.addEventListener('click', () => showImportDialog());
+ document.getElementById('download-template-btn')?.addEventListener('click', () => downloadTemplate());
+ document.getElementById('batch-delete-btn')?.addEventListener('click', () => batchDelete());
+ document.getElementById('search-keyword')?.addEventListener('keypress', (e) => {
+ if (e.key === 'Enter') InitialStock.search();
+ });
+ loadList();
+ }, 100);
+
+ return html;
+ });
+
+ function showImportDialog() {
+ const input = document.createElement('input');
+ input.type = 'file';
+ input.accept = '.xlsx,.xls,.csv';
+ input.onchange = async (e) => {
+ const file = e.target.files[0];
+ if (!file) return;
+
+ const formData = new FormData();
+ formData.append('file', file);
+
+ try {
+ const overlay = document.getElementById('overlay');
+ overlay.classList.remove('hidden');
+
+ const res = await fetch('/api/initial-stock/import', {
+ method: 'POST',
+ body: formData,
+ credentials: 'include'
+ });
+
+ overlay.classList.add('hidden');
+
+ const data = await res.json();
+ if (data.ok) {
+ alert(data.message || '导入成功');
+ loadList();
+ } else {
+ alert(data.error || '导入失败');
+ }
+ } catch (err) {
+ document.getElementById('overlay').classList.add('hidden');
+ alert('导入失败: ' + err.message);
+ }
+ };
+ input.click();
+ }
+
+ function downloadTemplate() {
+ const headers = ['物料编码', '物料名称', '库存数量', '单位', '最小包装', '供应商', '备注'];
+ const example = ['PCB-001', '主控PCB板', '50', 'pcs', '10', '深圳电子', ''];
+
+ const csvContent = [headers.join(','), example.join(',')].join('\n');
+ const blob = new Blob(['\ufeff' + csvContent], { type: 'text/csv;charset=utf-8;' });
+ const link = document.createElement('a');
+ link.href = URL.createObjectURL(blob);
+ link.download = '期初库存导入模板.csv';
+ link.click();
+ }
+
+ async function loadList() {
+ try {
+ const res = await API.get('/api/initial-stock');
+ stockList = res.list || [];
+ renderList();
+ } catch (e) {
+ console.error('加载库存列表失败:', e);
+ document.getElementById('stock-list').innerHTML = '| 加载失败 |
';
+ }
+ }
+
+ function renderList() {
+ const tbody = document.getElementById('stock-list');
+ const keyword = (document.getElementById('search-keyword')?.value || '').toLowerCase();
+
+ let filtered = stockList;
+ if (keyword) {
+ filtered = stockList.filter(item =>
+ (item.material_code || '').toLowerCase().includes(keyword) ||
+ (item.material_name || '').toLowerCase().includes(keyword)
+ );
+ }
+
+ const totalPages = Math.ceil(filtered.length / pageSize);
+ const start = (currentPage - 1) * pageSize;
+ const pageData = filtered.slice(start, start + pageSize);
+
+ if (pageData.length === 0) {
+ tbody.innerHTML = '| 暂无数据 |
';
+ } else {
+ tbody.innerHTML = pageData.map(item => `
+
+ |
+ ${escapeHtml(item.material_code || '')} |
+ ${escapeHtml(item.material_name || '')} |
+ ${item.stock_qty || 0} |
+ ${escapeHtml(item.unit || 'pcs')} |
+ ${item.min_package || 1} |
+ ${escapeHtml(item.supplier || '-')} |
+ ${escapeHtml(item.remark || '-')} |
+ ${formatTime(item.updated_at)} |
+
+
+
+ |
+
+ `).join('');
+ }
+
+ renderPagination(totalPages);
+ }
+
+ function renderPagination(totalPages) {
+ const container = document.getElementById('pagination');
+ if (totalPages <= 1) {
+ container.innerHTML = '';
+ return;
+ }
+
+ let html = '';
+ html += ``;
+ html += `第 ${currentPage} / ${totalPages} 页`;
+ html += ``;
+ container.innerHTML = html;
+ }
+
+ function openModal(item = null) {
+ editingId = item?.id || null;
+ document.getElementById('modal-title').textContent = item ? '编辑期初库存' : '新增期初库存';
+ document.getElementById('material-code').value = item?.material_code || '';
+ document.getElementById('material-name').value = item?.material_name || '';
+ document.getElementById('stock-qty').value = item?.stock_qty || 0;
+ document.getElementById('unit').value = item?.unit || 'pcs';
+ document.getElementById('min-package').value = item?.min_package || 1;
+ document.getElementById('supplier').value = item?.supplier || '';
+ document.getElementById('remark').value = item?.remark || '';
+ document.getElementById('stock-modal').style.display = 'flex';
+ }
+
+ function closeModal() {
+ document.getElementById('stock-modal').style.display = 'none';
+ editingId = null;
+ }
+
+ async function save() {
+ const data = {
+ material_code: document.getElementById('material-code').value.trim(),
+ material_name: document.getElementById('material-name').value.trim(),
+ stock_qty: parseInt(document.getElementById('stock-qty').value) || 0,
+ unit: document.getElementById('unit').value.trim() || 'pcs',
+ min_package: parseInt(document.getElementById('min-package').value) || 1,
+ supplier: document.getElementById('supplier').value.trim(),
+ remark: document.getElementById('remark').value.trim()
+ };
+
+ if (!data.material_code || !data.material_name) {
+ alert('请填写物料编码和物料名称');
+ return;
+ }
+
+ try {
+ if (editingId) {
+ await API.put(`/api/initial-stock/${editingId}`, data);
+ alert('更新成功');
+ } else {
+ await API.post('/api/initial-stock', data);
+ alert('创建成功');
+ }
+ closeModal();
+ loadList();
+ } catch (e) {
+ alert(e.message || '操作失败');
+ }
+ }
+
+ async function edit(id) {
+ const item = stockList.find(x => x.id === id);
+ if (item) openModal(item);
+ }
+
+ async function deleteStock(id) {
+ if (!confirm('确定要删除这条库存记录吗?')) return;
+ try {
+ await API.delete(`/api/initial-stock/${id}`);
+ alert('删除成功');
+ loadList();
+ } catch (e) {
+ alert(e.message || '删除失败');
+ }
+ }
+
+ async function batchDelete() {
+ const checked = document.querySelectorAll('.row-checkbox:checked');
+ if (checked.length === 0) {
+ alert('请先选择要删除的项');
+ return;
+ }
+ if (!confirm(`确定要删除选中的 ${checked.length} 条库存记录吗?`)) return;
+
+ const ids = Array.from(checked).map(cb => parseInt(cb.dataset.id));
+ try {
+ await API.post('/api/initial-stock/batch-delete', { ids });
+ alert('批量删除成功');
+ loadList();
+ } catch (e) {
+ alert(e.message || '批量删除失败');
+ }
+ }
+
+ function toggleSelectAll(checkbox) {
+ document.querySelectorAll('.row-checkbox').forEach(cb => cb.checked = checkbox.checked);
+ }
+
+ function search() {
+ currentPage = 1;
+ renderList();
+ }
+
+ function resetSearch() {
+ document.getElementById('search-keyword').value = '';
+ currentPage = 1;
+ renderList();
+ }
+
+ function goPage(page) {
+ currentPage = page;
+ renderList();
+ }
+
+ function formatTime(ts) {
+ if (!ts) return '-';
+ try {
+ const d = new Date(ts);
+ return d.toLocaleString('zh-CN', { hour12: false });
+ } catch {
+ return ts;
+ }
+ }
+
+ function escapeHtml(str) {
+ if (!str) return '';
+ return String(str).replace(/&/g, '&').replace(//g, '>').replace(/"/g, '"');
+ }
+
+ window.InitialStock = {
+ search,
+ resetSearch,
+ edit,
+ delete: deleteStock,
+ closeModal,
+ save,
+ toggleSelectAll,
+ goPage
+ };
+})();
diff --git a/frontend/js/components/purchase-demand.js b/frontend/js/components/purchase-demand.js
new file mode 100644
index 0000000..9b18854
--- /dev/null
+++ b/frontend/js/components/purchase-demand.js
@@ -0,0 +1,419 @@
+// 采购需求清单管理
+(() => {
+ let demandList = [];
+ let productList = [];
+ let currentPage = 1;
+ const pageSize = 20;
+
+ const statusMap = {
+ 'pending': { text: '待处理', color: 'var(--warning)' },
+ 'ordered': { text: '已下单', color: 'var(--info)' },
+ 'received': { text: '已收货', color: 'var(--primary)' },
+ 'completed': { text: '已完成', color: 'var(--success)' },
+ 'cancelled': { text: '已取消', color: 'var(--text-2)' }
+ };
+
+ Router.register('/plan-mgmt/purchase-demand', async () => {
+ const html = `
+
+
+
+
+
+
+
💡
+
+ 采购需求计算公式:
+ 客户订单数量 × BOM单机用量 - 期初库存 = 净需求 → 按最小包装向上取整 = 实际采购数量
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
📋 计算将:
+
+ - 根据产品的BOM展开所有物料
+ - 计算每种物料的总需求量
+ - 扣减期初库存得出净需求
+ - 按最小包装取整得出实际采购量
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ `;
+
+ setTimeout(() => {
+ document.getElementById('calc-demand-btn')?.addEventListener('click', () => openCalcModal());
+ document.getElementById('calc-from-orders-btn')?.addEventListener('click', () => calcFromOrders());
+ document.getElementById('batch-delete-btn')?.addEventListener('click', () => batchDelete());
+ document.getElementById('search-keyword')?.addEventListener('keypress', (e) => {
+ if (e.key === 'Enter') PurchaseDemand.search();
+ });
+ loadList();
+ loadProducts();
+ }, 100);
+
+ return html;
+ });
+
+ async function loadList() {
+ try {
+ const res = await API.get('/api/purchase-demand');
+ demandList = res.list || [];
+ renderList();
+ } catch (e) {
+ console.error('加载采购需求失败:', e);
+ document.getElementById('demand-list').innerHTML = '| 加载失败 |
';
+ }
+ }
+
+ async function loadProducts() {
+ try {
+ const res = await API.get('/api/bom/products');
+ productList = res.list || [];
+ const select = document.getElementById('product-select');
+ if (select) {
+ select.innerHTML = '' +
+ productList.map(p => ``).join('');
+ }
+ } catch (e) {
+ console.error('加载产品列表失败:', e);
+ }
+ }
+
+ function renderList() {
+ const tbody = document.getElementById('demand-list');
+ const keyword = (document.getElementById('search-keyword')?.value || '').toLowerCase();
+ const filterStatus = document.getElementById('filter-status')?.value || '';
+
+ let filtered = demandList;
+ if (keyword) {
+ filtered = filtered.filter(item =>
+ (item.demand_no || '').toLowerCase().includes(keyword) ||
+ (item.material_code || '').toLowerCase().includes(keyword) ||
+ (item.material_name || '').toLowerCase().includes(keyword)
+ );
+ }
+ if (filterStatus) {
+ filtered = filtered.filter(item => item.status === filterStatus);
+ }
+
+ const totalPages = Math.ceil(filtered.length / pageSize);
+ const start = (currentPage - 1) * pageSize;
+ const pageData = filtered.slice(start, start + pageSize);
+
+ if (pageData.length === 0) {
+ tbody.innerHTML = '| 暂无数据 |
';
+ } else {
+ tbody.innerHTML = pageData.map(item => {
+ const status = statusMap[item.status] || { text: item.status, color: 'var(--text-2)' };
+ return `
+
+ |
+ ${escapeHtml(item.demand_no || '')} |
+ ${escapeHtml(item.material_code || '')} |
+ ${escapeHtml(item.material_name || '')} |
+ ${item.order_qty || 0} |
+ ${item.bom_unit_qty || 1} |
+ ${item.total_demand || 0} |
+ ${item.initial_stock || 0} |
+ ${item.net_demand || 0} |
+ ${item.min_package || 1} |
+ ${item.actual_purchase_qty || 0} |
+ ${escapeHtml(item.unit || 'pcs')} |
+ ${escapeHtml(item.supplier || '-')} |
+ ${status.text} |
+
+
+
+ |
+
+ `;
+ }).join('');
+ }
+
+ renderPagination(totalPages);
+ }
+
+ function renderPagination(totalPages) {
+ const container = document.getElementById('pagination');
+ if (totalPages <= 1) {
+ container.innerHTML = '';
+ return;
+ }
+
+ let html = '';
+ html += ``;
+ html += `第 ${currentPage} / ${totalPages} 页`;
+ html += ``;
+ container.innerHTML = html;
+ }
+
+ function openCalcModal() {
+ document.getElementById('product-select').value = '';
+ document.getElementById('order-qty').value = 1;
+ document.getElementById('calc-modal').style.display = 'flex';
+ }
+
+ function closeCalcModal() {
+ document.getElementById('calc-modal').style.display = 'none';
+ }
+
+ async function doCalculate() {
+ const productCode = document.getElementById('product-select').value;
+ const orderQty = parseInt(document.getElementById('order-qty').value) || 0;
+
+ if (!productCode) {
+ alert('请选择产品');
+ return;
+ }
+ if (orderQty <= 0) {
+ alert('订单数量必须大于0');
+ return;
+ }
+
+ try {
+ const res = await API.post('/api/purchase-demand/calculate', {
+ product_code: productCode,
+ order_qty: orderQty
+ });
+
+ closeCalcModal();
+ alert(res.message || '计算完成');
+ loadList();
+
+ // 显示计算结果摘要
+ if (res.list && res.list.length > 0) {
+ const totalPurchase = res.list.reduce((sum, item) => sum + (item.actual_purchase_qty || 0), 0);
+ console.log(`采购需求计算完成,需求编号: ${res.demand_no},共 ${res.list.length} 种物料,总采购数量: ${totalPurchase}`);
+ }
+ } catch (e) {
+ alert(e.message || '计算失败');
+ }
+ }
+
+ async function calcFromOrders() {
+ if (!confirm('将根据所有客户订单自动计算采购需求,确定继续吗?')) {
+ return;
+ }
+
+ try {
+ const res = await API.post('/api/purchase-demand/calculate-from-orders', {});
+ alert(res.message || '计算完成');
+ loadList();
+ } catch (e) {
+ alert(e.message || '计算失败');
+ }
+ }
+
+ let updatingId = null;
+
+ function updateStatus(id) {
+ updatingId = id;
+ const item = demandList.find(x => x.id === id);
+ document.getElementById('update-status').value = item?.status || 'pending';
+ document.getElementById('update-remark').value = item?.remark || '';
+ document.getElementById('status-modal').style.display = 'flex';
+ }
+
+ function closeStatusModal() {
+ document.getElementById('status-modal').style.display = 'none';
+ updatingId = null;
+ }
+
+ async function doUpdateStatus() {
+ if (!updatingId) return;
+
+ const status = document.getElementById('update-status').value;
+ const remark = document.getElementById('update-remark').value.trim();
+
+ try {
+ await API.put(`/api/purchase-demand/${updatingId}`, { status, remark });
+ closeStatusModal();
+ alert('更新成功');
+ loadList();
+ } catch (e) {
+ alert(e.message || '更新失败');
+ }
+ }
+
+ async function deleteDemand(id) {
+ if (!confirm('确定要删除这条采购需求吗?')) return;
+ try {
+ await API.delete(`/api/purchase-demand/${id}`);
+ alert('删除成功');
+ loadList();
+ } catch (e) {
+ alert(e.message || '删除失败');
+ }
+ }
+
+ async function batchDelete() {
+ const checked = document.querySelectorAll('.row-checkbox:checked');
+ if (checked.length === 0) {
+ alert('请先选择要删除的项');
+ return;
+ }
+ if (!confirm(`确定要删除选中的 ${checked.length} 条采购需求吗?`)) return;
+
+ const ids = Array.from(checked).map(cb => parseInt(cb.dataset.id));
+ try {
+ await API.post('/api/purchase-demand/batch-delete', { ids });
+ alert('批量删除成功');
+ loadList();
+ } catch (e) {
+ alert(e.message || '批量删除失败');
+ }
+ }
+
+ function toggleSelectAll(checkbox) {
+ document.querySelectorAll('.row-checkbox').forEach(cb => cb.checked = checkbox.checked);
+ }
+
+ function search() {
+ currentPage = 1;
+ renderList();
+ }
+
+ function resetSearch() {
+ document.getElementById('search-keyword').value = '';
+ document.getElementById('filter-status').value = '';
+ currentPage = 1;
+ renderList();
+ }
+
+ function goPage(page) {
+ currentPage = page;
+ renderList();
+ }
+
+ function escapeHtml(str) {
+ if (!str) return '';
+ return String(str).replace(/&/g, '&').replace(//g, '>').replace(/"/g, '"');
+ }
+
+ window.PurchaseDemand = {
+ search,
+ resetSearch,
+ delete: deleteDemand,
+ updateStatus,
+ closeCalcModal,
+ closeStatusModal,
+ doCalculate,
+ doUpdateStatus,
+ toggleSelectAll,
+ goPage
+ };
+})();
diff --git a/圆通@3x.svg b/圆通@3x.svg
new file mode 100644
index 0000000..075977d
--- /dev/null
+++ b/圆通@3x.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/拼多多.svg b/拼多多.svg
new file mode 100644
index 0000000..ab63c6b
--- /dev/null
+++ b/拼多多.svg
@@ -0,0 +1 @@
+
\ No newline at end of file