feat(audit, frontend, backend): introduce audit scripts and enhance error handling
- Added new npm scripts for auditing frontend size and inline TODOs, improving code quality management. - Enhanced error handling in the `nuscoreApiRoutes` to return specific validation error statuses, improving API response clarity. - Updated SQL migration documentation to establish a clear process for manual migrations and ensure backward compatibility. - Refactored various components to align with new design standards, enhancing UI consistency across the application.
This commit is contained in:
@@ -188,6 +188,18 @@ router.put('/submit/:uuid', async (req, res) => {
|
||||
const { uuid } = req.params;
|
||||
const reportData = req.body;
|
||||
|
||||
console.log('[nuscore submit] request', {
|
||||
uuid,
|
||||
wo: reportData?.wo ?? null,
|
||||
isCompleted: reportData?.isCompleted ?? null,
|
||||
homePin: reportData?.homePin ?? null,
|
||||
guestPin: reportData?.guestPin ?? null,
|
||||
releaseSignatureHome: reportData?.signature?.releaseSignatureHome ?? null,
|
||||
releaseSignatureGuest: reportData?.signature?.releaseSignatureGuest ?? null,
|
||||
lineupSignatureHome: reportData?.signature?.lineupSignatureHome ?? null,
|
||||
lineupSignatureGuest: reportData?.signature?.lineupSignatureGuest ?? null
|
||||
});
|
||||
|
||||
try {
|
||||
// Hole Cookies für diese UUID (falls vorhanden)
|
||||
// Versuche zuerst UUID, dann Code als Fallback
|
||||
@@ -229,12 +241,26 @@ router.put('/submit/:uuid', async (req, res) => {
|
||||
responseData = { message: responseText };
|
||||
}
|
||||
|
||||
if (!response.ok) {
|
||||
console.log('[nuscore submit] response', {
|
||||
uuid,
|
||||
httpStatus: response.status,
|
||||
resultState: responseData?.resultState ?? null,
|
||||
validationErrors: responseData?.validationErrors ?? [],
|
||||
releaseSignatureHome: responseData?.object?.signature?.releaseSignatureHome ?? null,
|
||||
releaseSignatureGuest: responseData?.object?.signature?.releaseSignatureGuest ?? null,
|
||||
lineupSignatureHome: responseData?.object?.signature?.lineupSignatureHome ?? null,
|
||||
lineupSignatureGuest: responseData?.object?.signature?.lineupSignatureGuest ?? null
|
||||
});
|
||||
|
||||
const resultState = responseData?.resultState;
|
||||
const validationErrors = Array.isArray(responseData?.validationErrors) ? responseData.validationErrors : [];
|
||||
|
||||
if (!response.ok || resultState === 'VALIDATION_ERROR') {
|
||||
console.error(`❌ Submit fehlgeschlagen: HTTP ${response.status}`, responseData);
|
||||
return res.status(response.status).json({
|
||||
return res.status(resultState === 'VALIDATION_ERROR' ? 422 : response.status).json({
|
||||
error: 'Fehler beim Absenden des Spielberichts',
|
||||
details: responseData,
|
||||
status: response.status,
|
||||
status: resultState === 'VALIDATION_ERROR' ? 422 : response.status,
|
||||
statusText: response.statusText
|
||||
});
|
||||
}
|
||||
@@ -294,6 +320,18 @@ router.put('/validate/:uuid', async (req, res) => {
|
||||
const { uuid } = req.params;
|
||||
const reportData = req.body;
|
||||
|
||||
console.log('[nuscore validate] request', {
|
||||
uuid,
|
||||
wo: reportData?.wo ?? null,
|
||||
isCompleted: reportData?.isCompleted ?? null,
|
||||
homePin: reportData?.homePin ?? null,
|
||||
guestPin: reportData?.guestPin ?? null,
|
||||
releaseSignatureHome: reportData?.signature?.releaseSignatureHome ?? null,
|
||||
releaseSignatureGuest: reportData?.signature?.releaseSignatureGuest ?? null,
|
||||
lineupSignatureHome: reportData?.signature?.lineupSignatureHome ?? null,
|
||||
lineupSignatureGuest: reportData?.signature?.lineupSignatureGuest ?? null
|
||||
});
|
||||
|
||||
try {
|
||||
// Hole Cookies für diese UUID (falls vorhanden)
|
||||
// Versuche zuerst UUID, dann Code als Fallback
|
||||
@@ -335,6 +373,17 @@ router.put('/validate/:uuid', async (req, res) => {
|
||||
responseData = { message: responseText };
|
||||
}
|
||||
|
||||
console.log('[nuscore validate] response', {
|
||||
uuid,
|
||||
httpStatus: response.status,
|
||||
resultState: responseData?.resultState ?? null,
|
||||
validationErrors: responseData?.validationErrors ?? [],
|
||||
releaseSignatureHome: responseData?.object?.signature?.releaseSignatureHome ?? null,
|
||||
releaseSignatureGuest: responseData?.object?.signature?.releaseSignatureGuest ?? null,
|
||||
lineupSignatureHome: responseData?.object?.signature?.lineupSignatureHome ?? null,
|
||||
lineupSignatureGuest: responseData?.object?.signature?.lineupSignatureGuest ?? null
|
||||
});
|
||||
|
||||
// Speichere neue Cookies falls vorhanden
|
||||
const newCookies = extractCookies(response.headers.raw()['set-cookie']);
|
||||
if (Object.keys(newCookies).length > 0) {
|
||||
|
||||
@@ -102,7 +102,7 @@ Diese Liste beschreibt die naechsten sinnvollen Optimierungsschritte nach dem zu
|
||||
|
||||
## Prioritaet D
|
||||
|
||||
- [ ] Selten genutzte Spezialviews optisch komplett an das Theme angleichen.
|
||||
- [x] Selten genutzte Spezialviews optisch komplett an das Theme angleichen.
|
||||
Wahrscheinliche Kandidaten:
|
||||
- `MatchReportDialog.vue`
|
||||
- `NuscoreAnalyzer.vue`
|
||||
@@ -110,25 +110,42 @@ Diese Liste beschreibt die naechsten sinnvollen Optimierungsschritte nach dem zu
|
||||
- `PermissionsView.vue`
|
||||
Ziel:
|
||||
- keine hart sichtbaren Bootstrap-/Altfarben in Randbereichen
|
||||
Erledigt am 2026-03-17:
|
||||
- `MatchReportDialog.vue`, `NuscoreAnalyzer.vue` und `PermissionsView.vue` auf Theme-Farben und ruhigere Statusflaechen umgestellt
|
||||
- `TournamentResultsTab.vue` als verbleibender sichtbarer Tournament-Randbereich an die Badge-/Statuslogik des Themes angepasst
|
||||
|
||||
- [ ] Zeichnungs-/Court-Komponenten visuell und technisch aufraeumen.
|
||||
- [x] Zeichnungs-/Court-Komponenten visuell und technisch aufraeumen.
|
||||
Kandidaten:
|
||||
- `CourtDrawingTool.vue`
|
||||
- `CourtDrawingRender.vue`
|
||||
Grund:
|
||||
- grosse Komponenten
|
||||
- funktionale Farbsemantik gemischt mit Alt-Styling
|
||||
Erledigt am 2026-03-17:
|
||||
- Court-Paletten zentralisiert
|
||||
- Tool-/Render-Flaechen, Buttons und Grid-Zustaende ans Theme angepasst
|
||||
- verbliebene harte Altfarben im sichtbaren UI deutlich reduziert
|
||||
|
||||
## Querliegende Verbesserungen
|
||||
|
||||
- [ ] Manuelle SQL-Migrationen kuenftig direkt mit jeder Schemaaenderung in `docs/manual_sql_migrations.md` mitpflegen.
|
||||
- [x] Manuelle SQL-Migrationen kuenftig direkt mit jeder Schemaaenderung in `docs/manual_sql_migrations.md` mitpflegen.
|
||||
Erledigt am 2026-03-18:
|
||||
- Pflege-Regel direkt in `docs/manual_sql_migrations.md` dokumentiert
|
||||
- SQL-Migrationsdoku damit nicht mehr nur Sammelablage, sondern verbindlicher Prozesspunkt
|
||||
|
||||
- [ ] Fuer grosse Frontend-Dateien eine grobe Zielgroesse einfuehren.
|
||||
- [x] Fuer grosse Frontend-Dateien eine grobe Zielgroesse einfuehren.
|
||||
Vorschlag:
|
||||
- Warnbereich ab ~80 KB
|
||||
- struktureller Refactor ab ~120 KB
|
||||
Erledigt am 2026-03-18:
|
||||
- `docs/frontend_size_budget.md` angelegt
|
||||
- Audit-Skript `npm run audit:frontend-size` ergaenzt
|
||||
|
||||
- [ ] Inline-TODOs/FIXMEs regelmaessig in Repo-Doku ueberfuehren und aus produktiven Hauptdateien entfernen.
|
||||
- [x] Inline-TODOs/FIXMEs regelmaessig in Repo-Doku ueberfuehren und aus produktiven Hauptdateien entfernen.
|
||||
Erledigt am 2026-03-18:
|
||||
- `docs/inline_todo_registry.md` angelegt
|
||||
- Audit-Skript `npm run audit:inline-todos` ergaenzt
|
||||
- Produktivcode per Audit geprueft: keine offenen Marker in `frontend/src` und `backend`
|
||||
|
||||
## Aktuelle groesste Kandidaten nach Dateigroesse
|
||||
|
||||
|
||||
44
docs/frontend_size_budget.md
Normal file
44
docs/frontend_size_budget.md
Normal file
@@ -0,0 +1,44 @@
|
||||
# Frontend Size Budget
|
||||
|
||||
Stand: 2026-03-18
|
||||
|
||||
## Ziel
|
||||
|
||||
Grosse Frontend-Dateien sollen frueh sichtbar werden, bevor einzelne Views oder Komponenten wieder schwer wartbar werden.
|
||||
|
||||
## Regeln
|
||||
|
||||
- `warn` ab grob `80 KB`
|
||||
- `refactor` ab grob `120 KB`
|
||||
|
||||
Diese Werte sind bewusst pragmatisch und gelten fuer:
|
||||
|
||||
- `.vue`
|
||||
- `.js`
|
||||
- `.ts`
|
||||
- `.scss`
|
||||
|
||||
unter `frontend/src/`.
|
||||
|
||||
## Verwendung
|
||||
|
||||
Audit aus dem Repo-Root:
|
||||
|
||||
```bash
|
||||
npm run audit:frontend-size
|
||||
```
|
||||
|
||||
Das Skript liefert eine sortierte Liste der groessten Frontend-Dateien und markiert sie als:
|
||||
|
||||
- `ok`
|
||||
- `warn`
|
||||
- `refactor`
|
||||
|
||||
## Aktueller Umgang
|
||||
|
||||
- `warn` bedeutet: bei der naechsten groesseren Aenderung Struktur pruefen.
|
||||
- `refactor` bedeutet: keine weitere groessere Fachlogik ohne vorherige oder parallele Extraktion in Subkomponenten/Services.
|
||||
|
||||
## Aktuelle Kandidaten
|
||||
|
||||
Die aktuelle Liste wird bewusst nicht statisch doppelt gepflegt. Stattdessen ist `npm run audit:frontend-size` die Referenz.
|
||||
30
docs/inline_todo_registry.md
Normal file
30
docs/inline_todo_registry.md
Normal file
@@ -0,0 +1,30 @@
|
||||
# Inline TODO Registry
|
||||
|
||||
Stand: 2026-03-18
|
||||
|
||||
## Regel
|
||||
|
||||
Produktiver Frontend-/Backend-Code soll keine offenen `TODO`, `FIXME` oder `XXX` Marker dauerhaft enthalten.
|
||||
|
||||
Wenn waehrend der Entwicklung ein solcher Marker noetig ist:
|
||||
|
||||
1. so kurz wie moeglich im Code belassen
|
||||
2. in eine Doku-Datei oder Issue-Liste ueberfuehren
|
||||
3. den Marker im Produktivcode wieder entfernen
|
||||
|
||||
## Audit
|
||||
|
||||
Aus dem Repo-Root:
|
||||
|
||||
```bash
|
||||
npm run audit:inline-todos
|
||||
```
|
||||
|
||||
## Aktueller Stand
|
||||
|
||||
Zum Stand `2026-03-18` gibt es keine offenen `TODO`-/`FIXME`-/`XXX`-Marker mehr in:
|
||||
|
||||
- `frontend/src`
|
||||
- `backend`
|
||||
|
||||
Verbleibende TODO-Abschnitte liegen nur noch in Dokumentation, z.B. fuer Android-Portierung oder fachliche Backlog-Punkte.
|
||||
@@ -2,6 +2,20 @@
|
||||
|
||||
Diese Datei sammelt SQL-Aenderungen, die auf Live/Test manuell eingespielt werden muessen, wenn das Produktions-Setup keine automatische Schema-Migration ausfuehrt.
|
||||
|
||||
## Pflege-Regel
|
||||
|
||||
Ab jetzt gilt fuer jede Schemaaenderung:
|
||||
|
||||
1. SQL fuer `test` und `live` direkt hier eintragen.
|
||||
2. Datum, betroffene Tabelle/Felder und optionalen Backfill dokumentieren.
|
||||
3. Erst danach den Frontend-/Backend-Teil als abgeschlossen markieren.
|
||||
|
||||
Ergaenzend:
|
||||
|
||||
- Die produktive Anwendung soll sich nicht auf `sync({ alter: true })` verlassen.
|
||||
- Manuelle Migrationsschritte gehoeren in diese Datei, nicht nur in Chat-Verlaeufe oder Commit-Messages.
|
||||
- Wenn eine Aenderung rueckwaertskompatibel ist, soll das hier explizit vermerkt werden.
|
||||
|
||||
## 2026-03-17
|
||||
|
||||
### `predefined_activities.exclude_from_stats`
|
||||
@@ -49,3 +63,7 @@ Optionaler Schutz gegen doppelte Teilnehmerzeilen pro Trainingstag:
|
||||
ALTER TABLE participants
|
||||
ADD UNIQUE KEY participants_diary_date_member_unique (diary_date_id, member_id);
|
||||
```
|
||||
|
||||
Rueckwaertskompatibilitaet:
|
||||
|
||||
- Bestehende Datensaetze bleiben durch den Default `present` gueltig.
|
||||
|
||||
@@ -14,13 +14,33 @@
|
||||
ref="canvas"
|
||||
:width="config.canvas.width"
|
||||
:height="config.canvas.height"
|
||||
style="margin:0 auto;"
|
||||
class="render-canvas"
|
||||
></canvas>
|
||||
</div>
|
||||
|
||||
</template>
|
||||
|
||||
<script>
|
||||
const COURT_RENDER_PALETTE = {
|
||||
table: '#315f43',
|
||||
frame: '#ffffff',
|
||||
frameBorder: '#102418',
|
||||
line: '#d9e4dd',
|
||||
net: '#ffffff',
|
||||
netBorder: '#102418',
|
||||
startSelected: '#c84c32',
|
||||
startMuted: '#8d948f',
|
||||
startMutedBorder: '#4d5650',
|
||||
arrowPrimary: '#c84c32',
|
||||
arrowSecondary: '#1f6a8a',
|
||||
arrowTertiary: '#4f7f32',
|
||||
arrowQuaternary: '#8a5a1f',
|
||||
markerFill: '#ffffff',
|
||||
markerStroke: '#102418',
|
||||
surface: '#eef3ef',
|
||||
buttonDisabled: '#7b847e'
|
||||
};
|
||||
|
||||
export default {
|
||||
name: 'CourtDrawingRender',
|
||||
props: {
|
||||
@@ -51,16 +71,16 @@ export default {
|
||||
// Tabelle etwas schmaler und weiter rechts platzieren
|
||||
width: 500,
|
||||
height: 300,
|
||||
color: '#2d5a2d',
|
||||
color: COURT_RENDER_PALETTE.table,
|
||||
borderWidth: 2,
|
||||
borderColor: '#000000',
|
||||
borderColor: COURT_RENDER_PALETTE.frameBorder,
|
||||
outerFrameWidth: 5,
|
||||
outerFrameColor: '#ffffff',
|
||||
outerFrameColor: COURT_RENDER_PALETTE.frame,
|
||||
outerFrameBorderWidth: 0.5,
|
||||
outerFrameBorderColor: '#000000'
|
||||
outerFrameBorderColor: COURT_RENDER_PALETTE.frameBorder
|
||||
},
|
||||
horizontalLine: { color: '#cfd8dc', width: 2, gap: 10, edgeMargin: 10 },
|
||||
net: { color: '#ffffff', width: 4, overhang: 14, borderColor: '#000000', borderWidth: 0.5 },
|
||||
horizontalLine: { color: COURT_RENDER_PALETTE.line, width: 2, gap: 10, edgeMargin: 10 },
|
||||
net: { color: COURT_RENDER_PALETTE.net, width: 4, overhang: 14, borderColor: COURT_RENDER_PALETTE.netBorder, borderWidth: 0.5 },
|
||||
serviceLine: { color: 'rgba(255,255,255,0.0)', width: 0 },
|
||||
startCircles: {
|
||||
radius: 10,
|
||||
@@ -68,18 +88,18 @@ export default {
|
||||
x: 20,
|
||||
topYOffset: 35,
|
||||
bottomYOffset: 35,
|
||||
selectedColor: '#ff4444',
|
||||
unselectedColor: '#9e9e9e',
|
||||
selectedBorderColor: '#ffffff',
|
||||
unselectedBorderColor: '#555555',
|
||||
selectedColor: COURT_RENDER_PALETTE.startSelected,
|
||||
unselectedColor: COURT_RENDER_PALETTE.startMuted,
|
||||
selectedBorderColor: COURT_RENDER_PALETTE.frame,
|
||||
unselectedBorderColor: COURT_RENDER_PALETTE.startMutedBorder,
|
||||
selectedBorderWidth: 2,
|
||||
unselectedBorderWidth: 1
|
||||
},
|
||||
arrows: {
|
||||
primaryColor: '#d32f2f', // rechts -> target (rot)
|
||||
secondaryColor: '#1565c0', // zurück (blau)
|
||||
tertiaryColor: '#2e7d32', // dritter Schlag (grün)
|
||||
quaternaryColor: '#6a1b9a', // vierter Schlag (violett)
|
||||
primaryColor: COURT_RENDER_PALETTE.arrowPrimary,
|
||||
secondaryColor: COURT_RENDER_PALETTE.arrowSecondary,
|
||||
tertiaryColor: COURT_RENDER_PALETTE.arrowTertiary,
|
||||
quaternaryColor: COURT_RENDER_PALETTE.arrowQuaternary,
|
||||
width: 6,
|
||||
headLength: 24,
|
||||
vhOffsetX: 5,
|
||||
@@ -104,8 +124,8 @@ export default {
|
||||
},
|
||||
hitMarker: {
|
||||
radius: 10.5,
|
||||
fill: '#ffffff',
|
||||
stroke: '#000000',
|
||||
fill: COURT_RENDER_PALETTE.markerFill,
|
||||
stroke: COURT_RENDER_PALETTE.markerStroke,
|
||||
lineWidth: 1
|
||||
}
|
||||
}
|
||||
@@ -159,7 +179,7 @@ export default {
|
||||
const { width, height } = this.config.canvas;
|
||||
// clear and background
|
||||
this.ctx.clearRect(0, 0, width, height);
|
||||
this.ctx.fillStyle = '#f0f0f0';
|
||||
this.ctx.fillStyle = COURT_RENDER_PALETTE.surface;
|
||||
this.ctx.fillRect(0, 0, width, height);
|
||||
this.drawTable();
|
||||
this.drawStartCircles();
|
||||
@@ -350,8 +370,8 @@ export default {
|
||||
// clamp innerhalb Canvas
|
||||
x = Math.max(4, Math.min(this.config.canvas.width - textWidth - 4, x));
|
||||
y = Math.max(14, Math.min(this.config.canvas.height - 4, y));
|
||||
ctx.fillStyle = '#ffffff';
|
||||
ctx.strokeStyle = '#000000';
|
||||
ctx.fillStyle = COURT_RENDER_PALETTE.markerFill;
|
||||
ctx.strokeStyle = COURT_RENDER_PALETTE.markerStroke;
|
||||
ctx.lineWidth = 2;
|
||||
ctx.strokeText(text, x, y);
|
||||
ctx.fillText(text, x, y);
|
||||
@@ -681,6 +701,10 @@ canvas {
|
||||
height: auto;
|
||||
}
|
||||
|
||||
.render-canvas {
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.animation-controls {
|
||||
text-align: center;
|
||||
margin-bottom: 1rem;
|
||||
@@ -706,8 +730,7 @@ canvas {
|
||||
.btn-animate:disabled {
|
||||
opacity: 0.6;
|
||||
cursor: not-allowed;
|
||||
background-color: #6c757d;
|
||||
border-color: #6c757d;
|
||||
background-color: #7b847e;
|
||||
border-color: #7b847e;
|
||||
}
|
||||
</style>
|
||||
|
||||
|
||||
@@ -217,8 +217,7 @@
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
class="btn-primary btn-small"
|
||||
style="vertical-align:bottom"
|
||||
class="btn-primary btn-small add-stroke-button"
|
||||
@click="addNextStroke"
|
||||
:disabled="additionalStrokes.length >= 4"
|
||||
>
|
||||
@@ -239,6 +238,25 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
const COURT_TOOL_PALETTE = {
|
||||
table: '#315f43',
|
||||
frame: '#ffffff',
|
||||
frameBorder: '#102418',
|
||||
line: '#ffffff',
|
||||
startSelected: '#c84c32',
|
||||
startMuted: '#8d948f',
|
||||
targetSelected: '#6f8f3b',
|
||||
targetBorder: '#102418',
|
||||
targetText: '#102418',
|
||||
arrowPrimary: '#c84c32',
|
||||
arrowSupport: '#1f6a8a',
|
||||
arrowAccent: '#8a5a1f',
|
||||
arrowHighlight: '#c7a42a',
|
||||
arrowAlternate: '#7a4ea3',
|
||||
canvasSurface: '#eef3ef',
|
||||
accentText: '#f1d26b'
|
||||
};
|
||||
|
||||
export default {
|
||||
name: 'CourtDrawingTool',
|
||||
props: {
|
||||
@@ -270,40 +288,40 @@ export default {
|
||||
table: {
|
||||
width: 500,
|
||||
height: 300,
|
||||
color: '#2d5a2d',
|
||||
color: COURT_TOOL_PALETTE.table,
|
||||
borderWidth: 2,
|
||||
borderColor: '#000000',
|
||||
borderColor: COURT_TOOL_PALETTE.frameBorder,
|
||||
outerFrameWidth: 5,
|
||||
outerFrameColor: '#ffffff',
|
||||
outerFrameColor: COURT_TOOL_PALETTE.frame,
|
||||
outerFrameBorderWidth: 0.5,
|
||||
outerFrameBorderColor: '#000000'
|
||||
outerFrameBorderColor: COURT_TOOL_PALETTE.frameBorder
|
||||
},
|
||||
net: {
|
||||
width: 4,
|
||||
color: '#ffffff',
|
||||
color: COURT_TOOL_PALETTE.frame,
|
||||
borderWidth: 1,
|
||||
borderColor: '#000000',
|
||||
borderColor: COURT_TOOL_PALETTE.frameBorder,
|
||||
overhang: 15
|
||||
},
|
||||
horizontalLine: {
|
||||
width: 1,
|
||||
color: '#ffffff',
|
||||
color: COURT_TOOL_PALETTE.line,
|
||||
gap: 10,
|
||||
edgeMargin: 10
|
||||
},
|
||||
serviceLine: {
|
||||
width: 1,
|
||||
color: '#ffffff'
|
||||
color: COURT_TOOL_PALETTE.line
|
||||
},
|
||||
startCircles: {
|
||||
radius: 12,
|
||||
x: 10,
|
||||
topYOffset: 30,
|
||||
bottomYOffset: 30,
|
||||
selectedColor: '#ff0000',
|
||||
unselectedColor: '#808080',
|
||||
selectedBorderColor: '#ffffff',
|
||||
unselectedBorderColor: '#000000',
|
||||
selectedColor: COURT_TOOL_PALETTE.startSelected,
|
||||
unselectedColor: COURT_TOOL_PALETTE.startMuted,
|
||||
selectedBorderColor: COURT_TOOL_PALETTE.frame,
|
||||
unselectedBorderColor: COURT_TOOL_PALETTE.frameBorder,
|
||||
selectedBorderWidth: 2,
|
||||
unselectedBorderWidth: 1
|
||||
},
|
||||
@@ -313,14 +331,14 @@ export default {
|
||||
middleXOffset: 40,
|
||||
topYOffset: 40,
|
||||
bottomYOffset: 40,
|
||||
selectedColor: '#00ff00',
|
||||
unselectedColor: '#ffffff',
|
||||
selectedBorderColor: '#ffffff',
|
||||
unselectedBorderColor: '#000000',
|
||||
selectedColor: COURT_TOOL_PALETTE.targetSelected,
|
||||
unselectedColor: COURT_TOOL_PALETTE.frame,
|
||||
selectedBorderColor: COURT_TOOL_PALETTE.frame,
|
||||
unselectedBorderColor: COURT_TOOL_PALETTE.targetBorder,
|
||||
selectedBorderWidth: 2,
|
||||
unselectedBorderWidth: 1,
|
||||
font: '12px Arial',
|
||||
textColor: '#000000',
|
||||
textColor: COURT_TOOL_PALETTE.targetText,
|
||||
transparency: 0.3
|
||||
},
|
||||
leftTargetCircles: {
|
||||
@@ -329,17 +347,17 @@ export default {
|
||||
rightXOffset: 40,
|
||||
topYOffset: 40,
|
||||
bottomYOffset: 40,
|
||||
selectedColor: '#00ff00',
|
||||
unselectedColor: '#ffffff',
|
||||
selectedBorderColor: '#00ff00',
|
||||
unselectedBorderColor: '#000000',
|
||||
selectedColor: COURT_TOOL_PALETTE.targetSelected,
|
||||
unselectedColor: COURT_TOOL_PALETTE.frame,
|
||||
selectedBorderColor: COURT_TOOL_PALETTE.targetSelected,
|
||||
unselectedBorderColor: COURT_TOOL_PALETTE.targetBorder,
|
||||
selectedBorderWidth: 2,
|
||||
unselectedBorderWidth: 1,
|
||||
font: '12px Arial',
|
||||
textColor: '#000000'
|
||||
textColor: COURT_TOOL_PALETTE.targetText
|
||||
},
|
||||
arrow: {
|
||||
color: '#ff0000', // Hauptschlag immer rot
|
||||
color: COURT_TOOL_PALETTE.arrowPrimary,
|
||||
width: 6,
|
||||
cap: 'round',
|
||||
headSize: 8,
|
||||
@@ -351,7 +369,7 @@ export default {
|
||||
counterOffset: 15
|
||||
},
|
||||
pen: {
|
||||
color: '#ff0000',
|
||||
color: COURT_TOOL_PALETTE.arrowPrimary,
|
||||
width: 3,
|
||||
cap: 'round',
|
||||
join: 'round'
|
||||
@@ -480,7 +498,7 @@ export default {
|
||||
|
||||
if (forceRedraw || isEmpty) {
|
||||
// Hintergrund
|
||||
ctx.fillStyle = '#f0f0f0';
|
||||
ctx.fillStyle = COURT_TOOL_PALETTE.canvasSurface;
|
||||
ctx.fillRect(0, 0, canvas.width, canvas.height);
|
||||
} else {
|
||||
}
|
||||
@@ -869,8 +887,8 @@ export default {
|
||||
}
|
||||
|
||||
// Pfeil zeichnen (von rechts nach links = blau)
|
||||
ctx.strokeStyle = '#007bff'; // Blau
|
||||
ctx.fillStyle = '#007bff'; // Blau
|
||||
ctx.strokeStyle = COURT_TOOL_PALETTE.arrowSupport;
|
||||
ctx.fillStyle = COURT_TOOL_PALETTE.arrowSupport;
|
||||
ctx.lineWidth = config.arrow.width;
|
||||
ctx.lineCap = config.arrow.cap;
|
||||
|
||||
@@ -883,7 +901,7 @@ export default {
|
||||
const endY = targetY;
|
||||
|
||||
// Übungsnummer über der Linie zeichnen
|
||||
ctx.fillStyle = '#007bff'; // Blau
|
||||
ctx.fillStyle = COURT_TOOL_PALETTE.arrowSupport;
|
||||
ctx.font = config.arrow.counterFont;
|
||||
ctx.textAlign = 'center';
|
||||
ctx.textBaseline = 'middle';
|
||||
@@ -1011,7 +1029,12 @@ export default {
|
||||
if (!this.additionalStrokes || this.additionalStrokes.length === 0) return;
|
||||
const max = this.additionalStrokes.length;
|
||||
// 1: rot, 2: blau, 3: gelb, 4: violett, ab 5: pink
|
||||
const colors = ['#007bff', '#FFD700', '#6a1b9a', '#ff69b4'];
|
||||
const colors = [
|
||||
COURT_TOOL_PALETTE.arrowSupport,
|
||||
COURT_TOOL_PALETTE.arrowHighlight,
|
||||
COURT_TOOL_PALETTE.arrowAlternate,
|
||||
COURT_TOOL_PALETTE.arrowAccent
|
||||
];
|
||||
const rightRadius = this.config.targetCircles.radius;
|
||||
const leftRadius = this.config.leftTargetCircles.radius;
|
||||
|
||||
@@ -1026,7 +1049,7 @@ export default {
|
||||
const side = i % 2 === 0 ? 'left' : 'right';
|
||||
const targetNum = parseInt(stroke.targetPosition);
|
||||
// Farben: 2. blau, 3. gelb, 4. violett, ab dem 5. pink
|
||||
const color = (i < colors.length) ? colors[i] : '#ff69b4';
|
||||
const color = (i < colors.length) ? colors[i] : COURT_TOOL_PALETTE.arrowAccent;
|
||||
let toCenter, toPoint;
|
||||
if (side === 'left') {
|
||||
toCenter = this.computeLeftTargetCenter(tableX, tableY, tableWidth, tableHeight, targetNum);
|
||||
@@ -1619,11 +1642,12 @@ export default {
|
||||
|
||||
<style scoped>
|
||||
.court-drawing-tool {
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 8px;
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: 14px;
|
||||
padding: 1rem;
|
||||
background: white;
|
||||
background: var(--surface-color, #ffffff);
|
||||
margin: 1rem 0;
|
||||
box-shadow: 0 10px 26px rgba(15, 23, 42, 0.06);
|
||||
}
|
||||
|
||||
.tool-header {
|
||||
@@ -1635,7 +1659,7 @@ export default {
|
||||
|
||||
.tool-header h4 {
|
||||
margin: 0;
|
||||
color: #333;
|
||||
color: var(--text-color);
|
||||
}
|
||||
|
||||
.tool-controls {
|
||||
@@ -1646,29 +1670,31 @@ export default {
|
||||
.canvas-container {
|
||||
text-align: center;
|
||||
margin: 1rem 0;
|
||||
border: 2px solid #ddd;
|
||||
border-radius: 4px;
|
||||
background: #f9f9f9;
|
||||
padding: 0.5rem;
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: 12px;
|
||||
background: var(--background-soft);
|
||||
padding: 0.75rem;
|
||||
}
|
||||
|
||||
canvas {
|
||||
border: 1px solid #ccc;
|
||||
border-radius: 4px;
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: 8px;
|
||||
cursor: crosshair;
|
||||
background: white;
|
||||
background: var(--surface-color, #ffffff);
|
||||
}
|
||||
|
||||
|
||||
.btn-small {
|
||||
padding: 0.2rem 0.4rem;
|
||||
padding: 0.25rem 0.5rem;
|
||||
font-size: 0.75rem;
|
||||
border-radius: 4px;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 999px;
|
||||
border: 1px solid var(--border-color);
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
transition: transform 0.2s ease, opacity 0.2s ease, border-color 0.2s ease;
|
||||
min-width: 2.5rem;
|
||||
text-align: center;
|
||||
background: var(--surface-color, #ffffff);
|
||||
color: var(--text-color);
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
@@ -1679,20 +1705,20 @@ canvas {
|
||||
|
||||
.btn-primary:hover {
|
||||
opacity: 0.95;
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
.btn-secondary {
|
||||
background-color: #6c757d;
|
||||
color: white;
|
||||
border-color: #6c757d;
|
||||
background: var(--surface-color, #ffffff);
|
||||
color: var(--text-color);
|
||||
border-color: var(--border-color);
|
||||
}
|
||||
|
||||
.btn-secondary:hover {
|
||||
background-color: #5a6268;
|
||||
border-color: #5a6268;
|
||||
border-color: var(--primary-color);
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
/* Schlagart-Buttons (VH/RH) - Grün */
|
||||
.btn-stroke {
|
||||
background: linear-gradient(135deg, var(--primary-color), var(--primary-hover));
|
||||
color: var(--text-on-primary);
|
||||
@@ -1701,39 +1727,41 @@ canvas {
|
||||
|
||||
.btn-stroke:hover {
|
||||
opacity: 0.95;
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
.btn-stroke.btn-secondary {
|
||||
background-color: #6c757d;
|
||||
color: white;
|
||||
border-color: #6c757d;
|
||||
background: var(--surface-color, #ffffff);
|
||||
color: var(--text-color);
|
||||
border-color: var(--border-color);
|
||||
}
|
||||
|
||||
.btn-stroke.btn-secondary:hover {
|
||||
background-color: #5a6268;
|
||||
border-color: #5a6268;
|
||||
border-color: var(--primary-color);
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
/* Schlagtyp-Buttons (US/OS/TS/FL/BL) - Orange Hintergrund */
|
||||
.btn-stroke-type {
|
||||
background: #fd7e14;
|
||||
color: white;
|
||||
background: linear-gradient(135deg, #b56a1d, #8f5316);
|
||||
color: var(--text-on-primary);
|
||||
border-color: transparent;
|
||||
}
|
||||
|
||||
.btn-stroke-type:hover {
|
||||
background: #e8650e;
|
||||
border-color: #e8650e;
|
||||
opacity: 0.95;
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
.btn-stroke-type.btn-secondary {
|
||||
color: white !important;
|
||||
opacity: 0.6;
|
||||
background: rgba(181, 106, 29, 0.12);
|
||||
color: #8f5316 !important;
|
||||
border-color: rgba(181, 106, 29, 0.22);
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.btn-stroke-type.btn-secondary:hover {
|
||||
background-color: #e8650e !important;
|
||||
border-color: #e8650e !important;
|
||||
opacity: 0.8;
|
||||
background: rgba(181, 106, 29, 0.18);
|
||||
border-color: rgba(181, 106, 29, 0.3) !important;
|
||||
}
|
||||
|
||||
.group-label {
|
||||
@@ -1741,7 +1769,7 @@ canvas {
|
||||
margin: 0 0 4px 0;
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
color: #495057;
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
input[type="color"] {
|
||||
@@ -1759,14 +1787,14 @@ input[type="range"] {
|
||||
.exercise-selection {
|
||||
margin-top: 1rem;
|
||||
padding: 1rem;
|
||||
background: #f8f9fa;
|
||||
border-radius: 4px;
|
||||
border: 1px solid #ddd;
|
||||
background: var(--background-soft);
|
||||
border-radius: 12px;
|
||||
border: 1px solid var(--border-color);
|
||||
}
|
||||
|
||||
.exercise-selection h5 {
|
||||
margin: 0 0 1rem 0;
|
||||
color: #333;
|
||||
color: var(--text-color);
|
||||
}
|
||||
|
||||
.selection-group {
|
||||
@@ -1787,7 +1815,7 @@ input[type="range"] {
|
||||
display: block;
|
||||
margin-bottom: 0.5rem;
|
||||
font-weight: 500;
|
||||
color: #495057;
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
.stroke-buttons {
|
||||
@@ -1804,7 +1832,7 @@ input[type="range"] {
|
||||
display: block;
|
||||
margin-bottom: 0.5rem;
|
||||
font-weight: 500;
|
||||
color: #495057;
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
.spin-buttons {
|
||||
@@ -1834,10 +1862,10 @@ input[type="range"] {
|
||||
}
|
||||
|
||||
.exercise-info {
|
||||
background: white;
|
||||
background: var(--surface-color, #ffffff);
|
||||
padding: 0.75rem;
|
||||
border-radius: 4px;
|
||||
border: 1px solid #ccc;
|
||||
border-radius: 10px;
|
||||
border: 1px solid var(--border-color);
|
||||
}
|
||||
|
||||
.exercise-info p {
|
||||
@@ -1862,29 +1890,33 @@ input[type="range"] {
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
border: none;
|
||||
border-radius: 2px;
|
||||
background: var(--primary-color);
|
||||
color: #ffffff;
|
||||
border-radius: 6px;
|
||||
background: var(--primary-soft);
|
||||
color: var(--primary-dark);
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
line-height: 1;
|
||||
cursor: pointer;
|
||||
border: 1px solid rgba(29, 90, 61, 0.14);
|
||||
}
|
||||
|
||||
.grid-btn.is-active {
|
||||
color: #FFD700; /* gelb */
|
||||
background: linear-gradient(135deg, var(--primary-color), var(--primary-hover));
|
||||
color: var(--text-on-primary);
|
||||
font-weight: 700; /* fett */
|
||||
}
|
||||
|
||||
/* Ausgewählte Aufschlag-/Seiten-Buttons: fette gelbe Schrift */
|
||||
.btn-stroke.btn-primary {
|
||||
color: #FFD700 !important;
|
||||
color: COURT_TOOL_PALETTE.accentText !important;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
/* Ausgewählte Schlagtyp-Buttons: fette gelbe Schrift */
|
||||
.btn-stroke-type.btn-primary {
|
||||
color: #FFD700 !important;
|
||||
color: COURT_TOOL_PALETTE.accentText !important;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.add-stroke-button {
|
||||
align-self: flex-end;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -27,15 +27,15 @@
|
||||
<span>{{ $t('matchReportApi.general') }}</span>
|
||||
</button>
|
||||
|
||||
<button class="section-btn" :class="{ active: activeSection === 'homeLineup', certified: isHomeLineupCertified }"
|
||||
@click="setActiveSection('homeLineup')">
|
||||
<button class="section-btn" :class="{ active: activeSection === 'homeLineup', certified: isHomeLineupCertified, disabled: teamNotAppeared === 'home' }"
|
||||
@click="setActiveSection('homeLineup')" :disabled="teamNotAppeared === 'home'">
|
||||
<div class="section-icon">👥</div>
|
||||
<span>{{ $t('matchReportApi.homeLineup') }}</span>
|
||||
<span v-if="isHomeLineupCertified" class="certified-badge">✓</span>
|
||||
</button>
|
||||
|
||||
<button class="section-btn" :class="{ active: activeSection === 'guestLineup', certified: isGuestLineupCertified }"
|
||||
@click="setActiveSection('guestLineup')">
|
||||
<button class="section-btn" :class="{ active: activeSection === 'guestLineup', certified: isGuestLineupCertified, disabled: teamNotAppeared === 'guest' }"
|
||||
@click="setActiveSection('guestLineup')" :disabled="teamNotAppeared === 'guest'">
|
||||
<div class="section-icon">👥</div>
|
||||
<span>{{ $t('matchReportApi.guestLineup') }}</span>
|
||||
<span v-if="isGuestLineupCertified" class="certified-badge">✓</span>
|
||||
@@ -48,15 +48,15 @@
|
||||
<span v-if="isGreetingCompleted" class="completed-badge">✅</span>
|
||||
</button>
|
||||
|
||||
<button class="section-btn" :class="{ active: activeSection === 'result', disabled: !canOpenNextStages }"
|
||||
@click="setActiveSection('result')" :disabled="!canOpenNextStages">
|
||||
<button class="section-btn" :class="{ active: activeSection === 'result', disabled: !canOpenResultStage }"
|
||||
@click="setActiveSection('result')" :disabled="!canOpenResultStage">
|
||||
<div class="section-icon">⚽</div>
|
||||
<span>{{ $t('matchReportApi.result') }}</span>
|
||||
<span v-if="isMatchCompleted" class="locked-indicator">🔒</span>
|
||||
</button>
|
||||
|
||||
<button class="section-btn" :class="{ active: activeSection === 'completion', disabled: !canOpenNextStages }"
|
||||
@click="setActiveSection('completion')" :disabled="!canOpenNextStages">
|
||||
<button class="section-btn" :class="{ active: activeSection === 'completion', disabled: !canOpenCompletionStage }"
|
||||
@click="setActiveSection('completion')" :disabled="!canOpenCompletionStage">
|
||||
<div class="section-icon">✅</div>
|
||||
<span>{{ $t('matchReportApi.completion') }}</span>
|
||||
</button>
|
||||
@@ -143,6 +143,7 @@
|
||||
<label for="homePin">{{ $t('matchReportApi.homePin') }}:</label>
|
||||
<div class="pin-input-wrapper">
|
||||
<input id="homePin" v-model="homePin" type="password" :class="pinInputClasses('home')"
|
||||
:disabled="isLineupLocked('home')"
|
||||
@input="onPinChange('home', $event)" />
|
||||
<button @click="signLineup('home')" :class="signButtonClasses('home')"
|
||||
:disabled="!canSignLineup('home')">
|
||||
@@ -236,8 +237,7 @@
|
||||
<div class="pin-input-wrapper">
|
||||
<input id="guestPin" v-model="guestPin" type="password" :class="pinInputClasses('guest')"
|
||||
autocomplete="new-password" autocapitalize="off" spellcheck="false"
|
||||
:readonly="!(match && match.guestPin)"
|
||||
@focus="(match && match.guestPin) ? null : (guestPin='', $event.target.removeAttribute('readonly'))"
|
||||
:disabled="isLineupLocked('guest')"
|
||||
@input="onPinChange('guest', $event)" />
|
||||
<button @click="signLineup('guest')" :class="signButtonClasses('guest')"
|
||||
:disabled="!canSignLineup('guest')">
|
||||
@@ -569,7 +569,7 @@
|
||||
v-model="finalHomePin"
|
||||
type="password"
|
||||
class="pin-input"
|
||||
:placeholder="match.homePin || 'PIN eingeben'"
|
||||
placeholder="PIN eingeben"
|
||||
:disabled="isMatchCompleted || teamNotAppeared === 'home'"
|
||||
/>
|
||||
<span v-if="teamNotAppeared === 'home'" class="pin-not-required">(nicht erforderlich - Mannschaft nicht angetreten)</span>
|
||||
@@ -582,7 +582,7 @@
|
||||
v-model="finalGuestPin"
|
||||
type="password"
|
||||
class="pin-input"
|
||||
:placeholder="match.guestPin || 'PIN eingeben'"
|
||||
placeholder="PIN eingeben"
|
||||
:disabled="isMatchCompleted || teamNotAppeared === 'guest'"
|
||||
/>
|
||||
<span v-if="teamNotAppeared === 'guest'" class="pin-not-required">(nicht erforderlich - Mannschaft nicht angetreten)</span>
|
||||
@@ -594,8 +594,8 @@
|
||||
<button
|
||||
@click="submitMatchReport"
|
||||
class="btn-primary submit-btn"
|
||||
:disabled="isMatchCompleted || !areAllMatchResultsValid() || !areStartAndEndTimesValid()"
|
||||
:class="{ 'disabled': isMatchCompleted || !areAllMatchResultsValid() || !areStartAndEndTimesValid() }"
|
||||
:disabled="isSubmitDisabled"
|
||||
:class="{ 'disabled': isSubmitDisabled }"
|
||||
>
|
||||
{{ getSubmitButtonText() }}
|
||||
</button>
|
||||
@@ -646,10 +646,10 @@
|
||||
<button @click="closePinModal" class="pin-modal-close">×</button>
|
||||
</div>
|
||||
<div class="pin-modal-body">
|
||||
<p>Diese Aufstellung ist zertifiziert. Bitte geben Sie die PIN für beide Mannschaften ein:</p>
|
||||
<p>Diese Aufstellung ist zertifiziert. Bitte geben Sie die passende PIN ein, um sie wieder zu bearbeiten.</p>
|
||||
|
||||
<div class="pin-modal-inputs">
|
||||
<div class="pin-input-group">
|
||||
<div v-if="pinModalRequiresHomePin()" class="pin-input-group">
|
||||
<label for="modalHomePin">PIN Heimmannschaft:</label>
|
||||
<input
|
||||
id="modalHomePin"
|
||||
@@ -664,7 +664,7 @@
|
||||
</small>
|
||||
</div>
|
||||
|
||||
<div class="pin-input-group">
|
||||
<div v-if="pinModalRequiresGuestPin()" class="pin-input-group">
|
||||
<label for="modalGuestPin">PIN Gastmannschaft:</label>
|
||||
<input
|
||||
id="modalGuestPin"
|
||||
@@ -835,18 +835,29 @@ Wir wünschen den Spielen einen schönen, spannenden und fairen Verlauf und begr
|
||||
if (this.isMatchCompleted) {
|
||||
return false;
|
||||
}
|
||||
// WO-Fall: keine Ergebniserfassung, Abschluss darf aber direkt geöffnet werden
|
||||
if (this.teamNotAppeared !== null) {
|
||||
return true;
|
||||
}
|
||||
// Wenn eine Mannschaft nicht angetreten ist, muss die andere Mannschaft ihre Aufstellung signieren
|
||||
if (this.teamNotAppeared === 'home') {
|
||||
// Heim nicht angetreten -> Gast muss Aufstellung signieren
|
||||
return this.isGuestLineupCertified;
|
||||
}
|
||||
if (this.teamNotAppeared === 'guest') {
|
||||
// Gast nicht angetreten -> Heim muss Aufstellung signieren
|
||||
return this.isHomeLineupCertified;
|
||||
}
|
||||
// Beide Mannschaften angetreten -> beide müssen Aufstellung signieren
|
||||
return this.isHomeLineupCertified && this.isGuestLineupCertified;
|
||||
},
|
||||
canOpenResultStage() {
|
||||
if (this.teamNotAppeared !== null) {
|
||||
return false;
|
||||
}
|
||||
return this.canOpenNextStages;
|
||||
},
|
||||
canOpenCompletionStage() {
|
||||
if (this.isMatchCompleted) {
|
||||
return false;
|
||||
}
|
||||
if (this.teamNotAppeared !== null) {
|
||||
return true;
|
||||
}
|
||||
return this.canOpenNextStages;
|
||||
},
|
||||
currentPlayMode() {
|
||||
return (this.meetingDetails && this.meetingDetails.playMode) || (this.meetingData && this.meetingData.playMode) || '';
|
||||
},
|
||||
@@ -874,8 +885,25 @@ Wir wünschen den Spielen einen schönen, spannenden und fairen Verlauf und begr
|
||||
|
||||
// Prüfe ob das Match bereits abgeschlossen ist
|
||||
isMatchCompleted() {
|
||||
if (typeof this.meetingDetails?.isCompleted === 'boolean') {
|
||||
return this.meetingDetails.isCompleted;
|
||||
}
|
||||
if (typeof this.meetingData?.isCompleted === 'boolean') {
|
||||
return this.meetingData.isCompleted;
|
||||
}
|
||||
return this.match && this.match.isCompleted === true;
|
||||
},
|
||||
isSubmitDisabled() {
|
||||
if (this.isMatchCompleted) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (this.teamNotAppeared !== null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return !this.areAllMatchResultsValid() || !this.areStartAndEndTimesValid();
|
||||
}
|
||||
},
|
||||
async mounted() {
|
||||
await this.loadData();
|
||||
@@ -911,7 +939,11 @@ Wir wünschen den Spielen einen schönen, spannenden und fairen Verlauf und begr
|
||||
},
|
||||
watch: {
|
||||
teamNotAppeared(newValue, oldValue) {
|
||||
if (newValue !== oldValue && this.meetingData) {
|
||||
if (newValue === oldValue) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.meetingData) {
|
||||
if (newValue === null) {
|
||||
// Entferne das wo-Flag beim Zurücksetzen
|
||||
this.meetingData.wo = null;
|
||||
@@ -920,9 +952,43 @@ Wir wünschen den Spielen einen schönen, spannenden und fairen Verlauf und begr
|
||||
this.applyTeamNotAppeared();
|
||||
}
|
||||
}
|
||||
|
||||
if (newValue === 'home') {
|
||||
this.isHomeLineupCertified = false;
|
||||
this.homePin = '';
|
||||
this.finalHomePin = '';
|
||||
this.activeSection = 'completion';
|
||||
return;
|
||||
}
|
||||
|
||||
if (newValue === 'guest') {
|
||||
this.isGuestLineupCertified = false;
|
||||
this.guestPin = '';
|
||||
this.finalGuestPin = '';
|
||||
this.activeSection = 'completion';
|
||||
return;
|
||||
}
|
||||
|
||||
if (newValue === null && this.activeSection === 'completion' && !this.canOpenNextStages) {
|
||||
this.activeSection = 'general';
|
||||
}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
logSignatureState(label, payload) {
|
||||
const signature = payload?.signature || null;
|
||||
console.log(`[match-report] ${label}`, {
|
||||
wo: payload?.wo ?? null,
|
||||
isCompleted: payload?.isCompleted ?? null,
|
||||
homePin: payload?.homePin ?? null,
|
||||
guestPin: payload?.guestPin ?? null,
|
||||
releaseSignatureHome: signature?.releaseSignatureHome ?? null,
|
||||
releaseSignatureGuest: signature?.releaseSignatureGuest ?? null,
|
||||
lineupSignatureHome: signature?.lineupSignatureHome ?? null,
|
||||
lineupSignatureGuest: signature?.lineupSignatureGuest ?? null
|
||||
});
|
||||
},
|
||||
|
||||
// Effektive Spieleranzahl (berücksichtigt Braunschweiger-Regel)
|
||||
getEffectivePlayerCount(team) {
|
||||
const actualCount = this.getSelectedPlayerCount(team);
|
||||
@@ -1140,6 +1206,17 @@ Wir wünschen den Spielen einen schönen, spannenden und fairen Verlauf und begr
|
||||
// Intelligente Navigation nach Zertifizierung
|
||||
navigateAfterCertification() {
|
||||
console.log('🧭 Intelligente Navigation nach Zertifizierung...');
|
||||
|
||||
if (this.teamNotAppeared !== null) {
|
||||
if (this.canOpenNextStages) {
|
||||
this.setActiveSection('completion');
|
||||
console.log('🎯 WO-Fall: angetretene Mannschaft signiert → Abschluss');
|
||||
} else {
|
||||
this.setActiveSection('general');
|
||||
console.log('🎯 WO-Fall: warte auf Signatur der angetretenen Mannschaft');
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Prüfe ob beide Aufstellungen jetzt zertifiziert sind
|
||||
const bothCertified = this.isHomeLineupCertified && this.isGuestLineupCertified;
|
||||
@@ -1295,9 +1372,9 @@ Wir wünschen den Spielen einen schönen, spannenden und fairen Verlauf und begr
|
||||
},
|
||||
|
||||
initializeFinalPins() {
|
||||
// Initialisiere PIN-Felder mit den Werten aus dem Match-Objekt
|
||||
this.finalHomePin = this.match.homePin || '';
|
||||
this.finalGuestPin = this.match.guestPin || '';
|
||||
// Nur lokale Klartext-PINs vorbefuellen, niemals nuscore-Hashes.
|
||||
this.finalHomePin = this.isLikelyPinHash(this.match.homePin) ? '' : (this.match.homePin || '');
|
||||
this.finalGuestPin = this.isLikelyPinHash(this.match.guestPin) ? '' : (this.match.guestPin || '');
|
||||
},
|
||||
|
||||
// Text für den Absenden-Button basierend auf Zustand
|
||||
@@ -1464,17 +1541,17 @@ Wir wünschen den Spielen einen schönen, spannenden und fairen Verlauf und begr
|
||||
console.log('⚠️ Mannschaft nicht angetreten - überspringe Match-Ergebnis- und Zeit-Validierung');
|
||||
}
|
||||
|
||||
// Validiere Aufstellungen (nur für angetretene Mannschaften)
|
||||
// WO-Fall: keine Aufstellungssignaturen erzwingen
|
||||
const isHomeNotAppeared = this.teamNotAppeared === 'home';
|
||||
const isGuestNotAppeared = this.teamNotAppeared === 'guest';
|
||||
|
||||
if (!isHomeNotAppeared && !this.isHomeLineupCertified) {
|
||||
|
||||
if (this.teamNotAppeared === null && !isHomeNotAppeared && !this.isHomeLineupCertified) {
|
||||
alert('❌ Der Spielbericht kann nicht abgesendet werden, da die Heimmannschaft ihre Aufstellung noch nicht signiert hat.\n\n' +
|
||||
'Bitte signieren Sie die Aufstellung der Heimmannschaft, bevor Sie den Spielbericht absenden.');
|
||||
return;
|
||||
}
|
||||
|
||||
if (!isGuestNotAppeared && !this.isGuestLineupCertified) {
|
||||
|
||||
if (this.teamNotAppeared === null && !isGuestNotAppeared && !this.isGuestLineupCertified) {
|
||||
alert('❌ Der Spielbericht kann nicht abgesendet werden, da die Gastmannschaft ihre Aufstellung noch nicht signiert hat.\n\n' +
|
||||
'Bitte signieren Sie die Aufstellung der Gastmannschaft, bevor Sie den Spielbericht absenden.');
|
||||
return;
|
||||
@@ -1505,6 +1582,7 @@ Wir wünschen den Spielen einen schönen, spannenden und fairen Verlauf und begr
|
||||
console.log('🔄 Aktualisiere Match-Daten...');
|
||||
this.updateMatchData(matchData);
|
||||
console.log('✅ Match-Daten aktualisiert');
|
||||
this.logSignatureState('payload before validate', matchData);
|
||||
|
||||
// Für WebSocket-Broadcast: clubId und gameCode mitsenden
|
||||
const clubId = this.$store?.getters?.currentClub;
|
||||
@@ -1519,18 +1597,35 @@ Wir wünschen den Spielen einen schönen, spannenden und fairen Verlauf und begr
|
||||
throw new Error('nuLigaMeetingUuid nicht gefunden');
|
||||
}
|
||||
|
||||
const validationResult = await this.validateReport(matchData);
|
||||
if (validationResult?.resultState === 'VALIDATION_ERROR') {
|
||||
const validationMessage = Array.isArray(validationResult.validationErrors) && validationResult.validationErrors.length > 0
|
||||
? validationResult.validationErrors.join('\n')
|
||||
: 'Validierung fehlgeschlagen';
|
||||
throw new Error(validationMessage);
|
||||
}
|
||||
|
||||
const submitPayload = validationResult?.object
|
||||
? JSON.parse(JSON.stringify(validationResult.object))
|
||||
: matchData;
|
||||
this.logSignatureState('payload before submit', submitPayload);
|
||||
|
||||
const response = await fetch(`${backendBaseUrl}/api/nuscore/submit/${uuid}`, {
|
||||
method: 'PUT',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify(matchData)
|
||||
body: JSON.stringify(submitPayload)
|
||||
});
|
||||
|
||||
const result = await response.json();
|
||||
console.log('[match-report] submit response', result);
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(result.error || `HTTP ${response.status}: ${response.statusText}`);
|
||||
const validationMessage = Array.isArray(result?.details?.validationErrors) && result.details.validationErrors.length > 0
|
||||
? result.details.validationErrors.join('\n')
|
||||
: null;
|
||||
throw new Error(validationMessage || result.error || `HTTP ${response.status}: ${response.statusText}`);
|
||||
}
|
||||
|
||||
console.log('✅ Spielbericht erfolgreich abgesendet:', result);
|
||||
@@ -1546,18 +1641,20 @@ Wir wünschen den Spielen einen schönen, spannenden und fairen Verlauf und begr
|
||||
}
|
||||
},
|
||||
|
||||
async validateReport() {
|
||||
async validateReport(matchDataOverride = null) {
|
||||
try {
|
||||
if (!this.meetingData || !this.meetingData.nuLigaMeetingUuid) {
|
||||
console.warn('⚠️ Keine Meeting-UUID verfügbar für Validierung');
|
||||
return;
|
||||
return null;
|
||||
}
|
||||
|
||||
// Erstelle eine Kopie des aktuellen Match-Objekts
|
||||
const matchData = JSON.parse(JSON.stringify(this.match));
|
||||
|
||||
// Aktualisiere die Match-Daten mit unseren Eingaben
|
||||
this.updateMatchData(matchData);
|
||||
const matchData = matchDataOverride
|
||||
? JSON.parse(JSON.stringify(matchDataOverride))
|
||||
: JSON.parse(JSON.stringify(this.match));
|
||||
|
||||
if (!matchDataOverride) {
|
||||
this.updateMatchData(matchData);
|
||||
}
|
||||
|
||||
const uuid = this.meetingData.nuLigaMeetingUuid;
|
||||
|
||||
@@ -1572,7 +1669,17 @@ Wir wünschen den Spielen einen schönen, spannenden und fairen Verlauf und begr
|
||||
});
|
||||
|
||||
const result = await response.json();
|
||||
console.log('[match-report] validate response', result);
|
||||
|
||||
if (result?.object) {
|
||||
this.meetingDetails = JSON.parse(JSON.stringify(result.object));
|
||||
this.meetingData = {
|
||||
...(this.meetingData || {}),
|
||||
...JSON.parse(JSON.stringify(result.object))
|
||||
};
|
||||
this.applyLineupCertificationFromMeetingDetails();
|
||||
}
|
||||
|
||||
if (response.ok) {
|
||||
console.log('✅ Validierung erfolgreich:', result);
|
||||
|
||||
@@ -1588,10 +1695,13 @@ Wir wünschen den Spielen einen schönen, spannenden und fairen Verlauf und begr
|
||||
} else {
|
||||
console.warn('⚠️ Validierung mit Fehlern:', result);
|
||||
}
|
||||
|
||||
return result;
|
||||
|
||||
} catch (error) {
|
||||
console.error('❌ Fehler beim Validieren:', error);
|
||||
// Validierungsfehler sind nicht kritisch, daher kein Alert
|
||||
return null;
|
||||
}
|
||||
},
|
||||
|
||||
@@ -1626,14 +1736,22 @@ Wir wünschen den Spielen einen schönen, spannenden und fairen Verlauf und begr
|
||||
|
||||
// NUR unsere spezifischen Änderungen eintragen:
|
||||
|
||||
// Wenn eine Mannschaft nicht angetreten ist, nur wo-Flag setzen und nichts anderes ändern
|
||||
const isHomeNotAppeared = this.teamNotAppeared === 'home';
|
||||
const isGuestNotAppeared = this.teamNotAppeared === 'guest';
|
||||
|
||||
// Abschluss-PINs explizit auf die korrekten Team-Felder schreiben.
|
||||
// Im WO-Fall nur fuer die angetretene Mannschaft.
|
||||
if (!isHomeNotAppeared && this.finalHomePin && this.finalHomePin.trim() !== '') {
|
||||
matchData.homePin = this.finalHomePin.trim();
|
||||
}
|
||||
if (!isGuestNotAppeared && this.finalGuestPin && this.finalGuestPin.trim() !== '') {
|
||||
matchData.guestPin = this.finalGuestPin.trim();
|
||||
}
|
||||
|
||||
// Wenn eine Mannschaft nicht angetreten ist, nur wo-Flag und PIN der angetretenen Mannschaft setzen
|
||||
if (this.teamNotAppeared !== null) {
|
||||
// Nur das wo-Flag ist bereits gesetzt (durch applyTeamNotAppeared)
|
||||
// Alle anderen Daten bleiben unverändert (Original aus baseData)
|
||||
// isCompleted bleibt false, PINs bleiben unverändert, keine Match-Ergebnisse ändern
|
||||
// Keine Zeitangaben, Spieleranzahl, Positionen oder Protest-Informationen ändern
|
||||
console.log('⚠️ Mannschaft nicht angetreten - nur wo-Flag wird gesetzt, alle anderen Daten bleiben unverändert');
|
||||
return; // Frühzeitiger Return - keine weiteren Änderungen
|
||||
console.log('⚠️ Mannschaft nicht angetreten - nur wo-Flag und PIN der angetretenen Mannschaft werden gesetzt');
|
||||
return;
|
||||
}
|
||||
|
||||
// 1. Spieleranzahl aktualisieren (aus Aufstellung)
|
||||
@@ -2068,23 +2186,118 @@ Wir wünschen den Spielen einen schönen, spannenden und fairen Verlauf und begr
|
||||
|
||||
/**
|
||||
* Setzt die Aufstellungs-Bestätigung (certified) aus den Meeting-Details.
|
||||
* In nuscore wird nach PIN-Eingabe ein Hash gespeichert (homePin/guestPin).
|
||||
* Ist dieser gesetzt, gilt die Aufstellung als bereits bestätigt.
|
||||
* Wichtig: NICHT aus homePin/guestPin ableiten. Diese Hashes koennen bereits
|
||||
* vor einer bestaetigten Aufstellung vorhanden sein und sind nur fuer die
|
||||
* PIN-Validierung gedacht.
|
||||
*/
|
||||
applyLineupCertificationFromMeetingDetails() {
|
||||
if (!this.meetingDetails) return;
|
||||
|
||||
const homePinSet = this.meetingDetails.homePin != null && String(this.meetingDetails.homePin).trim() !== '';
|
||||
const guestPinSet = this.meetingDetails.guestPin != null && String(this.meetingDetails.guestPin).trim() !== '';
|
||||
|
||||
if (homePinSet) {
|
||||
this.isHomeLineupCertified = true;
|
||||
const state = this.getExplicitLineupCertificationState();
|
||||
if (state.home !== null) {
|
||||
this.isHomeLineupCertified = state.home;
|
||||
}
|
||||
if (guestPinSet) {
|
||||
this.isGuestLineupCertified = true;
|
||||
if (state.guest !== null) {
|
||||
this.isGuestLineupCertified = state.guest;
|
||||
}
|
||||
},
|
||||
|
||||
normalizeCertificationValue(value) {
|
||||
if (typeof value === 'boolean') return value;
|
||||
if (typeof value === 'number') {
|
||||
if (value === 1) return true;
|
||||
if (value === 0) return false;
|
||||
return null;
|
||||
}
|
||||
if (typeof value !== 'string') return null;
|
||||
|
||||
const normalized = value.trim().toLowerCase();
|
||||
const trueValues = new Set(['true', '1', 'yes', 'done', 'signed', 'confirmed', 'certified', 'complete', 'completed', 'reviewed', 'locked', 'readonly', 'read-only']);
|
||||
const falseValues = new Set(['false', '0', 'no', 'open', 'editable', 'pending', 'draft', 'unlocked']);
|
||||
|
||||
if (trueValues.has(normalized)) return true;
|
||||
if (falseValues.has(normalized)) return false;
|
||||
return null;
|
||||
},
|
||||
|
||||
readValueByPath(source, path) {
|
||||
return path.split('.').reduce((current, segment) => {
|
||||
if (current == null || typeof current !== 'object') {
|
||||
return undefined;
|
||||
}
|
||||
return current[segment];
|
||||
}, source);
|
||||
},
|
||||
|
||||
extractTeamCertificationFromSource(source, team) {
|
||||
if (!source || typeof source !== 'object') return null;
|
||||
|
||||
const lineupSignaturePath = team === 'home'
|
||||
? 'signature.lineupSignatureHome'
|
||||
: 'signature.lineupSignatureGuest';
|
||||
const lineupSignatureValue = this.readValueByPath(source, lineupSignaturePath);
|
||||
if (lineupSignatureValue !== undefined) {
|
||||
return typeof lineupSignatureValue === 'string'
|
||||
? lineupSignatureValue.trim() !== ''
|
||||
: Boolean(lineupSignatureValue);
|
||||
}
|
||||
|
||||
if (Object.prototype.hasOwnProperty.call(source, 'signature') && source.signature === null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const teamMarkers = team === 'home'
|
||||
? ['home', 'cluba', 'teama', 'lineuphome', 'homelineup']
|
||||
: ['guest', 'clubb', 'teamb', 'lineupguest', 'guestlineup'];
|
||||
const statusMarkers = ['certified', 'signed', 'confirmed', 'reviewed', 'clearance', 'locked', 'readonly', 'signature'];
|
||||
|
||||
const walk = (value, path = []) => {
|
||||
if (!value || typeof value !== 'object') return null;
|
||||
|
||||
for (const [rawKey, child] of Object.entries(value)) {
|
||||
const key = String(rawKey);
|
||||
const nextPath = [...path, key];
|
||||
const joinedPath = nextPath.join('.').toLowerCase();
|
||||
|
||||
if (!joinedPath.includes('pin') &&
|
||||
teamMarkers.some(marker => joinedPath.includes(marker)) &&
|
||||
statusMarkers.some(marker => joinedPath.includes(marker))) {
|
||||
if (joinedPath.includes('lineupsignature') && typeof child === 'string') {
|
||||
return child.trim() !== '';
|
||||
}
|
||||
const normalized = this.normalizeCertificationValue(child);
|
||||
if (normalized !== null) {
|
||||
return normalized;
|
||||
}
|
||||
}
|
||||
|
||||
if (child && typeof child === 'object') {
|
||||
const nested = walk(child, nextPath);
|
||||
if (nested !== null) {
|
||||
return nested;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
return walk(source);
|
||||
},
|
||||
|
||||
getExplicitLineupCertificationState() {
|
||||
const state = { home: null, guest: null };
|
||||
|
||||
for (const source of [this.meetingDetails, this.meetingData]) {
|
||||
if (state.home === null) {
|
||||
state.home = this.extractTeamCertificationFromSource(source, 'home');
|
||||
}
|
||||
if (state.guest === null) {
|
||||
state.guest = this.extractTeamCertificationFromSource(source, 'guest');
|
||||
}
|
||||
}
|
||||
|
||||
return state;
|
||||
},
|
||||
|
||||
/** Entwurf für Broadcast: meetingDetails-Form mit aktuellen Satzergebnissen aus results. */
|
||||
getDraftMatchData() {
|
||||
if (!this.meetingDetails || !Array.isArray(this.meetingDetails.matches)) return null;
|
||||
@@ -2961,6 +3174,7 @@ Wir wünschen den Spielen einen schönen, spannenden und fairen Verlauf und begr
|
||||
},
|
||||
|
||||
togglePlayerSelection(player, team) {
|
||||
if (this.isLineupLocked(team)) return;
|
||||
if (!this.meetingDetails) return;
|
||||
|
||||
const teamKey = team === 'home' ? 'teamLineupHomePlayers' : 'teamLineupGuestPlayers';
|
||||
@@ -2982,6 +3196,7 @@ Wir wünschen den Spielen einen schönen, spannenden und fairen Verlauf und begr
|
||||
},
|
||||
|
||||
toggleDoublePosition(player, team, doublePosition) {
|
||||
if (this.isLineupLocked(team)) return;
|
||||
if (!this.meetingDetails) return;
|
||||
|
||||
const teamKey = team === 'home' ? 'teamLineupHomePlayers' : 'teamLineupGuestPlayers';
|
||||
@@ -3074,6 +3289,9 @@ Wir wünschen den Spielen einen schönen, spannenden und fairen Verlauf und begr
|
||||
|
||||
|
||||
onPinChange(team, event) {
|
||||
if (this.isLineupLocked(team)) {
|
||||
return;
|
||||
}
|
||||
const pin = event.target.value;
|
||||
if (team === 'home') {
|
||||
this.homePin = pin;
|
||||
@@ -3158,6 +3376,9 @@ Wir wünschen den Spielen einen schönen, spannenden und fairen Verlauf und begr
|
||||
|
||||
// Aufstellung signieren
|
||||
async signLineup(team) {
|
||||
if (this.isLineupLocked(team)) {
|
||||
return;
|
||||
}
|
||||
const pin = team === 'home' ? this.homePin : this.guestPin;
|
||||
|
||||
if (!pin) {
|
||||
@@ -3345,6 +3566,9 @@ Wir wünschen den Spielen einen schönen, spannenden und fairen Verlauf und begr
|
||||
},
|
||||
|
||||
canSignLineup(team) {
|
||||
if (this.isLineupLocked(team)) {
|
||||
return false;
|
||||
}
|
||||
const pin = team === 'home' ? this.homePin : this.guestPin;
|
||||
|
||||
// PIN prüfen
|
||||
@@ -3586,14 +3810,20 @@ Wir wünschen den Spielen einen schönen, spannenden und fairen Verlauf und begr
|
||||
// PIN-Modal für zertifizierte Aufstellungen
|
||||
openPinModal(team) {
|
||||
this.pinModalTeam = team;
|
||||
// Heim-PIN nur vorausfüllen, wenn sie ursprünglich verwendet wurde
|
||||
this.pinModalHomePin = this.originalHomePin || '';
|
||||
// Gast-PIN nur vorausfüllen, wenn sie ursprünglich verwendet wurde UND vom Frontend-Match geliefert wurde (echte PIN)
|
||||
this.pinModalGuestPin = (this.originalGuestPin && this.match && this.match.guestPin) ? this.originalGuestPin : '';
|
||||
this.pinModalGuestPin = this.originalGuestPin || '';
|
||||
this.pinModalError = '';
|
||||
this.showPinModal = true;
|
||||
},
|
||||
|
||||
pinModalRequiresHomePin() {
|
||||
return this.pinModalTeam === 'homeLineup' || this.pinModalTeam === 'home';
|
||||
},
|
||||
|
||||
pinModalRequiresGuestPin() {
|
||||
return this.pinModalTeam === 'guestLineup' || this.pinModalTeam === 'guest';
|
||||
},
|
||||
|
||||
closePinModal() {
|
||||
this.showPinModal = false;
|
||||
this.pinModalTeam = null;
|
||||
@@ -3603,40 +3833,47 @@ Wir wünschen den Spielen einen schönen, spannenden und fairen Verlauf und begr
|
||||
},
|
||||
|
||||
async submitPinModal() {
|
||||
// Beide PINs sind Pflicht
|
||||
if (!this.pinModalHomePin || !this.pinModalGuestPin) {
|
||||
this.pinModalError = 'Bitte geben Sie die Heim- und Gast-PIN ein';
|
||||
const requiresHomePin = this.pinModalRequiresHomePin();
|
||||
const requiresGuestPin = this.pinModalRequiresGuestPin();
|
||||
|
||||
if (requiresHomePin && !this.pinModalHomePin) {
|
||||
this.pinModalError = 'Bitte geben Sie die Heim-PIN ein';
|
||||
return;
|
||||
}
|
||||
|
||||
// Prüfen gegen ursprünglich verwendete PINs
|
||||
const homePinValid = this.pinModalHomePin === this.originalHomePin;
|
||||
const guestPinValid = this.pinModalGuestPin === this.originalGuestPin;
|
||||
if (requiresGuestPin && !this.pinModalGuestPin) {
|
||||
this.pinModalError = 'Bitte geben Sie die Gast-PIN ein';
|
||||
return;
|
||||
}
|
||||
|
||||
const homePinValid = !requiresHomePin || await this.validatePin('home', this.pinModalHomePin);
|
||||
const guestPinValid = !requiresGuestPin || await this.validatePin('guest', this.pinModalGuestPin);
|
||||
|
||||
if (homePinValid && guestPinValid) {
|
||||
console.log('✅ PIN erfolgreich - entsperre Aufstellung und wechsle Tab');
|
||||
|
||||
// Nur den angeforderten Tab entsperren
|
||||
if (this.pinModalTeam === 'homeLineup' || this.pinModalTeam === 'home') {
|
||||
if (requiresHomePin) {
|
||||
this.isHomeLineupCertified = false;
|
||||
this.closePinModal();
|
||||
// Wechsle sofort zu Heim-Aufstellungs-Tab
|
||||
this.setActiveSection('homeLineup');
|
||||
console.log('🎯 Wechsle zu Heim-Aufstellungs-Tab');
|
||||
} else if (this.pinModalTeam === 'guestLineup' || this.pinModalTeam === 'guest') {
|
||||
this.isGuestLineupCertified = false;
|
||||
this.closePinModal();
|
||||
// Wechsle sofort zu Gast-Aufstellungs-Tab
|
||||
this.setActiveSection('guestLineup');
|
||||
console.log('🎯 Wechsle zu Gast-Aufstellungs-Tab');
|
||||
} else {
|
||||
this.closePinModal();
|
||||
}
|
||||
if (requiresGuestPin) {
|
||||
this.isGuestLineupCertified = false;
|
||||
}
|
||||
|
||||
const targetSection = requiresHomePin ? 'homeLineup' : 'guestLineup';
|
||||
this.closePinModal();
|
||||
this.activeSection = targetSection;
|
||||
console.log(`🎯 Wechsle zu ${targetSection}`);
|
||||
} else {
|
||||
this.pinModalError = 'Eine oder beide PINs sind nicht korrekt';
|
||||
this.pinModalError = requiresHomePin && requiresGuestPin
|
||||
? 'Eine oder beide PINs sind nicht korrekt'
|
||||
: (requiresHomePin ? 'Die Heim-PIN ist nicht korrekt' : 'Die Gast-PIN ist nicht korrekt');
|
||||
}
|
||||
},
|
||||
|
||||
isLineupLocked(team) {
|
||||
return team === 'home' ? this.isHomeLineupCertified : this.isGuestLineupCertified;
|
||||
},
|
||||
|
||||
// Prüfe ob Aufstellung-Tab geöffnet werden kann
|
||||
canOpenLineupTab(team) {
|
||||
if (team === 'home') {
|
||||
@@ -3648,6 +3885,15 @@ Wir wünschen den Spielen einen schönen, spannenden und fairen Verlauf und begr
|
||||
|
||||
// Tab-Wechsel mit PIN-Prüfung
|
||||
setActiveSection(section) {
|
||||
if ((section === 'homeLineup' && this.teamNotAppeared === 'home') ||
|
||||
(section === 'guestLineup' && this.teamNotAppeared === 'guest')) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (section === 'result' && this.teamNotAppeared !== null) {
|
||||
return;
|
||||
}
|
||||
|
||||
if ((section === 'homeLineup' && this.isHomeLineupCertified) ||
|
||||
(section === 'guestLineup' && this.isGuestLineupCertified)) {
|
||||
this.openPinModal(section);
|
||||
@@ -3672,6 +3918,10 @@ Wir wünschen den Spielen einen schönen, spannenden und fairen Verlauf und begr
|
||||
hour: '2-digit',
|
||||
minute: '2-digit'
|
||||
});
|
||||
},
|
||||
|
||||
isLikelyPinHash(value) {
|
||||
return typeof value === 'string' && /^[a-f0-9]{64,}$/i.test(value.trim());
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
<button @click="showAnalyzer = !showAnalyzer" class="toggle-btn">
|
||||
{{ showAnalyzer ? '🔼 ' + $t('matchReport.hideAnalyzer') : '🔍 ' + $t('matchReport.analyzeNuscore') }}
|
||||
</button>
|
||||
<button @click="createLocalCopy" class="toggle-btn" style="margin-left: 10px;">
|
||||
<button @click="createLocalCopy" class="toggle-btn toggle-btn-secondary">
|
||||
💾 {{ $t('matchReport.createLocalCopy') }}
|
||||
</button>
|
||||
</div>
|
||||
@@ -202,28 +202,37 @@ export default {
|
||||
}
|
||||
|
||||
.analyzer-toggle {
|
||||
display: flex;
|
||||
gap: 0.65rem;
|
||||
flex-wrap: wrap;
|
||||
padding: 10px;
|
||||
background: #f8f9fa;
|
||||
border-bottom: 1px solid #dee2e6;
|
||||
background: var(--background-soft);
|
||||
border-bottom: 1px solid var(--border-color);
|
||||
}
|
||||
|
||||
.toggle-btn {
|
||||
background: var(--primary-color);
|
||||
color: white;
|
||||
background: linear-gradient(135deg, var(--primary-color), var(--primary-hover));
|
||||
color: var(--text-on-primary);
|
||||
border: none;
|
||||
padding: 8px 16px;
|
||||
border-radius: 4px;
|
||||
border-radius: 999px;
|
||||
cursor: pointer;
|
||||
font-size: 14px;
|
||||
transition: background-color 0.2s;
|
||||
transition: transform 0.2s ease, opacity 0.2s ease;
|
||||
}
|
||||
|
||||
.toggle-btn:hover {
|
||||
background: var(--primary-hover);
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
.toggle-btn-secondary {
|
||||
background: var(--surface-color, white);
|
||||
color: var(--text-color);
|
||||
border: 1px solid var(--border-color);
|
||||
}
|
||||
|
||||
.match-info {
|
||||
background: #f8f9fa;
|
||||
background: var(--background-soft);
|
||||
padding: 16px;
|
||||
border-radius: 8px;
|
||||
margin-bottom: 16px;
|
||||
@@ -231,19 +240,19 @@ export default {
|
||||
|
||||
.match-info h4 {
|
||||
margin: 0 0 12px 0;
|
||||
color: #007bff;
|
||||
color: var(--primary-dark);
|
||||
font-size: 1.1rem;
|
||||
}
|
||||
|
||||
.match-info p {
|
||||
margin: 4px 0;
|
||||
font-size: 0.9rem;
|
||||
color: #555;
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
.code-display {
|
||||
font-family: 'Courier New', monospace;
|
||||
background: #e3f2fd;
|
||||
background: var(--primary-soft);
|
||||
padding: 2px 6px;
|
||||
border-radius: 4px;
|
||||
margin-right: 8px;
|
||||
@@ -260,21 +269,21 @@ export default {
|
||||
}
|
||||
|
||||
.copy-btn:hover {
|
||||
background: #f0f0f0;
|
||||
background: var(--background-soft);
|
||||
}
|
||||
|
||||
.instructions {
|
||||
background: #f8f9fa;
|
||||
background: var(--background-soft);
|
||||
padding: 12px;
|
||||
border-radius: 6px;
|
||||
margin-top: 12px;
|
||||
border-left: 4px solid #007bff;
|
||||
border-left: 4px solid var(--primary-color);
|
||||
}
|
||||
|
||||
.instructions p {
|
||||
margin: 0 0 8px 0;
|
||||
font-weight: 600;
|
||||
color: #007bff;
|
||||
color: var(--primary-dark);
|
||||
}
|
||||
|
||||
.instructions ol {
|
||||
@@ -285,7 +294,7 @@ export default {
|
||||
.instructions li {
|
||||
margin: 4px 0;
|
||||
font-size: 0.9rem;
|
||||
color: #555;
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
.report-content {
|
||||
|
||||
@@ -405,7 +405,8 @@ export default {
|
||||
<style scoped>
|
||||
.nuscore-analyzer {
|
||||
padding: 20px;
|
||||
background: #f8f9fa;
|
||||
background: var(--background-soft);
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: 8px;
|
||||
margin: 20px 0;
|
||||
}
|
||||
@@ -425,28 +426,29 @@ export default {
|
||||
.analyze-btn, .download-btn, .create-btn {
|
||||
padding: 10px 20px;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
border-radius: 999px;
|
||||
cursor: pointer;
|
||||
font-weight: bold;
|
||||
transition: background-color 0.2s;
|
||||
transition: transform 0.2s ease, opacity 0.2s ease;
|
||||
}
|
||||
|
||||
.analyze-btn {
|
||||
background: var(--primary-color);
|
||||
color: white;
|
||||
background: linear-gradient(135deg, var(--primary-color), var(--primary-hover));
|
||||
color: var(--text-on-primary);
|
||||
}
|
||||
|
||||
.analyze-btn:hover:not(:disabled) {
|
||||
background: var(--primary-hover);
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
.download-btn {
|
||||
background: var(--secondary-color);
|
||||
color: white;
|
||||
background: var(--surface-color, white);
|
||||
color: var(--text-color);
|
||||
border: 1px solid var(--border-color);
|
||||
}
|
||||
|
||||
.download-btn:hover:not(:disabled) {
|
||||
background: var(--secondary-hover);
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
.create-btn {
|
||||
@@ -466,7 +468,7 @@ export default {
|
||||
.resource-list {
|
||||
max-height: 300px;
|
||||
overflow-y: auto;
|
||||
border: 1px solid #ddd;
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: 4px;
|
||||
background: white;
|
||||
}
|
||||
@@ -475,7 +477,7 @@ export default {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 8px 12px;
|
||||
border-bottom: 1px solid #eee;
|
||||
border-bottom: 1px solid var(--border-color);
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
@@ -484,7 +486,7 @@ export default {
|
||||
}
|
||||
|
||||
.resource-item.downloaded {
|
||||
background: #d4edda;
|
||||
background: rgba(46, 125, 50, 0.12);
|
||||
}
|
||||
|
||||
.resource-type {
|
||||
@@ -497,7 +499,7 @@ export default {
|
||||
flex: 1;
|
||||
font-family: monospace;
|
||||
font-size: 12px;
|
||||
color: #666;
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
.resource-size {
|
||||
@@ -507,15 +509,15 @@ export default {
|
||||
}
|
||||
|
||||
.status {
|
||||
color: #28a745;
|
||||
color: var(--success-color);
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.log-content {
|
||||
max-height: 200px;
|
||||
overflow-y: auto;
|
||||
background: #000;
|
||||
color: #0f0;
|
||||
background: #1b232b;
|
||||
color: #b9f7c3;
|
||||
padding: 10px;
|
||||
border-radius: 4px;
|
||||
font-family: monospace;
|
||||
|
||||
@@ -142,7 +142,7 @@
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div v-if="selectedLeague && selectedLeague !== ''" class="tab-content">
|
||||
<div v-if="selectedLeague && selectedLeague !== ''" class="tab-content active">
|
||||
<div v-show="activeTab === 'schedule'" class="tab-panel">
|
||||
<slot name="schedule-panel" />
|
||||
</div>
|
||||
|
||||
@@ -647,20 +647,20 @@ export default {
|
||||
align-items: center;
|
||||
padding: 0.35rem 0.7rem;
|
||||
border-radius: 999px;
|
||||
background: #f3f4f6;
|
||||
color: #4b5563;
|
||||
background: var(--background-soft);
|
||||
color: var(--text-muted);
|
||||
font-size: 0.9rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.results-chip-live {
|
||||
background: #dcfce7;
|
||||
color: #166534;
|
||||
background: rgba(46, 125, 50, 0.14);
|
||||
color: var(--success-color);
|
||||
}
|
||||
|
||||
.results-chip-open {
|
||||
background: #fef3c7;
|
||||
color: #92400e;
|
||||
background: rgba(181, 106, 29, 0.14);
|
||||
color: #8f5316;
|
||||
}
|
||||
|
||||
.results-section-header {
|
||||
@@ -676,19 +676,19 @@ export default {
|
||||
|
||||
/* Farbmarkierungen für Spiele */
|
||||
.match-finished {
|
||||
background-color: #e9ecef !important;
|
||||
background-color: var(--background-soft) !important;
|
||||
}
|
||||
|
||||
.match-finished td {
|
||||
color: #626262 !important;
|
||||
color: var(--text-muted) !important;
|
||||
}
|
||||
|
||||
.match-live {
|
||||
background-color: #d4edda !important;
|
||||
background-color: rgba(46, 125, 50, 0.12) !important;
|
||||
}
|
||||
|
||||
.match-live:not(.match-finished) td {
|
||||
color: #155724 !important;
|
||||
color: var(--success-color) !important;
|
||||
}
|
||||
|
||||
.match-finished.match-live {
|
||||
@@ -701,8 +701,8 @@ export default {
|
||||
|
||||
/* Aktives Match hervorheben - auch wenn es abgeschlossen ist */
|
||||
.active-match {
|
||||
background-color: #fff3cd !important;
|
||||
border-left: 3px solid #ffc107 !important;
|
||||
background-color: rgba(181, 106, 29, 0.12) !important;
|
||||
border-left: 3px solid #b56a1d !important;
|
||||
}
|
||||
|
||||
.active-match.match-finished {
|
||||
@@ -710,7 +710,7 @@ export default {
|
||||
}
|
||||
|
||||
.active-match.match-finished td {
|
||||
color: #856404 !important;
|
||||
color: #8f5316 !important;
|
||||
}
|
||||
|
||||
.active-match.match-live {
|
||||
@@ -718,24 +718,24 @@ export default {
|
||||
}
|
||||
|
||||
.active-match.match-live td {
|
||||
color: #856404 !important;
|
||||
color: #8f5316 !important;
|
||||
}
|
||||
|
||||
.gave-up-result {
|
||||
color: #626262;
|
||||
color: var(--text-muted);
|
||||
}
|
||||
.gave-up-badge-small {
|
||||
margin-left: 0.25rem;
|
||||
font-size: 0.8rem;
|
||||
color: #721c24;
|
||||
color: var(--danger-color);
|
||||
}
|
||||
.no-edit-hint {
|
||||
font-size: 0.85rem;
|
||||
color: #6c757d;
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
.active-match:hover {
|
||||
background-color: #ffe69c !important;
|
||||
background-color: rgba(181, 106, 29, 0.18) !important;
|
||||
}
|
||||
|
||||
.distribute-tables-bar {
|
||||
@@ -748,14 +748,14 @@ export default {
|
||||
margin-top: 1rem;
|
||||
margin-bottom: 1rem;
|
||||
padding: 0.9rem 1rem;
|
||||
border: 1px solid #dbeafe;
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: 12px;
|
||||
background: #f8fbff;
|
||||
background: var(--surface-color, #ffffff);
|
||||
}
|
||||
|
||||
.results-next-step-muted {
|
||||
border-color: #e5e7eb;
|
||||
background: #f9fafb;
|
||||
border-color: var(--border-color);
|
||||
background: var(--background-soft);
|
||||
}
|
||||
|
||||
.ranking-class-gap {
|
||||
@@ -776,23 +776,23 @@ export default {
|
||||
}
|
||||
|
||||
.status-open {
|
||||
background: #e5e7eb;
|
||||
color: #4b5563;
|
||||
background: var(--background-soft);
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
.status-live {
|
||||
background: #dcfce7;
|
||||
color: #166534;
|
||||
background: rgba(46, 125, 50, 0.14);
|
||||
color: var(--success-color);
|
||||
}
|
||||
|
||||
.status-finished {
|
||||
background: #dbeafe;
|
||||
color: #1d4ed8;
|
||||
background: var(--primary-soft);
|
||||
color: var(--primary-dark);
|
||||
}
|
||||
|
||||
.status-gaveup {
|
||||
background: #fee2e2;
|
||||
color: #991b1b;
|
||||
background: rgba(200, 76, 50, 0.14);
|
||||
color: var(--danger-color);
|
||||
}
|
||||
|
||||
.action-row {
|
||||
@@ -806,8 +806,9 @@ export default {
|
||||
width: 3.5rem;
|
||||
padding: 0.15rem 0.25rem;
|
||||
font-size: 0.85rem;
|
||||
border: 1px solid #ced4da;
|
||||
border-radius: 4px;
|
||||
background: #fff;
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: 8px;
|
||||
background: var(--surface-color, #ffffff);
|
||||
color: var(--text-color);
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -593,11 +593,11 @@ export default {
|
||||
|
||||
.header h1 {
|
||||
margin: 0 0 10px 0;
|
||||
color: #2c3e50;
|
||||
color: var(--text-color);
|
||||
}
|
||||
|
||||
.subtitle {
|
||||
color: #7f8c8d;
|
||||
color: var(--text-muted);
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
@@ -608,12 +608,13 @@ export default {
|
||||
}
|
||||
|
||||
.error {
|
||||
background-color: #fee;
|
||||
color: #c33;
|
||||
background-color: rgba(180, 66, 66, 0.12);
|
||||
color: var(--danger-color);
|
||||
}
|
||||
|
||||
.role-legend {
|
||||
background: #f8f9fa;
|
||||
background: var(--background-soft);
|
||||
border: 1px solid var(--border-color);
|
||||
padding: 20px;
|
||||
border-radius: 8px;
|
||||
margin-bottom: 30px;
|
||||
@@ -621,7 +622,7 @@ export default {
|
||||
|
||||
.role-legend h3 {
|
||||
margin-top: 0;
|
||||
color: #2c3e50;
|
||||
color: var(--text-color);
|
||||
}
|
||||
|
||||
.roles-grid {
|
||||
@@ -635,30 +636,31 @@ export default {
|
||||
background: white;
|
||||
padding: 15px;
|
||||
border-radius: 6px;
|
||||
border: 1px solid #e0e0e0;
|
||||
border: 1px solid var(--border-color);
|
||||
}
|
||||
|
||||
.role-name {
|
||||
font-weight: 600;
|
||||
color: #2c3e50;
|
||||
color: var(--text-color);
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
.role-description {
|
||||
font-size: 0.9em;
|
||||
color: #666;
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
.members-table {
|
||||
background: white;
|
||||
padding: 20px;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
||||
border: 1px solid var(--border-color);
|
||||
box-shadow: var(--shadow-soft);
|
||||
}
|
||||
|
||||
.members-table h3 {
|
||||
margin-top: 0;
|
||||
color: #2c3e50;
|
||||
color: var(--text-color);
|
||||
}
|
||||
|
||||
table {
|
||||
@@ -670,18 +672,18 @@ table {
|
||||
th, td {
|
||||
padding: 12px;
|
||||
text-align: left;
|
||||
border-bottom: 1px solid #e0e0e0;
|
||||
border-bottom: 1px solid var(--border-color);
|
||||
}
|
||||
|
||||
th {
|
||||
background-color: #f8f9fa;
|
||||
background-color: var(--background-soft);
|
||||
font-weight: 600;
|
||||
color: #2c3e50;
|
||||
color: var(--text-color);
|
||||
}
|
||||
|
||||
.role-select {
|
||||
padding: 6px 10px;
|
||||
border: 1px solid #ddd;
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: 4px;
|
||||
font-size: 0.95em;
|
||||
}
|
||||
@@ -697,23 +699,23 @@ th {
|
||||
}
|
||||
|
||||
.role-admin {
|
||||
background-color: #e3f2fd;
|
||||
color: #1976d2;
|
||||
background-color: var(--primary-soft);
|
||||
color: var(--primary-dark);
|
||||
}
|
||||
|
||||
.role-trainer {
|
||||
background-color: #f3e5f5;
|
||||
color: #7b1fa2;
|
||||
background-color: rgba(106, 27, 154, 0.12);
|
||||
color: #6a1b9a;
|
||||
}
|
||||
|
||||
.role-team_manager {
|
||||
background-color: #fff3e0;
|
||||
color: #f57c00;
|
||||
background-color: rgba(212, 132, 30, 0.14);
|
||||
color: #9c5700;
|
||||
}
|
||||
|
||||
.role-member {
|
||||
background-color: #f5f5f5;
|
||||
color: #616161;
|
||||
background-color: var(--background-soft);
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
.owner-badge {
|
||||
@@ -729,13 +731,13 @@ th {
|
||||
}
|
||||
|
||||
.status-active {
|
||||
background-color: #e8f5e9;
|
||||
color: #2e7d32;
|
||||
background-color: rgba(46, 125, 50, 0.12);
|
||||
color: var(--success-color);
|
||||
}
|
||||
|
||||
.status-inactive {
|
||||
background-color: #ffebee;
|
||||
color: #c62828;
|
||||
background-color: rgba(180, 66, 66, 0.12);
|
||||
color: var(--danger-color);
|
||||
}
|
||||
|
||||
.status-badge.clickable {
|
||||
@@ -749,8 +751,8 @@ th {
|
||||
|
||||
.btn-small {
|
||||
padding: 6px 12px;
|
||||
background-color: #2196f3;
|
||||
color: white;
|
||||
background: linear-gradient(135deg, var(--primary-color), var(--primary-hover));
|
||||
color: var(--text-on-primary);
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
@@ -758,7 +760,7 @@ th {
|
||||
}
|
||||
|
||||
.btn-small:hover {
|
||||
background-color: #1976d2;
|
||||
opacity: 0.95;
|
||||
}
|
||||
|
||||
.muted {
|
||||
@@ -787,7 +789,7 @@ th {
|
||||
max-height: 90vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.3);
|
||||
box-shadow: var(--shadow-strong);
|
||||
}
|
||||
|
||||
.dialog-header {
|
||||
@@ -795,12 +797,12 @@ th {
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 20px;
|
||||
border-bottom: 1px solid #e0e0e0;
|
||||
border-bottom: 1px solid var(--border-color);
|
||||
}
|
||||
|
||||
.dialog-header h2 {
|
||||
margin: 0;
|
||||
color: #2c3e50;
|
||||
color: var(--text-color);
|
||||
}
|
||||
|
||||
.close-btn {
|
||||
@@ -808,7 +810,7 @@ th {
|
||||
border: none;
|
||||
font-size: 28px;
|
||||
cursor: pointer;
|
||||
color: #666;
|
||||
color: var(--text-muted);
|
||||
padding: 0;
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
@@ -818,7 +820,7 @@ th {
|
||||
}
|
||||
|
||||
.close-btn:hover {
|
||||
color: #333;
|
||||
color: var(--text-color);
|
||||
}
|
||||
|
||||
.dialog-body {
|
||||
@@ -828,7 +830,7 @@ th {
|
||||
}
|
||||
|
||||
.info-text {
|
||||
background-color: #f8f9fa;
|
||||
background-color: var(--background-soft);
|
||||
padding: 12px;
|
||||
border-radius: 4px;
|
||||
margin-bottom: 20px;
|
||||
@@ -841,7 +843,7 @@ th {
|
||||
}
|
||||
|
||||
.permission-group {
|
||||
border: 1px solid #e0e0e0;
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: 6px;
|
||||
padding: 15px;
|
||||
}
|
||||
@@ -854,7 +856,7 @@ th {
|
||||
|
||||
.permission-group h4 {
|
||||
margin: 0 0 12px 0;
|
||||
color: #2c3e50;
|
||||
color: var(--text-color);
|
||||
font-size: 1em;
|
||||
}
|
||||
|
||||
@@ -872,7 +874,7 @@ th {
|
||||
}
|
||||
|
||||
.permission-action-label {
|
||||
color: #2c3e50;
|
||||
color: var(--text-color);
|
||||
}
|
||||
|
||||
.permission-checkbox {
|
||||
@@ -889,7 +891,7 @@ th {
|
||||
|
||||
.dialog-footer {
|
||||
padding: 15px 20px;
|
||||
border-top: 1px solid #e0e0e0;
|
||||
border-top: 1px solid var(--border-color);
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 10px;
|
||||
@@ -904,27 +906,27 @@ th {
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
background-color: #2196f3;
|
||||
color: white;
|
||||
background: linear-gradient(135deg, var(--primary-color), var(--primary-hover));
|
||||
color: var(--text-on-primary);
|
||||
}
|
||||
|
||||
.btn-primary:hover {
|
||||
background-color: #1976d2;
|
||||
opacity: 0.95;
|
||||
}
|
||||
|
||||
.btn-secondary {
|
||||
background-color: #f5f5f5;
|
||||
color: #333;
|
||||
background-color: var(--background-soft);
|
||||
color: var(--text-color);
|
||||
}
|
||||
|
||||
.btn-secondary:hover {
|
||||
background-color: #e0e0e0;
|
||||
background-color: var(--surface-hover);
|
||||
}
|
||||
|
||||
.btn-reset {
|
||||
background: #f5f5f5;
|
||||
border: 1px solid #ddd;
|
||||
color: #333;
|
||||
background: var(--background-soft);
|
||||
border: 1px solid var(--border-color);
|
||||
color: var(--text-color);
|
||||
padding: 6px 10px;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
@@ -944,18 +946,17 @@ th {
|
||||
}
|
||||
|
||||
.state-inherit {
|
||||
background: #f5f5f5;
|
||||
color: #333;
|
||||
background: var(--background-soft);
|
||||
color: var(--text-color);
|
||||
}
|
||||
|
||||
.state-allow {
|
||||
background: #e8f5e9;
|
||||
color: #2e7d32;
|
||||
background: rgba(46, 125, 50, 0.12);
|
||||
color: var(--success-color);
|
||||
}
|
||||
|
||||
.state-deny {
|
||||
background: #ffebee;
|
||||
color: #c62828;
|
||||
background: rgba(180, 66, 66, 0.12);
|
||||
color: var(--danger-color);
|
||||
}
|
||||
</style>
|
||||
|
||||
|
||||
@@ -6,6 +6,8 @@
|
||||
"dev": "concurrently \"cd backend && npm run dev\" \"cd frontend && npm run dev\"",
|
||||
"dev:backend": "cd backend && npm run dev",
|
||||
"dev:frontend": "cd frontend && npm run dev",
|
||||
"audit:frontend-size": "bash ./scripts/audit_frontend_size.sh",
|
||||
"audit:inline-todos": "bash ./scripts/audit_inline_todos.sh",
|
||||
"test": "echo \"Error: no test specified\" && exit 1"
|
||||
},
|
||||
"keywords": [],
|
||||
|
||||
41
scripts/audit_frontend_size.sh
Normal file
41
scripts/audit_frontend_size.sh
Normal file
@@ -0,0 +1,41 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||
TARGET_DIR="${1:-$ROOT_DIR/frontend/src}"
|
||||
WARN_BYTES=$((80 * 1024))
|
||||
REFACTOR_BYTES=$((120 * 1024))
|
||||
|
||||
if [[ ! -d "$TARGET_DIR" ]]; then
|
||||
echo "Directory not found: $TARGET_DIR" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "Frontend size audit"
|
||||
echo "Target: $TARGET_DIR"
|
||||
echo "Warn threshold: ${WARN_BYTES} bytes (~80 KB)"
|
||||
echo "Refactor threshold: ${REFACTOR_BYTES} bytes (~120 KB)"
|
||||
echo
|
||||
|
||||
find "$TARGET_DIR" -type f \( -name '*.vue' -o -name '*.js' -o -name '*.ts' -o -name '*.scss' \) -printf '%s %p\n' \
|
||||
| sort -nr \
|
||||
| awk -v warn="$WARN_BYTES" -v refactor="$REFACTOR_BYTES" '
|
||||
BEGIN {
|
||||
printf "%-12s %-12s %s\n", "Size (KB)", "Status", "File";
|
||||
print "---------------------------------------------------------------";
|
||||
}
|
||||
{
|
||||
size_bytes = $1;
|
||||
size_kb = size_bytes / 1024;
|
||||
$1 = "";
|
||||
sub(/^ /, "", $0);
|
||||
status = "ok";
|
||||
if (size_bytes >= refactor) {
|
||||
status = "refactor";
|
||||
} else if (size_bytes >= warn) {
|
||||
status = "warn";
|
||||
}
|
||||
printf "%-12.1f %-12s %s\n", size_kb, status, $0;
|
||||
}
|
||||
' \
|
||||
| sed -n '1,40p'
|
||||
22
scripts/audit_inline_todos.sh
Normal file
22
scripts/audit_inline_todos.sh
Normal file
@@ -0,0 +1,22 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||
|
||||
echo "Inline TODO/FIXME audit"
|
||||
echo "Target: $ROOT_DIR/frontend/src + $ROOT_DIR/backend"
|
||||
echo
|
||||
|
||||
if ! rg -n "TODO|FIXME|XXX" \
|
||||
"$ROOT_DIR/frontend/src" \
|
||||
"$ROOT_DIR/backend" \
|
||||
-g '*.vue' \
|
||||
-g '*.js' \
|
||||
-g '*.ts' \
|
||||
-g '*.scss' \
|
||||
-g '*.sql' \
|
||||
--glob '!**/dist/**' \
|
||||
--glob '!**/node_modules/**' \
|
||||
--glob '!**/package-lock.json'; then
|
||||
echo "No inline TODO/FIXME/XXX markers found in product code."
|
||||
fi
|
||||
Reference in New Issue
Block a user